feat(interrupts): AG-UI interrupt lifecycle overhaul (ephemeral)#970
feat(interrupts): AG-UI interrupt lifecycle overhaul (ephemeral)#970AlemTuzlak wants to merge 82 commits into
Conversation
…M-event catalog
- Add optional in-band `cursor` to StreamChunk + `cursor` input on chat().
- Add a *replayAndResume() seam: when a cursor is supplied and a ResumeSource
capability is provided, replay the persisted event tail; if the run is still
running and the adapter supports re-attach, continue live. No-op without a
resume source (a non-persisted run is unchanged).
- Move the generic LockStore + LocksCapability ('locks') into core so it is a
single shared token across the sandbox and persistence layers.
- Add ResumeSource capability/contract and a typed catalog of well-known CUSTOM
event names (file.changed, process.*, approval.*, artifact.*, sandbox.*).
…andbox bridge - @tanstack/ai-persistence: store contracts, withPersistence middleware, memoryPersistence, cursor utilities, approval controller, resume-source adapter, history projection. Fully optional; works with and without sandbox. - @tanstack/ai-persistence-sql: one SQL store impl behind a minimal SqlDriver (sqlite|postgres dialect) + versioned migrations + DDL. - Backends: -sqlite (node:sqlite/better-sqlite3), -postgres (pg), -cloudflare (D1, compile-verified), -drizzle and -prisma (BYO). - @tanstack/ai-sandbox-persistence: durable SQL-backed SandboxStore + withPersistenceBridge wiring durable store/locks into withSandbox (agent mode).
- Track the latest in-band cursor per active run; expose getResumeState(), resume(), and maybeAutoResume() with an `autoResume` opt-out (default on). - Pass `cursor` through the connection adapter's RunAgentInput payload so the server can replay a run. streamResponse reuses the original runId on resume.
…ip config - New docs/persistence/overview.md (+ nav entry, addedAt) with server + client + agent-mode usage. - New ai-core/persistence agent skill. - Changeset covering the feature; knip ignore for the optional pg peer dep.
# Conflicts: # docs/media/transcription.md
# Conflicts: # examples/ts-react-chat/src/routes/generations.image.tsx # examples/ts-react-chat/src/routes/generations.video.tsx # testing/e2e/src/routes/$provider/index.tsx
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (1)
testing/e2e/tests/tool-approval.spec.ts (1)
13-13: 📐 Maintainability & Code Quality | 🟡 Minor | 💤 Low valueRestore the suite-title encoding.
The title contains
—instead of the intended em dash—, which degrades Playwright report readability.🛠️ Proposed fix
- test.describe(`${provider} — tool-approval`, () => { + test.describe(`${provider} — tool-approval`, () => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@testing/e2e/tests/tool-approval.spec.ts` at line 13, Restore the intended em dash character in the suite title template within the tool-approval test.describe declaration, replacing the mojibake `—` while preserving the provider interpolation and remaining title text.
🧹 Nitpick comments (3)
packages/ai-react/src/use-chat.ts (1)
421-432: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDead branching in
resolveInterrupts— both arms are identical.The
typeof resolution === 'boolean'check has no effect; both branches callclient.resolveInterrupts(resolution)with no distinction. Simplify by removing the conditional.♻️ Suggested simplification
const resolveInterrupts = useCallback( ( resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined), ) => { - if (typeof resolution === 'boolean') { - client.resolveInterrupts(resolution) - } else { - client.resolveInterrupts(resolution) - } + client.resolveInterrupts(resolution) }, [client], )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-react/src/use-chat.ts` around lines 421 - 432, Remove the redundant typeof resolution conditional in resolveInterrupts and directly call client.resolveInterrupts(resolution), preserving the existing callback signature and client dependency.packages/ai-react/src/types.ts (1)
210-213: 📐 Maintainability & Code Quality | 🔵 TrivialResolver return type
=> undefinedis stricter than typical callback typing.Typing the function-resolver branch as
(interrupt: ChatInterrupt<TTools>) => undefinedforces callers' arrow functions to literally evaluate toundefined; any other return value (even though it's discarded at runtime) will fail type-checking. Consider=> voidinstead, which is the idiomatic TS pattern for callbacks whose return value is intentionally ignored, unless there's a good reason to hard-enforceundefinedhere.♻️ Suggested typing change
resolveInterrupts: { (approved: boolean): void - (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void + (resolver: (interrupt: ChatInterrupt<TTools>) => void): void }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-react/src/types.ts` around lines 210 - 213, Update the function-resolver overload in resolveInterrupts to accept callbacks returning void instead of requiring a literal undefined return. Keep the approved boolean overload unchanged and preserve the existing ChatInterrupt<TTools> parameter type.docs/interrupts/generic.md (1)
14-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPage shows only the client-side flow; no server-side snippet for emitting a generic interrupt.
This page documents both the concept and a full client rendering example for generic interrupts, but never shows how a server route actually emits one (the
responseSchema/interrupt outcome). Per docs guidelines, pages that "cover both server and client behavior" should include snippets for both halves —docs/architecture/approval-flow-processing.mddoes this for tool approvals (Server setup + client UI sections), but this page has no equivalent server example.As per coding guidelines, "When a documentation page covers both server and client behavior, include snippets for both halves: the server endpoint and the client consumption."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/interrupts/generic.md` around lines 14 - 104, Add a concise server-side example before or alongside the client section showing how a server route emits a generic interrupt outcome, including a JSON-compatible responseSchema and interrupt message/reason, then resumes or processes the submitted value server-side. Keep the existing GenericInterruptForm and GenericInterrupts client flow unchanged, and use the established generic interrupt/server API symbols from the codebase rather than inventing a separate protocol.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/reference/functions/toolDefinition.md`:
- Around line 146-159: Mark the new resolveInterrupt example block in the
documentation as non-type-checked by changing its TypeScript fence to the
project’s `ts ignore` convention. Keep the existing branch-specific
resolveInterrupt examples unchanged.
In `@examples/ts-react-chat/src/routes/generations.structured-chat.tsx`:
- Line 243: Annotate the recipe variable in the structured chat generation flow
as Partial<Recipe> while retaining the existing part.data, part.partial, and
empty-object fallback behavior, so later property access such as recipe.cuisine
remains type-safe.
In `@examples/ts-react-chat/src/routes/generations.structured-output.tsx`:
- Line 236: Restore the type assertion on payload.value.object in the setResult
call within the streaming result update flow, casting it to the state’s expected
PartialResult-compatible type so the unknown StreamChunkPayload value satisfies
SetStateAction<PartialResult | null>.
In `@packages/ai-angular/src/inject-chat.ts`:
- Around line 275-278: Update the pendingInterrupts computed selector to return
interruptState().pendingInterrupts instead of interruptState().interrupts. Keep
the interrupts, interruptErrors, and resuming selectors unchanged so
hasPendingInterrupt and approval-gating flows receive only pending interrupts.
In `@packages/ai-preact/src/use-chat.ts`:
- Around line 58-65: Initialize interruptState from the initial resume snapshot
synchronously, matching resumeState and the Angular/Svelte hooks. Update the
useState initializer around interruptState to derive its value from the newly
constructed client’s getInterruptState() rather than hardcoded empty arrays,
while preserving the existing resuming and interrupt error state.
In `@packages/ai-svelte/tests/create-chat-types.test.ts`:
- Around line 232-233: Remove the duplicate Generic type alias declaration in
the test scope, retaining a single `type Generic = Extract<Interrupt, { kind:
'generic' }>` definition so the file typechecks without changing its usage.
In `@packages/ai/src/activities/chat/tools/approval-schema.ts`:
- Around line 198-205: Update hashSchemaInput so non-JSON-convertible schemas do
not all use the constant standardValidator identity. Preserve the
undefined-schema hash behavior, but for schemas where
schemaToWire(schema).jsonSchema is absent, either reject hashing explicitly or
derive the identity from schema-specific stable metadata such as vendor and a
caller-supplied version/id, ensuring schema changes produce different hashes for
interrupt resume drift checks.
In `@packages/ai/src/activities/chat/tools/json-schema-validator.ts`:
- Around line 105-138: Refactor compileJsonSchema202012 to reuse a module-scoped
Ajv2020 instance with the existing options and full-format registration, rather
than constructing Ajv2020 and calling addFormats on every invocation. Keep
schema compilation and JsonSchemaCompilationError handling within
compileJsonSchema202012, using the shared instance to compile each independent
schema.
In `@packages/ai/src/activities/chat/tools/tool-calls.ts`:
- Around line 455-461: Preserve explicit null tool-argument edits by updating
editedApprovalArgs to return a wrapper such as { value: unknown } when the
approved resolution has its own editedArgs property, including null, and
undefined otherwise. At packages/ai/src/activities/chat/tools/tool-calls.ts
lines 455-461, check for the editedArgs property explicitly; at lines 845-852
and 917-924, replace nullish-coalescing fallback with the wrapper-unwrapping
logic so input is updated whenever an edit exists, including an intentional
null.
---
Duplicate comments:
In `@testing/e2e/tests/tool-approval.spec.ts`:
- Line 13: Restore the intended em dash character in the suite title template
within the tool-approval test.describe declaration, replacing the mojibake `—`
while preserving the provider interpolation and remaining title text.
---
Nitpick comments:
In `@docs/interrupts/generic.md`:
- Around line 14-104: Add a concise server-side example before or alongside the
client section showing how a server route emits a generic interrupt outcome,
including a JSON-compatible responseSchema and interrupt message/reason, then
resumes or processes the submitted value server-side. Keep the existing
GenericInterruptForm and GenericInterrupts client flow unchanged, and use the
established generic interrupt/server API symbols from the codebase rather than
inventing a separate protocol.
In `@packages/ai-react/src/types.ts`:
- Around line 210-213: Update the function-resolver overload in
resolveInterrupts to accept callbacks returning void instead of requiring a
literal undefined return. Keep the approved boolean overload unchanged and
preserve the existing ChatInterrupt<TTools> parameter type.
In `@packages/ai-react/src/use-chat.ts`:
- Around line 421-432: Remove the redundant typeof resolution conditional in
resolveInterrupts and directly call client.resolveInterrupts(resolution),
preserving the existing callback signature and client dependency.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 300877ad-3d76-45c8-be03-b57ded65d2e7
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (268)
.changeset/ag-ui-interrupts.mddocs/architecture/approval-flow-processing.mddocs/config.jsondocs/interrupts/generic.mddocs/interrupts/migration.mddocs/interrupts/multiple.mddocs/interrupts/overview.mddocs/interrupts/tool-approval.mddocs/reference/functions/toolDefinition.mddocs/tools/client-tools.mddocs/tools/tool-approval.mdexamples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.tsexamples/ts-react-chat/.env.exampleexamples/ts-react-chat/README.mdexamples/ts-react-chat/package.jsonexamples/ts-react-chat/src/components/Header.tsxexamples/ts-react-chat/src/lib/guitar-tools.tsexamples/ts-react-chat/src/lib/interrupt-lab/InterruptLabPage.tsxexamples/ts-react-chat/src/lib/interrupt-lab/client-ui.test.tsexamples/ts-react-chat/src/lib/interrupt-lab/client-ui.tsexamples/ts-react-chat/src/lib/interrupt-lab/scenarios.tsexamples/ts-react-chat/src/lib/interrupt-lab/server.test.tsexamples/ts-react-chat/src/lib/interrupt-lab/server.tsexamples/ts-react-chat/src/lib/realtime-tools.tsexamples/ts-react-chat/src/lib/server-audio-adapters.tsexamples/ts-react-chat/src/lib/server-fns.tsexamples/ts-react-chat/src/routeTree.gen.tsexamples/ts-react-chat/src/routes/api.generate.image.tsexamples/ts-react-chat/src/routes/api.generate.video.tsexamples/ts-react-chat/src/routes/api.interrupts.tsexamples/ts-react-chat/src/routes/api.mcp-apps-call.tsexamples/ts-react-chat/src/routes/api.mcp-manual.tsexamples/ts-react-chat/src/routes/api.mcp-pool.tsexamples/ts-react-chat/src/routes/api.structured-output.tsexamples/ts-react-chat/src/routes/api.tanchat.tsexamples/ts-react-chat/src/routes/example.runtime-context.tsxexamples/ts-react-chat/src/routes/generation-hooks.tsxexamples/ts-react-chat/src/routes/generations.audio.tsxexamples/ts-react-chat/src/routes/generations.image.tsxexamples/ts-react-chat/src/routes/generations.speech.tsxexamples/ts-react-chat/src/routes/generations.structured-chat.tsxexamples/ts-react-chat/src/routes/generations.structured-output.tsxexamples/ts-react-chat/src/routes/generations.summarize.tsxexamples/ts-react-chat/src/routes/generations.video.tsxexamples/ts-react-chat/src/routes/image-tool-repro.tsxexamples/ts-react-chat/src/routes/interrupts.tsxexamples/ts-react-chat/src/routes/issue-176-tool-result.tsxexamples/ts-react-chat/src/routes/mcp-apps.tsxexamples/ts-react-chat/src/routes/mcp-demo.tsxexamples/ts-react-chat/src/routes/realtime.tsxexamples/ts-react-chat/src/routes/threads.tsxpackages/ai-acp/tests/compatible.test.tspackages/ai-acp/tests/sandbox-provisioning.test.tspackages/ai-angular/package.jsonpackages/ai-angular/src/inject-chat.tspackages/ai-angular/src/inject-generate-audio.tspackages/ai-angular/src/inject-generate-image.tspackages/ai-angular/src/inject-generate-speech.tspackages/ai-angular/src/inject-generate-video.tspackages/ai-angular/src/inject-generation.tspackages/ai-angular/src/inject-summarize.tspackages/ai-angular/src/inject-transcription.tspackages/ai-angular/src/types.tspackages/ai-angular/tests/inject-audio-recorder.test.tspackages/ai-angular/tests/inject-chat-types.test.tspackages/ai-angular/tests/inject-chat.test.tspackages/ai-angular/tests/inject-generation.test.tspackages/ai-angular/tests/test-utils.tspackages/ai-anthropic/tests/anthropic-adapter.test.tspackages/ai-client/package.jsonpackages/ai-client/src/chat-client.tspackages/ai-client/src/chat-persistence-controller.tspackages/ai-client/src/cleared-stream-tracker.tspackages/ai-client/src/client-persistor.tspackages/ai-client/src/connection-adapters.tspackages/ai-client/src/generation-client.tspackages/ai-client/src/generation-types.tspackages/ai-client/src/index.tspackages/ai-client/src/interrupt-manager.tspackages/ai-client/src/interrupt-recovery-parse.tspackages/ai-client/src/storage-adapters.tspackages/ai-client/src/types.tspackages/ai-client/src/video-generation-client.tspackages/ai-client/tests/chat-client-interrupt-correlation.test.tspackages/ai-client/tests/chat-client-interrupts.test.tspackages/ai-client/tests/chat-client-resume.test.tspackages/ai-client/tests/chat-client.test.tspackages/ai-client/tests/chat-persistence-controller.test.tspackages/ai-client/tests/cleared-stream-tracker.test.tspackages/ai-client/tests/client-persistor.test.tspackages/ai-client/tests/connection-adapters-xhr.test.tspackages/ai-client/tests/generation-client.test.tspackages/ai-client/tests/generation-resume-state.test.tspackages/ai-client/tests/interrupts-types.test-d.tspackages/ai-client/tests/storage-adapters-types.test-d.tspackages/ai-client/tests/storage-adapters.test.tspackages/ai-client/tests/test-utils.tspackages/ai-event-client/src/index.tspackages/ai-event-client/tests/emit.test.tspackages/ai-event-client/tests/generation-events-types.test-d.tspackages/ai-gemini/tests/text-interactions-adapter.test.tspackages/ai-openrouter/tests/openrouter-responses-adapter.test.tspackages/ai-preact/src/types.tspackages/ai-preact/src/use-chat.tspackages/ai-preact/tests/mcp-apps.test.tsxpackages/ai-preact/tests/test-utils.tspackages/ai-preact/tests/use-chat-types.test.tspackages/ai-preact/tests/use-chat.test.tspackages/ai-react/src/types.tspackages/ai-react/src/use-chat.tspackages/ai-react/src/use-generate-audio.tspackages/ai-react/src/use-generate-image.tspackages/ai-react/src/use-generate-speech.tspackages/ai-react/src/use-generate-video.tspackages/ai-react/src/use-generation.tspackages/ai-react/src/use-summarize.tspackages/ai-react/src/use-transcription.tspackages/ai-react/tests/mcp-apps.test.tsxpackages/ai-react/tests/test-utils.tspackages/ai-react/tests/use-chat-fetcher.test.tspackages/ai-react/tests/use-chat-options-probe.test.tspackages/ai-react/tests/use-chat-structured-output.test.tspackages/ai-react/tests/use-chat-types.test.tspackages/ai-react/tests/use-chat.test.tspackages/ai-react/tests/use-generation.test.tspackages/ai-solid/src/types.tspackages/ai-solid/src/use-chat.tspackages/ai-solid/src/use-generate-audio.tspackages/ai-solid/src/use-generate-image.tspackages/ai-solid/src/use-generate-speech.tspackages/ai-solid/src/use-generate-video.tspackages/ai-solid/src/use-generation.tspackages/ai-solid/src/use-summarize.tspackages/ai-solid/src/use-transcription.tspackages/ai-solid/tests/test-utils.tspackages/ai-solid/tests/use-chat-types.test.tspackages/ai-solid/tests/use-chat.test.tspackages/ai-solid/tests/use-generation.test.tspackages/ai-svelte/src/create-chat.svelte.tspackages/ai-svelte/src/create-generate-audio.svelte.tspackages/ai-svelte/src/create-generate-image.svelte.tspackages/ai-svelte/src/create-generate-speech.svelte.tspackages/ai-svelte/src/create-generate-video.svelte.tspackages/ai-svelte/src/create-generation.svelte.tspackages/ai-svelte/src/create-summarize.svelte.tspackages/ai-svelte/src/create-transcription.svelte.tspackages/ai-svelte/src/types.tspackages/ai-svelte/tests/create-chat-types.test.tspackages/ai-svelte/tests/create-generation.test.tspackages/ai-svelte/tests/test-utils.tspackages/ai-svelte/tests/use-chat.test.tspackages/ai-utils/src/base64.tspackages/ai-utils/src/index.tspackages/ai-utils/tests/base64.test.tspackages/ai-vue/src/types.tspackages/ai-vue/src/use-chat.tspackages/ai-vue/src/use-generate-audio.tspackages/ai-vue/src/use-generate-image.tspackages/ai-vue/src/use-generate-speech.tspackages/ai-vue/src/use-generate-video.tspackages/ai-vue/src/use-generation.tspackages/ai-vue/src/use-summarize.tspackages/ai-vue/src/use-transcription.tspackages/ai-vue/tests/test-utils.tspackages/ai-vue/tests/use-chat-types.test.tspackages/ai-vue/tests/use-chat.test.tspackages/ai-vue/tests/use-generation.test.tspackages/ai/package.jsonpackages/ai/skills/ai-core/tool-calling/SKILL.mdpackages/ai/src/activities/chat/index.tspackages/ai/src/activities/chat/mcp/manager.tspackages/ai/src/activities/chat/mcp/types.tspackages/ai/src/activities/chat/messages.tspackages/ai/src/activities/chat/middleware/index.tspackages/ai/src/activities/chat/middleware/types.tspackages/ai/src/activities/chat/stream/processor.tspackages/ai/src/activities/chat/tools/approval-schema.tspackages/ai/src/activities/chat/tools/json-schema-validator.tspackages/ai/src/activities/chat/tools/tool-calls.tspackages/ai/src/activities/chat/tools/tool-definition.tspackages/ai/src/activities/generateAudio/index.tspackages/ai/src/activities/generateImage/index.tspackages/ai/src/activities/generateSpeech/index.tspackages/ai/src/activities/generateTranscription/index.tspackages/ai/src/activities/generateVideo/index.tspackages/ai/src/activities/generation-run.tspackages/ai/src/activities/middleware/index.tspackages/ai/src/activities/middleware/run.tspackages/ai/src/activities/middleware/types.tspackages/ai/src/activities/stream-generation-result.tspackages/ai/src/client.tspackages/ai/src/custom-events.tspackages/ai/src/index.tspackages/ai/src/interrupt-resume.tspackages/ai/src/interrupt-serialization.tspackages/ai/src/interrupts.tspackages/ai/src/stream-to-response.tspackages/ai/src/types.tspackages/ai/src/utilities/chat-params.tspackages/ai/tests/ag-ui-wire.test.tspackages/ai/tests/agent-loop-strategies.test.tspackages/ai/tests/chat-mcp-manager.test.tspackages/ai/tests/chat-params.test.tspackages/ai/tests/chat-structured-output-null-normalization.test.tspackages/ai/tests/chat-structured-output-stream.test.tspackages/ai/tests/chat.test.tspackages/ai/tests/content-guard-middleware.test.tspackages/ai/tests/custom-events.test.tspackages/ai/tests/debug-logging-chat.test.tspackages/ai/tests/devtools-system-prompt-mirror.test.tspackages/ai/tests/extend-adapter.test.tspackages/ai/tests/generation-params.test.tspackages/ai/tests/generation-result-transforms.test.tspackages/ai/tests/generation-run-identity-replay.test.tspackages/ai/tests/interrupts-types.test-d.tspackages/ai/tests/interrupts.test.tspackages/ai/tests/lazy-tool-manager.test.tspackages/ai/tests/message-updaters.test.tspackages/ai/tests/messages.test.tspackages/ai/tests/middleware.test.tspackages/ai/tests/middlewares/otel.test.tspackages/ai/tests/multimodal-tool-result.test.tspackages/ai/tests/strategies.test.tspackages/ai/tests/stream-generation.test.tspackages/ai/tests/stream-processor.test.tspackages/ai/tests/stream-to-response.test.tspackages/ai/tests/summarize-max-length.test.tspackages/ai/tests/system-prompts.test.tspackages/ai/tests/test-utils.tspackages/ai/tests/tool-cache-middleware.test.tspackages/ai/tests/tool-call-manager.test.tspackages/ai/tests/tool-calls-null-input.test.tspackages/ai/tests/tool-definition.test.tspackages/ai/tests/tool-result.test.tspackages/ai/tests/type-check.test.tspackages/ai/tests/usage-cost-types.test.tspnpm-workspace.yamltesting/e2e/src/components/AudioGenUI.tsxtesting/e2e/src/components/ChatUI.tsxtesting/e2e/src/components/ImageGenUI.tsxtesting/e2e/src/components/TTSUI.tsxtesting/e2e/src/components/TranscriptionUI.tsxtesting/e2e/src/components/VideoGenUI.tsxtesting/e2e/src/lib/feature-support.tstesting/e2e/src/lib/features.tstesting/e2e/src/lib/media-providers.tstesting/e2e/src/lib/types.tstesting/e2e/src/routes/$provider/$feature.tsxtesting/e2e/src/routes/$provider/index.tsxtesting/e2e/src/routes/api.audio.stream.tstesting/e2e/src/routes/api.audio.tstesting/e2e/src/routes/api.chat.tstesting/e2e/src/routes/api.image.stream.tstesting/e2e/src/routes/api.image.tstesting/e2e/src/routes/api.middleware-test.tstesting/e2e/src/routes/api.multimodal-tool-result-wire.tstesting/e2e/src/routes/api.openai-shell-skills-wire.tstesting/e2e/src/routes/api.openrouter-web-tools-wire.tstesting/e2e/src/routes/api.otel-usage.tstesting/e2e/src/routes/api.tools-test.tstesting/e2e/src/routes/api.tts.stream.tstesting/e2e/src/routes/api.tts.tstesting/e2e/src/routes/api.video.stream.tstesting/e2e/src/routes/api.video.tstesting/e2e/src/routes/devtools-generation-hooks.tsxtesting/e2e/src/routes/middleware-test.tsxtesting/e2e/src/routes/tools-test.tsxtesting/e2e/tests/tool-approval.spec.ts
💤 Files with no reviewable changes (4)
- packages/ai-client/src/client-persistor.ts
- packages/ai-client/tests/client-persistor.test.ts
- examples/ts-react-chat/src/routes/realtime.tsx
- packages/ai/src/stream-to-response.ts
| // Both fields are typed via the schema, so `recipe.title` etc. accesses | ||
| // below are checked at compile time. | ||
| const recipe = part.data ?? part.partial ?? ({} as Partial<Recipe>) | ||
| const recipe = part.data ?? part.partial ?? {} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix TypeScript inference for the fallback object.
Removing the as Partial<Recipe> cast is good, but without an explicit type annotation on the variable, TypeScript infers the fallback {} as an empty object type. Accessing properties like recipe.cuisine later will cause a type error under strict mode because {} has no properties.
Explicitly annotating the variable as Partial<Recipe> resolves the type error safely.
💻 Proposed fix
- const recipe = part.data ?? part.partial ?? {}
+ const recipe: Partial<Recipe> = part.data ?? part.partial ?? {}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const recipe = part.data ?? part.partial ?? {} | |
| const recipe: Partial<Recipe> = part.data ?? part.partial ?? {} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/ts-react-chat/src/routes/generations.structured-chat.tsx` at line
243, Annotate the recipe variable in the structured chat generation flow as
Partial<Recipe> while retaining the existing part.data, part.partial, and
empty-object fallback behavior, so later property access such as recipe.cuisine
remains type-safe.
| const interrupts = computed(() => interruptState().interrupts) | ||
| const pendingInterrupts = computed(() => interruptState().interrupts) | ||
| const interruptErrors = computed(() => interruptState().interruptErrors) | ||
| const resuming = computed(() => interruptState().resuming) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
pendingInterrupts selector returns the wrong field.
Line 276 reads interruptState().interrupts instead of interruptState().pendingInterrupts. Per the ChatInterruptState contract (interrupts, pendingInterrupts, interruptErrors, resuming are distinct fields), this makes the exposed pendingInterrupts signal identical to interrupts — consumers gating UI on "still awaiting resolution" interrupts will see already-resolved ones too, breaking approval-gating flows that this PR's ChatUI relies on (hasPendingInterrupt).
🐛 Proposed fix
- const pendingInterrupts = computed(() => interruptState().interrupts)
+ const pendingInterrupts = computed(() => interruptState().pendingInterrupts)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const interrupts = computed(() => interruptState().interrupts) | |
| const pendingInterrupts = computed(() => interruptState().interrupts) | |
| const interruptErrors = computed(() => interruptState().interruptErrors) | |
| const resuming = computed(() => interruptState().resuming) | |
| const interrupts = computed(() => interruptState().interrupts) | |
| const pendingInterrupts = computed(() => interruptState().pendingInterrupts) | |
| const interruptErrors = computed(() => interruptState().interruptErrors) | |
| const resuming = computed(() => interruptState().resuming) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-angular/src/inject-chat.ts` around lines 275 - 278, Update the
pendingInterrupts computed selector to return interruptState().pendingInterrupts
instead of interruptState().interrupts. Keep the interrupts, interruptErrors,
and resuming selectors unchanged so hasPendingInterrupt and approval-gating
flows receive only pending interrupts.
| const [interruptState, setInterruptState] = useState< | ||
| ChatInterruptState<TTools> | ||
| >(() => ({ | ||
| interrupts: EMPTY_INTERRUPTS, | ||
| pendingInterrupts: EMPTY_INTERRUPTS, | ||
| interruptErrors: EMPTY_INTERRUPT_ERRORS, | ||
| resuming: false, | ||
| })) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
interruptState isn't seeded from initialResumeSnapshot, unlike resumeState.
resumeState correctly bootstraps from options.initialResumeSnapshot?.resumeState (line 56), but interruptState is always initialized to empty arrays regardless of a supplied resume snapshot. It only gets corrected once the mount useEffect calls syncResumeState(client) (line 295), so the first render shows no pending interrupts even when one was supplied.
Contrast with packages/ai-angular/tests/inject-chat.test.ts and packages/ai-svelte/tests/use-chat.test.ts (both in this PR): both assert interrupts reflects the staged/error interrupts from initialResumeSnapshot immediately, with no flush()/tick() beforehand — i.e., siblings project this state synchronously, this hook does not.
Based on the Angular and Svelte test expectations for synchronous interrupt-state projection from initialResumeSnapshot (packages/ai-angular/tests/inject-chat.test.ts lines 23-51, packages/ai-svelte/tests/use-chat.test.ts lines 21-48), this hook should seed interruptState the same way, likely by deriving the initial value from the newly constructed client.getInterruptState() rather than a hardcoded empty default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-preact/src/use-chat.ts` around lines 58 - 65, Initialize
interruptState from the initial resume snapshot synchronously, matching
resumeState and the Angular/Svelte hooks. Update the useState initializer around
interruptState to derive its value from the newly constructed client’s
getInterruptState() rather than hardcoded empty arrays, while preserving the
existing resuming and interrupt error state.
| type Generic = Extract<Interrupt, { kind: 'generic' }> | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Duplicate type Generic declaration — compile error.
type Generic = Extract<Interrupt, { kind: 'generic' }> is declared twice in the same scope, which TypeScript rejects as a duplicate identifier and will fail typecheck.
🐛 Proposed fix
type Generic = Extract<Interrupt, { kind: 'generic' }>
- type Generic = Extract<Interrupt, { kind: 'generic' }>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type Generic = Extract<Interrupt, { kind: 'generic' }> | |
| type Generic = Extract<Interrupt, { kind: 'generic' }> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-svelte/tests/create-chat-types.test.ts` around lines 232 - 233,
Remove the duplicate Generic type alias declaration in the test scope, retaining
a single `type Generic = Extract<Interrupt, { kind: 'generic' }>` definition so
the file typechecks without changing its usage.
| export function hashSchemaInput(schema: SchemaInput | undefined): string { | ||
| if (schema === undefined) return digestInterruptJson('undefined') | ||
| const normalized = schemaToWire(schema) | ||
| const identity = normalized.jsonSchema ?? { | ||
| standardValidator: 'unserialized', | ||
| } | ||
| return digestInterruptJson(canonicalInterruptJson(identity)) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
hashSchemaInput collapses all non-JSON-convertible schemas to the same hash, defeating drift detection.
When schemaToWire(schema).jsonSchema is undefined (a StandardSchemaV1 that doesn't implement the JSON Schema extension), the identity used for hashing falls back to the constant { standardValidator: 'unserialized' }. Every such schema — regardless of actual shape — hashes to the exact same sha256:... value.
This is consumed in interrupt-resume.ts to detect schema drift: hashSchemaInput(tool.inputSchema) !== binding.inputSchemaHash (line 409) and hashSchemaInput(tool.outputSchema) !== binding.outputSchemaHash (line 377). For any tool whose input/output schema is a non-JSON-convertible standard schema, these checks can never fire — a tool's input/output schema could change entirely between the interrupt being issued and being resumed, and the "stale" detection would silently pass, letting resume proceed against a mismatched schema.
🛡️ Suggested direction
export function hashSchemaInput(schema: SchemaInput | undefined): string {
if (schema === undefined) return digestInterruptJson('undefined')
const normalized = schemaToWire(schema)
- const identity = normalized.jsonSchema ?? {
- standardValidator: 'unserialized',
- }
+ if (normalized.jsonSchema === undefined) {
+ throw new TypeError(
+ 'Schema drift detection requires a JSON-Schema-convertible SchemaInput.',
+ )
+ }
+ const identity = normalized.jsonSchema
return digestInterruptJson(canonicalInterruptJson(identity))
}Alternatively, incorporate something schema-specific (e.g. vendor + a caller-supplied version/id) into the identity instead of throwing, if rejecting non-convertible schemas outright is too disruptive.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function hashSchemaInput(schema: SchemaInput | undefined): string { | |
| if (schema === undefined) return digestInterruptJson('undefined') | |
| const normalized = schemaToWire(schema) | |
| const identity = normalized.jsonSchema ?? { | |
| standardValidator: 'unserialized', | |
| } | |
| return digestInterruptJson(canonicalInterruptJson(identity)) | |
| } | |
| export function hashSchemaInput(schema: SchemaInput | undefined): string { | |
| if (schema === undefined) return digestInterruptJson('undefined') | |
| const normalized = schemaToWire(schema) | |
| if (normalized.jsonSchema === undefined) { | |
| throw new TypeError( | |
| 'Schema drift detection requires a JSON-Schema-convertible SchemaInput.', | |
| ) | |
| } | |
| const identity = normalized.jsonSchema | |
| return digestInterruptJson(canonicalInterruptJson(identity)) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai/src/activities/chat/tools/approval-schema.ts` around lines 198 -
205, Update hashSchemaInput so non-JSON-convertible schemas do not all use the
constant standardValidator identity. Preserve the undefined-schema hash
behavior, but for schemas where schemaToWire(schema).jsonSchema is absent,
either reject hashing explicitly or derive the identity from schema-specific
stable metadata such as vendor and a caller-supplied version/id, ensuring schema
changes produce different hashes for interrupt resume drift checks.
| export function compileJsonSchema202012( | ||
| schema: unknown, | ||
| ): (value: unknown) => ReadonlyArray<JsonSchemaValidationIssue> { | ||
| assertSupportedSchemaTree(schema) | ||
| const ajv = new Ajv2020({ | ||
| allErrors: true, | ||
| strict: true, | ||
| strictRequired: false, | ||
| allowUnionTypes: true, | ||
| validateFormats: true, | ||
| coerceTypes: false, | ||
| useDefaults: false, | ||
| removeAdditional: false, | ||
| }) | ||
| addFormats(ajv, { mode: 'full' }) | ||
|
|
||
| let validate | ||
| try { | ||
| validate = ajv.compile(schema) | ||
| } catch (cause) { | ||
| throw new JsonSchemaCompilationError('Invalid Draft 2020-12 schema.', { | ||
| cause, | ||
| }) | ||
| } | ||
|
|
||
| return (value) => { | ||
| if (validate(value)) return [] | ||
| return (validate.errors ?? []).map((error) => ({ | ||
| keyword: error.keyword, | ||
| message: error.message ?? 'Schema validation failed.', | ||
| path: issuePath(error), | ||
| })) | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Reuse a single Ajv instance instead of constructing one per call.
compileJsonSchema202012 builds a brand-new Ajv2020 and re-registers all formats (addFormats(..., { mode: 'full' })) on every invocation. This function sits in a hot path — validateInterruptResumeBatch calls it (via validateSchemaValue) up to several times per interrupt item (payload, edited args, tool output, approval envelope), and ai-client's canValidateGeneric calls it on every hydration attempt. Constructing a fresh Ajv instance each time is unnecessary overhead since Ajv instances safely compile multiple independent (anonymous) schemas.
⚡ Suggested fix
+const ajv = new Ajv2020({
+ allErrors: true,
+ strict: true,
+ strictRequired: false,
+ allowUnionTypes: true,
+ validateFormats: true,
+ coerceTypes: false,
+ useDefaults: false,
+ removeAdditional: false,
+})
+addFormats(ajv, { mode: 'full' })
+
export function compileJsonSchema202012(
schema: unknown,
): (value: unknown) => ReadonlyArray<JsonSchemaValidationIssue> {
assertSupportedSchemaTree(schema)
- const ajv = new Ajv2020({
- allErrors: true,
- strict: true,
- strictRequired: false,
- allowUnionTypes: true,
- validateFormats: true,
- coerceTypes: false,
- useDefaults: false,
- removeAdditional: false,
- })
- addFormats(ajv, { mode: 'full' })
-
let validate📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function compileJsonSchema202012( | |
| schema: unknown, | |
| ): (value: unknown) => ReadonlyArray<JsonSchemaValidationIssue> { | |
| assertSupportedSchemaTree(schema) | |
| const ajv = new Ajv2020({ | |
| allErrors: true, | |
| strict: true, | |
| strictRequired: false, | |
| allowUnionTypes: true, | |
| validateFormats: true, | |
| coerceTypes: false, | |
| useDefaults: false, | |
| removeAdditional: false, | |
| }) | |
| addFormats(ajv, { mode: 'full' }) | |
| let validate | |
| try { | |
| validate = ajv.compile(schema) | |
| } catch (cause) { | |
| throw new JsonSchemaCompilationError('Invalid Draft 2020-12 schema.', { | |
| cause, | |
| }) | |
| } | |
| return (value) => { | |
| if (validate(value)) return [] | |
| return (validate.errors ?? []).map((error) => ({ | |
| keyword: error.keyword, | |
| message: error.message ?? 'Schema validation failed.', | |
| path: issuePath(error), | |
| })) | |
| } | |
| } | |
| const ajv = new Ajv2020({ | |
| allErrors: true, | |
| strict: true, | |
| strictRequired: false, | |
| allowUnionTypes: true, | |
| validateFormats: true, | |
| coerceTypes: false, | |
| useDefaults: false, | |
| removeAdditional: false, | |
| }) | |
| addFormats(ajv, { mode: 'full' }) | |
| export function compileJsonSchema202012( | |
| schema: unknown, | |
| ): (value: unknown) => ReadonlyArray<JsonSchemaValidationIssue> { | |
| assertSupportedSchemaTree(schema) | |
| let validate | |
| try { | |
| validate = ajv.compile(schema) | |
| } catch (cause) { | |
| throw new JsonSchemaCompilationError('Invalid Draft 2020-12 schema.', { | |
| cause, | |
| }) | |
| } | |
| return (value) => { | |
| if (validate(value)) return [] | |
| return (validate.errors ?? []).map((error) => ({ | |
| keyword: error.keyword, | |
| message: error.message ?? 'Schema validation failed.', | |
| path: issuePath(error), | |
| })) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai/src/activities/chat/tools/json-schema-validator.ts` around lines
105 - 138, Refactor compileJsonSchema202012 to reuse a module-scoped Ajv2020
instance with the existing options and full-format registration, rather than
constructing Ajv2020 and calling addFormats on every invocation. Keep schema
compilation and JsonSchemaCompilationError handling within
compileJsonSchema202012, using the shared instance to compile each independent
schema.
…rupts
The AG-UI interrupt lifecycle was extracted from a larger persistence stack
and shipped ephemeral-only, but carried persistence-shaped code that is never
exercised without a durable layer (which is not part of this package). Remove it.
Server (@tanstack/ai):
- Drop the never-provided InterruptPersistenceGateway capability
(getInterruptPersistence/provideInterruptPersistence) and its input types
(OpenInterruptBatchInput, CommitInterruptResolutionsInput). The two call
sites now always take the ephemeral branch.
- Remove InterruptCommitResult, the InterruptResumeValidationError.recovery
field, the recovery type guard, and the tanstack:interruptRecovery emission.
Client (@tanstack/ai-client):
- Remove the recovery state machine: getRecoveryState, getPersistedDrafts,
reportRecoveryError, restorePersistedDrafts, recoverInterrupts,
{start,recover}PersistedInterrupts, joinContinuationRun, the persisted-state
branch of applyResumeSnapshot, and the conflict/replay branches of
submitInterruptBatch (now a plain ephemeral submit).
- Delete interrupt-recovery-parse.ts; drop PersistedInterruptDraft and the
ChatContinuationLoader option/type.
Kept as a dormant extension seam for a future persistence layer: the optional
ConnectionAdapter.loadInterruptState hook and its InterruptRecoveryStateV1 /
InterruptRecoveryQuery types.
Docs: trim the dangling withChatPersistence step and the persistence-only
migration checklist / recovery sections to match the ephemeral-only surface.
Runtime behavior is unchanged (persistence was never provided). Verified:
typecheck + lint clean across ai/ai-client/6 framework packages, full unit
suites green, docs links pass, and the interrupt lab round-trip re-verified
against the rebuilt dist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| **Client tools are not on this list.** Running a tool in the browser and | ||
| returning its result is not something you resolve by hand — a tool with a | ||
| `.client()` implementation runs automatically and reports its own result. See | ||
| [Client Tools](../tools/client-tools). Approval is a separate axis: add | ||
| `needsApproval: true` to any tool (server or client) and it pauses on a | ||
| `tool-approval` interrupt first; a client tool then runs automatically once | ||
| approved. |
There was a problem hiding this comment.
Why aren't client tools included? That isn't explained here
Option A slim for #970: restore generation-resume, example/e2e media noise, import-only churn, and unrelated provider test touch-ups to main while keeping the AG-UI interrupt lifecycle (engine, InterruptManager, client chat persistence needed by ChatClient, framework useChat, interrupt lab, docs). Residue snapshot: branch chore/non-interrupt-residue-from-970 (pre-slim tip).
Restore pure import/style test churn to main. Drop dedicated storage-adapter, chat-persistence-controller, and cleared-stream-tracker unit tests from the ephemeral interrupts PR. Thin chat-client-resume to interrupt resume behavior only (drop persistence.server hydrate/persist matrix). Remove unused fake-indexeddb devDependency.
Defer interrupt resume until the parent stream settles so auto-executed client tools can continue. Surface client-tool validation failures, match native CTE reasons, tighten binding types, and lock resume validation with unit tests. Update docs/skill and e2e/example UIs to the bound interrupt API, and link the Interrupts Lab from the chat example nav and home page.
… from interrupts PR Restore main's ChatPersistor for optional message storage and keep ClearedStreamTracker for clear-during-stream. Durable resume adapters (storage-adapters, ChatPersistenceController, client/server persistence options) already live on feat/persistence and are out of the ephemeral interrupt surface.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
docs/architecture/approval-flow-processing.md (1)
139-139: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse
gpt-chat-latestin documentation examples instead ofgpt-5.5.These documentation snippets use the superseded
openaiText('gpt-5.5'). As per coding guidelines and previous reviews, switch them to the latest OpenAI chat model alias based on the adapter'smodel-meta.ts(e.g.,gpt-chat-latest).
docs/architecture/approval-flow-processing.md#L139-L139: replacegpt-5.5withgpt-chat-latest.packages/ai/skills/ai-core/tool-calling/SKILL.md#L78-L78: replacegpt-5.5withgpt-chat-latest.packages/ai/skills/ai-core/tool-calling/SKILL.md#L681-L681: replacegpt-5.5withgpt-chat-latest.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/architecture/approval-flow-processing.md` at line 139, Replace the superseded gpt-5.5 model name with gpt-chat-latest in the documentation examples at docs/architecture/approval-flow-processing.md:139-139, packages/ai/skills/ai-core/tool-calling/SKILL.md:78-78, and packages/ai/skills/ai-core/tool-calling/SKILL.md:681-681, preserving the existing adapter usage.Source: Coding guidelines
🧹 Nitpick comments (1)
testing/e2e/src/routes/tools-test.tsx (1)
464-487: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate lookup/cast logic in Approve and Deny handlers.
Both handlers repeat the exact same
as ReadonlyArray<{...}>cast +.find()lookup for the matchingtool-approvalinterrupt. The cast also erases whatever real (likely discriminated-union) typeinterruptsalready has. Extract a shared helper using a type predicate instead ofasso both call sites stay in sync and TypeScript actually narrows the result.♻️ Proposed refactor
+ function findApprovalInterrupt(toolCallId: string) { + return interrupts.find( + (item): item is Extract<typeof item, { kind: 'tool-approval' }> => + item.kind === 'tool-approval' && item.toolCallId === toolCallId, + ) + } + onClick={() => { const approvalId = tc.approval?.id if (!approvalId || respondedApprovals.current.has(approvalId)) { return } - const interrupt = ( - interrupts as ReadonlyArray<{ - kind: string - toolCallId?: string - resolveInterrupt: (approved: boolean) => void - }> - ).find( - (item) => item.kind === 'tool-approval' && item.toolCallId === tc.id, - ) + const interrupt = findApprovalInterrupt(tc.id) if (!interrupt) return ...(mirror the same change in the Deny handler at Line 511-522)
Also applies to: 506-529
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@testing/e2e/src/routes/tools-test.tsx` around lines 464 - 487, Extract the repeated interrupt lookup from the Approve and Deny handlers into a shared helper near the surrounding tool-approval logic. Have the helper search interrupts for the matching toolCallId and tool-approval kind using a type predicate, preserving the existing narrowed interrupt type without an `as ReadonlyArray` cast; update both handlers to call this helper and retain their existing approval/denial behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@docs/architecture/approval-flow-processing.md`:
- Line 139: Replace the superseded gpt-5.5 model name with gpt-chat-latest in
the documentation examples at
docs/architecture/approval-flow-processing.md:139-139,
packages/ai/skills/ai-core/tool-calling/SKILL.md:78-78, and
packages/ai/skills/ai-core/tool-calling/SKILL.md:681-681, preserving the
existing adapter usage.
---
Nitpick comments:
In `@testing/e2e/src/routes/tools-test.tsx`:
- Around line 464-487: Extract the repeated interrupt lookup from the Approve
and Deny handlers into a shared helper near the surrounding tool-approval logic.
Have the helper search interrupts for the matching toolCallId and tool-approval
kind using a type predicate, preserving the existing narrowed interrupt type
without an `as ReadonlyArray` cast; update both handlers to call this helper and
retain their existing approval/denial behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ce8b6d71-6ade-4bec-a413-dc706126281f
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (23)
docs/architecture/approval-flow-processing.mddocs/config.jsondocs/interrupts/generic.mddocs/interrupts/migration.mddocs/interrupts/multiple.mddocs/tools/client-tools.mdexamples/ts-react-chat/src/components/Header.tsxexamples/ts-react-chat/src/lib/interrupt-lab/InterruptLabPage.tsxexamples/ts-react-chat/src/lib/interrupt-lab/scenarios.tsexamples/ts-react-chat/src/routes/index.tsxpackages/ai-client/src/chat-client.tspackages/ai-client/src/interrupt-manager.tspackages/ai-client/src/types.tspackages/ai-client/tests/chat-client-interrupts.test.tspackages/ai-client/tests/chat-client-resume.test.tspackages/ai/skills/ai-core/tool-calling/SKILL.mdpackages/ai/src/activities/chat/tools/tool-calls.tspackages/ai/src/interrupt-resume.tspackages/ai/src/interrupts.tspackages/ai/tests/interrupt-resume.test.tstesting/e2e/src/components/ChatUI.tsxtesting/e2e/src/routes/$provider/$feature.tsxtesting/e2e/src/routes/tools-test.tsx
🚧 Files skipped from review as they are similar to previous changes (14)
- docs/interrupts/multiple.md
- docs/config.json
- docs/tools/client-tools.md
- docs/interrupts/migration.md
- packages/ai/src/interrupts.ts
- testing/e2e/src/routes/$provider/$feature.tsx
- examples/ts-react-chat/src/lib/interrupt-lab/scenarios.ts
- packages/ai-client/tests/chat-client-interrupts.test.ts
- packages/ai-client/tests/chat-client-resume.test.ts
- packages/ai-client/src/types.ts
- packages/ai/src/interrupt-resume.ts
- packages/ai/src/activities/chat/tools/tool-calls.ts
- packages/ai-client/src/chat-client.ts
- packages/ai-client/src/interrupt-manager.ts
…c lab runId Ephemeral resume now reconstructs client-tool pending items even when the client already wrote tool results into message history for UI. Align generic lab interrupt ids with the terminal provider runId so parentRunId correlates, surface interrupt validation messages in the lab sanitizer, and match the home-page Interrupts tile to other demo tiles.
Resolve conflicts with type-safe tool call events (#452): - Keep both InterruptSubmissionError and ProviderTool imports - Keep interrupt resume deniedToolResults + surface input on declined results - Keep AnyTool-based mergeAgentTools typing with RunAgentResumeItem for resume
SafeToolInput/Output treat schema generics defaulting to undefined as unknown, and MCP manager tests use AnyServerTool so fixtures can carry JSON Schema input without fighting the no-schema ServerTool default.
tombeckenham
left a comment
There was a problem hiding this comment.
I think this is looking good now. I've added a few nit pic comments, and had to fix a few things, but happy to approve
| import { transferTool } from '../tools/transfer' | ||
|
|
||
| export function TransferApprovals() { | ||
| const { interrupts, resuming } = useChat({ |
There was a problem hiding this comment.
You'd probably want to render the chat in this example the approval ui would appear alongside or in the same chat
| setErrors(['Enter valid JSON.']) | ||
| return | ||
| } | ||
| const result = z.fromJSONSchema(interrupt.responseSchema).safeParse(candidate) |
There was a problem hiding this comment.
Couldn't you pass value directly to safeParse?
| schema: unknown, | ||
| ): (value: unknown) => ReadonlyArray<JsonSchemaValidationIssue> { | ||
| assertSupportedSchemaTree(schema) | ||
| const ajv = new Ajv2020({ |
There was a problem hiding this comment.
This might be an expensive operation. The alternative is to initialize at module level, or create a factory but I never love that either. Happy to leave it
| @@ -0,0 +1,138 @@ | |||
| import Ajv2020 from 'ajv/dist/2020.js' | |||
There was a problem hiding this comment.
I wondered whether we could just use zod. Zod is a peer dependency, but to keep it that way you've added arv as a direct dependency. I'm happy to keep it as arv is smaller, but maybe consider if we could just use zod. Alem and I discussed and happy to keep arv
Drop residual Prisma lock entries left from the persistence-era history. Re-resolve only this branch's package.json deltas on top of main's lock.
- Drop unused PublicInterruptBinding and knip ignore for @standard-schema/spec - Clean interrupt-manager shadowing / unnecessary conditionals / async - Include serverPersistence in e2e provider links - Use AnyServerTool for heterogeneous tool arrays in code-mode example
…arness - Clear interrupt/resume state on successful continuation streams even when provider run ids diverge; always clear after a successful interrupt batch - Immediately reflect approval decisions on tool-call message parts (#532) - Hide submitting interrupts from the public list; block sends only on actionable pending/staged interrupts - Register client delete_file needsApproval stub so tools-test hydrates tool-approval interrupts; match resolve by toolCallId/approval id - Only render pending approval prompts in e2e ChatUI
Missing closing brace on the deny-button handler broke prettier parse.
…tovers - rewrite the interrupt guides to lead with the problem then the API: overview explains what pauses a run and how client tools fit; tool-approval covers server and client tools and renders the approval inline in the chat; multiple contrasts per-item vs root-helper resolution; generic frames the application-pause use case - remove stale references to a separately-shipped durable persistence layer from the interrupt, tools, and architecture docs (ephemeral-only in this PR) - address review feedback: explain client tools in the overview, render the chat alongside the approval UI, and validate the generic value directly - refresh docs/config.json updatedAt for the touched pages
Client-tool interrupt paths emit AG-UI MESSAGES_SNAPSHOT from ModelMessages, which rebuild tool-call parts as input-complete without output and wipe the complete/output state the client already applied from TOOL_CALL_END/RESULT. Reconcile snapshots by folding tool-result content into matching tool-call parts (and prefer pre-snapshot complete state when richer).
MESSAGES_SNAPSHOT reconciliation now folds tool-result into the sibling tool-call; update the anchor test to match.
Avoid a mutated outer `changed` flag that control-flow analysis treats as always falsy; detect part identity changes instead.
Replace the prose-only "when an answer is wrong" section in multiple.md with a runnable component that renders item errors and root interruptErrors, gates buttons on status and resuming (not just canResolve), and demonstrates both recovery paths (clearResolution and retryInterrupts).
Add a "Consume the decision on the server" section to tool-approval.md: a server execute snippet reading the (possibly edited) input, and an honest account of the payload branches (reject payload becomes the tool result the model reads; approve payload is decision metadata, not passed to execute, use editedArgs for values the tool needs).
…noble/hashes The library no longer transforms a generic interrupt's wire JSON Schema into a validator or validates the resolved value against it, on the client or the server. Values pass through as-is; the application validates them (e.g. with z.fromJSONSchema on the client and its own check on the server). Validation of a tool's code-authored Standard Schema (approvalSchema / inputSchema) is unchanged. - remove the ajv-based json-schema-validator and the ajv / ajv-formats deps - generic items are always resolvable; canResolve no longer depends on the library being able to compile the wire schema - replace @noble/hashes with a small bundled SHA-256 (same sha256:<hex> wire shape), verified against known vectors; drop the dependency - update the ts-react-chat interrupt lab to validate the generic payload itself with zod, and fix the docs that claimed the library validates
…nctuary Replace the over-built interrupt lab (durable dead-code, error sanitizer, DI harness, debug fetch, capability panels, ~1.1k lines of tests) with a lean, button-driven "Willowbrook Sanctuary" playground: one tool per scenario, wired to chat()/useChat, each triggered by a preset message. Covers server + client tools for approval-only, shared approvalSchema, branch (approve/reject) schemas, and edited args; a generic (app-emitted) interrupt; and batched approvals resolved per-item or via the root resolveInterrupts. Fixes found by clicking through every scenario: - don't force provider tool_choice on a continuation (resume); forcing it made the model re-call the approved tool instead of answering (empty reply). - key the generic interrupt id off the request runId (ctx.runId), not the provider chunk.runId, so the client's parentRunId correlates on resume (the mismatch surfaced as unknown-interrupt). - echo edited args in assignEnclosure output so the continuation reflects them. Add a regression test (api.interrupts.test.ts) covering both fixes, and give the UI a cozier theme with per-card animal avatars.
A denied approval writes a final tool result into history, so the tool call reads as completed and dropped out of the reconstructed pending batch, while the resume batch still references its `approval_` id. That failed the resume as `unknown-interrupt` (every deny broke). Ephemeral reconstruction now recovers `approval_`-referenced calls from history the same way it already recovers finalized `client_tool_` calls. - add a regression in chat.test.ts: resume a denial whose call already has a result in history resolves without RUN_ERROR and skips execution. - fix the sibling batch-validation test to use a Standard Schema input; the earlier ajv removal means raw JSON Schema args are no longer library- validated, so the atomic-batch check now exercises the Standard Schema path.
Extracts the AG-UI interrupts overhaul (from #935, which was stacked on the persistence PR #785) into its own PR targeting
main, excluding the persistence story. Interrupts run fully ephemeral here: resume reconstructs pending tool calls from the client's message history in a fresh child run. A follow-up stacked PR will add durable persistence back on top.What's in scope
RUN_FINISHEDwithoutcome.type === 'interrupt', interrupt descriptors/bindings, ephemeral resume viaparentRunId+resumebatch.interruptManager(hydrate / resolve / batch submit),useChatintegration across frameworks.ts-react-chatinterrupt-lab example for manual testing.Explicitly out of scope (deferred to the stacked persistence PR)
@tanstack/ai-persistence,-drizzle,-prisma,-cloudflare.joinRun, server-side interrupt state fetchers).Notable fixes
parentRunIdso ephemeral resume correlates the interrupted run to its continuation child run.MESSAGES_SNAPSHOTreset the live maps, so the approval UI state still updates.interrupts-v2) e2e routes/fixtures/specs and persistence-era recovery tests; these return with persistence.Testing
tool-approval.spec.tse2e green across all providers (approve continuation, denial, issue Tool name is undefined in messages after executing a tool that needs approval #532 follow-up).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
/interruptsUI flow.Documentation