feat(engine): the manager — chat orders, team, triage, verdicts, bootstrap

Phase 3 of the AI-workforce engine (TODO-agent-2):

- engine/manager.py: ManagerTurns.chat (CREATE_TASK/PAUSE/RESUME/CANCEL,
  HIRE/UPDATE_EMPLOYEE/FIRE, KNOWLEDGE), triage (ASSIGN/REJECT/ASK_USER),
  final_review (COMPLETE/REVISE +cycle), error_review (RESOLVE_ERROR/CANCEL),
  bootstrap_team; post_user_message now delegates to it.
- One billing path: call_model/BillingScope in runner.py is the single model
  entry point (fixes the agybot chat-billing bug).
- Team changes capped at 3/turn, employee cap, last manager cannot be fired,
  hires pick a granted model of the requested tier strictly.
- container_engine.py wires the real WorkforceBootstrapPort.
- Fixes: REVISE is a body tag (prompt and parser now agree); ASSIGN
  employee="" means pick-for-me; the runner's protocol-error retry is capped by
  engine_max_protocol_retries (was unbounded until the turn timeout).
- 20 new manager tests + regression tests; report in
  docs/plan/reports/agent-2-phase-3.md.
This commit is contained in:
mjalmoudy94
2026-10-14 18:12:56 +03:30
parent c6488bfadf
commit 581f703910
15 changed files with 2385 additions and 99 deletions
+14
View File
@@ -162,6 +162,20 @@ async def test_end_to_end_smoke_path(client: AsyncClient):
assert "model" not in str(employee.json()).lower()
# -- 8. send a manager message ----------------------------------------- #
# PLAN-v2 D3: the engine may only run on models the owner granted to this
# organization, so the manager's chat needs a grant before it can answer.
# Before the engine existed any active model would do; a chat without a
# grant now correctly answers nothing but the "waiting for a model" note.
models = await client.get("/api/admin/models", headers=headers)
assert models.status_code == 200, models.text
ai_model_id = models.json()[0]["id"]
grant = await client.put(
f"/api/admin/model-grants/{org_id}/models/{ai_model_id}",
json={"daily_credit_cap": None},
headers=headers,
)
assert grant.status_code in (200, 201), grant.text
thread = await client.post(
f"/api/organizations/{org_id}/threads",
json={"title": "گفت‌وگو با مدیر", "project_id": project_id},