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:
@@ -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
|
||||
Reference in New Issue
Block a user