Skip to content

worker(SDK): AskUserQuestion 等交互工具在 headless 下无法回传人类 → tool 报错;缺少 turn 中途问答通道 #9

Description

@bencode

问题

Worker 通过 Claude Agent SDK(headless)驱动 agent,但 SDK 模式下 agent 调用 AskUserQuestion(以及其它需要交互式宿主的工具,如 plan 模式外的 ExitPlanMode)时无法把问题回传给人类,导致该 tool 调用直接 报错(is_error / 1 failed,整个 turn 失败。

现实场景(飞书上观察到):agent 在 turn 中途想跟用户确认几个点 → 触发 AskUserQuestion → 在 headless worker 上没有任何宿主能应答这个交互请求 → tool 报错,用户只看到一个红色 error,问题也没送达。

注:worker 跑在 Mac mini / 服务器上,没有 TTY、没有交互式 Claude Code TUI,纯 SDK headless。

根因(代码定位)

packages/cli/src/session/runner/

  1. query.ts — SDK 启动选项写死 permissionMode: 'bypassPermissions',且没有传 canUseTool 回调
    const options: Options = {
      cwd: config.worktreePath,
      permissionMode: planMode ? 'plan' : 'bypassPermissions',
      // ❌ 无 canUseTool;❌ 无 disallowedTools
      ...
    }
  2. stream.ts — SDK 消息流是单向的:for await (message of messages) → emitEvent('sdk_event', message)。没有任何从人类回灌进正在进行的 turn 的通道。
  3. turn.ts — turn 模型是「请求/应答」:一个 turn 跑到结束,人类的回复是下一个新 turn(resume),无法在 turn 中途暂停等人回答。

因此 AskUserQuestion 这种「turn 中途需要人应答、并把结果作为 tool_result 接着往下跑」的工具,在当前架构里无处落地,只能报错。

SDK 能力核对(@anthropic-ai/claude-agent-sdk@0.3.167,worker 实际安装版本)

  • Options.canUseTool?: CanUseTool,签名为 (toolName, input, opts) => Promise<PermissionResult>是 async —— 宿主可以在回调里 park 住、等人类应答后再 resolve,这正是「turn 中途暂停等输入」所需的钩子。
  • SDK 注释明确:交互式 ask 路径通过 can_use_toolcontrol_request 浮出。
  • Options.disallowedTools?: string[] —— 可直接屏蔽某个工具,模型会改用别的方式(纯文本提问)。
  • AskUserQuestionInput:1–4 个问题,每题 2–4 个选项(label + description + 可选 preview),SDK 自动追加 "Other"。AskUserQuestionOutput 回结构化的选择结果。

已有的可复用基础设施(重要)

baton 已经有 server→worker 的反向控制通道:worker daemon 通过 SSE 订阅 /workers/me/streamdaemon.ts),runner.ts 已经在处理 user_messageinterrupt/abort Esc 会 abort 在飞的 turn)。也就是说「把人类的答案推回给正在 park 的 worker」所需的传输层已经存在,只差一个新的事件类型 + 在 turn 内 park/resolve 的逻辑。Web 端也已经能渲染 AskUserQuestion(截图里就是 web 解析出来的 AskUserQuestion questions:[{...)。


方案(分两步,先不实现)

Track A — 立即止血(低成本,无架构改动)

query.tsOptions 里加 disallowedTools: ['AskUserQuestion'](必要时连同未接入的交互工具一起),并在 system prompt 追加一句「你运行在 headless 中继环境,需要确认时请用纯文本在回复里提问,不要使用 AskUserQuestion」。

  • 效果:模型不再调用会报错的工具,转而把问题作为普通文本写进 assistant 消息 —— 这条路径在当前 turn 模型里已经完全跑通(人类在 web/飞书读到 → 回复 → 触发新 turn)。截图里的 error 立刻消失。
  • 代价:失去「结构化选项 + 点选」的体验,只有纯文本问答。
  • 适合:作为 hotfix 先上,挡住报错。

Track B — 正式打通交互式问答(中等成本,真正的体验)

AskUserQuestion(及未来的权限 ask)经由 baton 已有的人类通道做成真正的「中途提问 → 人点选 → 同一 turn 继续」:

  1. 拦截:两种实现路径,二选一
    • (a) 在 query.tscanUseTool 回调拦截交互请求;注意需评估 bypassPermissionscanUseTool 是否被触发,可能要把权限模式从 bypass 调整为可触发 ask 的模式。
    • (b) 更稳:用一个同名 in-process MCP 工具覆盖 AskUserQuestion,工具体内 emit 事件 + 阻塞等人类答案,返回 AskUserQuestionOutput。这条路绕开权限模式耦合,行为可控。
  2. 浮出:拦截后 emit 一个新事件 question_pending(携带结构化 questions/options),持久化,并把 session 置为子状态 awaiting_input
  3. 渲染:web 渲染选项点选器(已有 AskUserQuestion 渲染基础);飞书/钉钉渲染成带序号的列表,回数字即可。
  4. 回灌:人类的答案经一个新 endpoint → server 走已有的 /workers/me/streamquestion_answer 事件推给正在 park 的 worker → resolve 掉 canUseTool/MCP 工具的 Promise(返回结构化结果)→ 同一个 in-flight turn 继续往下跑,无需 resume。
  5. 超时/看门狗turn.ts 现在有 30min turn ceiling 看门狗,park 等人期间要么把 ceiling 调长、要么在 awaiting_input 期间暂停看门狗,避免「人还没回就被 abort」;同时 awaiting_input 不能被 idle-reaper 当成空闲回收。

建议:先上 Track A 止血,再排期 Track B(优先考虑 (b) MCP 覆盖方案,耦合最小、最可控)。


验收(待实现时)

  • Track A:agent 不再因 AskUserQuestion 报错;需要确认时以纯文本提问,人类可在飞书/web 正常回复并继续。
  • Track B:agent 触发结构化提问时,飞书/web 出现可点选项;人选择后同一 turn 继续,结果作为 tool_result 进入上下文;超时与 idle 回收行为正确。

相关文件

  • packages/cli/src/session/runner/query.ts(SDK options:permissionMode / 缺 canUseTool / disallowedTools
  • packages/cli/src/session/runner/stream.ts(单向消息流)
  • packages/cli/src/session/runner/turn.ts(turn 生命周期 / 看门狗 / abort)
  • packages/cli/src/session/runner.ts + packages/cli/src/worker/daemon.ts(已有 server→worker SSE 控制通道:user_message / interrupt

本 issue 由 rooma worker 经大鱼指派记录,仅记录方案、暂不实现,等待排期与确认。

Metadata

Metadata

Assignees

No one assigned

    Labels

    actor:claude-codeMeant for Claude Code (human-triggered)enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions