Files
mjalmoudy94 df7da8c75c feat(engine): workforce engine core — tag protocol, pipelines, optimistic state
Phase 1 of TODO-agent-2: pure logic plus the tables it writes to.

- domain/workforce/pipeline.py: Stage/PipelineKey/Outcome, the four pipelines,
  TRANSITIONS, next_stage, STAGE_ROLE (role+mode), resolve_pipeline with the
  no-reviewer fallback, task-kind defaults, stage -> coarse tasks.status, and
  the TaskPipelineState record (with the optimistic version).
- application/engine/protocol.py: the [[TAG attr="v"]]body[[/TAG]] parser and
  its self-closing form, the per-role/mode allow-list, literal bodies (a
  bracket inside a body is not a tag), typed readers (ASSIGN/CREATE_TASK/
  team changes/KNOWLEDGE/EDIT_FILE blocks) and the per-reply limits
  (3 tasks, 3 team changes, 3 knowledge items).
- application/engine/state.py: advance() with the optimistic compare-and-set
  and park_dependents() so a failed dependency never deadlocks its children.
- db/models_engine.py + migration a2b3c4d5e6f2: task_pipeline_state,
  employee_profiles, engine_turns, gate_results as side tables (tasks and
  employees stay locked). Linear head; alembic check reports no drift.
- repositories/engine.py + 58 new tests (test_engine_protocol,
  test_engine_pipeline) covering every pipeline's happy path, illegal
  transitions, exactly-one-winner concurrent advance, dependency parking and
  all parser edge cases.

Suite: 594 passed, ruff clean, mypy clean.
2026-10-14 15:35:31 +03:30

9.0 KiB

Agent 2 — Phase 1 report: engine core (protocol, state machine, pipelines)

Date: 2026-10-14 · Branch: dev (this repo's integration branch) · Status: done, all checks green

Environment note: the TODO file assumes the ext4 worktree (/home/mamad/work/smart-org-wt/agent2, branch agent2/ai-workforce). This checkout is the dev repo itself and already contains the other agents' work (org wizard, invites, delivery infra), so the engine is being implemented here directly on dev at the user's instruction. No locked file was touched; no CHANGE REQUEST is needed so far.

What was done

1.1 app/domain/workforce/pipeline.py (new, pure)

  • Stage (13 values: the eight pipeline stages plus the side states pending_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 the ordered tuple of stages ending in completed.
  • TRANSITIONS — every legal stage move, with the reasoning in comments: a paused or parked task resumes through triage (no paused_from column exists, and re-triage is the safe route); merge_request is platform-executed and nobody acts in it; terminal stages never move.
  • next_stage(pipeline, current, outcome) with Outcome (ok, revise, reject, ask_user, error, resolve). ok walks the pipeline list one step; the others have fixed targets — work sent back always returns to the developer.
  • STAGE_ROLE → (role, mode) or None for stages with no AI turn. Manager modes are triage / final / error (plus chat), every other role is work.
  • 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 to plan_code_test.
  • TASK_KIND_DEFAULTS: feature → plan_code_test/medium, bugfix → code_test/low, refactor → plan_code_review_test/high, docs → code_test/low, analysis → analysis_only/low.
  • STAGE_STATUS: stage → coarse tasks.status (new/running/waiting_approval/done/failed/ cancelled) so existing APIs and the old worker keep working against one truthful column.
  • TaskPipelineState dataclass: pipeline, kind, risk, stage, cycle/max_cycles, branch, last_diff_hash/same_diff_count, retry_count, waiver_reason, parent/depends, version (optimistic lock).

1.2 app/application/engine/protocol.py (new)

  • Parser for [[TAG attr="v" ...]]body[[/TAG]] and [[TAG attr="v"/]], tolerant of whitespace, single- or double-quoted values.
  • A body is literal: an outer tag wins and the scanner moves past its close, so brackets inside a body (a source file full of [[) are never re-parsed.
  • TagName enum for all 28 tags; ALLOWED_TAGS is the per-role/mode matrix from the TODO (manager triage/final/error/chat, planner, developer, reviewer, tester) plus the six tool tags granted to working roles only — the manager never edits code.
  • Unknown tags, disallowed tags, stray closings, unclosed tags and malformed attribute fragments are dropped and reported as one error line each, in order, for the model to read next turn.
  • BODY_TAGS: only tags that carry a body must be closed; a forgotten / on a bodyless tag ([[RUN cmd="pytest"]]) is read as self-closing instead of failing the reply.
  • Limits: max 3 CREATE_TASK, max 3 team changes across HIRE+UPDATE_EMPLOYEE+FIRE together, max 3 KNOWLEDGE per reply. Extras are dropped and reported.
  • Typed readers: AssignTag, TaskDraft, TeamChange, KnowledgeItem, EditBlock (<<<OLD … === … NEW>>>), plus edit_blocks_of which refuses a malformed block rather than half-applying it. ASSIGN refuses an unknown pipeline name.

1.3 app/infrastructure/db/models_engine.py (stub filled) + migration a2b3c4d5e6f2

Four tables, all 1:1/side tables — no column was added to the locked tasks / employees:

  • task_pipeline_state (PK task_id), with the denormalised organization_id so the scheduler's hot query stays single-table.
  • employee_profiles (PK employee_id): ai_model_id, effort, roles CSV, hired_by, busy_task_id.
  • engine_turns: id, task/run/employee, role, mode, stage, prompt_chars, reply_excerpt (Text, bounded on write), tags JSON, tool_calls, ok, error_kind, started/finished.
  • gate_results: id, task, gate, ok, detail (bounded), created.

Migration alembic/versions/a2b3c4d5e6f2_ai_workforce.py: alembic upgrade head applies cleanly and alembic check reports no drift between the models and the migration chain.

Deviation (documented in the migration docstring): the TODO pins down_revision = "a0f0e1d2c3b4", which was right while each agent had its own branch. On the merged dev chain the heads already form a line (a3b4c5d6e7f4 → a1b2c3d4e5f1 → d5e6f7a8b9c0), so this migration chains after d5e6f7a8b9c0. Result: one linear head (a2b3c4d5e6f2), no merge revision needed.

1.4 app/application/engine/state.py (new)

  • advance(states, task_id, expected_from, to): checks legality in the domain first, then the repository's compare-and-set (WHERE task_id AND stage AND version) performs the move. A lost race raises ConflictError ("این کار هم‌اکنون توسط موتور دیگری در حرکت است"), an unknown task raises NotFoundError.
  • park_dependents(states, task_id): a failed or cancelled dependency moves every dependent to pending_user_decision (agybot bug M1). The function refuses to act on a healthy task — the guard is in the function, not in the caller — and leaves dependents that already wait on a human alone. A lost race on a dependent is skipped, not fatal.
  • app/infrastructure/repositories/engine.py (new): PipelineStateRepository (advance/patch/ list_at_stages/list_dependents), EmployeeProfileRepository, EngineTurnRepository, GateResultRepository. advance/patch verify the version actually moved rather than trusting rowcount, so the check holds on SQLite and PostgreSQL alike.

1.5 Tests

  • tests/test_engine_pipeline.py (24 tests): all four pipelines walk manager review → completed step by step, each step also legal; sent-back work returns to the developer; illegal transitions refused; the stage/role/mode map; stage → coarse status; pipeline resolution priority and the reviewer fallback; plus DB-backed tests for the version bump, the illegal move, exactly one winner in a concurrent advance, the 404, the dependency-parking rule (and the healthy-task guard), optimistic patch, cascade deletes and the employee profile round trip.
  • tests/test_engine_protocol.py (34 tests): paired/self-closing parsing, whitespace tolerance, nested brackets staying literal, tag order, unknown/disallowed/stray/unclosed/malformed rejections, the full per-role matrix, toolkit-for-working-roles-only, typed readers (assign/task draft/team changes/knowledge/edit blocks) and all three limits.

Checks (PLAN-v2 §7.4)

cd apps/api && .venv/bin/python -m pytest -q        → 594 passed, 1 skipped (whole suite; 58 new)
cd apps/api && .venv/bin/ruff check .               → All checks passed!
cd apps/api && .venv/bin/mypy app                   → Success: no issues found in 144 source files
alembic upgrade head (scratch sqlite)               → applies through a2b3c4d5e6f2, single head
alembic check (scratch sqlite)                      → No new upgrade operations detected.

The web suite was not touched (no UI in Phase 1).

Deviations & notes for the reviewer

  1. Implemented on dev instead of an agent2/ai-workforce worktree (user instruction; the worktree path in the TODO is a Linux path unavailable here).
  2. Migration down_revision chained to the merged head (above) to keep one linear chain.
  3. task_pipeline_state.organization_id added beyond the TODO's column list: it is denormalised from the task so the scheduler never joins back, and it keeps the tenant filter single-table.
  4. resolve_pipeline applies the no-reviewer fallback to an explicit manager choice too. A review stage nobody can act on is a dead end; the manager who wants a reviewer should hire one.
  5. Team-change limit counts HIRE + UPDATE_EMPLOYEE + FIRE together (one budget per turn), which is what "max 3 team changes per turn" means.
  6. New repository module app/infrastructure/repositories/engine.py (the engine columns live beside models_engine.py); repositories/workforce.py was left untouched.

Open questions / CHANGE REQUESTs

  • None for Phase 1. Expected for later phases: org automation policy behind ModelGrantPort (TODO 4.1 says to assume enabled until then) and the worker hook for memory expiry.

Next

Phase 2 — engine/prompt.py, engine/tools.py (sandbox execution, git refusal, path safety), engine/runner.py (run_turn with the scripted fake model, error classification, usage events) and tests/test_engine_runner.py + tests/engine_fakes.py.