Files
social-app/docs/plans/2026-03-03-interrupt-resume-fixes-design.md
T

2.8 KiB
Raw Blame History

Agent Interrupt/Resume 遗留问题修复设计

1. 目标

本次修复一次性完成以下三项遗留问题:

  1. state_snapshot 并发一致性问题(并发 resume 竞争)
  2. expires_at 过期未强校验问题
  3. state_snapshot 缺少强类型与版本化问题

2. 设计决策

采用方案 2(严格重构):

  • state_snapshot 仅接受新结构,不再兼容旧结构
  • 统一快照版本为 version = 2
  • 使用强类型模型约束快照结构与状态迁移
  • resume 入口引入行级锁语义,避免并发双写

3. 状态快照模型

state_snapshot 顶层结构:

{
  "version": 2,
  "pending_tool_call": {
    "interrupt_id": "int-1",
    "tool_name": "srv.transfer_funds",
    "tool_args": {"to": "u2", "amount": 100},
    "status": "PENDING_APPROVAL",
    "expires_at": "2026-03-03T12:00:00Z",
    "decision": null,
    "result": null,
    "updated_at": "2026-03-03T11:59:00Z"
  },
  "run_context": {
    "thread_id": "t1",
    "run_id": "r1"
  }
}

说明:

  • version 必须为 2,否则拒绝处理
  • pending_tool_call 字段缺失或类型错误,按无效快照处理
  • run_context 仅保留 interrupt/resume 必需字段

4. 状态机约束

仅允许以下迁移:

  • PENDING_APPROVAL -> APPROVED_EXECUTING -> EXECUTED
  • PENDING_APPROVAL -> REJECTED
  • PENDING_APPROVAL -> EXPIRED

非法状态迁移必须返回错误,不做隐式修复。

5. 并发与过期语义

  • resume 前先对目标 session 加锁再读取快照
  • 同一 interrupt_id 并发 resume 只能有一个请求成功
  • expires_at < now(UTC),先迁移为 EXPIRED,再返回 410

6. 错误语义(RFC7807

  • 409 Conflict: run/interrupt 不匹配,或并发冲突导致状态已消费
  • 410 Gone: 挂起调用已过期
  • 422 Unprocessable Entity: state_snapshot 非法或版本不匹配
  • 404 Not Found: 目标 session/run 不存在

7. 测试策略

采用 TDD,先写失败测试后实现:

  • 快照版本校验(version != 2
  • 快照结构校验(必填字段/类型)
  • 并发 resume 幂等竞争(仅一个成功)
  • 过期校验(返回 410 + 状态置 EXPIRED
  • 合法状态迁移路径覆盖

8. 验证命令

  • uv run pytest backend/tests/unit/v1/agent -v
  • uv run pytest backend/tests/integration/v1/agent/test_chat_routes.py -v
  • uv run pytest backend/tests/integration/v1/agent/test_interrupt_resume_flow.py -v
  • cd backend && uv run ruff check src/v1/agent
  • cd backend && uv run basedpyright src/v1/agent

9. 风险与回滚

  • 风险:旧快照不再兼容,可能触发运行时拒绝
  • 处置:通过明确 422 错误暴露不合规数据,结合日志定位并人工修复数据
  • 回滚:回退本次变更并恢复旧快照解析逻辑(仅在紧急故障时)