Skip to content

feat(teamharness): task state transition engine (table + history + events + progress) - #1233

Draft
LUOSENGWA wants to merge 5 commits into
agentscope-ai:mainfrom
LUOSENGWA:feat/workflow-transition-engine
Draft

feat(teamharness): task state transition engine (table + history + events + progress)#1233
LUOSENGWA wants to merge 5 commits into
agentscope-ai:mainfrom
LUOSENGWA:feat/workflow-transition-engine

Conversation

@LUOSENGWA

Copy link
Copy Markdown
Contributor

Draft — stacked on #1183 + #1206. This PR implements Part 2 (engine) of the
workflow engine design in
#1223 on top of the
branches of #1183 and
#1206. Once both merge,
this branch rebases onto main (the stack integration commit is re-evaluated
at that time). The diff against main therefore currently includes the
#1183/#1206 content — review scope for this PR is the last commit.

Summary

Task state transitions move from "each call site hand-writes status with
uneven guards" to one table + one entry point + one auditable history:

  1. Transition table (single source of truth)
    plugins/teamharness/contracts/task-transitions.json defines the legal
    state graph. The Python write side (TRANSITIONS constant +
    _assert_transition) and the Go read-side tests load the same file and
    assert consistency, so cross-language drift fails in CI.
  2. _transition_task() — single mutation entry — every task state change
    goes through: table validation → status write → history[] append (cap 50,
    oldest dropped) → task meta + project node synced in one batch. All five
    MCP call sites (delegate_task / ack_task / submit_task /
    accept_task_result / cancel_task) are refactored onto it.
  3. Three guard tightenings (behavior change, below): out-of-order
    transitions that were silently accepted now return structured errors with
    the corrective action in the message.
  4. accept_task_result now updates the task meta — previously it only
    wrote the project meta, so the task meta and the graph node could diverge
    forever. Closed.
  5. report_progress (new action) — workers self-report progress on long
    tasks: a from == to history entry (action progress), note required
    (≤ 200 chars, truncated + flagged), no state change, no room
    notification.
  6. Controller read sidetasks_detail[].history pass-through on
    ?includeTasks=true; new GET /api/v1/projects/{id}/events?limit=&cursor=
    (read-time aggregation of all task histories into one ascending timeline;
    no new storage, no write-side hook); CancelTask records the transition
    (actor = authz actor) in the same read-modify-write batch, with no
    duplicate entry on the retry-convergence path.

Behavior changes (explicit)

Transition Before After
ack_task from planned / prepared silently accepted (stamped in_progress) rejected: ack_task: task is 'planned'; delegate_task it first
submit_task from planned / prepared silently accepted (any non-terminal passed) rejected: submit_task: task is 'planned'; ack_task it first
accept_task_result from any non-terminal accepted (no source guard) rejected unless the task is submitted
accept_task_result write set project meta only project meta and task meta (gap fix)
cancel_task (MCP + Controller) status + reason fields same, plus a history[] entry (reason in note)
task meta schema additive history[] field (ts/from/to/action/actor/note?, cap 50)
GET .../workflow?includeTasks=true tasks_detail[] + history[] per task (omitempty; older metas unaffected)
GET .../events — (no route) ascending transition timeline, opaque offset cursor, limit 50 (cap 200)
report_progress — (unknown action) history-only progress entry; role-gated to worker/remote-member

Same-state re-entry (e.g. a repeated ack_task on in_progress) stays a
legal idempotent no-op: no error, no history entry.

The tightenings are safe for well-behaved agents: the skill contract already
orders delegate → ack → execute → submit, and the error message for any
out-of-order attempt is the fix instruction.

What's included

  1. plugins/teamharness/contracts/task-transitions.json (new) — states /
    terminal / transitions
  2. plugins/teamharness/mcp/server.pyTRANSITIONS / TERMINAL_TASK_STATUSES
    constants, _assert_transition, _transition_task (+ _append_transition_history,
    actor = role:account), the five call-site refactorings, report_progress
    (action enum + schema + routing)
  3. agentteams-controllertaskTransition type + taskDetail.History
    (omitempty, malformed entries skipped); GetProjectEvents handler +
    collectProjectEvents (concurrent 8-read, same scope/ownership rules as
    readTasksDetail, no cross-scope fallback); route registration;
    CancelTask history entry (skipped on retry-convergence)
  4. Tests — Ruby test-transition-table.rb (new; registered in
    run-integration-tests.sh): fixture consistency, full lifecycle with
    history-chain integrity (each entry starts where the previous ended,
    non-decreasing RFC 3339 timestamps, actor format), out-of-order
    rejection with guidance strings, idempotent re-entry, cap 50 drop-oldest,
    report_progress branches (role gate / empty note / truncation flag / no
    notification / state gates), cancel trace; Go: fixture vs
    isTerminalTaskStatus consistency, history pass-through (absent /
    malformed), events endpoint (sort + tie-break / cursor continuation /
    limit bounds + cap / empty project / 404 / denied→404 / cross-scope no
    fallback), cancel trace incl. retry-convergence no-duplicate
  5. test-taskflow.rb minimal adaptation (1 fixture): the
    secret-artifact case delegated a task with no assignee, leaving it
    prepared (no notification target); it then submitted straight from
    prepared, which the old permissive guard silently accepted. It now
    delegates with an explicit assignedTo and walks the full lifecycle.
  6. Docs — new docs/design/teamharness/task-transition-engine.md;
    project-task-runtime-design.md (table + history + prepared state);
    docs/usage/project-workflow-api.md + zh-cn (events endpoint, history
    field); task-execution skill (one progress self-reporting paragraph)

Tests

  • Ruby: test-transition-table.rb green; test-taskflow.rb full regression
    green; test-projectflow.rb / test-contracts.rb / test-server.rb /
    test-message.rb / test-filesync.rb green
  • Python: test_continuation.py 66/66; copaw test_taskflow_tool.py 99/99
  • Go: go build + go test ./... green (20 packages; 7 new server tests, 0
    regressions)
  • run-integration-tests.sh runs clean through the TeamHarness suite; the
    only failing step on this stack is a pre-existing upstream CLI contract
    test (loongsuite probe definition), present on the stack base commit
    before any change in this PR

Compatibility

  • Additive object-storage schema: history[] is a new task-meta field;
    readers without it (old controllers, the copaw worker) are unaffected
  • No CRD changes; no worker-runtime changes beyond the three guard
    tightenings listed above
  • The new route does not shadow .../history or .../history/{timestamp}
  • Controller CancelTask write volume is unchanged (the meta was already
    read-modify-written; the entry is added in the same batch)

Known limitations (recorded, not fixed here)

  • Agent (MCP) and controller writes to the same task meta already race
    (pull-before-write vs ETag conditional write). history[] rides the same
    write transaction as the existing task fields — no new race, same
    exposure.
  • The events endpoint is a read-time aggregation: no SSE push in v1 —
    clients poll. Project-level intervention events stay on the /history
    snapshot endpoint (the two are complementary and documented as such).
  • No cross-language runtime table sharing (the Python process and the Go
    binary share no deployment surface) — the shared artifact is the fixture
    plus the two-sided test assertions.

Worker-runtime cross-check (#1223 evaluation requirement)

copaw/src/copaw_worker/task.py is the worker-side local state machine over
the same TaskMeta model. Transition-point comparison after this PR:

Aspect copaw task.py (worker local) TeamHarness MCP (after this PR) Status
Terminal set {completed, revision, blocked, cancelled} same (fixture terminal) ✅ aligned
Result vocabulary SUCCESS / SUCCESS_WITH_NOTES / REVISION_NEEDED / BLOCKED / INTERRUPTED same ✅ aligned
delegate_task stamps prepared, commits assigned after the notification lands same ✅ same semantics
ack_task terminal reject + from ∈ {assigned, in_progress} from ∈ {assigned, in_progress} ✅ aligned (MCP side was looser before this PR)
submit_task terminal reject + no re-submit from submitted from ∈ {assigned, in_progress} ⚠️ MCP is stricter: copaw would also allow planned/preparedsubmitted. Direction is safe (the MCP side refuses states the worker side merely tolerates); the tightening guidance applies. Tracked as follow-up, not fixed here.
accept_task_result requires submitted (terminal idempotent replay allowed) from == submitted ✅ aligned
cancel_task non-terminal + submission fence non-terminal + submission fence ✅ aligned
Transition history / table none (local projection, no audit trail) new: table + history[] ➕ engine-side only

摘要

任务状态转换从"各调用点手改 status、守卫宽严不一"升级为一张表 + 一个入口 + 一条可审计历史——工作流引擎设计(#1223)的第 2 部分(引擎侧)。

草稿 —— 叠在 #1183 + #1206 分支之上。 两者合并后本分支 rebase 到 main(栈集成 commit 届时重评)。当前对 main 的 diff 含 #1183/#1206 内容,本 PR 的评审范围是最后一个 commit。

  1. 转换表(单一事实来源) —— plugins/teamharness/contracts/task-transitions.json 定义合法状态图;Python 写侧(TRANSITIONS 常量 + _assert_transition)与 Go 读侧测试加载同一文件并断言一致,跨语言漂移在 CI 暴露。
  2. _transition_task() 单一变更入口 —— 转换表校验 → 写 status → 追加 history[](上限 50,丢最旧)→ task meta 与 project 节点同批同步。五个 MCP 调用点全部收口。
  3. 三处守卫收紧(行为变更,见下表):原先静默接受的越序转换现在返回结构化错误,错误消息即修复指引。
  4. accept_task_result 现在同步更新 task meta —— 原先只写 project meta,task meta 与图节点可永久分叉,缺口已修复。
  5. report_progress(新 action) —— 长任务进度自报:from == to 的 history 条目(action progress),note 必填(≤200 字符,超长截断+标记),不改状态、不发房间通知。
  6. Controller 读侧 —— ?includeTasks=true 透传 tasks_detail[].history;新端点 GET /api/v1/projects/{id}/events?limit=&cursor=(读时聚合全部任务 history 成升序时间线,零新存储、无写侧钩子);CancelTask 在同一 read-modify-write 批次写入 history 条目(actor = 授权主体,重试收敛路径不重复记)。

行为变更(明示)

转换 之前 之后
ack_taskplanned / prepared 静默接受(直接置 in_progress 拒绝:ack_task: task is 'planned'; delegate_task it first
submit_taskplanned / prepared 静默接受(非终态即放行) 拒绝:submit_task: task is 'planned'; ack_task it first
accept_task_result 自任意非终态 接受(无来源守卫) submitted 可验收
accept_task_result 写入集 仅 project meta project meta task meta(缺口修复)
cancel_task(MCP + Controller) status + reason 字段 同前,history[] 条目(reason 入 note
task meta schema 新增 history[] 字段(ts/from/to/action/actor/note?,上限 50)
GET .../workflow?includeTasks=true tasks_detail[] + 每任务 history[](omitempty,旧 meta 不受影响)
GET .../events —(无路由) 升序转换时间线,不透明 offset 游标,limit 50(上限 200)
report_progress —(未知 action) 仅 history 的进度条目;角色限定 worker/remote-member

同状态重入(如 in_progress 上重复 ack_task)仍是合法幂等 no-op:不报错、不记 history。

收紧对合规 agent 无影响:skill 契约本就规定 delegate → ack → 执行 → submit 顺序,越序时错误消息即修复指引。

包含内容

  1. plugins/teamharness/contracts/task-transitions.json(新)—— states / terminal / transitions
  2. plugins/teamharness/mcp/server.py —— TRANSITIONS / TERMINAL_TASK_STATUSES 常量、_assert_transition_transition_task(+ _append_transition_history,actor = role:account)、五个调用点重构、report_progress(action 枚举 + schema + 路由)
  3. agentteams-controller —— taskTransition 类型 + taskDetail.History(omitempty,畸形条目跳过);GetProjectEvents 处理器 + collectProjectEvents(并发 8 读,与 readTasksDetail 同 scope/所有权规则,不跨 scope 回退);路由注册;CancelTask history 条目(重试收敛跳过)
  4. 测试 —— Ruby test-transition-table.rb(新,已注册进 run-integration-tests.sh):fixture 一致性、全生命周期 history 链完整性(条目首尾相接、RFC 3339 时间戳非递减、actor 格式)、越序拒绝(含引导文案)、幂等重入、cap 50 丢最旧、report_progress 全分支(角色门 / 空 note / 截断标记 / 无通知 / 状态门)、cancel 留痕;Go:fixture 与 isTerminalTaskStatus 一致性、history 透传(缺失/畸形)、events 端点(排序 + 同秒 tie-break / 游标续读 / limit 边界 + 上限 / 空项目 / 404 / denied→404 / 跨 scope 不回退)、cancel 留痕(含重试收敛不重复)
  5. test-taskflow.rb 最小适配(1 个 fixture):secret-artifact 用例原以"无 assignee 的 delegate 停在 prepared 也能 submit"的旧宽行为通过;现补 assignedTo 走完整生命周期(注释写明原因)
  6. 文档 —— 新 docs/design/teamharness/task-transition-engine.mdproject-task-runtime-design.md(转换表 + history + prepared 状态);docs/usage/project-workflow-api.md + zh-cn(events 端点、history 字段);task-execution skill(进度自报一段)

测试

  • Ruby:test-transition-table.rb 绿;test-taskflow.rb 全量回归绿;test-projectflow.rb / test-contracts.rb / test-server.rb / test-message.rb / test-filesync.rb 绿
  • Python:test_continuation.py 66/66;copaw test_taskflow_tool.py 99/99
  • Go:go build + go test ./... 绿(20 包;server 包新增 7 个测试,0 回归)
  • run-integration-tests.sh TeamHarness 套件全跑通;本栈上唯一失败是 upstream 既有的 CLI 契约测试(loongsuite 探针定义),在本 PR 改动前的栈基线 commit 上即存在

兼容性

  • 对象存储 schema 纯增量:history[] 是 task meta 新字段,旧读者(旧 controller、copaw worker)不受影响
  • 不改 CRD;除上表三处守卫收紧外不动 worker runtime
  • 新路由不与 .../history / .../history/{timestamp} 冲突
  • Controller CancelTask 写入量不变(meta 本就 read-modify-write,条目加在同一批次)

已知限制(登记,不在本 PR 修复)

  • agent(MCP)与 controller 写同一 task meta 的既有竞态(pull-before-write vs ETag 条件写):history[] 与现有 task 字段同写事务、同待遇——不新增竞态
  • events 端点是读时聚合:v1 无 SSE 推送,客户端轮询;项目级干预事件留在 /history 快照端点(两者互补,文档写明)
  • 不跨语言共享运行时表(Python 进程与 Go 二进制无共享部署面)——共享的是 fixture + 双端测试断言

Worker runtime 对照(#1223 评估要求)

copaw/src/copaw_worker/task.py 是 worker 侧基于同一 TaskMeta 模型的本地状态机。本 PR 后的转换点对照:

方面 copaw task.py(worker 本地) TeamHarness MCP(本 PR 后) 状态
终态集 {completed, revision, blocked, cancelled} 同(fixture terminal ✅ 一致
Result 词表 SUCCESS / SUCCESS_WITH_NOTES / REVISION_NEEDED / BLOCKED / INTERRUPTED ✅ 一致
delegate_task stamp prepared,通知落地后提交 assigned ✅ 同语义
ack_task 终态拒绝 + from ∈ {assigned, in_progress} from ∈ {assigned, in_progress} ✅ 一致(MCP 侧此前更宽,本次对齐)
submit_task 终态拒绝 + submitted 不重提 from ∈ {assigned, in_progress} ⚠️ MCP 更严:copaw 还允许 planned/preparedsubmitted。方向安全(MCP 侧拒绝的是 worker 侧"容忍"的状态),收紧引导适用。作为 follow-up 跟踪,不在本 PR 修复
accept_task_result 要求 submitted(终态幂等重放放行) from == submitted ✅ 一致
cancel_task 非终态 + submission 栅栏 非终态 + submission 栅栏 ✅ 一致
转换历史 / 表 无(本地投影,无审计轨迹) 新增:表 + history[] ➕ 仅引擎侧

…tack integration

- submit sync-failure: agentscope-ai#1183 _sync_failure_result contract (statePersisted)
  + agentscope-ai#1206 v2 withheld-notification clause; guard before notification
- reused-digest submit branch returns the reused completion notification
- _accept_task_result: resolve outstanding attention in place on task_meta
  (separate read-modify-write was clobbered by the terminal status write)
- test-taskflow.rb: fix latent heredoc escape bug (Result body \n), role
  env toggles for agentscope-ai#1183 runtime-identity-first role, mc fail-sync hook for
  per-file sync (mc cp), vocabulary PARTIAL/FAILED -> INTERRUPTED,
  changed-status resubmit -> digest-fence conflict expectation
- test-projectflow.rb: fixture runtime.yaml gains member role leader
…ess)

- contracts/task-transitions.json: single source of truth for task
  state transitions, loaded and asserted by both the Python write side
  and the Go read-side tests (cross-language drift guard).
- server.py: TRANSITIONS/TERMINAL constants + _assert_transition +
  _transition_task() as the single mutation entry (validate -> status ->
  history (cap 50) -> task meta + project node synced in one batch).
  delegate/ack/submit/cancel/accept refactored onto it; ack/submit
  tightened to from in {assigned, in_progress}, accept to from ==
  submitted (out-of-order transitions now return structured errors with
  the corrective action). accept_task_result now also updates the task
  meta (previously only the project meta was written).
- server.py: new report_progress action (worker/remote-member; note
  required, <=200 chars truncated+flagged; records a from==to history
  entry without changing state or sending a room notification).
- controller: taskDetail now passes through the task meta history;
  new GET /api/v1/projects/{id}/events read-time aggregation endpoint
  (ascending timeline, opaque offset cursor, limit 50/200, same
  auth/scope rules as workflow+history); CancelTask records the
  transition (actor=authzActor) in the same read-modify-write batch,
  with no duplicate entry on retry-convergence.
- tests: new Ruby test-transition-table.rb (fixture consistency, full
  lifecycle with chain integrity, out-of-order rejection, idempotent
  re-entry, cap, report_progress branches, cancel trace); Go tests for
  fixture vs isTerminalTaskStatus consistency, history pass-through
  (absent/malformed), events endpoint (sort/cursor/limit/empty/404/
  denied/cross-scope), cancel trace; test-taskflow.rb secret-artifact
  fixture fixed to delegate with an assignee (it relied on the old
  permissive submit guard).
- docs: transition engine design doc, runtime design update (table +
  history + prepared state), usage docs EN/ZH (events endpoint +
  history field), task-execution skill progress guidance.
@LUOSENGWA
LUOSENGWA force-pushed the feat/workflow-transition-engine branch from 0036950 to 95bffcd Compare September 10, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant