本页还没有译文
本页 32 段还没译成Tiếng Việt,下面显示的是英文原文。Generic Interrupts
还没翻译译自英文(tanstack.com)本页 32 段都还没译
Use a generic interrupt when the server needs data from the client but no tool call caused the request. For example, ask the user to select a plan before the model runs.
defineInterrupt defines the data that the client sees and the data it must
return. Register the same definition with chat() and useChat(). This gives
both sides the same types.
Define and emit an interrupt
Put the definition in a module that both the route and the client can import. The schemas must export JSON Schema.
// app/interrupts.ts
import { defineInterrupt } from '@tanstack/ai'
import { z } from 'zod'
export const reviewPlan = defineInterrupt({
id: 'review-plan',
payloadSchema: z.object({ title: z.string(), changes: z.array(z.string()) }),
responseSchema: z.object({ approved: z.boolean(), note: z.string().optional() }),
})payloadSchema describes display data that travels from the server to the
client. responseSchema describes data that travels from the client back to
the server. The display payload is optional. The response schema is required.
Return requests from onInterruptBoundary. The hook can run at each value in
INTERRUPT_BOUNDARY_PHASES:
beforeModel: before the adapter callafterModel: after the model stream endsbeforeTools: before tool executionafterTools: after tool results are inmessages
Requests from every middleware in the same boundary form one interrupt batch.
Pick the phase with a ctx.phase guard. If ctx.parentRunId is set, skip
the emit. If you do not, the same pause happens again.
When to use each phase, and what ctx contains there, is in
Lifecycle Boundaries.
// app/chat-middleware.ts
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'] },
}),
],
}
},
}interrupt() accepts only key, reason, message, expiresAt, and, when
declared, payload. It returns an immutable request. Do not put secrets in the
payload because the client receives it and persistence can store it.
Register it on the server
Register all definitions on chat({ interrupts }). A duplicate definition id
fails before the adapter starts.
// app/api/chat/route.ts
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { requestReview } from '../../chat-middleware'
import { reviewPlan } from '../../interrupts'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
...(params.parentRunId ? { parentRunId: params.parentRunId } : {}),
...(params.resume ? { resume: params.resume } : {}),
interrupts: [reviewPlan],
middleware: [requestReview],
})
return toServerSentEventsResponse(stream)
}An interrupt ends the current AG-UI run with one RUN_FINISHED event whose
outcome is interrupt. Resolving it starts a new run with parentRunId set to
the interrupted run. The continuation carries the response to the registered
middleware.
Resolve it in React
Register the same definitions with useChat. A bound generic interrupt has its
definition id, typed display payload, and typed resolveInterrupt method.
Check kind and definitionId. Then pass the item to a card that takes
GenericInterrupt<typeof reviewPlan>.
// app/plan-review.tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import type { GenericInterrupt } from '@tanstack/ai-react'
import { reviewPlan } from './interrupts'
function ReviewCard({
interrupt,
}: {
interrupt: GenericInterrupt<typeof reviewPlan>
}) {
return (
<article>
<h2>{interrupt.payload?.title}</h2>
<button onClick={() => interrupt.resolveInterrupt({ approved: true })}>
Approve
</button>
<button onClick={() => interrupt.cancel()}>Cancel</button>
</article>
)
}
export function PlanReview() {
const { interrupts } = useChat({
threadId: 'release-42',
connection: fetchServerSentEvents('/api/chat'),
interrupts: [reviewPlan],
})
return (
<>
{interrupts.map((interrupt) => {
if (interrupt.kind !== 'generic') return null
if (!('definitionId' in interrupt)) return null
if (interrupt.definitionId !== reviewPlan.id) return null
return <ReviewCard key={interrupt.id} interrupt={interrupt} />
})}
</>
)
}resolveInterrupt stages the answer. The client sends one continuation only
after every bound interrupt in the batch is resolved or cancelled. Use
cancel() when the user declines to provide data.
Read resumed values in middleware
onInterruptResolution does not run in the chat() call that paused. It runs
once at the start of the next chat() call, after the client answers.
That second call is a new run. useChat sends parentRunId and resume.
Each generic resume item carries the original request in metadata. The hook
runs after init onConfig and before onStart. ctx.phase is still 'init'.
for(definition) keeps the response type for that definition. all() reads
all registered definitions. all(definitionA, definitionB) narrows the
result to those definitions.
import type { ChatMiddleware } from '@tanstack/ai'
import { reviewPlan } from './interrupts'
export const applyReview: ChatMiddleware<unknown, typeof reviewPlan> = {
name: 'apply-review',
onInterruptResolution(_ctx, resumedInterrupts) {
for (const result of resumedInterrupts.for(reviewPlan)) {
if (result.status === 'resolved' && !result.response.approved) {
return { toolResume: 'stop' }
}
}
},
}Middleware can return toolResume: 'continue', 'cancel', or 'stop'.
When more than one middleware returns a value, stop wins over cancel, and
cancel wins over continue.
This hook cannot change prompts, tools, or messages. Store the answer on a
capability, then return those fields from onConfig when
ctx.phase === 'beforeModel'. The full order, plus a working example, is in
Apply Answers.
Try the four lifecycle phases
The React chat example has a playground for beforeModel, afterModel,
beforeTools, and afterTools. Each pause shows two typed cards:
reviewPlan and chooseAudience.
- Start
examples/ts-react-chat. - Open
/generic-interrupts. - Pick a phase, resolve both cards, and watch the selected policy.
External generic interrupts
An external system can emit a standard AG-UI generic interrupt. TanStack AI
shows it as kind: 'unbound' unless it has a valid TanStack binding. It stays
visible, but it has no resolve or cancel method. This keeps another system from
receiving a continuation that TanStack AI owns.
See Multiple Interrupts for mixed tool approvals and generic interrupts. See Chat persistence when an interrupt must survive a restart.
| You want to | Page |
|---|---|
Pick beforeModel, afterModel, beforeTools, or afterTools | Lifecycle Boundaries |
Apply the user answer to prompts, tools, or toolResume | Apply Answers |
| Mix generic interrupts with tool approvals | Multiple Interrupts |