跳到主要内容
版本:Next 🚧

API、事件与参考

Agent API

属性/方法签名说明
agent.channelHitlChannel | None绑定的 channel 或 None
agent.in_flight_hitl_requestHitlRequest | Nonechannel 当前 pending(同步属性)
load_pending_hitl_request()async → HitlRequest | None从 checkpointer 读取 pending(detach 后也能用)
detach()async → None发射 AgentSuspendedEvent 然后触发 HitlDetached
respond(*, question_id=, answer=)async → None恢复挂起的运行
abort_pending(reason=)async → None关闭对话(两阶段:发信号 + 追加合成拒绝)

事件

四个新事件在 agent 事件流上发射:

事件何时发射关键字段
HitlRequestEventchannel 收到新的 confirm/approve/askrequest: HitlRequest
HitlAnswerEventchannel.answer()channel.cancel() 触发question_id, answer, cancelled, timed_out
AgentSuspendedEventagent.detach() 被调用时 HITL pendingpending_request: HitlRequest
AgentAbortedEventagent.abort_pending() 关闭对话reason: str

追踪 span

安装 cubepi[tracing] extra 后,每次 HITL await 都会包装在一个 OpenTelemetry span 中:

Span 名称属性
hitl.approvehitl.tool_name, hitl.tool_call_id, hitl.outcome, hitl.from_resume, hitl.duration_seconds
hitl.confirmhitl.question_id, hitl.outcome, hitl.duration_seconds
hitl.askhitl.question_id, hitl.outcome, hitl.duration_seconds

hitl.outcome 可以是:approved, denied, edited, answered, cancelled, timed_out, aborted, detached

追踪导入是懒式的 —— 未安装 opentelemetry 时,channel 静默回退到 无操作 span。

错误参考

异常基类含义
HitlCancelled(reason)BaseException宿主调了 channel.cancel(qid)
HitlTimedOut(seconds)BaseExceptionper-call 或 channel 默认超时到期
HitlDetachedBaseExceptionHITL await 期间调了 agent.detach()
HitlAbortedBaseExceptionagent.abort_pending() 向 agent 发信号
HitlConcurrencyErrorExceptionchannel 已有 pending 时再次调用 confirm/approve/ask
HitlStaleAnswerExceptionchannel.answer(qid) 的 qid 与当前 pending 不匹配
HitlNoPendingRequestException调了 agent.respond(…) 但线程没有 pending_request
HitlDurabilityNotGuaranteedException自定义工具调了 CheckpointedChannel.ask() 但没有 allow_inside_custom_tool=True

HitlControlException(四个 BaseException 子类的父类)故意不被 cubepi.agent.tools._prepare_tool_call_execute_prepared 中现有 的广泛 except Exception: 处理器捕获 —— 这模仿了 asyncio.CancelledError 的模式。

测试辅助

from cubepi.hitl.testing import ScriptedChannel, NoopChannel

# ScriptedChannel:预编程答案,按顺序消费
ch = ScriptedChannel(answers=[
ApproveAnswer(decision="approve"),
{"color": "red"}, # ask 答案
])
assert len(ch.history) == 2 # 所有见过的 HitlRequest

# NoopChannel:自动批准一切。用于测试中的 subagent。
ch = NoopChannel()
assert (await ch.approve("bash", "tc", {})).decision == "approve"

架构说明

  • 每线程单一 pending。 Agent 循环是顺序的——每个 thread_id 最多 有一个 HITL 请求。并发 confirm/approve/ask 会抛 HitlConcurrencyError
  • Prompt-cache 前缀不变量。 暂停与恢复之间,消息列表仅通过在末尾 追加 tool-result 消息和下一条 assistant turn 来改变。不插入、重排或 修改先前的消息——否则会使 provider 端的 prompt cache 失效。
  • question_id == tool_call_id 适用于 approve 请求。 无需别名或 映射——已从工具流中追踪 call_id 的宿主可直接传递。
  • 恢复不重放。 恢复时将答案预加载到 channel 并重新进入循环。最后一条 assistant message 中未决的 tool call 决定了接下来执行什么。没有基于 节点的重放语义。