Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
17 KiB
TODO — Agent 2: AI Workforce Engine (manager AI, employees, dynamic pipelines, test/commit/push)
- Worktree:
/projects/smart-org-wt/agent2· Branch:agent2/ai-workforce(already created, already installed) - Read first:
docs/plan/PLAN-v2.md(all of it, especially §3 and §5). Then read fully:apps/api/app/application/services/workforce_service.py,app/domain/workforce/models.py+tools.py,app/interfaces/http/routers/workforce.py,app/infrastructure/repositories/workforce.py,app/application/ports/model_provider.py,app/application/ports/realtime.py,app/workers/ai_runtime.py,tests/test_workforce.py. - Reference (concepts only, NEVER import, never edit):
/projects/agybot/org_manager.py(lines ~23-101 roles/pipelines/statuses,_WORK_TRANSITIONS~413) and/projects/agybot/org_orchestrator.py(ROLE_TAGS/MODE_TAGS~206-223,STATUS_ROLE~178,DEFAULT_EFFORT_BY_ROLE~292,DIRECT_MANAGER_SYSTEM_PROMPT~662,_manager_pipeline~7671). Read with grep or by line range; those files are huge. - Your files: only the "Agent 2" column of PLAN-v2 §6, your block in
config.py/.env.example, and the migrationalembic/versions/a2b3c4d5e6f2_ai_workforce.py(down_revision = "a0f0e1d2c3b4"). New tables go inapp/infrastructure/db/models_engine.py.db/models.pyis locked, so new task and employee fields go into new 1:1 side tables (see 1.3), never intotasks/employeescolumns. - Never touch: the locked files and the other agents' files. Need something there? →
CHANGE REQUESTin your report. - Do NOT commit, push, merge or rebase. Never
cdoutside the worktree, never write to/tmp. - After each phase: run all checks (PLAN-v2 §7.4), write
docs/plan/reports/agent-2-phase-<K>.md, then STOP and wait.
You consume: ModelProviderPort (existing), ModelGrantPort, MemoryPort, RepoWorkspacePort,
SandboxProviderPort (existing), DeliveryHooks.on_branch_pushed, EventPublisher. Get them only from
the builders in app/application/container.py. Tests use the Fake/Null versions plus a scripted fake model
(you write it in tests/engine_fakes.py: returns queued responses in order and records the prompts it got).
You implement: WorkforceBootstrapPort (wired in container_engine.py).
Important behaviour rules
- The AI never runs git. It edits files and runs commands inside the sandbox; the platform does git through
RepoWorkspacePort. - Every model call writes a usage event exactly as the current
start_rundoes (keep that code path). Manager chat calls count too (agybot forgot this). - Users never see model names. Store
ai_model_idinternally; APIs for non-owners returntieronly. - Nothing in this engine deploys to production. The furthest it goes is "merged and pushed to
dev".
Phase 1 — Engine core: protocol, state machine, pipelines (pure logic, no I/O)
Create package app/application/engine/ (and app/domain/workforce/ additions where the logic is pure):
- 1.1
domain/workforce/pipeline.py:Stageenum:pending_manager_review,pending_analysis,pending_implementation,pending_review,pending_test,pending_final_review,merge_request,completed, plus side statespending_user_decision,error_review,paused,cancelled,failed.PIPELINES:plan_code_test(default),plan_code_review_test,code_test,analysis_only(no worktree, no merge). Each is an ordered list of stages.TRANSITIONS: dict[Stage, frozenset[Stage]]andnext_stage(pipeline, current, outcome).STAGE_ROLE: stage → (role, mode). Manager modes aretriage,final,error.resolve_pipeline(manager_choice, task_kind, app_default, has_active_reviewer): priority manager > kind > app default >plan_code_test. A review pipeline without an active reviewer falls back toplan_code_test.TASK_KINDS:feature,bugfix,refactor,docs,analysis→ default pipeline + risk.RoleKey.DEVELOPERis our "programmer". Keep the existing enum values; do not rename them.
- 1.2
engine/protocol.py: parser for[[TAG attr="v" ...]]body[[/TAG]]and self-closing[[TAG attr="v"/]]. Tolerant of whitespace; rejects unknown tags; returns typed dataclasses. Allowed tags per role/mode:- manager/triage:
ASSIGN,REJECT,ASK_USER,KNOWLEDGE - manager/final:
COMPLETE,REVISE,KNOWLEDGE - manager/error:
RESOLVE_ERROR,CANCEL,KNOWLEDGE - manager/chat:
CREATE_TASK,PAUSE_TASK,RESUME_TASK,CANCEL_TASK,HIRE,UPDATE_EMPLOYEE,FIRE,KNOWLEDGE - planner:
PLAN_SPEC,SPLIT,ASK_USER - developer:
WORK_REPORT - reviewer:
REVIEW_PASS,REVIEW_CHANGES - tester:
TEST_PASS,TEST_FAIL - tool tags (all working roles):
READ_FILE path,WRITE_FILE path(body = content),EDIT_FILE path(body =<<<OLD … === … NEW>>>search/replace blocks),LIST_DIR path,SEARCH pattern path,RUN cmd timeoutDisallowed tags are ignored and reported back to the model as an error line. Limits: max 3CREATE_TASKper reply, max 3 team changes per turn, max 3KNOWLEDGEper turn.
- manager/triage:
- 1.3 Tables in
models_engine.py:task_pipeline_state(task_id PK/FK, pipeline, kind, risk, stage, cycle, max_cycles, branch, last_diff_hash, same_diff_count, retry_count, waiver_reason, parent_task_id, depends_on JSON, version int for optimistic updates, updated_at)employee_profiles(employee_id PK/FK, ai_model_id, effort, roles CSV, hired_byuser|manager, busy_task_id)engine_turns(id, task_id, run_id, employee_id, role, mode, stage, prompt_chars, reply_excerpt ≤ 4000, tags JSON, tool_calls int, ok, error_kind, started_at, finished_at)gate_results(id, task_id, gatetests|diff_guard|no_progress|green|lint, ok, detail ≤ 8000, created_at) Map the coarsetasks.statusfrom the stage: new / running / waiting_approval / done / failed / cancelled. Existing APIs keep working.
- 1.4
engine/state.py:advance(task_id, expected_from, to)with an optimistic check (version). It fails cleanly when another worker moved the task first. A failed or cancelled dependency moves the dependents topending_user_decision, never leaving them stuck (agybot bug M1). - 1.5 Tests
tests/test_engine_protocol.py,tests/test_engine_pipeline.py: every pipeline walks start→completed on the happy path; every illegal transition is refused; concurrent advance (two sessions) → exactly one wins; the dependency failure rule; tag parsing edge cases (nested brackets in a body, unknown tags, the per-role matrix, limits).
Phase 2 — Agent turn runner (one employee, one turn, real tools in the sandbox)
- 2.1
engine/prompt.py, a prompt builder with a stable prefix (cache-friendly), in this order:- platform rules (short, yours)
- role protocol (allowed tags + format examples)
- toolkit description
- app profile (from
AppProfile: language, framework, commands; never secrets) MemoryPort.build_context(org, role, query=task text, budget_chars=2500)- task brief (≤ 1,200 chars) + stage-specific context (the planner's spec for the developer; the diff only for the reviewer; test output for the tester)
- the conversation so far in this turn (tool results) Hard cap on total characters; truncate oldest tool results first.
- 2.2
engine/tools.py: executes tool tags inside the sandbox throughSandboxProviderPort, rooted atWorktreeRef.sandbox_path.- Path safety: normalise; reject absolute paths outside the root,
..escapes and anything under.git. RUNhas a timeout (default 120s, max 600s) and output capped (last 8,000 chars). The command is passed to the sandbox as-is (the sandbox is the security boundary), butgit push,git remoteandgit configare refused with a message ("the platform handles git").EDIT_FILEapplies search/replace blocks and fails with a clear message when OLD is not found exactly once.- Each tool call writes a
tool_callsrow (existing table) with statussucceeded/failed, so the existing approvals UI finally sees real executions.
- Path safety: normalise; reject absolute paths outside the root,
- 2.3
engine/runner.py,run_turn(task, employee, role, mode) -> TurnResult. Loop:- model call → parse tags → execute tools → feed results back → until a terminal protocol tag appears, or
ENGINE_MAX_TOOL_CALLS_PER_TURN, orENGINE_TURN_TIMEOUT_SECONDS. - Model = the employee's
ai_model_id, which must be granted (ModelGrantPort.is_granted; if not, fail witherror_kind=authand tell the manager). - Error kinds
rate_limit,unavailable,network,timeoutare transient (retry later, up to 2 retries);auth,model,protocolare not. - Writes
engine_turns,run_stepsand usage events, and publishesrun.log/run.tool/run.status(batched, PLAN-v2 §5). - Cancellation:
cancel_runmust stop the loop between steps.
- model call → parse tags → execute tools → feed results back → until a terminal protocol tag appears, or
- 2.4 Tests
tests/test_engine_runner.pywith the scripted fake model +FakeRepoWorkspace+ a fake sandbox (dict filesystem + scripted command results; put it intests/engine_fakes.py). Cover: the write → run → report loop; path escape refused;git pushrefused; the tool-call cap; timeout; transient vs fatal errors; usage events counted per model call; an ungranted model refused; no secret from settings ever appears in a prompt (assert on the fake model's recorded prompts).
Phase 3 — Manager AI (orders → tasks → team)
- 3.1 Manager chat: rewrite
post_user_messageso the manager replies and may emitCREATE_TASK(title, kind, priority, depends_on, optional pipeline),PAUSE_TASK/RESUME_TASK/CANCEL_TASK, andHIRE/UPDATE_EMPLOYEE/FIRE. Tasks created this way get atask_pipeline_state, stagepending_manager_review, and are linked to the thread. The manager's prompt must ask for atomic tasks that do not touch the same files. Count chat usage. - 3.2 Team management:
HIRE role= tier= name=picks a granted model matching the tier/capabilities (ModelGrantPort.granted_models). Respect max employees (setting + later org policy), max 3 changes per turn, and "the last manager cannot be fired". Publishteam.changed. Users can still hire and fire from the UI (existing endpoints) but only choose role + tier, never a model. - 3.3 Triage turn (manager, mode triage): decide pipeline + assignee (
ASSIGN employee= pipeline=), orREJECT, orASK_USER(→pending_user_decision, message posted in the thread). Default employee selection: least busy active employee with the needed role;ASSIGN employee="name"uses fuzzy name matching inside the org only. - 3.4 Final turn (manager, mode final): sees the plan, a diff summary, gate results and the test output →
COMPLETE(→merge_request) orREVISE(→ back to implementation, cycle+1). The error turn (mode error) handles failed gates and errors →RESOLVE_ERROR(retry with a hint) orCANCEL. - 3.5
KNOWLEDGEtags →MemoryPort.record(kind=fact|rule, source=manager). - 3.6
WorkforceBootstrapPort.bootstrap_team(org, project): creates one manager + one planner + one developer + one tester on the strongest/balanced/fast granted tiers (whatever is granted; if nothing is granted, create the employees "inactive" and post a system message "منتظر تخصیص مدل توسط مالک سامانه"). Idempotent. Wire it incontainer_engine.py. - 3.7 Tests
tests/test_engine_manager.py: an order → ≤ 3 tasks with deps; HIRE limits; FIRE the last manager refused; ASK_USER path; idempotent bootstrap; cross-org isolation (the manager of org A can never assign an employee of org B, even by exact name).
Phase 4 — Pipeline execution, gates, commit, merge, push
- 4.1
engine/scheduler.py,tick(). Each tick:- finds tasks whose stage needs a turn and whose deps are completed, and a free employee with the role (one task per employee at a time)
- honours org automation (if Agent 1's policy says
automation_enabled=false, tasks wait inpending_manager_review; read it throughModelGrantPortwhen the CHANGE REQUEST lands, until then assume enabled) - runs turns concurrently with a per-org cap (3) and a global cap (setting)
workers/ai_runtime.pycallstick()in its loop everyENGINE_TICK_SECONDS(keep the one-pass mode for cron). Safe with two worker processes thanks to the optimisticadvance.
- 4.2 Worktree lifecycle: on the first developer/planner turn,
RepoWorkspacePort.create_task_worktree(project, task, base="dev"); the branch istask/<task_id>.analysis_onlygets none. The worktree is removed after completion, cancellation or failure. - 4.3 Gates after
WORK_REPORT(each writes agate_resultsrow and publishestask.gate):- diff guard:
RepoWorkspacePort.diff. Fail on changes under.git, CI files (.github/,.gitlab-ci.yml,.gitea/), symlinks pointing outside the repo, or files > 1 MB. An empty diff for a code task also fails. - no-progress: hash the diff; 2 identical hashes in a row →
error_review. - tests: run the app profile's
test_commandin the sandbox (timeout 600s), store the tail of the output. Red → back to the developer with the failure tail (one escalation to a stronger granted tier on the 2nd red), counting cycles. Green → tester stage. - green gate: the tester's
TEST_PASSis accepted only if the tests gate is green, ORTEST_PASS waive="reason"with a non-empty reason (stored inwaiver_reason, shown in UI). max_cyclesreached →error_review.
- diff guard:
- 4.4 Commit + merge + push on
merge_request:commit(message:task/<id>: <title>, author = employee display name + platform email), thensync_with_base, then re-run the tests if base changed (a semantic conflict is only caught by retests).- Then
merge_into(target="dev")andpush(branch="dev"). - A conflict → back to the developer with the conflicting files (max 3 times, then
error_review). - On success: stage
completed,DeliveryHooks.on_branch_pushed(org, project, "dev", sha, source="engine"),MemoryPort.on_task_finished(TaskOutcome)(never let its failure fail the task).
- 4.5 Budget: before each turn, check the org credit wallet (existing billing service public functions; read, do not edit) and the per-task cap. Out of budget →
pausedwith a Persian reason. - 4.6 Tests
tests/test_engine_flow.py, the end-to-end engine with all fakes. The scripted model plays manager → planner → developer → tester → manager; the fake sandbox makes tests red once and then green. Assert the stage sequence, gate rows, cycles, the commit/merge/push calls onFakeRepoWorkspace,on_branch_pushedcalled once with the right sha,on_task_finishedcalled once, and usage events. Plus the failure paths: no-progress, max cycles, merge conflict ×3, cancel mid-turn, a dependency failure.
Phase 5 — UI (orders, live task view, team)
- 5.1
lib/api-engine.ts: typed client functions for everything new (task detail with pipeline state, turns, gates, team with tier, hire/fire with role+tier). - 5.2
/manager: the chat shows task cards created by the manager inline (title, stage badge, link), live viaorg:+thread:channels; an "در انتظار تصمیم شما" banner forpending_user_decisionwith a reply box. - 5.3
/tasks: a board grouped by stage (columns: بررسی مدیر، تحلیل، پیادهسازی، بازبینی، تست، ادغام، انجامشده، نیازمند تصمیم) with a live update; filters; pause/resume/cancel (with confirm). - 5.4
/tasks/[id]:- a pipeline stepper (stages of its pipeline, current one highlighted)
- the turn timeline (who, role, when, outcome)
- a live log panel (subscribe to
run:<id>, auto-scroll, pause-scroll) - gate results (green/red with expandable output)
- a diff viewer (read-only, from the latest diff)
- the waiver reason when present
- a link to the pipeline run in
/apponce pushed (just a link with the sha; that page is Agent 3's)
- 5.5
/workforce: employees with role, tier, status, current task; hire (role + tier) and fire with confirm. No model names anywhere for non-owners (test it). - 5.6
/dashboard+/traces: show active tasks, a stage distribution and recent completions; keep the existing widgets working. - 5.7 Web tests
src/test/engine-*.test.tsx: board grouping, live log append and batching, no model name rendered (render with a fixture that contains a model name and assert it is absent).
Final report (after Phase 5)
docs/plan/reports/agent-2-final.md: summary, the engine architecture (one diagram in text), the tag protocol
reference table, every endpoint added or changed, tables, settings used, CHANGE REQUESTs still open (expected:
org policy in ModelGrantPort, the worker hook for memory expiry), manual QA steps, known limitations.