Agent Interrupt / Resume Lifecycle
把 ask_user 一类 HITL 行为建模为 Agent run 的一等 interrupt:required → answered → resumed → completed,区分 resume input 与新聊天消息,并要求 interaction 可持久化、可回放、可刷新恢复。
[!info] related notes
- 所属 MOC: agent-moc、bodysense-moc
- 相关概念: decision-relevant-evidence-gap、durable-sse-recovery-with-after-seq、bodysense-active-turn-state-machine
- 易混淆概念: new user turn vs resume input;thread identity vs public run identity
- 相关资源: bodysense-consultation-streaming-architecture
Agent Interrupt / Resume Lifecycle
一句话定义
Agent Interrupt / Resume Lifecycle 把 ask_user 等需要外部输入的节点建模为同一个 Agent execution thread 的受控暂停与恢复,而不是把问题和回答伪装成两条普通聊天消息。
running
→ interaction.required
→ interrupted / waiting_user
→ interaction.answered
→ resume authorized
→ run.resumed
→ running
→ completed / failed / cancelled
为什么 HITL 是 Runtime Control Flow,不是 UI Widget
从 UI 看,ask_user 可能只是:
一个问题卡片
+ 几个按钮
但从 Agent runtime 看,它改变的是:
execution can no longer make progress
until a typed external input arrives
所以核心不是“弹一个 modal”,而是:
Running computation
→ persisted waiting state
→ external input contract
→ validated resume transition
这和普通聊天 input 完全不同。
如果只在前端展示卡片,却没有 durable interrupt state,那么:
- refresh 后等待点丢失;
- 双击 answer 可能触发两次 resume;
- 网络失败后不知道回答有没有保存;
- replay 无法解释模型为什么后面改变路径。
因此 HITL 首先是 runtime architecture,UI 只是 projection。
为什么 ask_user 不是普通 Tool Call
普通 Tool Call 常见语义:
Agent asks tool
→ tool executes immediately
→ tool result returns
→ Agent continues
而 ask_user 需要:
Agent asks human
→ run cannot continue now
→ UI renders question
→ user may answer seconds/minutes/hours later
→ answer is validated/persisted
→ same semantic execution resumes
它改变了 run lifecycle。
因此它不是简单的:
tool.call + tool.result
而是:
runtime interrupt
为什么用户回答不是新的 User Chat Message
假设 Agent 问:
“麻木是否延伸到拇指?”
用户选择:
“是”
如果前端把“是”作为新独立 chat message:
Conversation
├─ Turn 1 user symptom
├─ Turn 1 assistant asks question
└─ Turn 2 user says yes
系统会模糊:
- 这个“是”回答哪个问题?
- 是否属于原 run?
- 如何关联 tool_call_id?
- reload 后怎么知道它是 interaction answer?
- retry 时会不会重复创建新 run?
更准确的模型:
Execution Thread T7
├─ Interaction X3 required
├─ X3 answer = yes
└─ T7 resumed with answer(X3)
所以:
Answer is resume input, not a new task intent.
为什么“同一个 Agent execution”与“同一个 public Run”要分开理解
这是 Consultation 中很容易混淆的一层。
Python/LangGraph 可能有一个长期 semantic thread/checkpoint identity:
Thread T7
Go/public runtime 可能把不同 execution segments 建模成:
Public Run R10
→ interrupted
resume command
→ Public Run R11 / resumed segment
具体项目可以选择同一个或新的 run identity,但必须明确。
所以至少要区分:
Semantic/Checkpoint Thread Identity
= 哪一条 Agent execution history 被继续
Public Durable Run Identity
= 对外业务/事件 envelope 怎样标识本次 execution segment
这两个概念不能因为都叫 “run” 就混在一起。
为什么 Identity 区分影响 Replay 和 Provenance
如果 resume 继续的是:
Thread T7
但产生新的 public Run R11,那么历史要能表达:
R11 resumes interrupted R10 / Thread T7
否则 audit 时会误以为:
R11 是一个全新无关 Agent task
反过来,如果 public run identity 保持不变,也要保证 seq/terminal lifecycle 可以表达:
interrupted → resumed → completed
核心不是哪种设计“唯一正确”,而是 identity semantics 必须稳定、可持久化、可测试。
Interaction 需要自己的 Identity
一个可恢复 interaction 至少需要:
interaction_id
thread/run identity
conversation_id
tool_call_id / origin
question
options/schema
status
answer
created_at
answered_at
如果只有自然语言 message text,就无法稳定建立控制关系。
interaction_id 回答:
“用户这次回答到底在恢复哪个等待点?”
为什么 tool_call_id 和 interaction_id 都可能需要
如果 ask_user 起源于一个 tool invocation:
tool_call_id
可以关联 Agent runtime 的调用轨迹。
但 interaction 作为 durable human-input lifecycle 可能还需要自己的:
interaction_id
因为一个 tool call 的 runtime identity 和一个业务 interaction 的生命周期语义不同。
例如未来一个 interaction 可能:
- 被重新展示;
- 过期;
- 被人工管理员取消;
- 有 revision/edit policy。
所以不要默认“ToolCall ID 就是所有层的 ID”。
状态机
一个典型 interaction lifecycle:
required / pending
↓ user answer
answered
↓ resume accepted
resumed / consumed
↓ agent proceeds
completed
也可能:
pending
→ expired
或者 run 被取消/失败。
不同项目可以命名不同,但最重要的是:
pending、answered、run resumed、run completed 是不同事实。
用户点了选项,并不代表后端 run 已经成功 resume。
为什么 answered 和 consumed/resumed 最好分开
假设:
User answer persisted successfully
↓
server crashes before resume transition
如果状态只有:
pending / done
你无法知道:
这个答案已经保存了吗?
Agent 已经用它继续了吗?
分开后:
X3.status = answered
T7.status = interrupted
恢复流程可以继续:
find answered but unconsumed interaction
→ resume exactly once
这本质上是 durable workflow 的 repair semantics。
StreamEvent 如何表达这个控制流
BodySense public event contract 已存在类似:
state.interaction.required
state.interaction.answered
run.interrupted
run.resumed
前端 ActiveTurn reducer 可以:
interaction.required
pendingInteraction = X3
status = interrupted
interaction.answered
X3.answer = ...
run.resumed
status = streaming
pendingInteraction = null
这比用“看到某个 assistant text 就弹窗”稳定得多。
为什么 Answer 与 Resume 也要分开
考虑:
User submits answer
↓
answer persisted
↓
resume request fails due network
此时真实状态可能是:
interaction answered ✅
run resumed ❌ / unknown
如果前端把二者当成同一步,会出现:
- 用户以为回答丢了;
- retry 又重复提交;
- run 被重复 resume;
- refresh 后状态无法解释。
所以 durable model 要能区分:
Answer Fact
vs
Resume Transition
Double Submit:为什么 Resume 必须 Idempotent
用户可能:
click Answer
network spinner
click again
或者浏览器自动 retry。
如果两个请求都:
resume graph from same checkpoint
可能产生:
- 两条分叉 execution;
- duplicate tool side effects;
- duplicate assistant messages;
- inconsistent checkpoints。
所以服务端应基于:
interaction_id
answer identity/version
current interaction status
resume request id
实现收敛。
理想 property:
SubmitAnswer(X3, A)
SubmitAnswer(X3, A) again
→ one durable answer
→ at most one semantic resume transition
同一个 Interaction 提交不同 Answer 怎么办
例如:
first: yes
second: no
不能简单都当 retry。
需要明确产品语义:
Immutable answer
一旦 answered:
second different answer → conflict
Editable before consumption
如果尚未 resume,可以允许 revision:
answer_version 1 → 2
Correction after resume
通常不能“修改过去”,而应产生新的 clarification/new turn。
无论哪种,都必须显式设计,不能由 last-write-wins 偶然决定。
HITL 的真正价值不是“人工兜底”
Human-in-the-loop 常被泛化成:
“高风险就让人确认一下。”
更工程化的理解是:
在 Agent state machine 中定义一个明确 interrupt boundary,把缺失的人类输入作为有 schema、有 identity、有 durable status 的运行时依赖。
这使 HITL 可以:
- replay;
- retry;
- audit;
- time out;
- resume;
- display after refresh。
Interrupt Input 应该 Typed,而不是任意字符串
如果问题是选项:
Yes / No / Unsure
resume contract 应校验:
answer ∈ allowed options
如果要求数字:
pain score 0..10
应校验范围。
不要把所有 human input 都压成:
{"answer": "some string"}
然后让 LLM 猜。
Typed interaction 能:
- 防 malformed resume;
- 降低 ambiguity;
- 支持 UI schema;
- 进入 durable provenance。
为什么 Refresh 后必须还能看到已回答 Interaction
如果 live 时:
Question X3
Answer: Yes
刷新页面后只剩:
assistant final answer
说明 Interaction 只存在于临时 UI,而没有成为完整历史 projection。
成熟模型应该能够从 durable events/read model 恢复:
X3 question
X3 answer
run resumed
这样历史才真正解释:
“Agent 为什么后来改变了推理路径?”
Pending Interaction 与 Interaction History 分开
当前页面需要两类信息:
Pending
当前正在阻塞 run、等待用户回答的 interaction
用于显示:
- option card;
- text field;
- approval UI。
History
已经回答/结束的 interactions
用于:
- thread timeline;
- replay;
- audit;
- refresh reconstruction。
只保存 pending list 会导致 answered 后信息消失。
Interrupt 与 ActiveTurn State Machine
ActiveTurn streaming
↓ interaction.required
ActiveTurn interrupted
↓ answer request
Interaction answered
↓ run.resumed event
ActiveTurn streaming
↓
completed
注意:
interrupted
≠
failed
这决定 UI 应该显示:
“等待你补充信息”
而不是:
“请求失败,请重试”
Interrupted 状态也不应该自动算 Terminal
interrupted 表示:
execution intentionally suspended
它还可能继续。
所以 durable run lifecycle 中:
interrupted
和:
completed / failed / cancelled
不是同一类 terminal state。
这对 after_seq recovery 很重要:看到 interaction.required 后不能把 run 当“结束了”。
Interrupt 与 EvidenceGap 的联系
Diagnosis/Treatment 已学过:
user_information gap
→ generic RAG cannot resolve
Consultation 的 ask_user 正是这种 gap 的一种 acquisition channel。
因此:
EvidenceGap
(user_information)
↓ policy chooses human input
Interaction Required
↓
Answer
↓
Runtime resumes with new user fact candidate/input
这把前面的 Evidence Acquisition 与 L3 HITL runtime 真正连了起来。
但仍要注意:
用户回答进入 durable BodyState 的方式需要 domain validation;interaction answer 也不自动等于 accepted domain Fact。
Answer 是 Observation/Input,不一定直接成为 Durable Fact
用户回答:
“是,我有拇指麻木”
Interaction 层可以保存这个 answer。
但 BodyState domain 可能还需要:
- source/provenance;
- timestamp;
- conflict handling;
- normalization;
- observation/fact distinction。
因此:
Interaction Answer
→ domain input
→ BodyState policy
→ accepted Observation/Fact
而不是:
Interaction Answer
= automatically final domain truth
Interrupt 与 Idempotency
用户可能因为网络卡顿连点两次提交。
服务端应该用:
interaction_id
request_id
current interaction status
确保:
same answer retry
→ converge
而不是:
resume run twice
同样,如果 interaction 已 answered,再收到同一个 request,需要明确:
- return previous success;
- 或 conflict;
- 但不能重复产生业务副作用。
Resume 前是否需要重新授权当前 World
这取决于 interaction 类型和停顿时间。
例如一个 information interrupt 等了 2 天,期间:
BodyState changed
Agent configuration pointer changed
safety status changed
不能只机械把旧 checkpoint 从字节层恢复就继续。
需要区分两类 identity:
Execution Identity
同一 semantic thread 应继续使用当时 pinned configuration,不能偷偷切到 current production pointer。
Business Eligibility
当前现实状态可能已经使旧 run 不再适合继续普通路径。
因此 resume 可能需要一个 deterministic revalidation:
Can this interrupted execution still continue under current business invariants?
如果不能:
cancel/supersede old run
→ start new analysis/turn
而不是一边要求 historical execution identity 不变,一边忽略现实业务已经变化。
为什么 Resume 必须 Pin 原 Agent Configuration
假设:
09:00 Thread T7 starts with Config C15
09:02 asks user, interrupted
10:00 production pointer promoted to C16
11:00 user answers
如果 resume 直接读取:
current config = C16
就形成:
same semantic execution
first half C15
second half C16
这破坏:
- configuration provenance;
- replay;
- qualification assumptions;
- failure attribution。
所以默认应:
resume exact C15
如果业务决定 C15 已被紧急撤回不能继续,则应该显式:
old run cannot resume
→ controlled cancellation/supersession
而不是悄悄换成 C16。
Configuration Pinning 与 Current Business Revalidation 并不矛盾
两个问题:
如果继续旧 execution,用哪套行为?
→ 原 C15
当前是否仍允许继续旧 execution?
→ current deterministic business facts decide
所以可以同时做到:
execution identity stable
+
current safety/domain eligibility rechecked
不要把“重新授权”错误实现成“换最新模型再试一次”。
Interaction Expiry
有些等待点不能永久有效。
例如:
“请确认是否批准现在执行这个敏感动作”
如果用户三天后才点 Approve,context 可能完全变化。
因此 interaction 可以有:
expires_at
context_revision
approval_scope
过期后:
late answer
→ rejected / require new interaction
而不是恢复旧 action。
Information Interrupt 也可能因 BodyState 变化而需要重新问。
Interrupt 的错误场景
Interaction already answered
不能再次随意覆盖,除非产品定义 revision/edit semantics。
Interaction expired
旧 answer 不应 resume 已失效等待点。
Run already completed
不能因为迟到的 UI response 把历史 run 重新打开。
Run cancelled
迟到 answer 应保持 historical record policy,但不能恢复 cancelled run。
Network failure after submit
客户端应先查询 durable interaction/run state,再决定 retry,而不是盲目再发送。
Replay
Historical replay 应能区分:
当时 Agent 提出了什么问题
用户回答了什么
什么时候 resume
而不是只保存最终 summary。
Late Answer Race
一个实际 race:
X3 pending
↓
server timeout policy expires X3
↓
client offline still shows form
↓
user submits old answer
服务端必须验证 current interaction state:
pending? answered? expired? cancelled?
不能因为 payload 有合法 interaction_id 就接受。
这是典型的 temporal validity 问题,只不过对象从 Treatment Proposal 换成 Interaction。
Approval Interrupt 与 Information Interrupt
HITL 不只一种语义。
Information Interrupt
Agent 缺少用户事实
→ ask_user
Answer 提供新 input。
Approval Interrupt
Agent 准备执行高风险/不可逆 action
→ request approval
Answer 表达 human intent/authorization input。
二者都可使用 interrupt/resume runtime,但 authority 不同。
信息回答提供输入;审批回答授予/拒绝一个动作边界。
不要因为 UI 都是一个按钮卡片就混成同一个 domain event。
Approval 的人类“同意”仍可能不是最终 Business Authority
和 Treatment Accept 一样:
User clicks Approve
可以是重要 authority input,但服务器仍可能需要:
- current safety check;
- resource/permission check;
- freshness;
- approval scope;
- irreversible action policy。
所以:
Human approval intent
≠
automatic unrestricted side-effect permission
HITL 是 authority chain 的一环,不等于绕过系统 policy。
Resume Tool Side Effects 为什么尤其需要谨慎
如果 resume 后第一步是:
send_message
write_record
activate_treatment
double resume 可能造成真实重复副作用。
所以:
resume idempotency
+
tool side-effect idempotency
+
interaction consumption identity
要一起设计。
只保证 UI 卡片不重复远远不够。
Checkpoint 与 Durable Interaction 为什么不是同一个东西
LangGraph checkpoint 可以保存内部执行状态:
node state
messages
interrupt continuation
但业务仍需要 durable Interaction:
question
answer
status
user-facing identity
created/answered times
原因:
- checkpoint schema 是 framework-owned;
- Interaction 是 product/domain control flow;
- UI/replay/audit 不应该解析框架内部 checkpoint 才知道用户回答过什么。
所以:
checkpoint
= execution resume mechanism
Interaction
= durable business/runtime contract
Process Crash 后如何恢复
如果:
answer persisted
process crashes before resume
启动/recovery 逻辑应能识别:
Interaction X3 answered
Execution T7 still interrupted
然后安全地 resume once。
如果 checkpoint 丢失,则要明确:
cannot resume exact execution
并转到新 run / degraded recovery,而不是假装继续原语义线程。
这说明 durable HITL 真正依赖:
interaction state
+
checkpoint state
+
run/config identity
三个部分共同存在。
Refresh Recovery
页面刷新后可以:
load durable thread/read model
→ discover active interrupted run
→ load pending interaction X3
→ render question/options
如果 X3 已 answered 但 run 尚未 resume:
render answered/pending resume state
而不是重新显示可提交表单导致 duplicate answer。
UI projection 应来自 durable status,而不是组件 memory。
Current BodySense 学习重点
现有 Consultation 已经有:
interaction.required/answeredevents;- ActiveTurn pendingInteraction;
resumeInteractionStream();- assistant-ui
resumeRun()integration; - durable event/recovery 基础。
历史架构计划还特别强调目标状态:
pending → answered → resumed → completed
以及 refresh/replay 后仍能显示 question + answer。
因此 L3 的重点不是“怎么做一个弹窗”,而是读懂:
一个 UI interaction 如何成为 Agent Runtime 的 durable control-flow state。
Behavioral Contracts
可以写成:
HI-01 required interaction must have stable interaction_id
HI-02 answer must validate against interaction schema
HI-03 same answer retry must not create duplicate resume
HI-04 answered != resumed
HI-05 expired/completed/cancelled interaction cannot resume old run
HI-06 resume uses original pinned configuration identity
HI-07 information answer does not automatically become durable domain Fact
HI-08 refresh reconstructs pending/answered interaction state
HI-09 replay must not re-execute real side effects
HI-10 double resume cannot duplicate action tool side effects
这些 contract 比“按钮点击成功”更接近 production HITL correctness。
测试清单
Normal path
required
→ answer
→ answered event
→ resume
→ completed
Double submit
same answer twice
→ one durable answer
→ one semantic resume
Conflicting second answer
根据产品 semantics:
conflict / revision
不能 silent last-write-wins。
Answer persisted, resume fails
refresh/retry
→ recover answered-but-unconsumed state
→ resume once
Config promotion during interrupt
started C15
production pointer now C16
resume → still C15 or explicit supersession
绝不能 silent C16 continuation。
Current state invalidates resume
BodyState/safety materially changed
→ business revalidation blocks old continuation
Expiry
late answer to expired interaction
→ no resume
Run terminal
run completed/cancelled
late answer
→ no reopen
Refresh
pending interaction 在 reload 后仍能正确恢复。
Replay
历史 interaction 的 question/answer 可展示,但 replay 不重新向真实用户发问或执行 side effect。
一个完整故障例子
T7/C15 asks X3
X3 pending
↓
user answers Yes
answer persisted
↓
HTTP response lost
↓
client retries
错误实现:
retry → resume again
→ duplicate tool execution
正确实现:
SubmitAnswer(X3, Yes)
→ sees same durable answer already exists
→ returns existing state
→ resume coordinator sees T7 already resumed / resume token consumed
→ no duplicate execution
如果第一次尚未 resume:
answer exists + T7 interrupted
→ safely resume once
这就是 durable idempotent workflow,而不是一个普通表单提交。
自测题
- 为什么
ask_user是 runtime interrupt,而不只是 tool result? - 为什么用户 answer 不应该自动成为一个新的 chat turn?
- Interaction ID、ToolCall ID、Thread ID、Public Run ID 分别可能回答什么?
- 为什么
answered和resumed必须分开? - Double submit 为什么可能造成真实 side effect 重复?
- Typed interaction schema 有什么价值?
- 为什么 resume 应 pin 原 configuration,而不能默默使用当前 production pointer?
- Configuration pinning 和 current business revalidation 为什么可以同时成立?
- Interaction Answer 为什么不一定直接成为 BodyState Fact?
- Approval interrupt 与 information interrupt 的 authority 有什么差别?
- Checkpoint 与 durable Interaction 为什么不是同一个层?
- Answer persisted 但 process crash 的状态应如何恢复?
最终心智模型
Chat Message
= conversation content
Interaction
= structured external-input contract
Interrupt
= runtime control state
Answer
= durable external input
Resume
= authorized/idempotent continuation transition
Checkpoint
= framework execution continuation mechanism
这六个概念不能因为都最终显示在聊天 UI 里就合并。
更重要的是:
HITL 的真正工程价值,是把人类输入从“随便发一句消息”提升成一个有 identity、有 schema、有 lifecycle、有 provenance、可恢复且受 authority 控制的 runtime primitive。