Timed user questions as two settlements
计时用户提问的两次结算
A question can block an agent even when useful work does not depend on the answer. Releasing the tool call after a wait must not discard the question: the user may answer after the agent has continued or after reopening the Session. A pending result is neither a skipped answer nor permission to proceed with work that requires approval. The foreground tool result and the eventual answer have different lifetimes. A `us
English
Problem
A question can block an agent even when useful work does not depend on the answer. Releasing the tool call after a wait must not discard the question: the user may answer after the agent has continued or after reopening the Session. A pending result is neither a skipped answer nor permission to proceed with work that requires approval.
The foreground tool result and the eventual answer have different lifetimes. A user-questions/request waterfall settles once; keeping it alive after returning pending creates a second answer path that outlives its caller. Countdown, focus, and editing also have different owners from durable question state.
Decision
Foreground answers use the existing Remote Event waterfall. A timeout ends that request and returns pending; a later answer uses a business RPC that steers a durable user message. The claim stream controls the foreground wait only. It neither transports answers nor replaces the Remote Event.
Modes and scope
- The default
mode: legacyretains the blocking schema andask()path. Explicitmode: timedaddstimeout, using the configured default of 120 seconds when omitted. - A positive integer timeout selects
askTimed();-1selects blockingask(). A native blocking call made with the timed schema still participates in timed-question projection and review; it is not identical to a legacy call. - Plan review retains
ask(), itsplan-reviewintent, andBAD_INTENTvalidation. This decision does not move plan review to another package. - Human interaction requires the exact live runtime root when an Agent is supplied. A stale instance fails with
CALLER_NOT_LIVE; an owned child fails withDELEGATED_CALLER. Durable Session lineage does not prevent a resumed runtime root from asking.
Ownership
| Owner | Responsibility |
|---|---|
tool-ask-user | Model schema, timeout validation, service selection, and pending-result text. |
UserQuestionService | Foreground waits and Client claims, unattended deadlines, late-answer validation and steering, and the durable question projection. |
| Gateway and Remote runtime | Existing request delivery, settlement, cancellation, and Remote stream transport; no question-specific timer or event-name branch. |
ui-user-questions | One card per Session and call, answer-channel selection, local countdown and drafts, and the reply Definition and renderer. |
ui-chat | Ordinary process/Turn folding. |
| Question-specific tool row | Pending/answered presentation and panel actions, composed in QuestionToolRow without adding rowAction to generic ToolRow. |
The question service, tool, and questions UI own their package contracts.
Foreground wait and claim lifetime
TimedQuestionWait owns a private cancellation signal, the original Host deadline, and Client claims. The Host runs a timer only while no answer UI holds a claim. Delivery to a transport queue is not a claim. Losing the last claim restores the original deadline, with immediate expiry if it has passed.
A timed request carries wait: { callId, timed: true }. Before attaching its answer channel, the Client opens attachWait, a business Remote stream that yields one Host-computed remainingMs frame and stays open for the claim's lifetime. Focus, edits, and heartbeats do not travel through this stream.
The Client retains its claim through delivery of the waterfall outcome to the Host. Returning an answer locally is not delivery: releasing the claim then could let an overdue Host timer beat that answer. The Host closes the claim when the foreground request settles. Explicit delegation releases the claim before next(); plugin teardown delegates and cancels its stream.
Client countdown expiry rejects the waterfall with ASK_TIMED_OUT. Unattended Host expiry aborts only the private wait signal with the same business error. askTimed() maps only that error to pending, maps parent cancellation to ASK_ABORTED, and propagates other failures. NO_PROVIDER waits on the same bounded wait lifetime, not a permanently unresolved promise. Foreground completion releases timers, claims, and the wait registry entry.
Durable state and late answers
The userQuestions projection reconstructs question state from existing Session events. For native calls, it recognises the timed schema by the timeout parameter recorded in request/header; legacy calls are not tracked. A valid tool/call opens a question. Its tool/result makes it continued for pending or TOOL_OUTCOME_UNKNOWN, settles it with a valid answer batch, or removes it for another result or failure. A PTC sub-call enters the projection when its tool/ptc-dispatch result is pending, since only run_code appears in the model request header.
A continued question accepts answer(agent, callId, answer). The method returns false for an unknown or non-continued call, throws REPLY_QUEUED while another reply awaits admission, and throws BAD_ANSWER unless the batch names each question exactly once. Accepted answers use agent.steer(createUserMessage(...)), source { kind: 'user-question-reply', callId, outcome: 'answered' }, and JSON text containing answer_to_pending_question, the call id, original questions, and answers. Remote Agent resolution from the Session id resumes a root when needed; the answer() body does not own that resume operation.
The inbox message is the durable queued reply. A per-Session reservation covers the interval from queuing through claim and admission; claiming a restored inbox reply creates the reservation again. Admission as user/message settles the question, while discard or a Turn that ends without admission releases the reservation for another answer. Settled answer batches remain in the projection so the original tool row can show late answers even though its own result remains pending. Unusable recorded reply text settles with an empty batch.
No new Session event type or wait-state log is needed. The message-source extension requires the repository's persistence-type acknowledgement. Hiding a panel does not manufacture a reply.
Card state and delivery races
QuestionCards shares one PendingQuestion between the live request and the continued projection, keyed by Session and callId. A forwarded request keeps its card through the claim's first frame even before the projection lists it. A blocking request without a call id has its own per-request card with a reload-unique key, so an old persisted draft cannot attach to another request. An open card submits through its waterfall; without that channel it disables submission. A continued card uses the late-answer RPC.
A waterfall submission has no delivery acknowledgement. The Gateway can discard an outcome that loses a settlement race. The card therefore retains its draft and stays busy until projection reconciliation. If the question becomes continued during submission, the controls re-arm with a resubmit hint; the user can submit the retained draft through RPC. An accepted RPC submission steers a reply toward the nearest step without settling the question, and the panel closes. A current tool wait must finish before that step can read the reply; if the steer is discarded, the question can be reopened and answered again. This is not automatic retry or a guarantee that every local submission reaches the model.
A tool-call-keyed card is removed when its call is absent from the active projection and no forwarded request remains. Removal closes the card and clears its draft. Neither a local submission nor a transport cancel frame alone decides that the question is answered. An unkeyed blocking card ends with its waterfall; closing that blocking panel rejects with ASK_CANCELLED.
Local countdown and panel actions
The Client computes Date.now() + remainingMs. Manual focus on a pristine answer surface pauses its countdown, and blur resumes the preserved remainder. Focus before the claim handshake remains effective when the deadline arrives; blur before the handshake does not create a timer. First edit or Take time freezes the countdown, with that disposition saved beside the browser-local draft.
Autofocus applies only when the answer channel is ready, no countdown is active, and the card is not locked. Hiding a timed-call panel leaves its claim and countdown running; its tool row can reopen it. Once a late reply enters the Inbox, the tool row displays its submitted answers and “View answers” expands the read-only transcript while admission is pending. A settled call opens a read-only panel with recorded answers. Discarding the queued reply restores the answer action.
Reply node and grouping
The questions UI identifies a late reply from source.kind, then parses its recorded JSON for question-and-answer presentation. Pasting identical JSON in an ordinary user message does not identify it as a reply. Unreadable recorded payloads retain their raw-text presentation.
The question-reply Definition claims the appended reply message on the Chat target through an exclusive match, so the ordinary message Definition builds no turn-trigger or context projection for it and the reply renders once, including for unscoped historical input.
The reply participates in ordinary process grouping and whole-Turn folding. It is not independently visible: reading it can require expanding both its Turn and process group. Turn disclosure stays before the group it controls, including a reply that opens the Turn.
This decision adds no independent-layout flag or user-message structure change. The reply declares no group-tree position, so it follows the default placement of process content.
Alternatives considered
Unconditional Host deadline or Gateway-owned question timers. An unconditional deadline overrides local editing and Take time. Putting the exception into pending Remote Events or branching on the question event in Remote dispatch makes generic transport own business policy. Claims keep that policy in the question service.
Keep the waterfall alive after returning pending. The request outlives the tool caller, and late waterfall answers compete with the RPC. Ending the foreground request gives each settlement one answer path.
Delegate every timed request immediately. Dispatching to an intentionally empty answerer set wastes the existing blocking-answer path and requires special handling of NO_PROVIDER. Delegation is for an unavailable answerer, not the timed mode itself.
Persist a whole-value wait/focus/edit stream or use Host focus leases. Durable question status is derivable from tool and admitted user events. Focus and editing are local UI state; logging them or synchronising focus leases adds Host state without making multiple Clients share one editing session.
Reclassify JSON inside generic MessageItem, borrow the relay form, or use the default context row. Parsing text to identify the business message confuses its source with its content; the relay form describes another agent's message, and a generic context row exposes JSON. A dedicated source, Definition, and renderer own the question presentation.
Independent reply layout through generic node metadata. It broadens a question feature into a conversation-infrastructure change. Ordinary folding is sufficient for this decision; placing the reply as user input through its group-tree position is separate work.
A log-only close event or a manufactured dismissal message on panel close. A log-only close hides the user's decision from the model and adds another durable mechanism. Manufacturing a reply treats hiding the panel as an answer. Panel close therefore leaves a timed question answerable.
A dedicated late-reply Session event beside the inbox message. The inbox splice records the queued reply and the admitted user message records its delivery. A third event duplicates those facts and adds another persistence type.
Verification
- Service tests and wait tests cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation.
- Projection tests distinguish legacy and timed schemas and cover native and PTC pending calls, other failures, resume repair, queued reply cancellation, and admitted settlement.
- Client listener tests and composer tests cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel actions.
- Grouping tests and browser replay cover ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel.
- Legacy round-trip, cancellation, plan-review intent, and Session-remount regressions remain covered by their owning tests. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests.
Consequences
The model can continue independent work while the question remains answerable. Business-owned timing and reply rendering reuse the existing transport and conversation APIs, at the cost of local rather than globally coordinated waiting and ordinary folded visibility.
- Take time affects only one Client. Another Client can answer or expire the shared waterfall; local focus does not establish a global hold.
- A terminal claim-stream failure can reject the foreground question rather than produce pending. No unattended-wait fallback is provided for that failure.
- Host and Client clocks need not align, but Client system-clock changes can move expiry and transport latency can delay the local countdown's start.
- Unanswered continued questions can remain after reopening a Session. Panel close records nothing, and visibility follows normal folding rather than an always-visible reply guarantee.
- A queued late reply can be canceled before the agent admits it. The question stays answerable until an admitted
user/messagerecords the answer. - A PTC sub-call interrupted before its
tool/ptc-dispatchresult is recorded cannot be reconstructed from the parentrun_codefailure, so it cannot take a late answer after recovery.
中文
Problem
即使还有不依赖回答的工作可做,问题仍可能阻塞 agent(智能体)。等待后放行工具调用不能丢弃问题:用户可能在 agent 继续执行后,或重新打开 Session 后才回答。pending 结果既不是跳过回答,也不是开展需要批准的工作的授权。
前台工具结果与最终回答的生命周期不同。user-questions/request waterfall(瀑布式事件)只结算一次;返回 pending 后仍保留它,会产生一条活过调用方的第二回答路径。倒计时、聚焦和编辑的归属也不同于持久问题状态。
Decision
前台回答继续使用现有 Remote Event waterfall。超时结束该请求并返回 pending;迟到回答通过业务 RPC steer 一条持久用户消息。接手 stream 只控制前台等待,既不传递回答,也不替代 Remote Event。
模式与范围
- 默认
mode: legacy保留阻塞式 schema 和ask()路径。显式mode: timed增加timeout,省略时使用配置默认值 120 秒。 - 正整数超时选择
askTimed();-1选择阻塞式ask()。timed schema 发出的原生阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。 - 计划评审保留
ask()、plan-review意图及BAD_INTENT校验。本决策不把计划评审迁移到其他包。 - 提供 Agent 时,人机交互要求它是运行时中确切的存活根实例。陈旧实例以
CALLER_NOT_LIVE失败;被其他 agent 拥有的子实例以DELEGATED_CALLER失败。持久 Session 的血缘关系不妨碍恢复后的运行时根实例提问。
职责归属
| 归属方 | 职责 |
|---|---|
tool-ask-user | 模型 schema、超时校验、服务选择和 pending 结果文本。 |
UserQuestionService | 前台等待与 Client 接手、无人接手时的 deadline、迟到回答校验与 steering(中途引导),以及持久问题投影。 |
| Gateway 与 Remote 运行时 | 现有请求投递、结算、取消及 Remote stream 传输;不包含问题专属计时器或按事件名分支。 |
ui-user-questions | 每个 Session 和调用的一张卡片、回答通道选择、本地倒计时与草稿,以及回复 Definition 和渲染器。 |
ui-chat | 普通过程/轮次折叠。 |
| 问题专用工具行 | pending/已回答呈现与面板操作,在 QuestionToolRow 中组合,不向通用 ToolRow 添加 rowAction。 |
前台等待与接手生命周期
TimedQuestionWait 拥有专属取消 signal、原始 Host deadline 和 Client 接手记录。Host 只在没有回答 UI 接手时运行计时器。投递到传输队列不算接手。最后一个接手方离开后恢复原 deadline,已经过期则立即到期。
计时请求携带 wait: { callId, timed: true }。Client 在挂接回答通道前打开业务 Remote stream attachWait,接收一帧由 Host 计算的 remainingMs,并在接手期间保持 stream 打开。聚焦、编辑和心跳不经过该 stream。
Client 将接手保留到 waterfall 结果交付给 Host。本地返回回答不等于交付:此时释放接手会让已经到期的 Host 计时器抢在回答之前结算。Host 在前台请求结算时关闭接手。显式委托在 next() 前释放接手;插件拆卸会委托请求并取消自己的 stream。
Client 倒计时到期以 ASK_TIMED_OUT 拒绝 waterfall。无人接手时,Host 到期只以同一业务错误中止等待专属 signal。askTimed() 只把该错误映射为 pending,把父级取消映射为 ASK_ABORTED,其余失败继续传播。NO_PROVIDER 等待同一个有界等待生命周期,而不是永不结算的 Promise。前台完成时释放计时器、接手记录及等待注册项。
持久状态与迟到回答
userQuestions 投影从现有 Session 事件重建问题状态。对原生调用,它通过 request/header 记录的 timeout 参数识别 timed schema,不跟踪 legacy 调用。有效的 tool/call 打开问题。其 tool/result 在 pending 或 TOOL_OUTCOME_UNKNOWN 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。PTC 子调用在 tool/ptc-dispatch 结果为 pending 时进入投影,因为模型请求头只列出 run_code。
已继续的问题接受 answer(agent, callId, answer)。未知或非已继续调用返回 false;另一条回复等待准入时抛出 REPLY_QUEUED;回答批次没有恰好包含每个问题一次时抛出 BAD_ANSWER。接受的回答通过 agent.steer(createUserMessage(...)) 送入 agent,来源为 { kind: 'user-question-reply', callId, outcome: 'answered' },JSON 文本包含 answer_to_pending_question、调用 id、原问题及答案。Remote 根据 Session id 解析 Agent 时按需恢复根实例;恢复操作不属于 answer() 方法体。
inbox 消息是持久的排队回复。逐 Session 预约覆盖从排队、领取到准入的期间;领取恢复后的 inbox 回复时会重建预约。回复作为 user/message 准入时问题结算;丢弃回复或 Turn 结束而未准入时释放预约,允许再次回答。已结算答案批次保留在投影中,让原工具行即使自身结果仍是 pending,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。
无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。隐藏面板不会伪造回复。
卡片状态与投递竞争
QuestionCards 按 Session 和 callId,让实时请求与已继续投影共享一个 PendingQuestion。转发请求在投影列出问题之前,仍会让卡片存活至接手 stream 的首帧。没有调用 id 的阻塞请求拥有独立的逐请求卡片,并使用跨重载唯一的键,避免旧草稿附着到另一请求。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。
waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。RPC 接受提交后会将回复作为 steer 送往最近一步,但不会立即结算问题,同时关闭面板。当前工具等待须先结束,该步才能读取回复;若 steer 被丢弃,可以重新打开问题并再次回答。这不是自动重试,也不保证每次本地提交都能到达模型。
按工具调用标识的卡片在调用不属于活动投影且没有转发中的请求时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 ASK_CANCELLED 拒绝请求。
本地倒计时与面板操作
Client 计算 Date.now() + remainingMs。手动聚焦未编辑的回答区会暂停倒计时,失焦后从保留的剩余时间继续。接手握手前的聚焦在 deadline 到达时仍生效;握手前失焦不会制造计时器。首次编辑或「慢慢回答」冻结倒计时,该状态与浏览器本地草稿一起保存。
只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。迟到回复进入 Inbox 后,工具行显示已提交的答案;等待准入期间,「查看回答」展开只读的行内记录。已结算调用打开只读面板,显示记录的答案。排队回复被丢弃后,回答操作恢复可用。
回复节点与分组
问题 UI 通过 source.kind 识别迟到回复,再解析已记录的 JSON 来呈现问答。普通用户消息粘贴相同 JSON 不会被识别为回复。无法读取的已记录载荷保留原始文本呈现。
问答回复 Definition 通过独占匹配在 Chat target 上认领追加的回复消息,普通消息 Definition 因此不再为它建立 turn-trigger 或 context 投影,回复只显示一次;无轮次归属的历史输入同样如此。
回复参与普通过程分组和整轮折叠,不保证独立可见:阅读时可能需要展开所属轮次与过程分组。轮次折叠控件始终在所控制的分组之前,包括由回复开启轮次的情况。
本决策不添加独立布局标记或用户消息结构变更。回复不声明分组树位置,因此按过程内容的默认位置放置。
Alternatives considered
无条件 Host deadline 或 Gateway 拥有问题计时器。 无条件 deadline 会覆盖本地编辑和「慢慢回答」。把例外放入 pending Remote Events,或在 Remote 派发中按问题事件分支,会让通用传输拥有业务策略。接手机制将策略留在问题服务中。
返回 pending 后继续保留 waterfall。 请求活过工具调用方,迟到的 waterfall 回答与 RPC 竞争。结束前台请求让每次结算各有一条回答路径。
立即委托所有计时请求。 向故意留空的回答方集合派发请求,浪费现有阻塞回答路径,还要求特殊处理 NO_PROVIDER。委托表示回答方不可用,不代表计时模式本身。
持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。 持久问题状态可从工具事件与已准入的用户事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。
在通用 MessageItem 内重新分类 JSON、借用 relay form,或使用默认 context 行。 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。
通过通用节点元数据让回复独立布局。 这会把问题功能扩大为对话基础设施变更。普通折叠足以满足本决策;通过分组树位置把回复放置为用户输入是单独的工作。
面板关闭时写仅入日志的关闭事件,或伪造放弃消息。 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。
在 inbox 消息之外增加专用迟到回复 Session 事件。 inbox 插入记录排队回复,准入的用户消息记录其交付。第三个事件重复这些事实,并增加另一种持久化类型。
验证
- 服务测试与等待测试覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
- 投影测试区分 legacy 与 timed schema,并覆盖原生及 PTC pending 调用、其他失败、恢复修复、排队回复取消和准入后结算。
- Client 监听器测试与composer 测试覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板操作。
- 分组测试与浏览器回放覆盖普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
- legacy 问答往返、取消、计划评审意图及 Session 重新挂载的回归仍由各自测试覆盖。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
Consequences
模型可以继续独立工作,问题同时保持可回答。业务层拥有的计时与回复渲染复用现有传输和对话 API,代价是等待由本地而非全局协调,可见性遵循普通折叠。
- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。
- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。
- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。
- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。
- 排队的迟到回复可能在 agent 准入前被取消。只有已准入的
user/message记录答案,问题在此之前保持可回答。 - PTC 子调用若在记录
tool/ptc-dispatch结果前中断,无法从父run_code的失败中重建,因此恢复后不能接受迟到回答。