Skip to content

[EPIC-X][LOU-X9] Built-in ask_question tool pauses durably until a human answers - #111

Merged
LinuxDevil merged 2 commits into
mainfrom
lou-x9-ask-question
Oct 1, 2026
Merged

LinuxDevil merged 2 commits into
mainfrom
lou-x9-ask-question

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

  • New built-in askQuestionTool() (src/tools/built-in/askQuestion.ts). It is registered by the opt-in createAgent({ askQuestion: true }), which defaults to false. Its input is { question: string; options?: string[]; allowFreeText?: boolean } and it sets needsApproval: true, so a call pauses the run through the existing approval mechanism.
  • PendingApproval gains two optional fields: kind?: 'tool' | 'question' and question?: { text, options?, allowFreeText? }. New exports are ApprovalKind, ApprovalQuestion, describeApproval() and ASK_QUESTION_TOOL_NAME. createAgent() records an ask_question pause as kind: 'question'.
  • The approval.requested event gains optional kind and question. The React reducer's pendingApproval exposes both fields too, and useLoushyAgent() gets answer(text).
  • agent.approvals.answer({ id, answer }) is a shorthand for resolve({ id, approved: true, note: answer }). The model then gets { answer, option? }, where option is the index of the matching option (case-insensitive). With allowFreeText: false, an answer that is not one of the options reaches the model as a tool error.
  • approved: false gives the model a kind: 'rejected' tool error that says "The user declined to answer the question".
  • ApproveToolCall now returns boolean | string. A string approves the call with that string as its note, so code can answer questions.
  • A tool that runs after an approval now receives the decision's note as ctx.approval.note (new optional ToolExecutionContext.approval). This is how ask_question reads the answer, and it works the same way inside sub-agents.

Why

Epic X matrix row "Agent asks the user a question" was ❌. eve ships a built-in ask_question that pauses the session until a human answers. This change reuses the durable approval path (ApprovalStore, checkpoints with status awaiting-approval, agent.resume), so questions survive sessions, stores and process restarts with no new persistence.

Acceptance criteria

  • askQuestionTool() is exported and registered with createAgent({ askQuestion: true }) (default false, documented). Its input is { question, options?, allowFreeText? }.
  • The call pauses through the approval mechanism, and the pending record is marked kind: 'question'. This change only adds fields to the record.
  • resolve({ id, approved: true, note }) and the new answer({ id, answer }) both continue the run with { answer, option? }.
  • approved: false gives a kind: 'rejected' tool error saying the user declined to answer.
  • Works inside sessions and with durable stores, including a restart between question and answer, with both memoryStore() and SqliteStore (D30 agent.resume).
  • The approval.requested event and the React reducer/hook expose kind: 'question' and pendingApproval.question. The hook has answer(text).
  • The approve callback receives the question and may return a string answer (boolean | string).
  • Tests with mockModel cover: ask, pause, answer, then the model sees the answer; decline; restart (memory and SQLite); options index; allowFreeText: false; hook reducer kind: 'question'; hook answer(); approve returning a string; the session and stream event.
  • Docs: new docs/approvals.md section "Asking the user a question" (snippet is type-checked); docs/react.md; docs/streaming.md row; docs/tools.md and docs/api-overview.md lines; README feature line; llms files regenerated; CHANGELOG (Added).

How verified

  • npx tsc --noEmit: no errors
  • npm run lint: 0 errors, 404 warnings, the same count as main
  • npm run test:types: 30 passed, no type errors
  • npx vitest run: 158 files passed; 2291 tests passed, 3 skipped
  • npm run build && npm run build --workspace=packages/create-loushy-agent: both exit 0
  • npm run docs:verify-snippets -- --skip-build: all 121 snippets type-check against src/, and 6 also run cleanly
  • npm run docs:llms:check: exit 0
  • npm run test:coverage && npm run fallow: coverage 2291 passed; fallow exit 0 with "No issues found" and 0 above threshold. One existing clone warning remains between AnthropicProvider and OpenAIProvider; this PR does not touch those files.

Files outside the listed scope, each a small addition:

  • src/types/tool.ts (ToolExecutionContext.approval) and src/execution/toolRunContext.ts, which pass the answer to the tool.
  • src/execution/agentRun.ts, which builds the approval.requested event.
  • src/tools/built-in/index.ts, which exports the new tool.
  • docs/tools.md and docs/api-overview.md.

AgentExecutor.ts, toolCallExecution.ts, src/session/*, src/cli/* and apps/* are unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR


Generated by Claude Code

claude added 2 commits October 1, 2026 16:52
…man answers

createAgent({ askQuestion: true }) (or askQuestionTool()) gives the agent
ask_question({ question, options?, allowFreeText? }). A call pauses through
the approval mechanism, so it is durable like any approval. The pending
record, the approval.requested event and the React pendingApproval carry
kind: 'question' and the question; agent.approvals.answer({ id, answer })
continues the run with { answer, option? }, declining yields a 'rejected'
tool error, the approve callback may return a string answer, and the hook
gets answer(text). An approved tool sees the decision's note as
ctx.approval.note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
@LinuxDevil
LinuxDevil merged commit d44a2b5 into main Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants