Agent Interrupt / Resume Lifecycle

把 ask_user 一类 HITL 行为建模为 Agent run 的一等 interrupt:required → answered → resumed → completed,区分 resume input 与新聊天消息,并要求 interaction 可持久化、可回放、可刷新恢复。

#type / synthesis #status / growing #tech / ai #tech / architecture #tech / dev / frontend #resource / agent #resource / bodysense

[!info] related notes

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_idinteraction_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。

为什么 answeredconsumed/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 / answered events;
  • 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,而不是一个普通表单提交。

自测题

  1. 为什么 ask_user 是 runtime interrupt,而不只是 tool result?
  2. 为什么用户 answer 不应该自动成为一个新的 chat turn?
  3. Interaction ID、ToolCall ID、Thread ID、Public Run ID 分别可能回答什么?
  4. 为什么 answeredresumed 必须分开?
  5. Double submit 为什么可能造成真实 side effect 重复?
  6. Typed interaction schema 有什么价值?
  7. 为什么 resume 应 pin 原 configuration,而不能默默使用当前 production pointer?
  8. Configuration pinning 和 current business revalidation 为什么可以同时成立?
  9. Interaction Answer 为什么不一定直接成为 BodyState Fact?
  10. Approval interrupt 与 information interrupt 的 authority 有什么差别?
  11. Checkpoint 与 durable Interaction 为什么不是同一个层?
  12. 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。

创建于 2026/8/22 更新于 2026/8/23