169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
# 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
|