- 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
Lifecycle Boundaries
Pause a generic interrupt after the model has a draft and before a tool runs.
Each agent iteration has four boundaries. You pick one with a ctx.phase guard in onInterruptBoundary.
This page assumes that you have a definition from Generic Interrupts.
Quick setup#
1. Pick a phase#
| Phase | When it runs | Use it to |
|---|---|---|
| beforeModel | After onConfig for this iteration, before the adapter call | Get a choice from the user before you pay for tokens |
| afterModel | After the model stream ends, before tools run | Review the draft that the model wrote |
| beforeTools | After the assistant tool-call message is in messages, before the tools run | Allow or block side effects |
| afterTools | After the tools finish and their results are in messages | Audit tool results before the model reads them |
2. Guard on the phase#
Return { interrupts } to pause. Return nothing to let the run continue. This example pauses at beforeModel on the first iteration only:
import type { ChatMiddleware } from '@tanstack/ai'
import { reviewPlan } from './interrupts'
export const requestReview: ChatMiddleware<unknown, typeof reviewPlan> = {
name: 'request-review',
onInterruptBoundary(ctx) {
if (ctx.phase !== 'beforeModel') return
if (ctx.parentRunId) return
if (ctx.iteration !== 0) return
return {
interrupts: [
reviewPlan.interrupt({
key: 'initial-plan',
reason: 'review-required',
message: 'Review the proposed plan.',
payload: {
title: 'Release plan',
changes: ['Add search', 'Add tests'],
},
}),
],
}
},
}3. Skip the pause on the continuation#
Keep the ctx.parentRunId guard from step 2. The continuation is a new chat() call, and every boundary hook runs again. If the hook returns the same request, the run pauses again. ctx.parentRunId is set only on a continuation, so the guard limits the pause to the original request.
How it works#
- The engine collects every request from every middleware at the same phase into one interrupt batch.
- That batch ends the current run with one interrupt outcome.
- onInterruptBoundary can only pause. It cannot change config. To change prompts, tools, or messages from the answer, see Apply Answers.
Examples for each phase#
beforeModel: get a choice first#
Ask for a plan, an audience, or a locale before the model writes:
- "Which brand voice must this reply use?"
- "Which ticket do I work on?"
- "Is this request in scope for this agent?"
afterModel: review the draft#
The model wrote text, and a human must accept it before tools run:
- Review an email draft before sendEmail.
- Review a SQL query before runQuery.
- Review a support reply before the user sees it.
beforeTools: gate the side effects#
The model asked for tools, and nothing has run yet:
- Confirm a bulk delete.
- Confirm a payment.
- Confirm a deploy.
This is close to tool approval. Use a generic interrupt in these cases:
- The question is more than a yes or no on one tool.
- You judge several tools as one batch.
afterTools: audit the results#
The tools ran, and a human must see the output before the model uses it:
- A search returned customer PII. Ask if it can stay in context.
- A code run made a diff. Ask if the next turn can apply it.
- A lookup returned a low-confidence match. Ask which record to keep.
Try it#
The React chat example has a playground for all four phases.
- Start examples/ts-react-chat.
- Open /generic-interrupts.
- Pick a phase and resolve both cards.
Advanced#
What ctx holds#
Every hook gets the same ChatMiddlewareContext. Its contents change from phase to phase.
The fields that you use most:
- phase and iteration.
- parentRunId: set on a continuation.
- messages: a read-only view.
- accumulatedContent: the assistant text for this model turn.
- context: your chat({ context }) value.
Other fields:
- systemPrompts, toolNames, hasTools, modelOptions.
- model and provider, fixed for this request.
- abort(reason) and defer(promise).
A change to ctx.messages does not change the engine config.
beforeModel#
onConfig for this iteration has already run. Prompts, tools, messages, and modelOptions hold the values after that merge.
- accumulatedContent is empty.
- messages is the conversation so far.
afterModel#
The model stream is complete.
- accumulatedContent has the assistant text for this turn.
- messages does not include this turn yet.
- The proposed tool calls are not on ctx, and they are not in messages yet.
If you need tool names or arguments, use beforeTools. You can also watch onChunk during modelStream.
beforeTools#
The engine has added the assistant message with toolCalls to messages. The tools have not run. Read the proposed calls here, before any side effect.
afterTools#
The tools have run. Result messages with role: 'tool' are in messages, and onToolPhaseComplete has already run. Read the results here, before the next model turn.