feat: persistence + resumable runs (provider-agnostic, optional, agent-mode)#785
feat: persistence + resumable runs (provider-agnostic, optional, agent-mode)#785AlemTuzlak wants to merge 50 commits into
Conversation
…xample New @tanstack/ai-claude-code package that runs Claude Code (via @anthropic-ai/claude-agent-sdk) as a TanStack AI chat backend. Unlike HTTP provider adapters, this is a harness adapter: Claude Code owns the agent loop and executes its built-in tools (bash, file edits, search) server-side. - Stream translator maps Agent SDK messages to AG-UI events; harness tool activity arrives as already-resolved TOOL_CALL_*/TOOL_CALL_RESULT pairs and runs always finish with stop/length (never tool_calls), so the engine never re-executes harness tools. Every started tool call is guaranteed a result (synthesized on abort) to keep the engine's pending-call scan safe. - TanStack toolDefinition() server tools are bridged into the harness as an in-process MCP server (raw JSON Schema passthrough, no zod round-trip). Client-side/approval tools fail fast — documented v1 limitation. - Stateful sessions: session id surfaced via a claude-code.session-id CUSTOM event; resume via modelOptions.sessionId (+ forkSession). - Structured output uses the SDK's native outputFormat json_schema. - settingSources defaults to ['project'] so servers don't inherit user-level ~/.claude config from the host machine. - E2E: excluded from the aimock matrix (subprocess can't carry X-Test-Id isolation); covered by 44 unit tests plus a gated live smoke spec (CLAUDE_CODE_E2E=1). Also adds examples/ts-react-coding-agent: a TanStack Start app demoing session resume, the harness tool timeline, read-only/edit permission modes, tool bridging, and a sandboxed scratch workspace — with the agent registry structured so future Codex/Gemini CLI harness adapters can slot in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ters Add two new coding-agent harness adapters alongside Claude Code: - @tanstack/ai-codex drives OpenAI Codex via @openai/codex-sdk with local tool execution, resumable sessions (modelOptions.sessionId), structured output, and a localhost MCP bridge for TanStack server tools. - @tanstack/ai-gemini-cli drives `gemini --acp` over the Agent Client Protocol with token-level streaming, resumable sessions, a configurable permission policy, and headless ACP auth method selection (authMethodId) so runs never stall on an interactive auth picker. Wire both into the ts-react-coding-agent example: the agent dropdown keeps every harness selectable, and a server function (createServerFn) reports which agents are actually configured at runtime so the UI can surface a setup dialog for unconfigured ones. Includes adapter docs and changesets. Co-authored-by: Cursor <cursoragent@cursor.com>
Add the @tanstack/ai-opencode package, an OpenCode harness adapter that drives OpenCode (via @opencode-ai/sdk) as a TanStack AI chat backend with local tool execution, token-level streaming, stateful sessions, and TanStack tool bridging over a localhost MCP server. Wires the adapter into the ts-react-coding-agent example, adds the OpenCode adapter docs page, and anchors the OpenCode.md gitignore entry so it no longer shadows the docs page on case-insensitive filesystems. Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # pnpm-lock.yaml
…e, withSandbox, workspace, policy - @tanstack/ai-sandbox: provider-agnostic SandboxHandle/SandboxProvider/SandboxCapabilities contracts - capability tokens (SandboxCapability + optional SandboxStore/Locks), in-memory store/lock defaults - defineSandbox lazy controller + ensure state machine (resume->restoreSnapshot->create+bootstrap) with capability-aware degradation - withSandbox middleware (setup provides handle; onFinish/onError snapshot+destroy) - defineWorkspace (git/local/none + skills + secrets), provider-agnostic bootstrapWorkspace - defineSandboxPolicy + evaluateCommand (glob, deny>ask>allow), compound sandbox key (secrets excluded) - export DefinedChatMiddleware/AnyChatMiddleware from @tanstack/ai for portable middleware authoring - 22 unit tests (ensure/policy/key/store); types + lint clean Refs sandbox proposal (Phase A).
…git helper - @tanstack/ai-sandbox-local-process: SandboxHandle over host fs/child_process (no isolation, dev loop) - virtual /workspace root mapped to a real host dir with path containment - exec/spawn (duplex stdin, streamed stdout), localhost port channel, env, fork via dir copy, durable fs resume-by-dir - core: createExecBackedGit helper (shared by providers without native git); bootstrap clones into the handle's own root - 10 unit tests (fs/exec/spawn/lifecycle/fork/bootstrap/ensure); types + lint clean
…runner - @tanstack/ai: TextOptions.capabilities carries the middleware capability context so harness adapters can read provided capabilities (getSandbox(options.capabilities)) from chatStream; populated by the engine - @tanstack/ai-sandbox: spawnNdjson/toLines — spawn an agent CLI in a sandbox and stream parsed NDJSON stdout (the reusable harness-execution primitive) - tests: toLines buffering + spawnNdjson parsing (core), real spawn+NDJSON via local-process (11) — 25 core tests; types + lint clean
…ential leakage Security review (PR #774): - argument injection: insert '--' end-of-options separators before positionals (clone url/target, add paths) and reject url/ref/dir/path values beginning with '-' (flag-smuggling guard) - secrets in argv: stop embedding the auth token in the clone URL (leaked via ps/logs); use a one-shot credential.helper that reads the token from the child ENV, single-quoted so the outer shell never expands it - 4 unit tests pinning: token absent from argv + present in env, '--' separators, leading-dash rejection, quote escaping
- @tanstack/ai-sandbox-docker: SandboxHandle over a Docker container - create/resume-by-id/restoreSnapshot(commit image)/destroy; durable fs across stop/start - exec + duplex spawn via dockerode exec + stream demux; fs over base64 piping (binary-safe, no tar dep) - commit-based snapshot + fork; host.docker.internal gateway for host MCP reachability; publishPorts -> ports.connect - exec-backed git reused from core - 3 integration tests (gated on a reachable daemon) — verified green against a real daemon: exec, fs+binary round-trip, snapshot, resume, spawn streaming, ensure+bootstrap - pnpm-workspace: declare dockerode's optional native deps (cpu-features, ssh2) as not-built (JS fallback, local socket)
- claudeCodeText now declares requires:[SandboxCapability] and spawns the claude CLI INSIDE the sandbox via sandbox.process (claude -p --output-format stream-json), reusing translateSdkStream for the stdout NDJSON - prompt fed via stdin (not argv); session id surfaced as before; emits a file.changed CUSTOM event with the git diff after the run - permission-mode/allowed/disallowed/add-dir/max-turns/system-prompt mapped to CLI flags; default permission-mode bypassPermissions (sandbox is isolated) - drop @anthropic-ai/claude-agent-sdk + @modelcontextprotocol/sdk deps; remove the in-process tool bridge (chat()-tools MCP proxy deferred — adapter rejects tools for now); provider-options self-contained - spawnNdjson gains an option to feed stdin - deterministic test via a fake claude CLI in a real local-process sandbox (24 tests); types + lint clean
Runnable demo (examples/sandbox-coding-agent) that runs Claude Code inside a sandbox to fix a bug end-to-end via chat() + withSandbox: - bootstraps a tiny git repo with a deliberate bug, asks the agent to fix it, streams output + prints the git diff - Docker provider by default (installs the claude CLI in setup); SANDBOX=local runs on the host process - README with prerequisites + run instructions for manual e2e verification
…lag mapping; changesets - SandboxPolicyCapability: withSandbox provides the definition policy (conditionally); harness adapters read it via getOptional - claude-code maps defineSandboxPolicy (default decision + fileWrite/network caps + tool-name command rules) onto --permission-mode/--allowedTools/--disallowedTools (best-effort; fine-grained command globs await the MCP permission-prompt tool) - changesets for the sandbox layer + updated claude-code changeset for the in-sandbox behavior - policy-map unit tests (5)
- docs/sandbox/overview.md: mental model, providers, defineWorkspace/defineSandboxPolicy, lifecycle/resume, events, the runnable example (no as-casts; latest model id) - docs/config.json: new Sandboxes section (addedAt 2026-06-16) - packages/ai-sandbox/skills/ai-sandbox: agent skill covering the sandbox APIs + critical rules - ship skills in the package files - test:docs green
…n-sandbox agent - startHostToolBridge: host-side Streamable-HTTP MCP server exposing chat() server tools; the in-sandbox claude calls mcp__tanstack__<tool>, proxied back to the host where execute() runs (closures/DB/secrets). Per-run bearer token; binds for host.docker.internal reachability from Docker - adapter wires --mcp-config when tools are present, picks localhost vs host.docker.internal by provider, and tears the bridge down after the run; tools no longer rejected - 3 host-side tests via the MCP SDK client (list/call/error/auth) — verified green without needing claude - docs + skill updated to describe the tool-proxy
- @tanstack/ai-sandbox-cloudflare: cloudflareSandbox() on @cloudflare/sandbox (edge, inside a Worker) - uniform SandboxHandle: exec, base64-backed fs, exec-backed git, exposePort preview URLs (previewHostname), setEnvVars; spawn via startProcess+onOutput queue - ephemeral disk + no GA snapshots -> durableFilesystem/snapshots false (withSandbox re-bootstraps across cold starts); background processes have no stdin (documented; stdin-fed harnesses need local-process/docker) - compiles against the real @cloudflare/sandbox types; 7 deterministic handle tests against a mock Sandbox (fs round-trip, exec, spawn queue, stdin limitation, port). Runtime verification pending a Workers runtime - align @cloudflare/workers-types version with the workspace (sherif)
- codexText declares requires:[SandboxCapability]; spawns 'codex exec --experimental-json' inside the sandbox (mirroring @openai/codex-sdk's own CLI invocation), prompt via stdin, JSONL thread events → existing translateThreadEvents - sandbox mode / approval policy / reasoning effort / add-dir / skip-git-repo-check / config mapped to codex CLI flags; resume via 'resume <id>' - drop @openai/codex-sdk + @modelcontextprotocol/sdk + the in-process tool bridge; provider-options self-contained; chat()-tools bridging deferred (rejects tools) - deterministic fake-codex-CLI test in a real local-process sandbox (27 tests); types/lint/knip/sherif clean
- geminiCliText declares requires:[SandboxCapability]; spawns 'gemini --acp' inside the sandbox and drives it over ACP via the sandbox's duplex process IO - new spawnHandleToAcpTransport adapts a SpawnHandle into the Uint8Array WebStreams ndJsonStream needs; all @agentclientprotocol/sdk protocol handling reused unchanged - drop local child_process spawn + @modelcontextprotocol/sdk + in-process tool bridge; chat()-tools bridging deferred (rejects tools); structuredOutput throws not-supported - transport-adapter + requires-sandbox tests (36); types/lint/knip/sherif clean
- opencodeText declares requires:[SandboxCapability]; spawns 'opencode serve' inside the sandbox, waits for readiness, exposes the port, and connects @opencode-ai/sdk's HTTP client via baseUrl (reusing startOpencodeSession's connect path) - new startOpencodeServerInSandbox helper (readiness detection + port exposure); Docker needs publishPorts:[port] - drop @modelcontextprotocol/sdk + in-process tool bridge; chat()-tools bridging deferred (rejects tools); structuredOutput throws not-supported; permission governed by the dynamic handler - server-helper + requires-sandbox tests (36); types/lint/knip/sherif clean
…4 harness adapters - move startHostToolBridge + BRIDGED_MCP_SERVER_NAME + hostForSandbox into @tanstack/ai-sandbox core (shared); add @modelcontextprotocol/sdk dep there; tool-bridge test relocated to core - claude-code: import bridge from core; build --mcp-config from the bridge (drop local bridge + dep) - codex: bridge tools via --config mcp_servers.<name>.url + bearer_token - gemini-cli: bridge tools via ACP newSession mcpServers (http + Authorization header) - opencode: bridge tools via OPENCODE_CONFIG_CONTENT mcp.remote (url + bearer header) at server spawn - all adapters no longer reject tools; bridged tool names feed the permission handlers; changesets updated - types/eslint/lib/build/knip/sherif green across all 5 packages
- @tanstack/ai-sandbox: shared resolveApproval (policy + client approvals -> allow/deny/needs-approval), stable approvalId, buildApprovalRequestedEvent (AG-UI CUSTOM 'approval-requested') - @tanstack/ai: TextOptions.approvals threaded from the engine's initialApprovals so harness adapters resolve ask-policy permission requests against the client's decisions (resume-based loop) - 11 unit tests for the resolver/keying/event; @tanstack/ai 1033 tests still pass
Wire client-in-the-loop approvals through every in-sandbox harness adapter, built on the shared approval primitives in @tanstack/ai-sandbox. - core bridge: optional permission-prompt tool on startHostToolBridge, and export PermissionToolResult so adapters can type their resolver. - claude-code: enforce the sandbox policy via --permission-prompt-tool; an `ask` action with no client decision yet emits an approval-requested event and denies, so the client approves and re-runs to continue. - gemini-cli / opencode: resolveInteractivePermission consults policy + client approvals, collects approval-requested events, and yields them after the stream (coercing nullable ACP tool titles). - codex: map defineSandboxPolicy onto codex exec`s coarse knobs (sandbox mode, approval_policy, network_access). codex exec is non-interactive with no per-action host callback, so the resume-based approval flow is not available for codex (documented); adds policy-map + tests. - changesets updated to describe the interactive-approval behavior.
Add provider-agnostic sandbox file-event hooks and a runnable demo that
uses them.
Hooks (@tanstack/ai-sandbox):
- watchWorkspace(handle, { onEvent }) + watchWithHooks(handle, hooks) emit
typed FileEvents (create/change/delete). A native fs.watch fast-path is
used when the provider advertises it; otherwise a portable `find -printf`
mtime snapshot-diff poll runs (no extra deps / image changes). .git and
node_modules are ignored by default.
- withSandboxFileEvents() middleware surfaces events into the chat() stream
as CUSTOM `sandbox.file` events, interleaved with the agent's output.
- local-process gains the native fs.watch seam (Node recursive watch on
Windows/macOS; Linux falls back to the poll).
Example (examples/sandbox-issue-triage):
- Fetches the first open issue on TanStack/ai, clones the repo into a
sandbox, runs Claude Code inside it to triage the issue and write
ISSUE-REPORT.md, reads it back via sandbox.fs, and writes a local report
with the observed file events appended. Two entrypoints: process + docker.
Docs/skill updated; changeset added.
…ry, runtime capability
…ithSandboxFileEvents
…s, refresh example README
…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