- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Wait for a person
Your agent needs a person before it can go on: a yes for a refund, the amount, a sign-in. A person can take a minute or a day. In that time the server can restart, and the answer can reach another instance. In a harness, each of these is a wait. The session log keeps it, so no process holds it, any instance can take the answer, and it can expire.
1. Ask from a tool#
Call step.ask in a durable tool. This tool asks for the amount of a refund, then pays it once:
import { toolDefinition } from '@tanstack/ai'
import { createHarnessHost, defineHarness, durableTool } from '@tanstack/ai-harness'
import { openaiText } from '@tanstack/ai-openai'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { z } from 'zod'
declare const payments: {
refund: (orderId: string, amount: number) => Promise<{ id: string }>
}
const refund = durableTool(
toolDefinition({
name: 'refund',
description: 'Refund an order',
inputSchema: z.object({ orderId: z.string() }),
}),
async ({ orderId }, { step }) => {
const amount = await step.ask({
name: 'amount',
message: `How much do we refund for ${orderId}?`,
schema: z.number().positive(),
expiresAt: Date.now() + 24 * 60 * 60 * 1000,
})
const paid = await step.do(`refund:${orderId}`, () =>
payments.refund(orderId, amount),
)
return { refundId: paid.id }
},
)
const support = defineHarness({
name: 'acme/support',
adapter: openaiText('gpt-6.1-sol'),
tools: [refund],
})- name identifies the question in this call. Use the same name on every run of the code.
- The session checks the answer against schema. Here amount is a number.
- expiresAt ends the wait if nobody answers in time. Then step.ask rejects. Without it, the wait has no time limit.
- secret: true asks for a key or a password. The log never keeps that answer.
When the host stops while the tool waits, the next host runs the tool again from the top. step.ask then returns the logged answer, or waits again. A finished step.do returns its stored value, so the refund runs once.
2. Find the wait#
session.waitFor resolves with the first wait of a run:
const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(support, { threadId: 'ticket-7' })
const turn = session.send('Refund order A-17.')
const wait = await session.waitForRun({
runId: turn.id,
signal: AbortSignal.timeout(60_000),
})
console.log(wait.kind, wait.message) // question How much do we refund for A-17?- Without runId, it resolves with the first wait of the session.
- session.snapshot().pendingWaits lists every open wait, oldest first. A wait that a chat run stopped for has its interrupt. A wait of the stopped turn has turn: true.
- A wait of an agent that the model calls as a tool has the agent name and the subagentRunId of that agent run. See When an agent tool waits.
Each wait has a kind:
| Kind | The run waits for |
|---|---|
| approval | A yes or a no for a tool call. |
| question | An answer that fits the schema of the wait: from step.ask, the question tool, or an MCP server. |
| sign-in | The user signs in to a connector. |
| client-tool | The output of a tool that runs in the client. |
| human | A person who takes over, from an interrupt of defineInterrupt. |
waitKind(interrupt) and isApproval(interrupt) give the kind of the interrupt of a wait too. Import them from @tanstack/ai-harness.
3. Answer it#
const hasInterrupt = session
.snapshot()
.pendingWaits.some(
(open) => open.waitId === wait.waitId && open.interrupt !== undefined,
)
if (hasInterrupt) {
await session.resolve({
resume: [{ interruptId: wait.waitId, status: 'resolved', payload: true }],
})
} else {
await session.answer({ questionId: wait.waitId, value: 40 })
}
await turnThe tool pays 40 and the turn finishes. Two inputs answer waits:
- answer: a wait with no interrupt: a step.ask question (of a durable tool, a code mode program, a command, or a background agent run), a question of an MCP server, and the human wait of a handoff. It checks the answer against the schema of the wait, and keeps a secret answer out of the log.
- resolve: a wait with an interrupt: a wait that a turn stopped for (an approval, the question tool, a client tool, a sign-in, a wait of an agent tool), and a wait of a background agent run. One resolve answers every wait of the stopped turn, or waits of one background run. A resolve of a wait with no interrupt is refused with use_answer.
Both are inputs. An answer that reaches another instance waits in the log until the owner takes it. host.submit stores an answer for a thread that has no open session.
How a wait ends#
Each wait ends with one outcome:
- answered: someone answered it.
- cancelled: the run stopped, or the signal of the question aborted.
- expired: expiresAt passed with no answer.
Only a sign-in wait has a default time limit: 15 minutes. The other kinds wait until someone answers, unless you set expiresAt. An interrupt of a turn with an expiresAt also ends on time.
session.waitFor and session.settled take a signal too. It stops your own wait for the session. It does not end the wait of the run.
Where a run can wait#
A run waits for a person only at these points:
- Before a tool call. permissions() opens one approval wait for each call that asks.
- The question tool. The model asks the user to pick an option.
- step.ask in a durable tool, in a command, in an agent run that started with runAgainAfterCrash: true, or in an agent that the model calls as a tool. A code mode program asks this way before a call that the rules ask for.
- Live waits: an MCP server that asks during a tool call (elicitation), and an OAuth sign-in. The host that runs the call keeps the thread until the answer comes. See Answer a question from a server.
ctx.session.ask in your own plugin throws an error that names these points. A command that called step.ask runs again from the top after a host stop, so put each side effect before the ask in step.do.
Who can answer#
On a host with access, its rules decide each answer:
- An answer is the answer action. With membersOnly(), the owner of the session and the user who started the run have it.
- A human wait takes only the operator action. With membersOnly(), only members with the role operator have it. The owner does not. To change that, pass membersOnly({ operator: { roles } }). See Shared threads.
What you have now#
- A refund that waits for a person, on any instance, and pays once.
- One list of everything that waits, with a kind for each item.
- Waits that expire on time, and that a restart does not lose.
To show waits in your own screen, see Build your own UI.