Ask User
This example shows how the runtime pauses when the model needs information from the user, surfaces a structured prompt, and resumes after the host injects an answer back into the conversation.
Use this when
- the model genuinely needs a decision from the user (target, region, plan, persona)
- you want a structured multi-choice prompt instead of free-form back-and-forth
- the answer should land in runtime state, not in prompt text the model has to remember
What it shows
- enabling the built-in
ask_user_questiontool viahumanInTheLoop.askUser - a pending question recorded on
state.pendingUserQuestions resolveUserQuestion(...)injecting the answer as a tool result so the model can finish the run
Run it
bash
cd examples
npm run example:ask-userCore code
ts
const agent = createAgent({
model: fakeModel,
humanInTheLoop: {
askUser: {
allowFreeText: true,
onQuestion: (event) => console.log("Asking:", event.questions),
},
},
});
const first = await agent.invoke({
messages: [{ role: "user", content: "Deploy the marketing site." }],
});
const pending = first.state!.pendingUserQuestions![0];
const resolved = agent.resolveUserQuestion(first.state!, {
id: pending.id,
answeredBy: "demo-user",
answers: {
"Which deployment target should I use?": { values: ["Fly.io"] },
"Pin a specific region?": { values: ["Auto"] },
},
});
const second = await agent.invoke(resolved);End-to-end flow
- The model calls
ask_user_questionwith one or more questions and options. - The runtime pauses, sets
ctx.__awaitingUserQuestion, and appends apendingUserQuestionsentry. - Your app reads
state.pendingUserQuestions, renders the form, and collects an answer set. agent.resolveUserQuestion(state, { id, answers })validates the answer and appends it as arole: "tool"message bound to the originaltool_call_id.agent.invoke(resolved)resumes — the model sees the answer as a normal tool result and produces the final response.
Why it matters
The model can stop guessing when the user holds the missing fact. By making the question part of runtime state (not a chat reply), you get a deterministic UI contract, easy snapshotting, and a structured trace event for telemetry.
Look for
- the
humanInTheLoop.askUserblock oncreateAgent - the paused state with
pendingUserQuestionsandctx.__awaitingUserQuestion - the
user_questionevents emitted throughonEventandonQuestion - the appended
role: "tool"message afterresolveUserQuestion
Production takeaway
allowFreeText is decided at agent construction time so every question and every UI form is rendered against the same contract. Flip it to false to enforce strict multi-choice; the model is told free-text is unavailable and the resolver rejects custom answers.
Expected output
- the agent prints the pending questions before asking the user
- after
resolveUserQuestion, the final assistant message reflects the choices state.pendingUserQuestions[0].statusflips from"pending"to"answered"withansweredAtandansweredByfilled in
Common failure modes
- forgetting
humanInTheLoop.askUseron the agent — the tool is never injected, so the model can't ask - sending answers keyed by something other than the original
questiontext (or theheader) — the resolver throwsMissing answer for required question - supplying
freeTextwhile running withallowFreeText: false— the resolver rejects with a clear error - calling
agent.invokewithout first callingresolveUserQuestion— the run resumes with the same pending question still blocking the next turn