feat: persistence + resumable runs (provider-agnostic, optional, agent-mode)#785
feat: persistence + resumable runs (provider-agnostic, optional, agent-mode)#785AlemTuzlak wants to merge 6 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.*).
…ttach markers - ai-sandbox now imports LocksCapability/LockStore/InMemoryLockStore from @tanstack/ai and re-exports them for back-compat (one global 'locks' token). - Add a token-identity test asserting the sandbox and core LocksCapability are the same object. - Mark the four harness adapters (claude-code, codex, gemini-cli, opencode) with `supportsReattach = true` so the engine continues a still-running in-sandbox run live after replaying the persisted tail.
…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.
|
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 |
🚀 Changeset Version Preview18 package(s) bumped directly, 30 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 767e1c6
☁️ Nx Cloud last updated this comment at |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
@tanstack/ai
@tanstack/ai-angular
@tanstack/ai-anthropic
@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-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-gemini-cli
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-persistence
@tanstack/ai-persistence-cloudflare
@tanstack/ai-persistence-drizzle
@tanstack/ai-persistence-postgres
@tanstack/ai-persistence-prisma
@tanstack/ai-persistence-sql
@tanstack/ai-persistence-sqlite
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-persistence
@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: |
|
Awesome to see that this is in progress, it was one of the final bits that prevented it from feeling completely batteries included! |
* chore(deps): add @mcp-ui/client + ext-apps for MCP Apps support * feat(ai): add UIResourcePart + UIResourceEvent types * feat(ai-mcp): capture serverId + _meta.ui.resourceUri at tool discovery * feat(ai): extend MCPToolSource with optional readResource * feat(ai-mcp): in-memory McpSessionStore (#785-shaped seam) * feat(ai-mcp): surface embedded ui:// resources separately from model text * feat(ai): reconcile ui-resource CUSTOM events into UIResourcePart * feat(ai-mcp): add public callTool method on MCPClient * feat(ai): emit ui-resource events from MCP tool results (eager read, fail-soft) * feat(ai-mcp): createMcpAppCallHandler + ./apps subpath (reconnect default, allowlist) * fix(ai-mcp): type callTool return as SDK union to unblock declaration build * feat(ai-client): createMcpAppBridge (framework-agnostic tool/prompt/link routing) * feat(ai-react): MCPAppResource wrapper for @mcp-ui/client AppRenderer * feat(ai-preact): MCPAppResource wrapper for @mcp-ui/client AppRenderer * docs(skills): document MCP Apps support in ai-mcp + tool-calling skills * docs: MCP Apps guide (server + client, React/Preact) * fix(ai-client): handle UIResourcePart variant in MessagePart union * fix(ai-react,ai-preact): narrow onCallTool structuredContent to satisfy CallToolResult * test(e2e): MCP Apps static render + interactive call + allowlist rejection * chore: add changeset and format MCP Apps files * fix: satisfy eslint in MCP Apps source (require-await, arrow signatures) * chore: align MCP Apps deps + knip ignore for optional @mcp-ui/client peer * docs: make MCP Apps snippets type-check under kiira (no as-casts) * fix(ai-client): bridge HTTP-status guard, drop dead onNotify/onIntent, warn on dropped link * feat(ai): add toolName to UIResourcePart (AppRenderer requires the MCP tool name) * fix(ai-mcp): pool readResource + call-handler prefix/serverId routing; drop dead extractUiResources * fix(ai-react,ai-preact): use part.toolName, run .tsx tests, coalesce result, empty-prompt guard * docs: correct MCP Apps skill/docs to shipped API; gpt-4o -> gpt-5.5 in tool-calling skill * fix(ai): emit ui-resource only on uri match; reconcile to tool-call owner message * fix(ai-mcp): drop harmful prefix-strip, pool readResource uri-ownership, sliding session TTL * fix: openLink throw-safety, meta doc honesty, prop JSDoc parity, stronger test assertions * docs: correct allowlist/prefix descriptions to match strip-free handler; pin ui:// exclusion test * fix(ai-mcp): type error detail as string|undefined to satisfy no-unnecessary-condition * ci: apply automated fixes * feat(ai-mcp): createMcpAppCallHandler takes clients; MCPClient.getInfo + MCPClients.getServers * test: adopt clients-based call handler + test-hygiene cleanup (remove casts, extract helpers) * docs: clients-based createMcpAppCallHandler API; clarify sandbox proxy URL * fix(ai-mcp): key app registry by serverId + collision throw, store->clients fallback, close e2e client, error causes * test(ai-mcp): cover registry keying/collision, store fallback, readResource error causes * fix(ai-mcp): getInfo/getServers retain only serializable TransportConfig (drop single-use Transport instances) * test(ai-mcp): instance-built clients expose no reconnectable transport descriptor * docs: multi-server interactive routing requires a per-server prefix * ci: apply automated fixes * fix(mcp-apps): reject malformed call args + validate openLink URL scheme Address PR review feedback: - createMcpAppCallHandler: reject a non-object args payload (array/primitive/ null) instead of silently coercing it to {}; absent args still defaults to {}. - createMcpAppBridge.openLink: only forward http(s)/mailto URLs to the host onLink handler; reject javascript:/data:/file:/etc. from untrusted widgets. - docs(SKILL): point Preact readers at the @tanstack/ai-preact/mcp-apps subpath. * fix(mcp-apps): address review — observability, fail-soft scope, type dedup Applies the PR-review findings on the MCP Apps surface: - processor: warn (not silently drop) a ui-resource event that resolves to no target message — a vanished widget is otherwise undebuggable client-side. - call-handler: add optional onError(err, { phase, req }) so the otherwise opaque server handler can report 'call' and 'close' failures; library stays console-free. - tool-calls: move emitCustomEvent out of the read try so an emit-path error can't be mislabeled as a read failure. - pool.readResource: attach ALL per-client errors via AggregateError instead of last-error-wins, so the owning server's failure isn't buried. - session-store: opportunistic expiry sweep on set() to bound growth for set-but-never-read threads. - types: extract shared McpResourceReadResult (kills the hand-copied shape); type the processor event as UIResourceEvent['value'] and drop the as-cast; narrow isToolCallResponse without a cast; fix orphaned/inaccurate JSDoc and add a per-run mutation note on bindReadResource. - docs: drop redundant updatedAt on the new page; document that unsafe link schemes are rejected even with an onLink handler. Tests: pin the "widget never enters model input" invariant; onLink-throws fail-soft; tool-result-still-flows on read failure; session-store sweep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * fix(mcp-apps): onError sync-throw safety + test-quality fixes from re-review Round-2 review of the prior fixes commit: - call-handler: extract reportError() so onError is invoked inside the promise chain — a SYNCHRONOUSLY-throwing hook no longer escapes during argument evaluation and can't break the handler's fail-soft result (the previous `Promise.resolve(onError(...)).catch()` only absorbed async rejections). - tests: cover the onError hook (phase 'call', phase 'close', and both sync- throw and async-reject safety) — previously untested. - tests: drop a tautological `not.toContain('ui-resource')` assertion and reword the messages.ts invariant comment to claim only the load-bearing uri/HTML checks; reword the session-store sweep test to state honestly that it guards set() correctness across the sweep, not the (unobservable) memory reclamation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * docs(example): add MCP Apps demo (static + interactive) to ts-react-chat Adds a `/mcp-apps` route demonstrating both kinds of MCP Apps end to end: - STATIC: an in-process MCP server (`api.mcp-apps-weather-server`) exposing a display-only `show_weather_card` tool whose `ui://weather/card` resource renders as a self-contained forecast card. - INTERACTIVE: the official Three.js MCP server (@modelcontextprotocol/ server-threejs) run on :3001, whose widget calls tools back through the bridge. Wiring: - `api.mcp-apps-chat` connects both servers via createMCPClient and streams `ui-resource` parts (tolerates :3001 being down). - `api.mcp-apps-call` mounts createMcpAppCallHandler over both for the interactive plane. - The page renders `ui-resource` parts with `MCPAppResource` + a `createMcpAppBridge`, seeds `toolInput` from the sibling tool-call part, and withholds the bridge for the static widget (display-only). Suggestion pills trigger each app. - Vendors the official sandbox-proxy page and serves it cross-origin on :8765 (a hard requirement of @mcp-ui/client AppRenderer); `dev` now runs the proxy, the Three.js server, and Vite via concurrently. Verified: page renders with no console errors, the static MCP server route and the Three.js server both respond, the proxy serves, and the example type-checks. The live model->tool->widget render requires a provider API key. * ci: apply automated fixes * feat(example): add interactive storefront MCP App + solar-system scene Address feedback on the /mcp-apps demo: - Add an INTERACTIVE storefront widget (api.mcp-apps-shop-server) that demonstrates the full bridge round-trip: clicking "Buy now" in the sandbox sends a tools/call over a hand-rolled MCP Apps app-bridge -> AppRenderer -> createMcpAppBridge -> POST /api/mcp-apps-call -> createMcpAppCallHandler -> buy_product() on the server -> order confirmation rendered back in the widget. The widget speaks the app-bridge protocol in plain JS (no build step). - Wire the shop server into the chat + call routes; gate the bridge on non-static widgets (the weather card stays display-only). - Change the Three.js suggestion to render a solar system instead of a cube. - Fix the tool-call note: show a check when done instead of a perpetual spinner. - Make the weather pill name a city so the tool fires deterministically. Verified live in the browser: static card, interactive buy round-trip (correct server order ids, no auto-fire), and the 3D solar system all render. * ci: apply automated fixes * feat(ai-react,ai-preact): add useMcpAppBridge hook A React/Preact wrapper over createMcpAppBridge that returns a stable bridge for a given threadId/callEndpoint while always invoking the latest chat.sendMessage and onLink (kept in refs) — removing the hand-written useMemo + exhaustive-deps disable the example previously needed. - Exported from the main entry of @tanstack/ai-react and @tanstack/ai-preact (no @mcp-ui/client needed — it only wraps the ai-client bridge); also re-export createMcpAppBridge / McpAppBridge / CreateMcpAppBridgeOptions. - Unit tests cover stable identity, recreation on threadId change, latest- callback invocation (no stale closure), and display-only openLink. - Use the hook in the ts-react-chat /mcp-apps example. - Update docs/mcp/apps.md (interactive example + API reference) and the ai-mcp SKILL to use the hook; bump the docs updatedAt and the changeset. * style: format useMcpAppBridge signatures --------- 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 Opus 4.8 (1M context) <noreply@anthropic.com>
What this adds
withPersistence(...)makes a run durable: server-authoritative thread history, run records, an append-only AG-UI event log, usage, and (agent mode) approvals + artifacts. A run with no persistence middleware is byte-for-byte unchanged.Resume. Each chunk carries an in-band opaque
cursor(a monotonic per-run sequence). A reconnecting client sendsrunId + cursor;chat({ cursor })replays the persisted tail and — for harness adapters that re-attach to the still-running in-sandbox process — continues live. The headless client tracks the cursor (resume()/getResumeState()/maybeAutoResume(),autoResumeopt-out).Event model. The persisted log is the AG-UI
StreamChunkstream — no parallel event type. Agent activity rides on a typed catalog of well-knownCUSTOMevents.Backends (shared SQL core + thin adapters).
@tanstack/ai-persistence-sql(one SQL impl behind a minimalSqlDriver), with-sqlite,-postgres,-cloudflare(D1), and BYO-drizzle/-prisma. Raw drivers auto-migrate (opt-out); ORMs own their schema.memoryPersistence()ships in core.Agent mode.
@tanstack/ai-sandbox-persistencebridges a durable SQLSandboxStore+ distributed lock intowithSandbox. The sharedlockscapability moves to@tanstack/ai(one token);@tanstack/ai-sandboxre-exports it for back-compat.Packages
@tanstack/ai-persistence,-sql,-sqlite,-postgres,-cloudflare,-drizzle,-prisma,@tanstack/ai-sandbox-persistence@tanstack/ai(cursor/resume/locks/custom-events),@tanstack/ai-sandbox(locks token),@tanstack/ai-client(auto-resume), 4 harness adapters (supportsReattach)Verification
~1530 tests green (core 1054, ai-sandbox 50, ai-client 384, new packages 42). sherif / knip / docs-links / publint all pass; every package typecheck + lint + build clean. SQLite/Drizzle/Prisma/Cloudflare-D1 are runtime-verified against real
node:sqlite; Postgres via a fake-pool plumbing test.Deferred (documented)
Notable, grounded design choices
events/resumecontracts live in core — thechat()resume seam consumes them without core depending on the persistence package.ModelMessagehas no id (ids live on the clientUIMessage).🤖 Generated with Claude Code