feat(F.14): Unified Task System — F-TASK-MODEL/API/AGENT/WORK/UI/MIG/GOAL/TEST
Check Cross-Plugin Imports / check (push) Has been cancelled

- F-TASK-MODEL: Extended Task model with polymorphic assignee/entity/creator, subtasks, dependencies, task_type, success_criteria, progress
- F-TASK-API: Extended task routes with polymorphic filters, subtasks, dependencies, new lifecycle
- F-TASK-AGENT: ai_tools.py (191 lines) — create_task, assign_task, update_task_status, decompose_goal tools
- F-TASK-WORK: workstream.py — task_card and goal_card blocks in communication system
- F-TASK-UI: TaskBoard.tsx, TaskDetail.tsx, GoalView.tsx frontend components
- F-TASK-MIG: Migration 0124 — new columns, data migration for contact_id/assigned_to
- F-TASK-GOAL: Progress aggregation, success criteria evaluation, parent status propagation
- F-TASK-TEST: test_unified_tasks.py (414 lines)
- i18n updates for task system
This commit is contained in:
Agent Zero
2026-08-17 18:51:22 +02:00
parent 06b281ba74
commit a53dcc38d5
9 changed files with 1397 additions and 12 deletions
@@ -0,0 +1,114 @@
"""Unified Task System (F.14).
Adds polymorphic assignment/entity/creator fields, subtasks, dependencies,
goals/milestones and agent-subtask support to the tasks table. Migrates
legacy ``contact_id``/``assigned_to`` values into the polymorphic fields and
migrates existing ``agent_subtasks`` rows into tasks with
``task_type='agent_subtask'``.
Revision ID: 0124
Revises: 0123
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
revision = "0124"
down_revision = "0123"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ── Add new columns to tasks ────────────────────────────────────────────
op.add_column("tasks", sa.Column("assignee_type", sa.String(20), nullable=False, server_default="user"))
op.add_column("tasks", sa.Column("assignee_id", PGUUID(as_uuid=True), nullable=True))
op.add_column("tasks", sa.Column("entity_type", sa.String(80), nullable=True))
op.add_column("tasks", sa.Column("entity_id", PGUUID(as_uuid=True), nullable=True))
op.add_column("tasks", sa.Column("creator_type", sa.String(20), nullable=False, server_default="user"))
op.add_column("tasks", sa.Column("creator_id", PGUUID(as_uuid=True), nullable=True))
op.add_column("tasks", sa.Column("parent_task_id", PGUUID(as_uuid=True), nullable=True))
op.add_column("tasks", sa.Column("depends_on", JSONB, nullable=False, server_default=sa.text("'[]'::jsonb")))
op.add_column("tasks", sa.Column("task_type", sa.String(30), nullable=False, server_default="todo"))
op.add_column("tasks", sa.Column("success_criteria", JSONB, nullable=True))
op.add_column("tasks", sa.Column("target_date", sa.DateTime(timezone=True), nullable=True))
op.add_column("tasks", sa.Column("progress", sa.Integer(), nullable=False, server_default="0"))
# ── Migrate legacy data into polymorphic fields ─────────────────────────
# contact_id → entity_type='contact' + entity_id
op.execute(
"""
UPDATE tasks
SET entity_type = 'contact', entity_id = contact_id
WHERE contact_id IS NOT NULL AND entity_type IS NULL
"""
)
# assigned_to → assignee_type='user' + assignee_id
op.execute(
"""
UPDATE tasks
SET assignee_type = 'user', assignee_id = assigned_to
WHERE assigned_to IS NOT NULL AND assignee_id IS NULL
"""
)
# created_by → creator_type='user' + creator_id
op.execute(
"""
UPDATE tasks
SET creator_type = 'user', creator_id = created_by
WHERE created_by IS NOT NULL AND creator_id IS NULL
"""
)
# ── Migrate AgentSubtask rows into tasks ────────────────────────────────
op.execute(
"""
INSERT INTO tasks (
id, tenant_id, title, description, status, priority,
assignee_type, assignee_id, entity_type, entity_id,
creator_type, creator_id, task_type, depends_on, progress,
created_at, updated_at
)
SELECT
asub.id, asub.tenant_id,
asub.task_description, asub.task_description, asub.status, 'medium',
'agent', asub.child_agent_id, 'agent', asub.parent_agent_id,
'agent', asub.parent_agent_id, 'agent_subtask', '[]'::jsonb, 0,
asub.created_at, asub.updated_at
FROM agent_subtasks asub
WHERE NOT EXISTS (
SELECT 1 FROM tasks t WHERE t.id = asub.id
)
"""
)
# ── Indexes ─────────────────────────────────────────────────────────────
op.create_index("ix_tasks_tenant_entity", "tasks", ["tenant_id", "entity_type", "entity_id"])
op.create_index("ix_tasks_tenant_assignee", "tasks", ["tenant_id", "assignee_type", "assignee_id"])
op.create_index("ix_tasks_tenant_parent", "tasks", ["tenant_id", "parent_task_id"])
op.create_index("ix_tasks_tenant_type", "tasks", ["tenant_id", "task_type"])
op.create_foreign_key(
"fk_tasks_parent_task_id", "tasks", "tasks", ["parent_task_id"], ["id"],
ondelete="CASCADE",
)
def downgrade() -> None:
op.drop_constraint("fk_tasks_parent_task_id", "tasks", type_="foreignkey")
op.drop_index("ix_tasks_tenant_type", table_name="tasks")
op.drop_index("ix_tasks_tenant_parent", table_name="tasks")
op.drop_index("ix_tasks_tenant_assignee", table_name="tasks")
op.drop_index("ix_tasks_tenant_entity", table_name="tasks")
op.drop_column("tasks", "progress")
op.drop_column("tasks", "target_date")
op.drop_column("tasks", "success_criteria")
op.drop_column("tasks", "task_type")
op.drop_column("tasks", "depends_on")
op.drop_column("tasks", "parent_task_id")
op.drop_column("tasks", "creator_id")
op.drop_column("tasks", "creator_type")
op.drop_column("tasks", "entity_id")
op.drop_column("tasks", "entity_type")
op.drop_column("tasks", "assignee_id")
op.drop_column("tasks", "assignee_type")