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.
This commit is contained in:
mjalmoudy94
2026-10-14 15:35:31 +03:30
parent c93b25741e
commit df7da8c75c
11 changed files with 3211 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
# 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`.