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:
@@ -0,0 +1,186 @@
|
||||
"""AI workforce engine: pipeline state, employee profiles, turns, gate results
|
||||
|
||||
Revision ID: a2b3c4d5e6f2
|
||||
Revises: d5e6f7a8b9c0
|
||||
Create Date: 2026-10-14 09:30:00.000000
|
||||
|
||||
Owned by the AI-workforce workstream (PLAN-v2 §6).
|
||||
|
||||
Deviation from TODO-agent-2, noted so the next reader is not surprised: the
|
||||
TODO pins ``down_revision = "a0f0e1d2c3b4"`` (the v2 foundation), which was
|
||||
correct while each agent worked on its own branch. On the merged ``dev``
|
||||
branch the agents have landed one by one, so this migration chains after the
|
||||
most recent head (``d5e6f7a8b9c0``, org setup drafts) to keep a linear
|
||||
history. Claude adds the merge revision for the two remaining heads.
|
||||
|
||||
No ``tasks`` or ``employees`` column is added: those tables are locked. The
|
||||
engine's per-task and per-employee data lives in 1:1 side tables instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "a2b3c4d5e6f2"
|
||||
down_revision: str | None = "d5e6f7a8b9c0"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"task_pipeline_state",
|
||||
sa.Column(
|
||||
"task_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("tasks.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
sa.Column(
|
||||
"organization_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("organizations.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"project_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("projects.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"pipeline",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default="plan_code_test",
|
||||
),
|
||||
sa.Column("kind", sa.String(length=16), nullable=False, server_default="feature"),
|
||||
sa.Column("risk", sa.String(length=8), nullable=False, server_default="medium"),
|
||||
sa.Column(
|
||||
"stage",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default="pending_manager_review",
|
||||
),
|
||||
sa.Column("cycle", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("max_cycles", sa.Integer(), nullable=False, server_default="6"),
|
||||
sa.Column("branch", sa.String(length=120), nullable=False, server_default=""),
|
||||
sa.Column("last_diff_hash", sa.String(length=64), nullable=False, server_default=""),
|
||||
sa.Column("same_diff_count", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("retry_count", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("waiver_reason", sa.Text(), nullable=False, server_default=""),
|
||||
sa.Column(
|
||||
"parent_task_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("depends_on", sa.JSON(), nullable=False, server_default="[]"),
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_pipeline_state_organization_id",
|
||||
"task_pipeline_state",
|
||||
["organization_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_task_pipeline_state_stage", "task_pipeline_state", ["stage"]
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"employee_profiles",
|
||||
sa.Column(
|
||||
"employee_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("employees.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
sa.Column("ai_model_id", sa.String(length=32), nullable=True),
|
||||
sa.Column(
|
||||
"effort", sa.String(length=16), nullable=False, server_default="medium"
|
||||
),
|
||||
sa.Column("roles", sa.String(length=255), nullable=False, server_default=""),
|
||||
sa.Column("hired_by", sa.String(length=8), nullable=False, server_default="user"),
|
||||
sa.Column(
|
||||
"busy_task_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_employee_profile_busy", "employee_profiles", ["busy_task_id"]
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"engine_turns",
|
||||
sa.Column("id", sa.String(length=32), primary_key=True),
|
||||
sa.Column(
|
||||
"task_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("tasks.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"run_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("runs.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"employee_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("employees.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("role", sa.String(length=24), nullable=False, server_default="developer"),
|
||||
sa.Column("mode", sa.String(length=16), nullable=False, server_default="work"),
|
||||
sa.Column("stage", sa.String(length=32), nullable=False, server_default=""),
|
||||
sa.Column("prompt_chars", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("reply_excerpt", sa.Text(), nullable=False, server_default=""),
|
||||
sa.Column("tags", sa.JSON(), nullable=False, server_default="{}"),
|
||||
sa.Column("tool_calls", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("ok", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("error_kind", sa.String(length=16), nullable=False, server_default=""),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("ix_engine_turn_task_stage", "engine_turns", ["task_id", "stage"])
|
||||
op.create_index("ix_engine_turn_run", "engine_turns", ["run_id"])
|
||||
|
||||
op.create_table(
|
||||
"gate_results",
|
||||
sa.Column("id", sa.String(length=32), primary_key=True),
|
||||
sa.Column(
|
||||
"task_id",
|
||||
sa.String(length=32),
|
||||
sa.ForeignKey("tasks.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("gate", sa.String(length=16), nullable=False, server_default="tests"),
|
||||
sa.Column("ok", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("detail", sa.Text(), nullable=False, server_default=""),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("ix_gate_result_task_gate", "gate_results", ["task_id", "gate"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_gate_result_task_gate", table_name="gate_results")
|
||||
op.drop_table("gate_results")
|
||||
op.drop_index("ix_engine_turn_run", table_name="engine_turns")
|
||||
op.drop_index("ix_engine_turn_task_stage", table_name="engine_turns")
|
||||
op.drop_table("engine_turns")
|
||||
op.drop_index("ix_employee_profile_busy", table_name="employee_profiles")
|
||||
op.drop_table("employee_profiles")
|
||||
op.drop_index("ix_task_pipeline_state_stage", table_name="task_pipeline_state")
|
||||
op.drop_index(
|
||||
"ix_task_pipeline_state_organization_id", table_name="task_pipeline_state"
|
||||
)
|
||||
op.drop_table("task_pipeline_state")
|
||||
Reference in New Issue
Block a user