Reject human interaction from runtime-owned subagents
拒绝运行时中归属于其他 agent 的 subagent 向人类发起交互
A one-shot subagent that calls `ask_user_question` can block indefinitely. The call waits for a human answer, but the child has no independently owned human channel, so the child's completion and the parent waiting on that completion both stall. Durable session lineage cannot decide whether an answerer exists. A child session may later be resumed as a new top-level runtime root, while a live runtime-owned child may c
English
Problem
A one-shot subagent that calls ask_user_question can block indefinitely. The call waits for a human answer, but the child has no independently owned human channel, so the child's completion and the parent waiting on that completion both stall.
Durable session lineage cannot decide whether an answerer exists. A child session may later be resumed as a new top-level runtime root, while a live runtime-owned child may carry a zero or absent durable delegation depth. Error guidance at the shared seam must also fit every consumer: exit_plan_mode uses ctx.userQuestions.ask() without calling ask_user_question.
Decision
When AskUserQuestionRequest.agent is present, UserQuestionService.ask() authenticates the exact live agent through ctx.agents and admits it only when ctx.agents.roots() contains that instance. A missing registry or stale same-id object fails with CALLER_NOT_LIVE; a live agent owned by another live agent fails with DELEGATED_CALLER. The check runs after the existing aborted and empty-batch guards and before intent validation or provider dispatch, so an owned child never creates a UI wait.
Runtime ownership is the authority. A lineage-bearing session resumed without an owner is a runtime root and may ask; a live child remains ineligible even when its durable delegationDepth is zero. Agentless programmatic calls retain the existing provider path.
The shared failure text is consumer-neutral and actionable: the child includes the unresolved question or decision in its final result. The parent already receives that result through the delegation contract and can decide whether to ask the human. Neither the service nor a child claims an upward messaging or answer-forwarding capability that does not exist.
This safety boundary is independent of the browser's composer election. The proposed semantic composer phases address how an already-pending interaction and a read-only subagent surface should be ordered; they do not weaken this runtime guard.
Alternatives considered
Use session.header.delegationDepth > 0. Rejected because durable lineage survives resume and does not attest the current process-local owner. It rejects valid resumed roots and can admit a live child whose durable header is incomplete.
Reject only inside dsh-tool-ask-user. Rejected because exit_plan_mode and direct callers share ctx.userQuestions.ask(). The service is the narrow operation boundary common to every human-interaction consumer.
Tell the child to delegate upward or wait for forwarding. Rejected because one-shot delegation exposes no child-to-parent request channel and no answer-forwarding protocol. The only guaranteed return path is the child's final result.
Rely on the browser composer fix. Rejected because presentation cannot make an ownerless human channel exist, and non-browser deployments still need the call to terminate.
Consequences
Runtime-owned child calls fail fast with a stable structured error instead of hanging. Exact live roots and agentless programmatic calls remain eligible, including resumed sessions with historical child lineage. ask_user_question and exit_plan_mode receive the same neutral corrective guidance, while their model-visible schemas and system-prompt prefixes remain unchanged; only the appended error result differs, so existing KV-cache prefixes remain reusable.
Testing
Service tests cover a zero-depth live child, a depth-one resumed runtime root, a missing registry, a stale same-id object, and provider non-invocation on every rejection. Tool and plan-mode tests prove both consumers surface the neutral DELEGATED_CALLER result and never reach the provider. The keyless assembled snapshot delegates to a child that attempts ask_user_question, pins the child's error tool result and final handoff, and proves the parent completes instead of waiting for an answer.
中文
问题
一次性 subagent 调用 ask_user_question 时可能无限阻塞。该调用会等待人类回答,但子级没有由自身独立拥有的人类交互通道,因此子级无法完成,等待其完成的父级也会随之停滞。
持久化会话谱系无法判断应答者是否存在。子会话之后可能恢复为新的顶层运行时根,而运行时中归属于其他 agent(智能体)的存活子级,其持久化委托深度却可能为零或缺失。共享 seam 上的错误指引还必须适用于每个消费方:exit_plan_mode 会使用 ctx.userQuestions.ask(),但不会调用 ask_user_question。
决策
如果存在 AskUserQuestionRequest.agent,UserQuestionService.ask() 会通过 ctx.agents 验证该 agent 就是注册表中的存活实例,并且只在 ctx.agents.roots() 包含该实例时才允许调用。缺失注册表或传入仅 id 相同的陈旧对象时,以 CALLER_NOT_LIVE 失败;存活 agent 归属于另一个存活 agent 时,以 DELEGATED_CALLER 失败。该检查位于现有的已中止和空批次守卫之后、意图校验或提供方分派之前,因此归属于其他 agent 的子级绝不会触发 UI 等待。
以运行时所有权为权限依据。携带谱系的会话在无所有者的情况下恢复时就是运行时根,可以提问;存活子级即使持久化 delegationDepth 为零,仍无资格提问。不带 agent 的程序化调用继续沿用现有提供方路径。
共享失败文本与具体消费方无关,并给出可执行指引:子级把尚未解决的问题或决策写入最终结果。委托约定本就会把该结果传给父级,父级可据此决定是否询问人类。服务和子级都不会宣称存在实际上并不存在的向上消息传递或回答转发能力。
该安全边界与浏览器的 composer 选举相互独立。提议的语义 composer 阶段解决已有待处理交互与只读 subagent 界面的排序方式;它不会削弱此运行时守卫。
备选方案
使用 session.header.delegationDepth > 0。 不予采用:持久化谱系会在恢复后继续存在,却不能证明当前进程内所有者。该方案会拒绝有效的已恢复根,也可能放行持久化 header 不完整的存活子级。
仅在 dsh-tool-ask-user 内拒绝。 不予采用:exit_plan_mode 与直接调用方共用 ctx.userQuestions.ask()。服务是所有人机交互消费方共同经过的最窄操作边界。
让子级向上委托或等待转发。 不予采用:一次性委托没有公开从子级向父级请求的通道,也没有回答转发协议。唯一有保证的返回路径是子级的最终结果。
依赖浏览器的 composer 修复。 不予采用:呈现方式无法凭空产生由所有者负责的人类通道,非浏览器部署仍然需要该调用能够终止。
影响
运行时中归属于其他 agent 的子级调用会以稳定的结构化错误快速失败,而不是挂起。注册表中的确切存活根和不带 agent 的程序化调用仍有资格提问,包括带有历史子级谱系的已恢复会话。ask_user_question 与 exit_plan_mode 会收到相同的中性纠正指引,而其模型可见 schema 和系统提示词前缀保持不变;只有追加的错误结果发生变化,因此现有 KV Cache 前缀仍可复用。
测试
服务测试覆盖持久化深度为零的存活子级、深度为一的已恢复运行时根、缺失注册表、仅 id 相同的陈旧对象,以及每次拒绝都不调用提供方。工具与 plan-mode 测试证明两个消费方都会呈现中性的 DELEGATED_CALLER 结果,且绝不触达提供方。无密钥组装快照委托一个尝试调用 ask_user_question 的子级,固定其错误工具结果和最终交接,并证明父级可以完成,而不是一直等待回答。