feat(interrupts): AG-UI interrupt lifecycle overhaul (ephemeral)#970
feat(interrupts): AG-UI interrupt lifecycle overhaul (ephemeral)#970AlemTuzlak wants to merge 80 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
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
examples/ts-react-chat/src/routes/generations.structured-output.tsx (1)
146-153: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRestore the
PartialResultcast
payload.value.objectis stillunknown, sosetResult(payload.value.object)won’t typecheck againstPartialResult | null. Keep the cast or add a real type guard before assigning.🤖 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-output.tsx` around lines 146 - 153, Restore the PartialResult cast when assigning payload.value.object to the result state, or add a type guard that narrows the unknown value to PartialResult before the setResult call. Update the assignment path using StreamChunkPayload while preserving the existing null handling.packages/ai-svelte/src/create-generation.svelte.ts (2)
1-1: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGate the raw
onResultpassthrough behinddisposed, like every other callback.Both files add a
disposedflag this PR and gateonError/onProgress/onChunk/onResultChange/onLoadingChange/onErrorChange/onStatusChange(and, in the video file,onJobCreated/onStatusUpdate/onJobIdChange/onVideoStatusChange) behind it — but the rawonResultpass-through to the user-supplied callback is left unguarded in both. If a generation resolves afterdispose()is called (e.g. component unmounted while a fetch is in flight), the user'sonResultwill still fire despite every other callback being suppressed post-disposal.
packages/ai-svelte/src/create-generation.svelte.ts#L184-186: wrapoptions.onResult?.(r)inif (!disposed).packages/ai-svelte/src/create-generate-video.svelte.ts#L188-190: wrapoptions.onResult?.(r)inif (!disposed).🛡️ Proposed fix
- onResult: ((r: TResult) => options.onResult?.(r)) as ( - result: TResult, - ) => TOutput | null | void, + onResult: ((r: TResult) => { + if (disposed) return + return options.onResult?.(r) + }) as (result: TResult) => TOutput | null | 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-svelte/src/create-generation.svelte.ts` at line 1, Guard the raw onResult callbacks in the generation result handlers with the existing disposed flag. Update the onResult handling in create-generation.svelte.ts and create-generate-video.svelte.ts so options.onResult is invoked only when disposed is false, matching the guards on the other callbacks.
254-292: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winClear
resumeSnapshoton reset
client.reset()clears the client state, but the wrapper still exposes the previousresumeSnapshot, soresumeState,pendingArtifacts, andresultArtifactscan stay stale after a reset. Reset the local snapshot too, or have the client emit a cleared snapshot.🤖 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/src/create-generation.svelte.ts` around lines 254 - 292, Update the reset flow around the returned reset API and local resumeSnapshot state so reset clears resumeSnapshot after invoking client.reset(). Ensure resumeState, pendingArtifacts, and resultArtifacts immediately return their empty/null defaults after reset, while preserving existing reset behavior.
🧹 Nitpick comments (11)
docs/tools/tool-approval.md (3)
282-288: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAvoid non-null assertions (
!) in documentation code samples.As per coding guidelines, documentation code samples must not use type-assertion casts and should type-check by narrowing or validating values. Please remove the non-null assertion or extract it to a local constant outside the closure.
♻️ Proposed refactor
<button key={i} onClick={() => addToolApprovalResponse({ - id: part.approval!.id, + id: part.approval.id, approved: true, }) }🤖 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/tools/tool-approval.md` around lines 282 - 288, Update the onClick handler around addToolApprovalResponse to remove the non-null assertion on part.approval. Narrow or validate part.approval before the closure, using a local constant if needed, while preserving the existing approval ID and approved: true behavior.Source: Coding guidelines
191-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAvoid non-null assertions (
!) in documentation code samples.As per coding guidelines, documentation code samples must not use type-assertion casts and should type-check by narrowing or validating values. The non-null assertion
part.approval!acts as a type assertion.Since
part.approvalis checked in the outerifstatement andpartis not reassigned, TypeScript should be able to narrow it without the!. If TypeScript loses the narrowing inside theonClickclosure, extract the ID to a constant before the return statement:
const approval = part.approval;♻️ Proposed refactor
<button onClick={() => addToolApprovalResponse({ - id: part.approval!.id, + id: part.approval.id, approved: true, }) }🤖 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/tools/tool-approval.md` around lines 191 - 197, Remove the non-null assertion from the approval ID access in the addToolApprovalResponse onClick handler, relying on the surrounding part.approval narrowing. If narrowing is not preserved inside the closure, capture part.approval in a local constant before returning the element and use that validated value.Source: Coding guidelines
201-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAvoid non-null assertions (
!) in documentation code samples.As per coding guidelines, documentation code samples must not use type-assertion casts and should type-check by narrowing or validating values. Please remove the non-null assertion or extract it to a local constant outside the closure.
♻️ Proposed refactor
<button onClick={() => addToolApprovalResponse({ - id: part.approval!.id, + id: part.approval.id, approved: false, }) }🤖 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/tools/tool-approval.md` around lines 201 - 207, Remove the non-null assertion from the onClick handler’s use of part.approval in the addToolApprovalResponse call. Narrow or validate part.approval before accessing its id, extracting a local value outside the closure if needed, while preserving the existing approved: false behavior.Source: Coding guidelines
packages/ai-client/src/generation-client.ts (1)
527-578: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffExtract the shared resume-snapshot persistence lifecycle and coalesce per-chunk writes.
observeResumeSnapshot,completePlainFetcherResumeSnapshot,persistResumeSnapshot,writeResumeSnapshot(and thegetResumeSnapshot/getResumePersistenceError/getStatusgetters) are identical in both clients, so any fix to the queue/persistence logic must be applied in two places and can silently drift. Separately,observeResumeSnapshotenqueues aserverPersistence.setItemwrite on every stream chunk; with last-write-wins semantics the intermediate writes are wasted I/O (notably for high-frequencyVIDEO_STATUSpolling chunks).
packages/ai-client/src/generation-client.ts#L527-L578: move these helpers into a shared module/mixin (e.g. aResumeSnapshotPersisterused by both clients) and coalesce writes (debounce/trailing-write or persist only on meaningful transitions:running→complete/error, artifact/result changes).packages/ai-client/src/video-generation-client.ts#L613-L664: replace the duplicated helpers with the shared implementation so both clients stay in sync.🤖 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-client/src/generation-client.ts` around lines 527 - 578, Extract the duplicated resume-snapshot lifecycle from generation-client.ts#L527-L578 and video-generation-client.ts#L613-L664, including observeResumeSnapshot, completePlainFetcherResumeSnapshot, persistResumeSnapshot, writeResumeSnapshot, and the related getters, into one shared ResumeSnapshotPersister implementation used by both clients. Add trailing-write coalescing or equivalent meaningful-transition persistence so per-chunk updates do not enqueue redundant serverPersistence.setItem calls, while preserving final complete/error snapshots and existing persistence-error behavior; update both sites to delegate to the shared implementation.packages/ai-client/tests/interrupts-types.test-d.ts (1)
162-164: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
toMatchTypeOfis deprecated; prefertoExtend.Per
expect-type's docs,.toMatchTypeOfis deprecated in favor of.toExtend(identical semantics) or.toMatchObjectType(stricter, plain-object-only). Not urgent since removal isn't planned, but new tests shouldn't introduce the deprecated form.♻️ Suggested fix
-expectTypeOf<Extract<Interrupt, { kind: 'tool-approval' }>>().toMatchTypeOf< +expectTypeOf<Extract<Interrupt, { kind: 'tool-approval' }>>().toExtend< ToolApprovalInterrupt<Tools[number]> >()🤖 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-client/tests/interrupts-types.test-d.ts` around lines 162 - 164, Replace the deprecated toMatchTypeOf assertion in the Interrupt type test with toExtend, preserving the existing Extract<Interrupt, { kind: 'tool-approval' }> and ToolApprovalInterrupt<Tools[number]> type relationship.packages/ai-client/tests/generation-resume-state.test.ts (1)
245-258: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest doesn't exercise "explicit stop" scenario despite its name.
Only a
RUN_STARTEDchunk is reduced; there's nofinishReason: 'stop'(or equivalent) chunk in the input, so the assertions aboutcancelled/cancelEndpointpass trivially regardless of whether the reducer actually models explicit stop correctly. This leaves the described guard unverified.♻️ Suggested fix: actually reduce a stop-related chunk
it('does not model explicit stop as durable cancel state', () => { const snapshot = reduceChunks([ { type: EventType.RUN_STARTED, threadId: 'thread-1', runId: 'run-1', timestamp: 1, }, + { + type: EventType.RUN_FINISHED, + threadId: 'thread-1', + runId: 'run-1', + finishReason: 'stop', + timestamp: 2, + }, ]) - expect(snapshot.status).toBe('running') + expect(snapshot.status).toBe('complete') expect(snapshot).not.toHaveProperty('cancelled') expect(snapshot).not.toHaveProperty('cancelEndpoint') })🤖 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-client/tests/generation-resume-state.test.ts` around lines 245 - 258, Update the test around reduceChunks to include a run-completion chunk representing an explicit stop, using the repository’s established finishReason or equivalent stop event shape. Keep the assertions that the resulting snapshot is not marked cancelled and has no cancelEndpoint, so the test verifies reducer behavior rather than only the initial RUN_STARTED state.packages/ai/src/activities/chat/tools/json-schema-validator.ts (1)
105-119: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winReuse a shared Ajv2020 instance
compileJsonSchema202012creates a freshAjv2020and re-registers formats on every validation. This helper is on the interrupt-resume and interrupt-manager validation paths, so the setup cost repeats on each schema check. Move the Ajv instance (andaddFormats) to module scope, or cache compiled validators by schema 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/json-schema-validator.ts` around lines 105 - 119, Update compileJsonSchema202012 to reuse a module-scoped Ajv2020 instance configured with the existing options and registered formats, rather than constructing Ajv2020 and calling addFormats on every invocation. Preserve the current validation behavior and schema compilation flow.packages/ai-angular/src/inject-chat.ts (1)
279-287: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRedundant if/else — both branches are identical.
resolveInterruptsbranches ontypeof resolution === 'boolean'but both arms callclient.resolveInterrupts(resolution)with no difference. Sinceclient.resolveInterruptsis already overloaded to accept bothbooleanand resolver-function forms, this wrapper can just forward the argument directly.♻️ Proposed simplification
const resolveInterrupts = ( resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined), ) => { - if (typeof resolution === 'boolean') { - client.resolveInterrupts(resolution) - } else { - client.resolveInterrupts(resolution) - } + client.resolveInterrupts(resolution) }🤖 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 279 - 287, Simplify resolveInterrupts by removing the redundant typeof-based if/else and directly forwarding resolution to client.resolveInterrupts; preserve support for both boolean and resolver-function inputs through the existing overloads.packages/ai-react/src/types.ts (1)
204-225: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd JSDoc for the new interrupt/resume return fields.
Every other member of
BaseUseChatReturnhas a doc comment; this new block (resumeState,interrupts,interruptErrors,resuming,resolveInterrupts,cancelInterrupts,retryInterrupts,resumeInterruptsUnsafe) has none except the two@deprecatedaliases. This is a sizable new public API surface for consumers to discover without editor tooltips.🤖 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 204 - 225, Add concise JSDoc comments to each new public member in BaseUseChatReturn: resumeState, interrupts, interruptErrors, resuming, resolveInterrupts, cancelInterrupts, retryInterrupts, and resumeInterruptsUnsafe. Describe each member’s purpose and usage, while preserving the existing deprecation comments for pendingInterrupts and resumeInterrupts.packages/ai-vue/src/use-generate-video.ts (1)
45-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winInconsistent with sibling composables: doesn't derive
persistence/initialResumeSnapshot/resume fields viaPick/Omit.
use-generate-image.ts,use-generate-speech.ts, anduse-transcription.tsderivepersistence/initialResumeSnapshotviaPick<UseGenerationOptions<...>, ...>and derive resume/artifact return fields viaOmit<UseGenerationReturn<TOutput>, 'generate'>. This file duplicates the same fields manually, risking type drift ifGenerationClientOptions/UseGenerationReturnchange later.♻️ Suggested alignment with sibling composables
+import type { + UseGenerationOptions, + UseGenerationReturn, +} from './use-generation' + -export interface UseGenerateVideoOptions<TOutput = VideoGenerateResult> { +export interface UseGenerateVideoOptions< + TOutput = VideoGenerateResult, +> extends Pick< + UseGenerationOptions<VideoGenerateInput, VideoGenerateResult, TOutput>, + 'persistence' | 'initialResumeSnapshot' +> { /** Connect-based adapter for streaming transport (server handles polling) */ connection?: ConnectConnectionAdapter ... - /** Server-side lightweight generation state persistence. */ - persistence?: GenerationPersistenceOptions - /** Initial lightweight resume snapshot restored by the app (read-only state). */ - initialResumeSnapshot?: GenerationResumeSnapshot ... } -export interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> { +export interface UseGenerateVideoReturn< + TOutput = VideoGenerateResult, +> extends Omit<UseGenerationReturn<TOutput>, 'generate'> { ... - resumeSnapshot: DeepReadonly<ShallowRef<GenerationResumeSnapshot | undefined>> - resumeState: DeepReadonly<ShallowRef<GenerationResumeState | null>> - pendingArtifacts: DeepReadonly<ShallowRef<Array<GenerationPendingArtifact>>> - resultArtifacts: DeepReadonly<ShallowRef<Array<PersistedArtifactRef>>> }Also applies to: 93-100
🤖 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-vue/src/use-generate-video.ts` around lines 45 - 48, Align useGenerateVideo’s option and return types with the sibling composables by deriving persistence and initialResumeSnapshot through Pick<UseGenerationOptions<...>, ...>, and deriving resume/artifact return fields through Omit<UseGenerationReturn<TOutput>, 'generate'>. Remove the duplicated manual field declarations while preserving the existing generate API and video-specific types.packages/ai-react/src/use-generation.ts (1)
240-243: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the resume-field derivation to avoid duplication with
use-generate-video.ts.The
resumeState/pendingArtifacts/resultArtifactsderivation fromresumeSnapshothere is byte-for-byte duplicated inuse-generate-video.ts(which has its own independent client-construction path rather than delegating touseGeneration). A shared helper (e.g.deriveResumeFields(snapshot)in a small shared module) would keep both in sync ifGenerationResumeSnapshot's shape evolves.♻️ Proposed shared helper
+function deriveResumeFields<TArtifact>( + snapshot: GenerationResumeSnapshot | undefined, +): { + resumeState: GenerationResumeState | null + pendingArtifacts: Array<GenerationPendingArtifact> + resultArtifacts: Array<TArtifact> +} { + return { + resumeState: snapshot?.resumeState ?? null, + pendingArtifacts: snapshot?.pendingArtifacts ?? [], + resultArtifacts: (snapshot?.result?.artifacts ?? []) as Array<TArtifact>, + } +}🤖 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-generation.ts` around lines 240 - 243, Extract the duplicated resumeSnapshot field derivation from the current hook and use-generate-video.ts into a shared helper such as deriveResumeFields(snapshot). Update both client-construction paths to call the helper while preserving the existing null and empty-array defaults for resumeState, pendingArtifacts, and resultArtifacts.
🤖 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/architecture/approval-flow-processing.md`:
- Around line 137-145: Update both documentation
snippets—docs/architecture/approval-flow-processing.md lines 137-145 and
docs/interrupts/tool-approval.md lines 72-80—to use the gpt-chat-latest alias in
their openaiText calls instead of gpt-5.5; no other example behavior should
change.
In `@docs/config.json`:
- Around line 104-112: Update the timestamp metadata for all pages modified or
added in this change, including the entries represented by the visible Tool
Approval Flow object and the referenced locations, to use "2026-07-20" for
updatedAt and, for newly added pages, addedAt as well.
In `@packages/ai-client/tests/chat-client-interrupts.test.ts`:
- Line 1: Re-save packages/ai-client/tests/chat-client-interrupts.test.ts as
UTF-8 without a BOM and replace the mojibake em-dashes in comments at lines 148,
328, and 395; likewise remove the leading BOM from
packages/ai-client/tests/chat-client-resume.test.ts before its import statement.
In `@packages/ai-solid/src/use-chat.ts`:
- Around line 331-334: Update resumeInterruptsUnsafe to await the underlying
client().resumeInterruptsUnsafe call, then invoke syncResumeState() after it
settles, matching the other resume/interrupt-affecting methods and keeping its
existing arguments and return behavior.
In `@packages/ai/src/activities/chat/index.ts`:
- Around line 3307-3328: Update applyResumeToolState to merge
state.genericInterrupts into the corresponding resume-state collection,
preserving each interrupt’s identifier and resolution like the existing
approvals, clientToolResults, and deniedToolResults handling. Ensure
reconstructed generic-interrupt resume resolutions are retained.
In `@testing/e2e/src/components/ChatUI.tsx`:
- Around line 36-38: Update the Send button’s disabled condition in ChatUI to
include hasPendingInterrupt, so it is disabled when an interrupt is pending or
the input is empty. Keep handleSubmit’s existing no-op guard and apply the same
pending-interrupt state consistently to the other affected input controls.
In `@testing/e2e/src/routes/`$provider/$feature.tsx:
- Around line 122-145: Update parsePendingInterrupts to validate every array
entry and throw a TypeError when an entry is not a record or lacks string
id/reason, matching deserializeMessages and deserializeResumeSnapshot behavior.
Preserve the existing normalization of valid toolCallId and metadata fields, but
remove the silent continue paths so malformed persisted data cannot produce a
partial list.
In `@testing/e2e/tests/tool-approval.spec.ts`:
- Line 13: Update the suite title in the tool-approval test.describe declaration
to use the intended em dash character (—) instead of the mojibake sequence
(—); preserve the existing provider interpolation and title text.
---
Outside diff comments:
In `@examples/ts-react-chat/src/routes/generations.structured-output.tsx`:
- Around line 146-153: Restore the PartialResult cast when assigning
payload.value.object to the result state, or add a type guard that narrows the
unknown value to PartialResult before the setResult call. Update the assignment
path using StreamChunkPayload while preserving the existing null handling.
In `@packages/ai-svelte/src/create-generation.svelte.ts`:
- Line 1: Guard the raw onResult callbacks in the generation result handlers
with the existing disposed flag. Update the onResult handling in
create-generation.svelte.ts and create-generate-video.svelte.ts so
options.onResult is invoked only when disposed is false, matching the guards on
the other callbacks.
- Around line 254-292: Update the reset flow around the returned reset API and
local resumeSnapshot state so reset clears resumeSnapshot after invoking
client.reset(). Ensure resumeState, pendingArtifacts, and resultArtifacts
immediately return their empty/null defaults after reset, while preserving
existing reset behavior.
---
Nitpick comments:
In `@docs/tools/tool-approval.md`:
- Around line 282-288: Update the onClick handler around addToolApprovalResponse
to remove the non-null assertion on part.approval. Narrow or validate
part.approval before the closure, using a local constant if needed, while
preserving the existing approval ID and approved: true behavior.
- Around line 191-197: Remove the non-null assertion from the approval ID access
in the addToolApprovalResponse onClick handler, relying on the surrounding
part.approval narrowing. If narrowing is not preserved inside the closure,
capture part.approval in a local constant before returning the element and use
that validated value.
- Around line 201-207: Remove the non-null assertion from the onClick handler’s
use of part.approval in the addToolApprovalResponse call. Narrow or validate
part.approval before accessing its id, extracting a local value outside the
closure if needed, while preserving the existing approved: false behavior.
In `@packages/ai-angular/src/inject-chat.ts`:
- Around line 279-287: Simplify resolveInterrupts by removing the redundant
typeof-based if/else and directly forwarding resolution to
client.resolveInterrupts; preserve support for both boolean and
resolver-function inputs through the existing overloads.
In `@packages/ai-client/src/generation-client.ts`:
- Around line 527-578: Extract the duplicated resume-snapshot lifecycle from
generation-client.ts#L527-L578 and video-generation-client.ts#L613-L664,
including observeResumeSnapshot, completePlainFetcherResumeSnapshot,
persistResumeSnapshot, writeResumeSnapshot, and the related getters, into one
shared ResumeSnapshotPersister implementation used by both clients. Add
trailing-write coalescing or equivalent meaningful-transition persistence so
per-chunk updates do not enqueue redundant serverPersistence.setItem calls,
while preserving final complete/error snapshots and existing persistence-error
behavior; update both sites to delegate to the shared implementation.
In `@packages/ai-client/tests/generation-resume-state.test.ts`:
- Around line 245-258: Update the test around reduceChunks to include a
run-completion chunk representing an explicit stop, using the repository’s
established finishReason or equivalent stop event shape. Keep the assertions
that the resulting snapshot is not marked cancelled and has no cancelEndpoint,
so the test verifies reducer behavior rather than only the initial RUN_STARTED
state.
In `@packages/ai-client/tests/interrupts-types.test-d.ts`:
- Around line 162-164: Replace the deprecated toMatchTypeOf assertion in the
Interrupt type test with toExtend, preserving the existing Extract<Interrupt, {
kind: 'tool-approval' }> and ToolApprovalInterrupt<Tools[number]> type
relationship.
In `@packages/ai-react/src/types.ts`:
- Around line 204-225: Add concise JSDoc comments to each new public member in
BaseUseChatReturn: resumeState, interrupts, interruptErrors, resuming,
resolveInterrupts, cancelInterrupts, retryInterrupts, and
resumeInterruptsUnsafe. Describe each member’s purpose and usage, while
preserving the existing deprecation comments for pendingInterrupts and
resumeInterrupts.
In `@packages/ai-react/src/use-generation.ts`:
- Around line 240-243: Extract the duplicated resumeSnapshot field derivation
from the current hook and use-generate-video.ts into a shared helper such as
deriveResumeFields(snapshot). Update both client-construction paths to call the
helper while preserving the existing null and empty-array defaults for
resumeState, pendingArtifacts, and resultArtifacts.
In `@packages/ai-vue/src/use-generate-video.ts`:
- Around line 45-48: Align useGenerateVideo’s option and return types with the
sibling composables by deriving persistence and initialResumeSnapshot through
Pick<UseGenerationOptions<...>, ...>, and deriving resume/artifact return fields
through Omit<UseGenerationReturn<TOutput>, 'generate'>. Remove the duplicated
manual field declarations while preserving the existing generate API and
video-specific types.
In `@packages/ai/src/activities/chat/tools/json-schema-validator.ts`:
- Around line 105-119: Update compileJsonSchema202012 to reuse a module-scoped
Ajv2020 instance configured with the existing options and registered formats,
rather than constructing Ajv2020 and calling addFormats on every invocation.
Preserve the current validation behavior and schema compilation flow.
🪄 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/tests/client-persistor.test.ts
- packages/ai-client/src/client-persistor.ts
- packages/ai/src/stream-to-response.ts
- examples/ts-react-chat/src/routes/realtime.tsx
| const stream = chat({ | ||
| adapter: openaiText('gpt-5.5'), | ||
| messages: params.messages, | ||
| threadId: params.threadId, | ||
| runId: params.runId, | ||
| parentRunId: params.parentRunId, | ||
| ...(params.resume ? { resume: params.resume } : {}), | ||
| tools: [deleteProject], | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the OpenAI adapter model-meta file and list its models
fd 'model-meta.ts' packages/ai-openai --exec cat {}Repository: TanStack/ai
Length of output: 50367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n-- docs/architecture/approval-flow-processing.md --\n'
sed -n '125,155p' docs/architecture/approval-flow-processing.md
printf '\n-- docs/interrupts/tool-approval.md --\n'
sed -n '60,90p' docs/interrupts/tool-approval.md
printf '\n-- OpenAI model entries around gpt-5.5 --\n'
rg -n "GPT_5_5|GPT_CHAT_LATEST|GPT_5_5_PRO|openaiText\\('gpt-5.5'\\)" packages -nRepository: TanStack/ai
Length of output: 8027
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('packages/ai-openai/src/model-meta.ts')
text = p.read_text()
for key in ["const GPT_5_5 =", "const GPT_CHAT_LATEST =", "export const OPENAI_CHAT_MODELS = ["]:
idx = text.find(key)
print(f"\n### {key} @ {idx}")
if idx != -1:
start = max(0, idx-250)
end = min(len(text), idx+900)
print(text[start:end])
PYRepository: TanStack/ai
Length of output: 3717
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [Path('docs/architecture/approval-flow-processing.md'), Path('docs/interrupts/tool-approval.md')]:
print(f"\n## {path}")
text = path.read_text().splitlines()
for i, line in enumerate(text, 1):
if "gpt-5.5" in line or "openaiText(" in line:
for j in range(max(1, i-3), min(len(text), i+4)+1):
print(f"{j:4}: {text[j-1]}")
print()
PYRepository: TanStack/ai
Length of output: 904
Use gpt-chat-latest in both snippets
Both docs examples still use openaiText('gpt-5.5'); switch them to the latest OpenAI chat model alias.
📍 Affects 2 files
docs/architecture/approval-flow-processing.md#L137-L145(this comment)docs/interrupts/tool-approval.md#L72-L80
🤖 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` around lines 137 - 145, Update
both documentation snippets—docs/architecture/approval-flow-processing.md lines
137-145 and docs/interrupts/tool-approval.md lines 72-80—to use the
gpt-chat-latest alias in their openaiText calls instead of gpt-5.5; no other
example behavior should change.
Source: Coding guidelines
| "addedAt": "2026-04-15", | ||
| "updatedAt": "2026-07-08" | ||
| "updatedAt": "2026-07-14" | ||
| }, | ||
| { | ||
| "label": "Tool Approval Flow", | ||
| "to": "tools/tool-approval", | ||
| "addedAt": "2026-04-15", | ||
| "updatedAt": "2026-07-08" | ||
| "updatedAt": "2026-07-14" | ||
| }, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update timestamps to today's date.
As per coding guidelines, when making substantive content changes or adding new pages, you must set or refresh the timestamps to today's date. The current date is July 20, 2026, but the updatedAt and addedAt fields modified in this PR are set to past dates (e.g., 2026-07-14, 2026-07-15, 2026-07-16, etc.).
Please update the modified updatedAt fields (and addedAt for newly added pages) to "2026-07-20".
Also applies to: 147-149, 181-183, 212-214, 361-363, 394-396, 437-439, 506-508, 874-876
🤖 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/config.json` around lines 104 - 112, Update the timestamp metadata for
all pages modified or added in this change, including the entries represented by
the visible Tool Approval Flow object and the referenced locations, to use
"2026-07-20" for updatedAt and, for newly added pages, addedAt as well.
Source: Coding guidelines
| const resumeInterruptsUnsafe = ( | ||
| resumeItems: Array<RunAgentResumeItem>, | ||
| state?: ChatResumeState, | ||
| ) => client().resumeInterruptsUnsafe(resumeItems, state) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
resumeInterruptsUnsafe doesn't resync resumeState/interrupt signals after resolving.
Every other resume/interrupt-affecting async method in this file (sendMessage, append, reload, addToolApprovalResponse, and even the deprecated resumeInterrupts alias at Line 304-311) calls syncResumeState() after the underlying client call settles. resumeInterruptsUnsafe — the recommended non-deprecated API per the JSDoc on its deprecated sibling — skips this, so resumeState(), interrupts(), interruptErrors(), and resuming() can remain stale after a resume until an unrelated state change triggers a resync.
🐛 Proposed fix
const resumeInterruptsUnsafe = (
resumeItems: Array<RunAgentResumeItem>,
state?: ChatResumeState,
- ) => client().resumeInterruptsUnsafe(resumeItems, state)
+ ) => client().resumeInterruptsUnsafe(resumeItems, state).finally(syncResumeState)📝 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 resumeInterruptsUnsafe = ( | |
| resumeItems: Array<RunAgentResumeItem>, | |
| state?: ChatResumeState, | |
| ) => client().resumeInterruptsUnsafe(resumeItems, state) | |
| const resumeInterruptsUnsafe = ( | |
| resumeItems: Array<RunAgentResumeItem>, | |
| state?: ChatResumeState, | |
| ) => client().resumeInterruptsUnsafe(resumeItems, state).finally(syncResumeState) |
🤖 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-solid/src/use-chat.ts` around lines 331 - 334, Update
resumeInterruptsUnsafe to await the underlying client().resumeInterruptsUnsafe
call, then invoke syncResumeState() after it settles, matching the other
resume/interrupt-affecting methods and keeping its existing arguments and return
behavior.
| /** Block new input while pending interrupts await resolution. */ | ||
| hasPendingInterrupt?: boolean | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Send button stays enabled while hasPendingInterrupt is true.
handleSubmit correctly no-ops when hasPendingInterrupt, but the Send button's disabled={!input.trim()} (line 287) doesn't factor in the new flag, so it remains clickable. Since this component drives e2e assertions, a spec checking for a disabled Send button during a pending interrupt would fail even though submission is safely blocked.
🛠️ Suggested fix
- disabled={!input.trim()}
+ disabled={!input.trim() || hasPendingInterrupt}Also applies to: 52-52, 64-69
🤖 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/components/ChatUI.tsx` around lines 36 - 38, Update the Send
button’s disabled condition in ChatUI to include hasPendingInterrupt, so it is
disabled when an interrupt is pending or the input is empty. Keep handleSubmit’s
existing no-op guard and apply the same pending-interrupt state consistently to
the other affected input controls.
| @@ -10,7 +10,7 @@ import { | |||
| import { providersFor } from './test-matrix' | |||
|
|
|||
| for (const provider of providersFor('tool-approval')) { | |||
| test.describe(`${provider} — tool-approval`, () => { | |||
| test.describe(`${provider} — tool-approval`, () => { | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Restore 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`, () => {📝 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.
| 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, Update the suite title
in the tool-approval test.describe declaration to use the intended em dash
character (—) instead of the mojibake sequence (—); preserve the existing
provider interpolation and title text.
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
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