Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Persistence (Advanced)
Embeddings
Reranking
Evaluate
Sandboxes (Advanced)
Class References
Function References
Interface References
Type Alias References
Variable References
Sandboxes

Let the Agent Save and Fork

You want the agent to mark a version or open a new direction. The route already knows threadId and runId. createSnapshotTools turns those values into host tools that you spread into chat().

The model can pass a label or a checkpoint id on this thread. It cannot pass a thread id. Your factory mints every new thread id.

ts
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import { withPersistence } from '@tanstack/ai-persistence'
import { createSnapshotTools, withSandbox } from '@tanstack/ai-sandbox'
import { instances, sandbox, snapshots } from './sandbox-server'

export function POST(threadId: string, runId: string) {
  return chat({
    threadId,
    runId,
    adapter: grokBuildText('composer-2.5'),
    messages: [{ role: 'user', content: 'Save this, then try a dark theme.' }],
    tools: [
      ...createSnapshotTools(snapshots, {
        threadId,
        runId,
        createThreadId: () => crypto.randomUUID(),
        onForked({ destinationThreadId }) {
          void destinationThreadId
        },
      }),
    ],
    middleware: [
      withPersistence(snapshots.persistence),
      withSandbox(sandbox, { instances, snapshots }),
    ],
  })
}
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import { withPersistence } from '@tanstack/ai-persistence'
import { createSnapshotTools, withSandbox } from '@tanstack/ai-sandbox'
import { instances, sandbox, snapshots } from './sandbox-server'

export function POST(threadId: string, runId: string) {
  return chat({
    threadId,
    runId,
    adapter: grokBuildText('composer-2.5'),
    messages: [{ role: 'user', content: 'Save this, then try a dark theme.' }],
    tools: [
      ...createSnapshotTools(snapshots, {
        threadId,
        runId,
        createThreadId: () => crypto.randomUUID(),
        onForked({ destinationThreadId }) {
          void destinationThreadId
        },
      }),
    ],
    middleware: [
      withPersistence(snapshots.persistence),
      withSandbox(sandbox, { instances, snapshots }),
    ],
  })
}

The tools run on the server. In a sandbox chat they are bridged back to the host. See Tools.

What each tool does

  • save_sandbox_snapshot: saves the live sandbox for the bound thread. The model passes label only.
  • fork_sandbox_snapshot: copies one checkpoint into a new empty thread. The model can pass checkpointId. When it omits that id, the tool copies the latest checkpoint. createThreadId() sets the destination thread id.
  • read_sandbox_snapshot_artifact: returns artifact metadata for a checkpoint on this thread. It does not return file bytes. Serve bytes from Send a Frozen File.

onForked runs after a successful fork. If you want the new branch to work right away, start chat() on destinationThreadId in that callback.

When save and fork run

save_sandbox_snapshot takes the writer lease for this thread. If this chat() already holds that lease through portable snapshots, the save fails with SANDBOX_SNAPSHOT_WRITER_CONFLICT. Save from a planner thread, or after this run ends.

fork_sandbox_snapshot takes the writer lease on the new thread. A fork during this run can succeed. It copies a saved checkpoint. It does not copy files the agent is still writing.

See Keep Files After Reload to create the snapshots object. See Save a Named Version and Branch From a Version for routes that you call yourself.