Introduce Devii agents subsystem with supervisor, policy, and evals

- Add agents package: controller, runner, store, tools, actions, evals, policy
- Wire agents into Devii service, session, registry, CLI, and dispatcher
- Extend database schema and config for agent persistence
- Add news and telegram bridge updates plus landing UI and docs
- Cover agents, news, and telegram flows with unit tests
This commit is contained in:
2026-10-26 02:54:44 +01:00
parent 407673bf57
commit 3bfde87fdb
50 changed files with 4102 additions and 78 deletions
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
@@ -0,0 +1,55 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.agents.actions import AGENT_ACTIONS
def _by_name():
return {action.name: action for action in AGENT_ACTIONS}
def test_six_agent_tools_with_agent_handler():
assert sorted(_by_name()) == [
"agent_cancel",
"agent_list",
"agent_result",
"agent_start",
"agent_status",
"ask_user",
]
for action in AGENT_ACTIONS:
assert action.handler == "agent"
assert action.method == "LOCAL"
assert action.summary.strip()
def test_auth_mirrors_tasks():
tools = _by_name()
assert tools["agent_start"].requires_auth is True
assert tools["agent_cancel"].requires_auth is True
assert tools["agent_status"].requires_auth is False
assert tools["agent_result"].requires_auth is False
assert tools["agent_list"].requires_auth is False
assert tools["ask_user"].requires_auth is False
def test_start_carries_the_four_part_contract():
params = {p.name: p for p in _by_name()["agent_start"].params}
assert params["category"].required is True
assert params["task"].required is True
assert params["output_format"].required is True
assert params["tools_extra"].type == "array"
assert "label" in params
assert "budget_hint" in params
def test_result_resumes_with_an_answer():
params = {p.name: p for p in _by_name()["agent_result"].params}
assert params["uid"].required is True
assert params["answer"].required is False
def test_uid_params_are_required():
tools = _by_name()
for name in ("agent_status", "agent_cancel"):
params = {p.name: p for p in tools[name].params}
assert params["uid"].required is True
@@ -0,0 +1,190 @@
# retoor <retoor@molodetz.nl>
import json
import pytest
from devplacepy.database import get_setting, set_setting
from devplacepy.services.devii.agents.controller import AgentController
from devplacepy.services.devii.agents.policy import CATEGORIES
from devplacepy.services.devii.agents.store import AgentStore
from devplacepy.services.devii.config import FIELD_AGENTS_ENABLED
from devplacepy.services.devii.errors import ToolInputError
from devplacepy.utils import generate_uid
from tests.conftest import run_async
class _Quota:
def __init__(self, reached=False):
self.reached = reached
class _StubSession:
def __init__(self, db, owner_id):
self.store = AgentStore(db, "user", owner_id)
self.owner_kind = "user"
self.owner_id = owner_id
self.channel = "main"
self.emitted = []
self.quota = _Quota()
def _quota_snapshot(self):
return {"limit_reached": self.quota.reached}
async def _emit(self, payload, buffer=True):
self.emitted.append(payload)
return True
async def emit_agent(self, payload):
self.emitted.append(payload)
return True
class _StubSupervisor:
def __init__(self, store=None):
self._store = store
self.resumed = []
self.cancelled = []
async def resume(self, uid, answer):
self.resumed.append((uid, answer))
if self._store is not None:
self._store.update(uid, {"status": "running"})
return True
async def cancel(self, uid, reason):
self.cancelled.append((uid, reason))
if self._store is not None:
self._store.update(uid, {"status": "cancelled"})
return True
@pytest.fixture
def flag_on(local_db):
previous = get_setting(FIELD_AGENTS_ENABLED, "")
set_setting(FIELD_AGENTS_ENABLED, "1")
try:
yield
finally:
set_setting(FIELD_AGENTS_ENABLED, previous or "")
@pytest.fixture
def session(local_db):
return _StubSession(local_db, f"ctl-{generate_uid()}")
def _start_args(**overrides):
args = {
"category": "research",
"task": "Compare static site generators.",
"output_format": "A markdown table.",
}
args.update(overrides)
return args
def test_start_validates_contract(local_db, session, flag_on):
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args(category="nope")))
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args(task=" ")))
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args(output_format="")))
with pytest.raises(ToolInputError):
run_async(
controller.dispatch(
"agent_start", _start_args(tools_extra=["missing_tool"])
)
)
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args(tools_extra=["run_js"])))
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args(budget_hint="huge")))
def test_start_queues_and_emits(local_db, session, flag_on):
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
raw = run_async(controller.dispatch(
"agent_start", _start_args(label="sg", budget_hint="quick"))
)
payload = json.loads(raw)
assert payload["status"] == "queued"
assert payload["run"]["category"] == "research"
assert session.emitted[0]["type"] == "agent"
assert session.emitted[0]["kind"] == "started"
def test_start_refuses_when_flag_off_or_quota_spent(local_db, session):
set_setting(FIELD_AGENTS_ENABLED, "")
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args()))
set_setting(FIELD_AGENTS_ENABLED, "1")
try:
session.quota.reached = True
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args()))
finally:
set_setting(FIELD_AGENTS_ENABLED, "")
def test_start_enforces_active_cap(local_db, session, flag_on):
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
run_async(controller.dispatch("agent_start", _start_args()))
run_async(controller.dispatch("agent_start", _start_args()))
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_start", _start_args()))
def test_status_result_cancel_list_round_trip(local_db, session, flag_on):
supervisor = _StubSupervisor(session.store)
controller = AgentController(session, supervisor=supervisor)
uid = json.loads(run_async(controller.dispatch("agent_start", _start_args())))["run"][
"uid"
]
status = json.loads(run_async(controller.dispatch("agent_status", {"uid": uid})))
assert status["run"]["status"] == "queued"
listed = json.loads(run_async(controller.dispatch("agent_list", {})))
assert listed["count"] == 1
cancelled = json.loads(run_async(controller.dispatch("agent_cancel", {"uid": uid})))
assert cancelled["status"] == "cancelled"
assert supervisor.cancelled == [(uid, "cancelled by orchestrator")]
with pytest.raises(ToolInputError):
run_async(controller.dispatch("agent_status", {"uid": "missing"}))
def test_result_resumes_needs_user(local_db, session, flag_on):
supervisor = _StubSupervisor(session.store)
controller = AgentController(session, supervisor=supervisor)
uid = json.loads(run_async(controller.dispatch("agent_start", _start_args())))["run"][
"uid"
]
controller._store.update(uid, {"status": "needs_user", "needs_question": "Budget?"})
payload = json.loads(
run_async(controller.dispatch("agent_result", {"uid": uid, "answer": "zero"}))
)
assert supervisor.resumed == [(uid, "zero")]
assert payload["run"]["uid"] == uid
def test_ask_user_returns_pause_marker(session):
controller = AgentController(session, supervisor=_StubSupervisor(session.store))
payload = json.loads(
run_async(controller.dispatch("ask_user", {"question": "Which budget?"}))
)
assert payload["status"] == "paused_for_user"
with pytest.raises(ToolInputError):
run_async(controller.dispatch("ask_user", {"question": " "}))
def test_categories_match_policy():
assert set(CATEGORIES) == {
"research",
"files",
"planning-report",
"email-triage",
"containers",
"long-mutation",
}
+193
View File
@@ -0,0 +1,193 @@
# retoor <retoor@molodetz.nl>
import json
from devplacepy.database import get_table
from devplacepy.services.devii.agents.runner import AgentRunner
from devplacepy.services.devii.agents.store import AgentStore
from devplacepy.services.devii.agents.supervisor import AgentSupervisor
from devplacepy.services.devii.cost.tracker import CostTracker, Pricing
from devplacepy.services.devii.store import UsageLedger
from devplacepy.services.devii.tasks.schedule import now_utc, to_iso
from tests.conftest import run_async
from devplacepy.utils import generate_uid
class _Settings:
daily_limit_usd = 1.0
plan_required = True
verify_required = True
ai_model = "test-model"
class _StubSession:
def __init__(self, owner_id):
self.cost = CostTracker(pricing=Pricing())
self.settings = _Settings()
self._ledger = UsageLedger()
self._llm = None
self.dispatcher = None
self.owner_kind = "user"
self.owner_id = owner_id
self.channel = "main"
self._agents = {}
self.emitted = []
async def _emit(self, payload, buffer=True):
self.emitted.append(payload)
return True
async def emit_agent(self, payload):
self.emitted.append(payload)
return True
def record_agent_spend(self, uid, usage, cost):
self._ledger.record(
self.owner_kind, self.owner_id, uid, usage, cost, self.settings.ai_model
)
def _record(owner, **overrides):
now = to_iso(now_utc())
record = {
"uid": generate_uid(),
"channel": "main",
"category": "research",
"label": "sg",
"task": "work",
"output_format": "text",
"tools_extra": "[]",
"effort": "standard",
"task_scope": 0,
"status": "running",
"progress": "started",
"result": None,
"error": None,
"checks": 1,
"nudges_used": 0,
"retries_used": 0,
"error_streak": 0,
"iterations": 0,
"tokens": 0,
"cost_usd": 0.0,
"needs_question": None,
"pending_ask_id": None,
"check_at": now,
"last_progress_at": now,
"created_at": now,
"updated_at": now,
}
record.update(overrides)
return record
def _setup(local_db):
owner = f"drv-{generate_uid()}"
store = AgentStore(local_db, "user", owner)
session = _StubSession(owner)
supervisor = AgentSupervisor(local_db, lambda row: session)
return owner, store, session, supervisor
def _runner(session, uid):
return AgentRunner(
session, uid, "sg", "research", "standard", "work", "text", [], False
)
def test_drive_done_finishes_and_ledgers(local_db, monkeypatch):
async def fake_loop(**kwargs):
kwargs["state"].iteration = 2
return "final report"
monkeypatch.setattr(
"devplacepy.services.devii.agents.runner.react_loop", fake_loop
)
owner, store, session, supervisor = _setup(local_db)
record = _record(owner)
store.create(record)
runner = _runner(session, record["uid"])
runner.cost.prompt_tokens = 50
runner.cost.completion_tokens = 10
supervisor._runners[record["uid"]] = runner
supervisor._running[record["uid"]] = {
"task": None,
"owner_id": owner,
"ledgered": {
"prompt_tokens": 0,
"completion_tokens": 0,
"cache_hit_tokens": 0,
"cache_miss_tokens": 0,
"cost": 0.0,
},
}
run_async(supervisor._drive(record["uid"], session))
row = store.get(record["uid"])
assert row["status"] == "done"
assert row["result"] == "final report"
kinds = [frame["kind"] for frame in session.emitted]
assert "done" in kinds
ledgered = get_table("devii_usage_ledger").find_one(turn_id=record["uid"])
assert ledgered is not None
assert int(ledgered["prompt_tokens"]) == 50
assert int(ledgered["completion_tokens"]) == 10
def test_drive_needs_user_parks_with_question(local_db, monkeypatch):
async def fake_loop(**kwargs):
messages = kwargs["messages"]
messages.append(
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "c9",
"function": {
"name": "ask_user",
"arguments": json.dumps({"question": "Budget?"}),
},
}
],
}
)
messages.append(
{
"role": "tool",
"tool_call_id": "c9",
"name": "ask_user",
"content": json.dumps({"status": "paused_for_user"}),
}
)
return "awaiting user"
monkeypatch.setattr(
"devplacepy.services.devii.agents.runner.react_loop", fake_loop
)
owner, store, session, supervisor = _setup(local_db)
record = _record(owner)
store.create(record)
supervisor._runners[record["uid"]] = _runner(session, record["uid"])
run_async(supervisor._drive(record["uid"], session))
row = store.get(record["uid"])
assert row["status"] == "needs_user"
assert row["needs_question"] == "Budget?"
assert row["pending_ask_id"] == "c9"
assert session.emitted[-1]["kind"] == "needs_user"
def test_drive_error_records_and_stays_runnable(local_db, monkeypatch):
async def fake_loop(**kwargs):
raise RuntimeError("boom")
monkeypatch.setattr(
"devplacepy.services.devii.agents.runner.react_loop", fake_loop
)
owner, store, session, supervisor = _setup(local_db)
record = _record(owner)
store.create(record)
supervisor._runners[record["uid"]] = _runner(session, record["uid"])
run_async(supervisor._drive(record["uid"], session))
row = store.get(record["uid"])
assert row["status"] == "running"
assert row["error"] == "boom"
+35
View File
@@ -0,0 +1,35 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.agents.actions import AGENT_ACTIONS
from devplacepy.services.devii.agents.evals import EVALS
from devplacepy.services.devii.agents.policy import CATEGORIES
from devplacepy.services.devii.registry import CATALOG
def test_twenty_unique_evals():
assert len(EVALS) == 20
assert len({item["uid"] for item in EVALS}) == 20
def test_evals_cover_channels_and_kinds():
channels = {item["channel"] for item in EVALS}
assert channels == {"main", "telegram"}
kinds = {item["expect"] for item in EVALS}
assert "direct" in kinds
assert kinds - {"direct"} <= set(CATEGORIES)
def test_evals_are_well_formed_and_grounded():
known = set(CATALOG.by_name()) | {a.name for a in AGENT_ACTIONS}
for item in EVALS:
assert item["prompt"].strip()
assert item["end_state"].strip()
assert item["channel"] in ("main", "telegram")
assert item["expect"] == "direct" or item["expect"] in CATEGORIES
for tool in item["tools"]:
assert tool in known
if item["expect"] != "direct":
assert "agent_start" in item["tools"]
assert {"agent_result", "agent_status", "agent_cancel"} & set(
item["tools"]
)
+85
View File
@@ -0,0 +1,85 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_setting, set_setting
from devplacepy.services.devii.config import FIELD_AGENTS_ENABLED
from devplacepy.services.devii.registry import CATALOG
from devplacepy.services.manager import service_manager
from devplacepy.services.devii import DeviiService
def _hub():
if service_manager.get_service("devii") is None:
service_manager.register(DeviiService())
svc = service_manager.get_service("devii")
return svc.hub(), svc
def _session(channel, owner_id):
hub, svc = _hub()
return hub.get_or_create(
"guest", owner_id, "guest", "", svc.instance_base_url(), is_admin=False, channel=channel
)
def _tool_names(session):
return {t["function"]["name"] for t in session.tools}
def _agent_names():
return {a.name for a in CATALOG.actions if a.handler == "agent"}
def test_flag_off_hides_every_agent_tool(local_db):
previous = get_setting(FIELD_AGENTS_ENABLED, "")
set_setting(FIELD_AGENTS_ENABLED, "")
try:
main = _session("main", "flag-off-main")
main._refresh_tools()
assert not (_tool_names(main) & _agent_names())
finally:
set_setting(FIELD_AGENTS_ENABLED, previous or "")
def test_flag_on_shows_five_supervision_tools(local_db):
previous = get_setting(FIELD_AGENTS_ENABLED, "")
set_setting(FIELD_AGENTS_ENABLED, "1")
try:
hub, svc = _hub()
user = hub.get_or_create(
"user",
"flag-on-user",
"flaguser",
"fake-key",
svc.instance_base_url(),
is_admin=False,
channel="main",
)
user._refresh_tools()
visible = _tool_names(user) & _agent_names()
assert visible == {
"agent_start",
"agent_status",
"agent_result",
"agent_cancel",
"agent_list",
}
guest = _session("main", "flag-on-guest")
guest._refresh_tools()
assert (_tool_names(guest) & _agent_names()) == {
"agent_status",
"agent_result",
"agent_list",
}
finally:
set_setting(FIELD_AGENTS_ENABLED, previous or "")
def test_docs_channel_ignores_flag(local_db):
previous = get_setting(FIELD_AGENTS_ENABLED, "")
set_setting(FIELD_AGENTS_ENABLED, "1")
try:
docs = _session("docs", "flag-on-docs")
docs._refresh_tools()
assert _tool_names(docs) == {"search_docs"}
finally:
set_setting(FIELD_AGENTS_ENABLED, previous or "")
+176
View File
@@ -0,0 +1,176 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.agents.policy import (
CATEGORIES,
CHECK_INTERVAL_MAX_SECONDS,
EFFORT_SCALE,
MAX_ERROR_STREAK,
NEEDS_USER_TIMEOUT_SECONDS,
ORCHESTRATOR_ONLY_TOOLS,
TERMINAL_STATUSES,
decide,
next_check_in,
)
from devplacepy.services.devii.registry import CATALOG
def _snapshot(**overrides):
snap = {
"status": "running",
"age_seconds": 10,
"idle_seconds": 5,
"iterations": 2,
"tokens": 1000,
"checks": 1,
"nudges_used": 0,
"retries_used": 0,
"error_streak": 0,
"quota_ok": True,
"failed": False,
"answered": False,
"progress": False,
"effort": "standard",
}
snap.update(overrides)
return snap
def test_terminal_statuses_always_settle():
for category in CATEGORIES:
for status in TERMINAL_STATUSES:
decision, _ = decide(_snapshot(status=status), category)
assert decision == "settle"
def test_spent_quota_cancels_first():
for category in CATEGORIES:
decision, reason = decide(_snapshot(quota_ok=False, progress=True), category)
assert decision == "cancel"
assert "quota" in reason
def test_budgets_cancel_at_every_scale():
for category in CATEGORIES:
entry = CATEGORIES[category]
for effort, scale in EFFORT_SCALE.items():
over_time = _snapshot(
age_seconds=int(entry.time_budget_seconds * scale) + 1,
effort=effort,
progress=True,
)
assert decide(over_time, category)[0] == "cancel"
over_iter = _snapshot(
iterations=int(entry.iteration_budget * scale) + 1,
effort=effort,
progress=True,
)
assert decide(over_iter, category)[0] == "cancel"
over_tokens = _snapshot(
tokens=int(entry.token_budget * scale) + 1,
effort=effort,
progress=True,
)
assert decide(over_tokens, category)[0] == "cancel"
at_edge = _snapshot(
age_seconds=int(entry.time_budget_seconds * scale),
iterations=int(entry.iteration_budget * scale),
tokens=int(entry.token_budget * scale),
effort=effort,
progress=True,
)
assert decide(at_edge, category)[0] == "continue"
def test_error_streak_cancels():
for category in CATEGORIES:
snap = _snapshot(error_streak=MAX_ERROR_STREAK, progress=True)
assert decide(snap, category)[0] == "cancel"
snap = _snapshot(error_streak=MAX_ERROR_STREAK - 1, progress=True)
assert decide(snap, category)[0] == "continue"
def test_failed_run_retries_then_cancels():
for category in CATEGORIES:
entry = CATEGORIES[category]
snap = _snapshot(failed=True, retries_used=0)
assert decide(snap, category)[0] == "retry_else"
snap = _snapshot(failed=True, retries_used=entry.max_retries)
decision, _ = decide(snap, category)
assert decision == "cancel"
def test_needs_user_waits_answers_and_times_out():
for category in CATEGORIES:
waiting = _snapshot(status="needs_user")
assert decide(waiting, category)[0] == "wait"
answered = _snapshot(status="needs_user", answered=True)
assert decide(answered, category)[0] == "continue"
stale = _snapshot(
status="needs_user", age_seconds=NEEDS_USER_TIMEOUT_SECONDS + 1
)
assert decide(stale, category)[0] == "cancel"
def test_progress_continues_and_grace_waits():
for category in CATEGORIES:
entry = CATEGORIES[category]
assert decide(_snapshot(progress=True), category)[0] == "continue"
assert (
decide(_snapshot(idle_seconds=entry.grace_seconds), category)[0]
== "wait"
)
def test_stalled_runs_retry_then_nudge_then_cancel():
for category in CATEGORIES:
entry = CATEGORIES[category]
stalled = {"idle_seconds": entry.grace_seconds + 1}
first, _ = decide(_snapshot(**stalled), category)
assert first == "retry_else"
spent_retry = _snapshot(**stalled, retries_used=entry.max_retries)
second, _ = decide(spent_retry, category)
assert second == "nudge"
spent_both = _snapshot(
**stalled,
retries_used=entry.max_retries,
nudges_used=entry.max_nudges,
)
assert decide(spent_both, category)[0] == "cancel"
def test_next_check_in_grows_and_caps():
assert next_check_in(0, 5) == 5
assert next_check_in(1, 5) == 7
assert next_check_in(2, 5) == 11
assert next_check_in(100, 5) == CHECK_INTERVAL_MAX_SECONDS
assert next_check_in(100, 600) == CHECK_INTERVAL_MAX_SECONDS
def test_denied_set_covers_user_contact_handlers():
by_name = CATALOG.by_name()
contact_handlers = {"client", "avatar", "interaction"}
for name, action in by_name.items():
if action.handler in contact_handlers:
assert name in ORCHESTRATOR_ONLY_TOOLS
for name in (
"agent_start",
"agent_status",
"agent_cancel",
"agent_result",
"agent_list",
):
assert name in ORCHESTRATOR_ONLY_TOOLS
def test_category_extras_name_real_tools():
known = set(CATALOG.by_name())
for entry in CATEGORIES.values():
assert entry.fragment.strip()
assert entry.time_budget_seconds > 0
assert entry.iteration_budget > 0
assert entry.token_budget > 0
assert entry.check_interval_seconds > 0
assert entry.grace_seconds > 0
for tool in entry.extras:
assert tool in known
assert tool not in ORCHESTRATOR_ONLY_TOOLS
@@ -0,0 +1,92 @@
# retoor <retoor@molodetz.nl>
import asyncio
from devplacepy.database import get_setting, set_setting
from devplacepy.services.devii.agents.runner import SUBAGENT_BASE_PROMPT
from devplacepy.services.devii.config import FIELD_AGENTS_ENABLED
from devplacepy.services.devii.session import ORCHESTRATION_FRAGMENT
from devplacepy.services.manager import service_manager
from devplacepy.services.devii import DeviiService
from tests.conftest import run_async
def _hub():
if service_manager.get_service("devii") is None:
service_manager.register(DeviiService())
svc = service_manager.get_service("devii")
return svc.hub(), svc
def _session(channel, owner_id):
hub, svc = _hub()
return hub.get_or_create(
"guest", owner_id, "guest", "", svc.instance_base_url(), is_admin=False, channel=channel
)
class _Conn:
def __init__(self):
self.frames = []
async def send_json(self, payload):
self.frames.append(payload)
def test_orchestration_block_follows_flag(local_db):
previous = get_setting(FIELD_AGENTS_ENABLED, "")
try:
set_setting(FIELD_AGENTS_ENABLED, "")
main = _session("main", "prompt-off")
assert "# ORCHESTRATION" not in main._compose_system_prompt()
set_setting(FIELD_AGENTS_ENABLED, "1")
assert "# ORCHESTRATION" in main._compose_system_prompt()
assert "agent_start" in main._compose_system_prompt()
assert "needs_user" in main._compose_system_prompt()
docs = _session("docs", "prompt-docs")
assert "# ORCHESTRATION" not in docs._compose_system_prompt()
finally:
set_setting(FIELD_AGENTS_ENABLED, previous or "")
def test_fragments_cover_contract_and_relay():
assert "four-part contract" in ORCHESTRATION_FRAGMENT
assert "budget_hint" in ORCHESTRATION_FRAGMENT
assert "agent_result" in ORCHESTRATION_FRAGMENT
assert "ui_prompt" in ORCHESTRATION_FRAGMENT
assert "ask_user" in SUBAGENT_BASE_PROMPT
assert "Confirmation-gated" in SUBAGENT_BASE_PROMPT
def test_trace_agent_emits_labelled_frame(local_db):
session = _session("main", "trace-agent")
conn = _Conn()
async def flow():
session.attach(conn)
try:
session.trace_agent("uid-1", "sg", "call", "fetch_url", "example.com")
await asyncio.sleep(0.2)
finally:
session.detach(conn)
run_async(flow())
assert conn.frames
frame = conn.frames[-1]
assert frame["type"] == "trace"
assert frame["agent"] == "sg"
assert frame["uid"] == "uid-1"
assert frame["name"] == "fetch_url"
def test_emit_agent_buffers_for_web_not_telegram(local_db):
main = _session("main", "emit-main")
telegram = _session("telegram", "emit-tg")
async def flow():
assert await main.emit_agent({"type": "agent", "kind": "done"}) is False
assert await telegram.emit_agent({"type": "agent", "kind": "done"}) is False
run_async(flow())
assert [f["type"] for f in main._buffer] == ["agent"]
assert telegram._buffer == []
+133
View File
@@ -0,0 +1,133 @@
# retoor <retoor@molodetz.nl>
import json
from devplacepy.services.devii.agents.runner import AgentRunner
from devplacepy.services.devii.cost.tracker import CostTracker, Pricing
from tests.conftest import run_async
class _Ledger:
def __init__(self):
self.spent = 0.0
def spent_24h(self, owner_kind, owner_id):
return self.spent
class _Settings:
daily_limit_usd = 1.0
plan_required = True
verify_required = True
class _StubSession:
def __init__(self):
self.cost = CostTracker(pricing=Pricing())
self.settings = _Settings()
self._ledger = _Ledger()
self._llm = None
self.dispatcher = None
self.owner_kind = "user"
self.owner_id = "owner-1"
def _runner(**overrides):
args = {
"session": _StubSession(),
"uid": "run-1",
"label": "run",
"category_key": "research",
"effort": "standard",
"task": "Compare static site generators.",
"output_format": "A markdown table.",
"tools": [],
"task_scope": False,
}
args.update(overrides)
return AgentRunner(**args)
def _ask_call(call_id, question):
return {
"id": call_id,
"function": {"name": "ask_user", "arguments": json.dumps({"question": question})},
}
def test_system_prompt_carries_contract_and_behavior():
runner = _runner(behavior_text="Be terse.")
prompt = runner.messages[0]["content"]
assert "no browser" in prompt
assert "ask_user" in prompt
assert "Be terse." in prompt
assert "Compare static site generators." in prompt
assert "A markdown table." in prompt
def test_pending_ask_found_and_answered():
runner = _runner()
runner.messages.append(
{"role": "assistant", "content": "", "tool_calls": [_ask_call("c1", "Budget?")]}
)
runner.messages.append(
{
"role": "tool",
"tool_call_id": "c1",
"name": "ask_user",
"content": json.dumps({"status": "paused_for_user"}),
}
)
assert runner._pending_ask() == ("c1", "Budget?")
runner.apply_answer("zero")
assert runner._pending_ask() is None
assert runner.messages[-1]["content"] == "The user answered: zero"
def test_quota_spent_short_circuits_without_llm():
session = _StubSession()
session._ledger.spent = 5.0
runner = _runner(session=session)
outcome, text = run_async(runner.run_once())
assert outcome == "error"
assert "quota" in text
def test_trace_tracks_progress_and_streak():
runner = _runner()
runner._trace("call", "fetch_url", "example.com")
assert runner.tool_calls == 1
runner._trace("err", "fetch_url", "boom")
assert runner.error_streak == 1
runner._trace("ok", "fetch_url", "")
assert runner.error_streak == 0
assert "fetch_url" in runner.progress_note
def test_task_scope_wraps_the_loop(monkeypatch):
from devplacepy.services.devii.tasks.context import in_task_run
seen = {}
async def fake_loop(**kwargs):
seen["scoped"] = in_task_run()
return "done"
monkeypatch.setattr(
"devplacepy.services.devii.agents.runner.react_loop", fake_loop
)
run_async(_runner(task_scope=True).run_once())
assert seen == {"scoped": True}
run_async(_runner(task_scope=False).run_once())
assert seen == {"scoped": False}
def test_counts_and_usage_start_at_zero():
runner = _runner()
assert runner.counts() == (0, 0, 0.0)
assert runner.usage() == {
"prompt_tokens": 0,
"completion_tokens": 0,
"cache_hit_tokens": 0,
"cache_miss_tokens": 0,
}
+190
View File
@@ -0,0 +1,190 @@
# retoor <retoor@molodetz.nl>
import multiprocessing
import tempfile
import dataset
from devplacepy.services.devii.agents.store import (
AgentStore,
claim,
due_rows,
prune_terminal,
snapshot_for,
)
from devplacepy.services.devii.tasks.store import memory_db
from devplacepy.services.devii.tasks.schedule import now_utc, to_iso
from devplacepy.utils import generate_uid
def _record(**overrides):
now = to_iso(now_utc())
record = {
"uid": generate_uid(),
"channel": "main",
"category": "research",
"label": None,
"task": "work",
"output_format": "text",
"tools_extra": "[]",
"effort": "standard",
"task_scope": 0,
"status": "queued",
"progress": "queued",
"result": None,
"error": None,
"checks": 0,
"nudges_used": 0,
"retries_used": 0,
"error_streak": 0,
"iterations": 0,
"tokens": 0,
"cost_usd": 0.0,
"needs_question": None,
"pending_ask_id": None,
"check_at": now,
"last_progress_at": now,
"created_at": now,
"updated_at": now,
}
record.update(overrides)
return record
def _store(db=None):
return AgentStore(memory_db() if db is None else db, "user", "owner-1")
def test_create_get_list_update_scope():
store = _store()
first = _record()
store.create(first)
assert store.get(first["uid"])["task"] == "work"
assert store.get("missing") is None
other = AgentStore(store.db, "user", "owner-2")
assert other.get(first["uid"]) is None
store.create(_record(status="running"))
assert len(store.list()) == 2
assert len(store.list(status="running")) == 1
store.update(first["uid"], {"status": "cancelled"})
assert store.get(first["uid"])["status"] == "cancelled"
def test_count_active_ignores_terminal():
store = _store()
for status in ("queued", "running", "needs_user", "done", "cancelled"):
store.create(_record(status=status))
assert store.count_active() == 3
def test_due_rows_and_claim():
db = memory_db()
store = AgentStore(db, "user", "owner-1")
now = to_iso(now_utc())
store.create(_record(status="queued", check_at=now))
store.create(_record(status="done", check_at=now))
rows = due_rows(db, now, 10)
assert [row["status"] for row in rows] == ["queued"]
assert claim(db, rows[0]["uid"], now) is True
assert claim(db, rows[0]["uid"], now) is False
assert store.get(rows[0]["uid"])["status"] == "checking"
def test_complete_only_wins_from_live_status():
store = _store()
record = _record(status="running")
store.create(record)
won = store.complete(
record["uid"],
{
"status": "done",
"result": "ok",
"error": None,
"iterations": 3,
"tokens": 100,
"cost_usd": 0.01,
"updated_at": to_iso(now_utc()),
},
)
assert won is True
assert store.get(record["uid"])["result"] == "ok"
lost = store.complete(
record["uid"],
{
"status": "done",
"result": "late",
"error": None,
"iterations": 4,
"tokens": 200,
"cost_usd": 0.02,
"updated_at": to_iso(now_utc()),
},
)
assert lost is False
assert store.get(record["uid"])["result"] == "ok"
def test_prune_terminal_keeps_live_rows():
db = memory_db()
store = AgentStore(db, "user", "owner-1")
store.create(_record(status="done", updated_at="2020-01-01T00:00:00"))
store.create(_record(status="running", updated_at="2020-01-01T00:00:00"))
assert prune_terminal(db, "2021-01-01T00:00:00") == 1
assert len(store.list()) == 1
def test_snapshot_for_measures_age_and_idle():
row = _record(created_at="2026-01-01T00:00:00", last_progress_at="2026-01-01T00:10:00")
from datetime import datetime, timezone
snap = snapshot_for(row, datetime(2026, 1, 1, 0, 20, tzinfo=timezone.utc))
assert snap["age_seconds"] == 1200
assert snap["idle_seconds"] == 600
assert snap["answered"] is False
def _claim_worker(path, uid, queue):
import sqlalchemy
db = dataset.connect(
f"sqlite:///{path}", engine_kwargs={"connect_args": {"timeout": 30}}
)
try:
for _ in range(50):
try:
queue.put(claim(db, uid, to_iso(now_utc())))
return
except sqlalchemy.exc.OperationalError as exc:
if "locked" not in str(exc).lower():
raise
queue.put(False)
finally:
queue.put(None)
def test_claim_race_exactly_one_winner():
with tempfile.NamedTemporaryFile(suffix=".db") as handle:
db = dataset.connect(
f"sqlite:///{handle.name}",
engine_kwargs={"connect_args": {"timeout": 30}},
)
store = AgentStore(db, "user", "owner-1")
record = _record()
store.create(record)
uid = record["uid"]
ctx = multiprocessing.get_context("fork")
queue = ctx.Queue()
workers = [
ctx.Process(target=_claim_worker, args=(handle.name, uid, queue))
for _ in range(8)
]
for worker in workers:
worker.start()
for worker in workers:
worker.join(60)
assert worker.exitcode == 0
results = []
while len(results) < 16:
results.append(queue.get(timeout=30))
wins = [item for item in results if item is True]
assert len(wins) == 1
@@ -0,0 +1,168 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.agents.store import AgentStore
from devplacepy.services.devii.agents.supervisor import AgentSupervisor
from devplacepy.services.devii.tasks.schedule import now_utc, to_iso
from devplacepy.utils import generate_uid
from tests.conftest import run_async
class _StubSession:
def __init__(self):
self.emitted = []
self._agents = {}
self.limit_reached = False
def _quota_snapshot(self):
return {"limit_reached": self.limit_reached}
async def _emit(self, payload, buffer=True):
self.emitted.append(payload)
return True
async def emit_agent(self, payload):
self.emitted.append(payload)
return True
def record_agent_spend(self, uid, usage, cost):
self.spent = (uid, usage, cost)
def _record(owner, **overrides):
now = to_iso(now_utc())
record = {
"uid": generate_uid(),
"owner_kind": "user",
"owner_id": owner,
"channel": "main",
"category": "research",
"label": "run",
"task": "work",
"output_format": "text",
"tools_extra": "[]",
"effort": "standard",
"task_scope": 0,
"status": "queued",
"progress": "queued",
"result": None,
"error": None,
"checks": 0,
"nudges_used": 0,
"retries_used": 0,
"error_streak": 0,
"iterations": 0,
"tokens": 0,
"cost_usd": 0.0,
"needs_question": None,
"pending_ask_id": None,
"check_at": now,
"last_progress_at": now,
"created_at": now,
"updated_at": now,
}
record.update(overrides)
return record
def _setup(local_db, resolve):
owner = f"sup-{generate_uid()}"
store = AgentStore(local_db, "user", owner)
supervisor = AgentSupervisor(local_db, resolve)
return owner, store, supervisor
def test_reap_stuck_on_start(local_db):
owner, store, supervisor = _setup(local_db, lambda row: None)
store.create(_record(owner, status="running"))
store.create(_record(owner, status="checking"))
store.create(_record(owner, status="needs_user"))
supervisor._reap_stuck()
statuses = sorted(row["status"] for row in store.list())
assert statuses == ["interrupted", "interrupted", "needs_user"]
def test_cancel_terminal_and_missing(local_db):
session = _StubSession()
owner, store, supervisor = _setup(local_db, lambda row: session)
record = _record(owner)
store.create(record)
assert run_async(supervisor.cancel(record["uid"], "stop")) is True
assert store.get(record["uid"])["status"] == "cancelled"
assert session.emitted[0]["kind"] == "cancelled"
assert run_async(supervisor.cancel(record["uid"], "stop")) is False
assert run_async(supervisor.cancel("missing", "stop")) is False
def test_check_interrupts_ownerless_rows(local_db):
owner, store, supervisor = _setup(local_db, lambda row: None)
record = _record(owner)
store.create(record)
run_async(supervisor._tick())
assert store.get(record["uid"])["status"] == "interrupted"
def test_queued_with_spent_quota_cancels(local_db):
session = _StubSession()
session.limit_reached = True
owner, store, supervisor = _setup(local_db, lambda row: session)
record = _record(owner)
store.create(record)
run_async(supervisor._tick())
assert store.get(record["uid"])["status"] == "cancelled"
def test_needs_user_waits_then_times_out(local_db):
session = _StubSession()
owner, store, supervisor = _setup(local_db, lambda row: session)
record = _record(owner, status="needs_user", needs_question="Budget?")
store.create(record)
run_async(supervisor._tick())
row = store.get(record["uid"])
assert row["status"] == "needs_user"
assert row["checks"] == 1
store.update(
record["uid"], {"created_at": "2020-01-01T00:00:00", "check_at": to_iso(now_utc())}
)
run_async(supervisor._tick())
assert store.get(record["uid"])["status"] == "cancelled"
def test_cancel_ledgers_accrued_spend(local_db):
from types import SimpleNamespace
from devplacepy.services.devii.agents.runner import AgentRunner
from devplacepy.services.devii.cost.tracker import CostTracker, Pricing
session = _StubSession()
session.cost = CostTracker(pricing=Pricing())
owner, store, supervisor = _setup(local_db, lambda row: session)
record = _record(owner)
store.create(record)
runner = AgentRunner(
session, record["uid"], "run", "research", "standard", "work", "text", [], False
)
runner.cost.prompt_tokens = 40
supervisor._runners[record["uid"]] = runner
supervisor._running[record["uid"]] = {
"task": SimpleNamespace(cancel=lambda: None),
"owner_id": owner,
"ledgered": {
"prompt_tokens": 0,
"completion_tokens": 0,
"cache_hit_tokens": 0,
"cache_miss_tokens": 0,
"cost": 0.0,
},
}
assert run_async(supervisor.cancel(record["uid"], "stop")) is True
assert session.spent[0] == record["uid"]
assert session.spent[1]["prompt_tokens"] == 40
def test_counts_reports_statuses(local_db):
owner, store, supervisor = _setup(local_db, lambda row: None)
store.create(_record(owner, status="running"))
store.create(_record(owner, status="done"))
counts = supervisor.counts()
assert counts.get("running", 0) >= 1
assert counts.get("done", 0) >= 1
+73
View File
@@ -0,0 +1,73 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.agents.policy import ORCHESTRATOR_ONLY_TOOLS
from devplacepy.services.devii.agents.tools import build_subagent_tools
from devplacepy.services.devii.registry import CATALOG
def _names(schemas):
return {s["function"]["name"] for s in schemas}
def _parent():
return CATALOG.tool_schemas_for(
authenticated=True, is_admin=False, is_primary_admin=False
)
def test_inherits_parent_minus_denied_plus_ask():
schemas = build_subagent_tools(_parent(), "research", (), True, False, False)
names = _names(schemas)
assert "ask_user" in names
assert "rsearch" in names
assert "ui_prompt" not in names
assert "run_js" not in names
assert "avatar_show" not in names
assert "agent_start" not in names
assert "delegate" in names
assert not (names & ORCHESTRATOR_ONLY_TOOLS)
def test_extras_union_and_role_filter():
schemas = build_subagent_tools(
_parent(), "long-mutation", ("container_exec",), True, False, False
)
names = _names(schemas)
assert "container_exec" not in names
schemas = build_subagent_tools(
CATALOG.tool_schemas_for(
authenticated=True, is_admin=True, is_primary_admin=False
),
"long-mutation",
("container_exec",),
True,
True,
False,
)
assert "container_exec" in _names(schemas)
def test_guests_lose_authed_tools_everywhere():
guest = CATALOG.tool_schemas_for(
authenticated=False, is_admin=False, is_primary_admin=False
)
schemas = build_subagent_tools(guest, "research", ("create_post",), False, False, False)
names = _names(schemas)
assert "create_post" not in names
assert "fetch_url" in names
assert "ask_user" in names
def test_no_drift_when_catalog_grows():
parent = _parent()
schemas = build_subagent_tools(parent, "files", (), True, False, False)
names = _names(schemas)
by_name = CATALOG.by_name()
for action in CATALOG.actions:
if action.name in ORCHESTRATOR_ONLY_TOOLS:
assert action.name not in names
elif action.name == "ask_user":
assert action.name in names
elif not action.requires_auth and not action.requires_admin:
assert action.name in names
assert by_name["project_write_file"].name in names
+49
View File
@@ -170,6 +170,55 @@ def test_grade_article_unparseable_returns_none(local_db, monkeypatch):
assert grade is None
class FlakyGradeClient(FakeClient_news_service):
def __init__(self, failures_before_success):
super().__init__([])
self.failures_before_success = failures_before_success
self.posts = 0
async def post(self, url, json=None, headers=None, timeout=None):
self.posts += 1
if self.posts <= self.failures_before_success:
return FakeResp_news_service(status=500, text="err")
return FakeResp_news_service(
json_data={"choices": [{"message": {"content": "8"}}]}
)
def test_grade_article_retries_transient_failures(local_db, monkeypatch):
from devplacepy.services.news import GRADE_ATTEMPTS
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
client = FlakyGradeClient(failures_before_success=2)
grade = run_async(
NewsService()._grade_article(
{"title": "FlakyArticle", "description": "d", "content": "c"},
AI_URL,
"m",
client,
)
)
assert grade == 8
assert client.posts == 3
def test_grade_article_gives_up_after_max_attempts(local_db, monkeypatch):
from devplacepy.services.news import GRADE_ATTEMPTS
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
client = FlakyGradeClient(failures_before_success=GRADE_ATTEMPTS + 5)
grade = run_async(
NewsService()._grade_article(
{"title": "FlakyArticle", "description": "d", "content": "c"},
AI_URL,
"m",
client,
)
)
assert grade is None
assert client.posts == GRADE_ATTEMPTS
def test_run_once_handles_api_failure(local_db, monkeypatch):
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
monkeypatch.setattr(base_mod, "get_setting", _settings_stub())
+105
View File
@@ -0,0 +1,105 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.telegram.bridge import TelegramConnection
from tests.conftest import run_async
class _Service:
def __init__(self):
self.sent = []
self.edits = []
async def send(self, chat_id, text, **kwargs):
self.sent.append(text)
return 100 + len(self.sent)
async def edit(self, chat_id, message_id, text, **kwargs):
self.edits.append(text)
return True
async def chat_action(self, chat_id, action="typing"):
return None
class _Session:
def resolve_query(self, query_id, payload):
pass
def resolve_interaction(self, interaction_id, payload):
pass
def _flow(payloads):
async def run():
import asyncio
service = _Service()
conn = TelegramConnection(service, _Session(), 7, 42)
try:
for payload in payloads:
await conn.send_json(payload)
finally:
conn.done.set()
conn._stop_typing()
await asyncio.gather(conn._typing_task, return_exceptions=True)
return service
return run_async(run())
def test_trace_without_agent_unchanged():
service = _flow([{"type": "trace", "event": "call", "name": "fetch_url"}])
assert service.edits == ["<i>Working... (fetch_url)</i>"]
assert service.sent == []
def test_trace_with_agent_labels_progress():
service = _flow(
[
{
"type": "trace",
"event": "call",
"name": "fetch_url",
"agent": "sg",
"uid": "uid-1",
}
]
)
assert service.edits == ["<i>Working... ([sg] fetch_url)</i>"]
assert service.sent == []
def test_agent_frame_while_open_edits_placeholder():
service = _flow(
[
{
"type": "agent",
"kind": "needs_user",
"uid": "uid-1",
"label": "sg",
"payload": "Budget?",
}
]
)
assert service.edits == ["<i>[sg] needs_user</i>"]
assert service.sent == []
def test_agent_frame_after_finalize_sends_new_message():
service = _flow(
[
{"type": "reply", "text": "On it, working in the background."},
{
"type": "agent",
"kind": "done",
"uid": "uid-1",
"label": "sg",
"payload": "The comparison is ready.",
},
]
)
assert len(service.edits) == 1
assert len(service.sent) == 1
assert "Subagent" in service.sent[0]
assert "sg" in service.sent[0]
assert "finished" in service.sent[0]