AS AgentScopeAPI 手册 / API Reference
MODULE C5 · resurrector · 240 XP · 45 MIN

挂起、恢复与幂等Park, resume and idempotency

先说问题The problem

审批点了两下,钱退了两次。或者 worker 被杀,重启后 agent 从头再跑一遍,把已经写过的三处副作用又写了一遍。社区最直白的一句话:checkpoint 不是 durable execution——它只给你存档点,没有失败检测、没有自动恢复、没有重复执行防护。

The approval got clicked twice and the money went out twice. Or the worker was killed, and on restart the agent ran the whole turn again and re-wrote all three side effects. The blunt community line: a checkpoint is not durable execution — it gives you a save point, with no failure detection, no automatic resumption and no duplicate-execution prevention.

先动手Do it first

先在页面上接线——页面只懂规则。然后跑下面那条判题,那才是真会失败的东西。Wire it on the page — the bench only knows the rules. Then run the check below; that is the part that can actually fail.

worker 在审批挂起期间被杀。排出恢复的正确顺序,以及它防住的那一步。

The worker is killed while a refund is parked. Order the recovery, ending with the failure the ordering prevents.

  1. 审批通过,worker 从 ASKING 恢复同一个 tool_callApproval lands and the worker resumes the same tool_call from ASKING
  2. 会话状态落成 awaiting_permission,进程可以安全退出The session parks as awaiting_permission and the process may exit
  3. 重放前先查幂等键,已执行过就跳过Before replaying, check the idempotency key and skip if it already ran
  4. 退款副作用第一次执行成功,写入 outbox 记录The refund executes once and writes its outbox record
  5. 进程在挂起点被杀;重启后从存储读回,而不是从头再跑The process dies at the park point; boot reads the state back instead of restarting
  6. 没有幂等键的第二遍:钱退了两次A second pass with no idempotency key: the money goes out twice
你拼出来的东西What you assembled
create_app(…)

为什么这样设计Why it is built this way

01挂起是持久化状态Parking is a durable state

工具停在 ToolCallState.ASKING,会话变 awaiting_permission,worker 可以完全消失。恢复靠的是读回上下文,不是靠进程还活着。

The tool parks in ToolCallState.ASKING, the session becomes awaiting_permission, and the worker may vanish entirely. Resumption reads the context back; it does not depend on the process surviving.

02决策要幂等Decisions must be idempotent

同一个 reply_id + tool call 的第二次点击必须是无操作。渠道适配层就靠这一点吃掉重复点击。

A second click on the same reply_id and tool call must be a no-op. The channel adapter relies on exactly this to swallow double clicks.

03副作用要带幂等键Side effects carry an idempotency key

退款、写回、发信,都接一个 idempotency_key。恢复重放时键相同,下游只发生一次。

Refund, write-back and email each take an idempotency_key. On replay the key matches and the downstream happens once.

04杀进程验收Acceptance by killing the process

真正的检查是:跑到第 7 步 kill -9,重启,断言副作用计数没有增加、状态从挂起继续。

The real check: kill -9 at step seven, restart, and assert the side-effect count did not grow and the state resumed from parked.

用到的 APIAPIs in play

代码The code

C5 · Python
# 1. find what is parked — reply_id is the ASSISTANT MESSAGE id, not a tool id
parked = parked_call(session_messages(client, agent_id, session_id))
#      -> (reply_id, tool_call_block) with block["state"] == "asking"

# 2. resume: the same POST /chat/ that started the run, distinguished by event type
await client.post("/chat/", json={
    "agent_id": agent_id, "session_id": session_id,
    "input": {
        "type": "USER_CONFIRM_RESULT",
        "reply_id": reply_id,
        "confirm_results": [{"confirmed": True, "tool_call_id": block["id"], "rules": [...]}],
    },
})

assert session_status(client, agent_id, session_id)["status"] == "idle"
assert ledger.count("refund") == 1        # clicked twice, or restarted: still one row

判题The check

<span class="ui-zh">运行这条</span><span class="ui-en">Run this</span>
python lab.py run c5

它断言了什么What it asserts

  • refund under DEFAULT -> asking on refund_order, 0 ledger rows, session awaiting_permission
  • worker killed with no shutdown handler; a second process over the same storage still reads awaiting_permission with call refund-1 asking
  • resumed -> idle, 1 ledger row(s) of 4200 cents, tool_result state success
  • the same reply_id + call answered twice more: still 1 row, the duplicate is swallowed
  • confirmed=False -> tool_result denied and still 1 row: denial is a real terminal state, not a retry

会踩的坑Where it hurts

  • 把「恢复」实现成「重新跑一遍整个任务」。任务里只要有非幂等副作用,恢复就是一次事故。

    Implementing resume as re-running the whole job. One non-idempotent side effect in that job turns a resume into an incident.

费曼闸门Say it back

为什么审批状态必须存在持久化上下文里,而不是存在内存的 pending 队列里?举一个只有前者能活下来的场景。

Why must approval state live in the durable context rather than an in-memory pending queue? Name a scenario only the former survives.

页面只存通行码的哈希。判题没真跑过,这里就解不开。The page stores a hash, not the code. Nothing unlocks until the command really ran.

基于官方固定版本源码静态生成Generated from pinned official sources · Python 2.0.11 · Java 2.0.4
签名与文档为静态抽取;如有疑问请以源码为准。Signatures and docs are extracted statically; verify against the source when in doubt.