Skip to content

fix(a2a): Pipeline 取消后收敛残留 working 的步骤到终态 - #297

Closed
ruanzhengxin-zhuxing wants to merge 2 commits into
mainfrom
harness/cf47f159
Closed

fix(a2a): Pipeline 取消后收敛残留 working 的步骤到终态#297
ruanzhengxin-zhuxing wants to merge 2 commits into
mainfrom
harness/cf47f159

Conversation

@ruanzhengxin-zhuxing

Copy link
Copy Markdown
Collaborator

问题

Pipeline 被取消后,A2A 快照中 intent_parsing 步骤仍停留在非终态 workingconclusion 为 null,而 run 级 status 已是 canceled。步骤终态与 run 终态不一致,前端长期显示"进行中",下游消费者无法据步骤状态判断真实终态。该现象在多个 pipeline session(83ef6fae60c24ef78c238aa89797a29af2379f4a6a80436297f5b5058d881ed521ac2ae1c63f47e297b667f1d8576b26)重复出现。

根因

src/iac_code/a2a/pipeline_snapshot.py_PipelineSnapshotReducer._apply 的终态分支只写 run 级状态并清理 pending / active 控制字段:

elif terminal_status is not None:
    self._snapshot["status"] = terminal_status
    self._snapshot["pendingTerminal"] = None
    self._snapshot["pendingInput"] = None
    self._snapshot["control"]["activeCandidateRunIds"] = []

被中途打断的 step 不会再收到 step_completed / step_failed,而 _upsert_step / _upsert_candidate / _upsert_candidate_step 创建节点时默认 status="working",因此这些节点永久滞留 working、无 conclusion

Web 端转录器 web/pipeline_transcript.py_on_pipeline_canceled / _on_pipeline_failed 早已实现同语义收敛(并在注释中明确目标是让中断的步骤"stops showing 进行中 forever"),快照 reducer 缺失该逻辑,导致两条投影对同一次 run 给出不一致结论。

改动

src/iac_code/a2a/pipeline_snapshot.py

  • 新增 _finalize_open_nodes(terminal_status, event),在终态分支末尾调用:把所有非终态 step / candidate / candidateStep 置为 run 的终态,写入 canceledAt / failedAt,先合并终止事件自带的 conclusion / errorSummary,仍缺失时补写合成结论 {"pipelineTerminated": true, "terminalStatus": ..., "reason": ...}
  • 导出谓词 is_terminated_node_conclusion,供下游区分合成结论与真实业务结论。

src/iac_code/a2a/pipeline_executor.py

  • _flat_pipeline_context_from_a2a_snapshot 跳过带 pipelineTerminated 标记的合成结论,避免取消后的 normal-chat 交接上下文把它当成真实步骤结论注入。

设计取舍

  • 不在 interrupt_received 上收敛:中断先经 interrupt_classified 分类,只有判定为取消才发 pipeline_canceled;在 interrupt_received 收敛会把回滚、追问类中断的步骤误置终态。收敛点放在 pipeline_canceled / pipeline_failed,语义等价且无误伤。
  • pipeline_completed 不参与收敛:把未完成的步骤标成 completed 等于伪造成功的业务结论。
  • pendingTerminal 事务语义不变:backup 待发布的终止事件仍只记录 pendingTerminal,待 backup_committed 转为权威事件后才触发收敛。
  • 幂等:已处于终态的节点不被覆盖,重复 reduce / 重放结果稳定。
  • 历史数据无需迁移:快照由事件流全量重放重建,重放即收敛。

测试

新增 11 个用例(tests/a2a/test_pipeline_snapshot.py 10 个、tests/a2a/test_pipeline_executor.py 1 个),覆盖 step / candidate / candidateStep 收敛、waiting_input 收敛、无 reason 时字段省略、pipeline_failed 保留 errorSummary、已终态结论不被覆盖、pipeline_completed 不伪造结论、pending_backup 不触发收敛、跨次 reduce 幂等、handoff 上下文跳过合成结论。

  • tests/a2a/ + tests/web/test_pipeline_transcript.py1557 passed
  • 全量 pytest tests/14331 passed / 5 skipped / 16 failed;16 项失败均与本次变更无关——13 项 tests/test_i18n.py*.pot(gitignore 的生成产物)在环境中缺失,另 3 项已在基线提交 88ed89c 上复现同样失败。
  • make lint(ruff check + ty check)通过。

附带提交说明

4edc8f1 style: apply ruff format ... 为纯格式化产物。仓库 pre-commit format 钩子按 uv.lock 锁定的 ruff 版本执行 ruff format src/ tests/ 会重写 16 个既有文件(CI 仅跑 ruff check 故未暴露),导致任何提交都无法通过钩子。将该机械改动单独成一次提交,使修复提交保持可评审。如不希望纳入,可只取 404be5a

The pre-commit format hook runs `ruff format src/ tests/` with the ruff
version resolved from uv.lock, which rewrites these files. CI only runs
`ruff check`, so the drift went unnoticed and made every local commit
fail the hook. This commit is pure formatter output with no behavior
change, so subsequent commits can pass the hook.
A run interrupted mid-step never emits step_completed/step_failed for the
step it was executing, so the snapshot reducer left that step at
status="working" with conclusion null even though the run itself had
already reached a terminal state. Canceled runs therefore reported
intent_parsing as still in progress forever, and downstream consumers
could not derive the real outcome from step state.

On pipeline_canceled/pipeline_failed, finalize every still-open step,
candidate and candidate step onto the run's terminal status with a
terminal timestamp and a non-empty conclusion. This mirrors
web.pipeline_transcript._on_pipeline_canceled so the snapshot and the
transcript agree on the outcome of one run.

pipeline_completed is deliberately excluded, since marking an unfinished
step as completed would fabricate a successful business conclusion.
Synthesized conclusions carry a pipelineTerminated marker so the
pipeline handoff context skips them instead of passing them on as real
step results.
@guima-why guima-why closed this Aug 25, 2026
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.

2 participants