feat: split persistence into state (middleware) + delivery (transport/durable-streams) durability#923
Conversation
…sport, remove event-log/cursor/resume seam
…onformance suite; remove delivery stores
…p cursor & generation delivery-resume
…erated API reference
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
🚀 Changeset Version Preview14 package(s) bumped directly, 35 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 254ea03
☁️ Nx Cloud last updated this comment at |
@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-persistence
@tanstack/ai-persistence-drizzle
@tanstack/ai-persistence-prisma
@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: |
The docs/reference/** API docs are auto-generated (pnpm generate-docs) and regenerated in dedicated PRs; keeping that churn out of this feature PR. Restored to the pre-PR base.
* feat(ai): cursor + chat() resume seam, shared locks capability, CUSTOM-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.*).
* feat(persistence): @tanstack/ai-persistence + SQL core + backends + sandbox 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).
* feat(ai-client): in-session auto-resume
- 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.
* docs(persistence): overview page, ai-persistence skill, changeset, knip 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.
* ci: apply automated fixes
* feat: finalize ai persistence
* feat: add mysql persistence chat example
* fix: quote mysql usage column
* feat: add cloudflare r2 artifact persistence
* feat: add cloudflare blob persistence primitives
* docs: split persistence guide by scenario
* feat(persistence): client resume + framework use-chat persistence updates
* feat(sandbox): consume persistence without bridge
* feat(persistence): add orm migration CLIs
* feat(sandbox): persist managed workspace files
* fix(ai): tolerate seed messages without parts
* docs(persistence): type-check persistence examples
* fix(client): preserve pending interrupts on mount
* ci: apply automated fixes
* feat: add generation persistence and resumability
* ci: apply automated fixes
* fix: address persistence ci failures
* ci: apply automated fixes
* fix(persistence): default migrations to opt-in
* feat(persistence): add SQL migration CLI docs
* docs(persistence): reorganize persistence guides
* fix(persistence): document internals and r2 prefixes
* Use shared base64 utils
* feat(persistence): split chat and generation middleware
Replace the dual-typed withPersistence cast with honestly typed
withChatPersistence (ChatMiddleware) and withGenerationPersistence
(GenerationMiddleware), sharing feature/run store helpers.
Update callers, tests, docs, skills, and changeset. Document that
runIds must stay unique across activities when both middlewares share
one AIPersistence backend, and how default run ids are allocated.
* ci: apply automated fixes
* feat: split persistence into state (middleware) + delivery (transport/durable-streams) durability (#923)
* feat(ai)!: split state vs delivery durability — StreamDurability transport, remove event-log/cursor/resume seam
* feat(ai-durable-stream): durable-streams protocol StreamDurability adapter
* feat(persistence)!: state-only middleware + memory backend + shared conformance suite; remove delivery stores
* feat(persistence)!: drizzle + prisma state backends; remove SQL-driver sprawl
* feat(client, frameworks)!: resumable SSE + interrupt-only resume; drop cursor & generation delivery-resume
* docs(persistence): state vs delivery durability guides, skills, regenerated API reference
* chore(persistence): migrate examples + e2e; changeset; workspace/knip config
* ci: apply automated fixes
* docs: drop regenerated API reference from this PR
The docs/reference/** API docs are auto-generated (pnpm generate-docs) and
regenerated in dedicated PRs; keeping that churn out of this feature PR.
Restored to the pre-PR base.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* refactor(ai)!: group batch under durability option ({ adapter, batch })
Transport helpers now take durability: { adapter, batch? } instead of
sibling durability + batch props. batch is a property of how the durability
sink buffers, not a peer of the response init.
* feat: add persistence and delivery durability
* docs: design full AG-UI interrupt support
* docs: plan full AG-UI interrupt support
* feat(ai-persistence-drizzle): user-owned schemas via injection + schema-emit CLI
Let projects own the TanStack AI Drizzle schema instead of the bundled copy:
- drizzlePersistence(db, { schema }) — stores now operate over injected table
objects, so renamed tables/columns (incl. drizzle `casing` conventions) and
extra app-owned columns (e.g. a userId on messages) work through the same
code paths. TanstackAiSchema pins only column data shapes; construction
validates tables/columns and throws DrizzleSchemaError with per-item detail.
- tanstack-ai-drizzle-schema CLI (--out/--stdout/--force) emits the schema
module into the project so the user's own drizzle-kit journal owns the DDL
(verified: drizzle-kit generate on the emitted file reproduces the bundled
migration exactly).
- Conformance suite runs against a fully renamed variant schema with an
app-owned ownership column; sync test pins the emitted asset structurally
identical to the bundled schema; type-level tests cover assignability.
- Docs: drizzle.md "Own the schema", migrations.md cross-ref, persistence
SKILL.md; package contract tests updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt
* feat: implement AG-UI interrupt lifecycle
* test: allow slow persistence integration setup
* feat(ai-persistence-prisma): renameable models via delegate mapping
Let applications rename the TanStack AI models in their copy of the fragment
(e.g. to avoid collisions with an existing Message/Run model) and map each
store to the renamed client delegate:
prismaPersistence(prisma, { models: { messages: 'chatMessage' } })
- New model-contract.ts: minimal structural delegate interfaces the stores
operate over (field surface pinned, delegate names free), PrismaModelMap,
and construction-time validation throwing PrismaModelError with per-store
detail. Stores refactored to consume delegates instead of PrismaClient.
- Bytes inputs pinned to Uint8Array<ArrayBuffer> (what Prisma generates);
blob.upsert result typed unknown with a re-read, matching the Drizzle
sibling, so user-generated clients stay assignable.
- Conformance suite runs through a fully renamed client; type tests assert
the generated delegates satisfy the structural contract; validation and
partial-map tests added.
- Docs: prisma.md "Rename the models" section; persistence SKILL.md note.
Prisma counterpart of the Drizzle schema-injection change (659464a): DB
names were already free via @map/@@Map and generation already ships via the
models CLI — client-level model names were the remaining hardcoded surface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt
* fix: support interrupts without persistence
* feat: add interrupt testing labs
* fix: complete interrupt resume workflows
* docs: split interrupts into its own section
Interrupts is a large feature; move it out of "Chat & Streaming" into a
dedicated top-level Interrupts section and rewrite the content to be
succinct and example-first with real, rendered React components.
- New docs/interrupts/ pages: overview, tool-approval, multiple, generic,
client-tool-execution, persistence, migration
- Remove the monolithic docs/chat/interrupts.md and docs/migration/interrupts.md
- Add the Interrupts nav section and drop the old entries in docs/config.json
- Repoint inbound cross-links (chat, tools, persistence, architecture, and the
generated toolDefinition reference) to the new paths
Examples now show handling a single interrupt, mapping/batching multiple
interrupts, and validating a generic responseSchema in the UI directly,
instead of abstract helper functions.
* docs: clarify interrupt kinds are pauses, not tool categories
The Overview "three kinds" table read as if a tool is either tool-approval
or client-tool-execution. Add a note plus a tool→interrupt matrix showing the
axes are independent: a client tool with needsApproval produces a tool-approval
interrupt first, then a client-tool-execution interrupt once approved.
* docs(interrupts): render examples by looping interrupts, not find+narrow
Replace the `interrupts.find(...)` + whole-component `return null` pattern in
the tool-approval, generic, and client-tool-execution examples with a
`interrupts.map()` that narrows on `kind` and renders per-kind UI. This handles
multiple pending items of a kind and matches the mixed-queue pattern on the
Multiple Interrupts page. Generic items get a per-item child component for their
editor state.
* docs: stop presenting client-tool execution as a resolvable interrupt
Client tools return results through their `.client()` implementation (which runs
automatically) or `addToolResult` — not by resolving a client-tool-execution
interrupt by hand. That manual path was redundant with the tool-result path and
misrepresented how client tools work.
- Delete docs/interrupts/client-tool-execution.md and its nav entry
- Overview: list only tool-approval and generic as kinds you resolve; note client
tools run automatically and approval is a separate axis
- client-tools.md: drop the manual `execution.resolveInterrupt(...)` snippet;
keep approval resolution + addToolResult as the manual escape hatch
* feat(interrupts)!: remove client-tool-execution from the public interrupt API
Client tools resolve through their `.client()` implementation (auto-executed)
or `addToolResult` — never as a bound interrupt. The `client-tool-execution`
kind was redundant public surface: `resolveInterrupt(output)` and
`addToolResult` were two doors to the same resolution, and it invited users to
hand-wire client-tool execution that already happens automatically.
The internal architecture is unchanged — the server still emits the
client-tool-execution interrupt as the browser handoff, the client processor
still auto-executes `.client()` tools off it, persistence still stores its
binding. Only the public exposure is removed.
- Remove `ClientToolExecutionInterrupt` from the `ChatInterrupt` union + exports
- Filter client-tool-execution items out of the published `interrupts` /
`pendingInterrupts` snapshot while keeping them in the internal batch so
`resolveClientToolOutput` (addToolResult) and auto-execution still work
- Exclude client-tool items from the `resolveInterrupts(cb)` completeness check
and the boolean-bulk path (they resolve out-of-band; `maybeSubmit` still gates
real submission on them)
- Update ai-client + framework type tests and the e2e to assert the client tool
auto-executes and stays out of the public interrupts array
* fix: make interrupts build/typecheck/lint/test/docs green (ephemeral-only)
- Reconcile getChunkRunId precedence for interrupt continuation correlation
- Add resume + loadInterruptState to client connection-adapter surface
- Fix ai-react interrupt-state handler TDZ guard
- Strip recovery/persistence from e2e fixture + example lab
- Fix doc links + kiira snippets for removed persistence surface
- Defer recovery-specific client tests (it.skip) to the persistence PR
* fix(interrupts): ephemeral resume wiring + drop persistence-era e2e
- forward parentRunId from the e2e chat route to chat() so ephemeral
interrupt resume correlates the child run (fixes tool-approval resume)
- resolve the approval tool-call's owning message from history when the
live active-message maps were reset by a MESSAGES_SNAPSHOT, so the
deprecated part.state approval UI still updates
- clear the deprecated approval part.state on approve/deny before the
compat resolve path runs
- remove interrupts-v2 (durable) e2e routes/fixtures/specs and the
persistence-era recovery tests/type assertions; these return with the
stacked persistence PR
- restore the Provider cast on the $provider route dropped during extraction
* ci: apply automated fixes
* refactor(interrupts): remove persistence residue from ephemeral interrupts
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>
* ci: apply automated fixes
* chore(interrupts): drop non-interrupt residue from PR surface
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).
* chore(interrupts): strip test noise and storage unit tests
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.
* fix(interrupts): resume race, error surfacing, docs/lab, bound API demos
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.
* refactor(ai-client): drop storage-adapters and persistence controller 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.
* fix(interrupts): resume client tools with history results; fix generic 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.
* fix(ai): typecheck after main merge with typed tool-call events
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.
* chore: reset pnpm-lock to main and reinstall for branch deps
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.
* ci: apply automated fixes
* fix: clear knip, lint, and example typecheck after interrupts merge
- 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
* fix(interrupts): clear resume state after approve; fix e2e approval harness
- 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
* fix(e2e): repair tools-test deny onClick syntax and format
Missing closing brace on the deny-button handler broke prettier parse.
* docs(interrupts): overhaul interrupt guides and strip persistence leftovers
- 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
* Fixed hyphen
* fix(ai): restore tool-call output after MESSAGES_SNAPSHOT
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).
* test(ai): expect tool-call complete+output after snapshot anchor
MESSAGES_SNAPSHOT reconciliation now folds tool-result into the
sibling tool-call; update the anchor test to match.
* fix(ai): satisfy no-unnecessary-condition in snapshot enrichment
Avoid a mutated outer `changed` flag that control-flow analysis treats
as always falsy; detect part identity changes instead.
* ci: apply automated fixes
* docs(interrupts): show error/recovery handling with a real component
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).
* docs(interrupts): show how the server consumes the approval decision
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).
* refactor(interrupts): app owns wire-schema validation; drop ajv and @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
* example(ts-react-chat): rebuild interrupt playground as a wildlife sanctuary
Replace the over-built interrupt lab (durable dead-code, error sanitizer, DI
harness, debug fetch, capability panels, ~1.1k lines of tests) with a lean,
button-driven "Willowbrook Sanctuary" playground: one tool per scenario, wired
to chat()/useChat, each triggered by a preset message.
Covers server + client tools for approval-only, shared approvalSchema, branch
(approve/reject) schemas, and edited args; a generic (app-emitted) interrupt;
and batched approvals resolved per-item or via the root resolveInterrupts.
Fixes found by clicking through every scenario:
- don't force provider tool_choice on a continuation (resume); forcing it made
the model re-call the approved tool instead of answering (empty reply).
- key the generic interrupt id off the request runId (ctx.runId), not the
provider chunk.runId, so the client's parentRunId correlates on resume
(the mismatch surfaced as unknown-interrupt).
- echo edited args in assignEnclosure output so the continuation reflects them.
Add a regression test (api.interrupts.test.ts) covering both fixes, and give
the UI a cozier theme with per-card animal avatars.
* fix(interrupts): recover a denied approval on ephemeral resume
A denied approval writes a final tool result into history, so the tool call
reads as completed and dropped out of the reconstructed pending batch, while
the resume batch still references its `approval_` id. That failed the resume
as `unknown-interrupt` (every deny broke). Ephemeral reconstruction now
recovers `approval_`-referenced calls from history the same way it already
recovers finalized `client_tool_` calls.
- add a regression in chat.test.ts: resume a denial whose call already has a
result in history resolves without RUN_ERROR and skips execution.
- fix the sibling batch-validation test to use a Standard Schema input; the
earlier ajv removal means raw JSON Schema args are no longer library-
validated, so the atomic-batch check now exercises the Standard Schema path.
* example(ts-react-chat): real animal photos + interrupt transparency
- swap the emoji card avatars for real animal photos (loremflickr, keyed by
species) with an emoji fallback if the image can't load
- render tool calls and results in the transcript (name, input args including
edits, output, and denial reasons) so it's clear what ran under the hood
- add a "Your decisions" log that captures exactly what was submitted, so
approve payloads/notes are visible even though they are decision metadata
the tool never receives
* example(ts-react-chat): restyle interrupt playground with a dark theme
Co-authored-by: Cursor <cursoragent@cursor.com>
* @
test(interrupts): port wildlife scenarios into the e2e harness
Add a deterministic interrupt playground (route /interrupts-test) that mirrors
the ts-react-chat wildlife example, plus 31 aimock-backed specs covering every
AG-UI interrupt shape across allow / deny / cancel via both per-item and root
batch resolution:
- server + client tool approvals: boolean, shared payload, branch payload,
edited args
- a generic (non-tool) interrupt (resolve-with-payload and cancel)
- batch sets: root approve-all / deny-all / cancel-all, per-item, and the
mixed resolver form
Server tool results are asserted via the approval-responded state (they are not
surfaced as client message parts); server edited-args are verified through an
emitCustomEvent echo. A shared approvalSchema requires a payload on the deny
decision too, so those scenarios carry a denyPayload. The ts-react-chat example
is unchanged.
@
* fix(interrupts): make interrupt ownership explicit, not assumed
An AG-UI `Interrupt` is a shared envelope. A workflow engine's durable
approval, or another agent framework's pause, can arrive on the same
stream. What makes a pause resumable through `chat()` is the binding this
package attaches to the interrupt's metadata — nothing else.
The client did not enforce that. `hydrateInterrupt` synthesised a binding
for any descriptor that lacked one, so a foreign interrupt rendered as a
resolvable generic interrupt whose answer was submitted against a run
with nothing pending — failing as `unknown-interrupt` only after the user
had filled in the form. That is the silent translation between approval
models the orchestration RFC warns about.
- Interrupts carrying no binding we understand surface as
`kind: 'unbound'`, `canResolve: false`, with no resolver. Pre-binding
TanStack descriptors keep their legacy metadata route. Unbound items
are excluded from the batch-completeness gate so they cannot deadlock
the interrupts that are ours.
- The binding carries a wire version. Readers reject a version they do
not recognise instead of duck-typing its fields; a binding written
before the field existed still reads. This is what lets a future
workflow-owned binding be rejected cleanly rather than mis-read.
- Export `INTERRUPT_BINDING_METADATA_KEY`, `withInterruptBinding()` and
`readInterruptBinding()` so other producers attach a binding through a
supported API rather than copying the metadata key, which was a
duplicated private const in three files.
- Classify off the binding alone. `Interrupt.reason` is free-form AG-UI
text another producer can also spell `tool_call`, so it is a display
hint only and never decides ownership. The emitted values are
unchanged.
- Enumerate the interrupt export surface instead of `export *`.
REVERSES A DELIBERATE DECISION — please review: 2d26b39 kept
`ConnectionAdapter.loadInterruptState` plus `InterruptRecoveryStateV1` /
`InterruptRecoveryQuery` as "a dormant extension seam for a future
persistence layer", after stripping the recovery state machine that used
them. This commit removes them, along with the `persistence-required` /
`atomic-commit-unsupported` / `recovery-unavailable` batch codes.
Reasoning: they describe a persisted, AI-domain interrupt object resumed
through an AI-owned recovery endpoint — which is option 1 of the three
the orchestration RFC (#979) has yet to choose between. Publishing them
as stable types pre-decides that. Nothing consumes the seam today, and
`origin/feat/persistence` (#785) has zero references to any of the three.
Easy to restore if the RFC lands on option 1; hard to unpublish if it
does not. Happy to put them back if the seam is wanted.
Also fixes two pre-existing repo failures the gate surfaced: unresolved
relative tool imports in two interrupt doc snippets (kiira), and knip
flagging the type-only `toolApprovalCapability` brand symbol.
* fix(ai-client): restore per-branch autocomplete for resolveInterrupt options
Replace the two-overload resolveInterrupt signature with a single generic
call signature discriminated on `approved`. Overloads broke editor
autocomplete: a half-typed options literal satisfies neither overload, so
TS resolved no signature and offered no contextual completions. A generic
`TApproved extends boolean` is inferred from the first argument and selects
the matching branch for the rest params, so `payload`/`editedArgs` and the
correct approve/reject schema fields complete per-branch while the wrong
branch's fields are still rejected.
* ci: apply automated fixes
* test(framework): fix @ts-expect-error placement for resolveInterrupt wrong-branch payload
The generic resolveInterrupt signature now reports the wrong-branch payload
error precisely on the offending inner payload property rather than as a
blanket 'no overload matches' on the whole call. Move the @ts-expect-error
directive above that inner line in the framework type tests so it still
suppresses the (now more precise) error.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jack Herrington <jherr@pobox.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Reworks the persistence feature (#785) by splitting the two concerns that were fused under one "persistence" banner, per review feedback:
withChatPersistence/withGenerationPersistencepersist thread messages, run status, interrupts, and generation artifacts — nothing else.StreamDurabilitysink on the transport helpers, backed by the durable streams protocol. We own zero delivery-event storage — the home-grown event-log/cursor/resume subsystem (a database wearing a transport costume, whoseDurableRunStreamwas a dead interface) is deleted.This is a branch-targeted rework of the unreleased #785, so removed APIs are deleted outright (no deprecation shims).
Delivery durability (transport)
toServerSentEvents(stream, { durability })—durabilityis aStreamDurabilityadapter; same option on every transport helper. Transport stays the user's choice; durable streams is the backing.memoryStream(zero-infra dev default) and@tanstack/ai-durable-stream'sdurableStream(request, { server })(durable-streams protocol) — new package.chat()is lazy: on a resume request the helper replays from the log and never iterates the stream, so the provider is never re-invoked. Batching lives in the helper.Last-Event-ID, zero cursor machinery.chat()loses itscursorparam;StreamChunkloses its in-bandcursor.publicEvents/internalEventsstores + tables,DurableRunStream,cursor.ts, coreresume.ts/maybeResume/ResumeSource,resume-source.ts, and the client cursor/auto-resume machinery.State durability (middleware + backends)
withChatPersistence/withGenerationPersistence— state-only, sharing one store contract + run-id space (a future Gemini-nested-generation unification is deferred to its own RFC).AIPersistencecontract:sqlPersistence({ dialect:'sqlite', url })(batteries),drizzlePersistence(db)(BYO),prismaPersistence(prisma)(BYO). Each ORM owns its own migrations natively — no more hand-authored DDL.ai-persistence-sql+-sqlite/-postgres/-cloudflarepackages, theSqlDriverabstraction, and the hand-rolled migration/CLI code — folded into the drizzle + prisma backends.@tanstack/ai-persistence/testkit) runs against memory + drizzle + prisma to keep the backends honest and the drizzle↔prisma schemas in sync (enforced by a coupling entry).Review
Ran a full CR loop to convergence (2 fix rounds). Highlights caught and fixed: a
durableStreamdefault-batch exactly-once violation on mid-batch reconnect (now per-chunk offset tagging), an offset-codecnow-sentinel throw,memoryStreamin-flight-join truncation + a join-hang, thrown-provider-error not persisted, adrizzle/prismablob-prefix wildcard bug,sqlPersistencedialect type-honesty, and removal of a mis-firing generation resume surface.Validation
test:sherif,test:knip, doc-link check, andtest:kiira(808 snippets) green.test:eslint/test:types/test:lib/test:buildgreen across all 12 affected library packages;test:types/buildgreen across all example apps +testing/**.Known follow-ups (not blocking; tracked as notes)
injectChatdoesn't expose theresumeState/pendingInterrupts/resumeInterruptssurface the other 5 frameworks do — pre-existing, untouched by this PR.LockStoresatisfiesvalidatePersistenceFeatures(['locks'])while being process-local — documented; a distinguishable dev-default marker could make validation warn.Stream-Next-Offset, strictly-after read" assumption is faithfully modeled by the test fakes but warrants a one-time confirmation against a live durable-streams service.joinRunaffordance yet (transparent SSE auto-resume covers the common case); would need aChatClient.joinRun.🤖 Generated with Claude Code