Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Runtime Context
runtimeContext passes a user id, tenant, or database client into a tool. Those values stay out of the prompt.
Pass them as runtime context. Your tools and middleware read them from ctx.context, with full types. The model never sees them.
Quick setup#
1. Declare the context that the tool needs#
Put the context type on .server<TContext>(). The tool reads the values from ctx.context:
// lib/ai/tools.ts
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
export type AppContext = {
userId: string
}
const notesByUser = new Map([['user_123', [{ title: 'Groceries' }]]])
export const listNotes = toolDefinition({
name: 'list_notes',
description: 'List notes for the current user',
inputSchema: z.object({}),
outputSchema: z.array(z.object({ title: z.string() })),
}).server<AppContext>(async (_input, ctx) => {
return notesByUser.get(ctx.context.userId) ?? []
})2. Pass the context to chat()#
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { listNotes } from '../lib/ai/tools'
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
// Replace this line with your auth check.
const userId = request.headers.get('x-user-id') ?? 'user_123'
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
runId,
tools: [listNotes],
context: { userId },
})
return toServerSentEventsResponse(stream)
}Ask "What are my notes?". The tool answers with the notes of the current user. If you remove context, TypeScript reports an error, because listNotes needs userId.
How it works#
The tools and middleware are the contract. The context value is what you supply at runtime.
- Each tool or middleware declares the context shape that it needs.
- chat() merges these needs and checks your context value against the result.
- The value goes only to your tools and middleware. It is not sent to the model.
- It is also not the AG-UI RunAgentInput.context field. See AG-UI context.
Declare the type in one of these places:
- toolDefinition(...).server<TContext>(...) for server tools.
- toolDefinition(...).client<TContext>(...) for client tools.
- ChatMiddleware<TContext> for middleware.
Server Runtime Context#
Middleware reads the same context. Give it the type with ChatMiddleware<TContext>:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import type { ChatMiddleware } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type AppContext = {
userId: string
tenantId: string
}
const auditMiddleware: ChatMiddleware<AppContext> = {
name: 'audit',
onStart(ctx) {
console.log('chat started', {
requestId: ctx.requestId,
userId: ctx.context.userId,
tenantId: ctx.context.tenantId,
})
},
}
export async function POST(request: Request) {
const { messages, threadId } = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
middleware: [auditMiddleware],
context: {
userId: 'user_123',
tenantId: 'tenant_456',
},
})
return toServerSentEventsResponse(stream)
}You can put any value in the context, for example a database client or an audit logger.
Client Runtime Context#
Client tools can have runtime context too. Pass it to useChat. It stays in the browser and is not sent to the server.
import { toolDefinition } from '@tanstack/ai'
import { createChatClientOptions } from '@tanstack/ai-client'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
type ClientContext = {
currentTabId: string
toast(message: string): void
}
const notifyUser = toolDefinition({
name: 'notify_user',
description: 'Show a notification in the current browser tab',
}).client<ClientContext>((_input, ctx) => {
ctx.context.toast(`Updated tab ${ctx.context.currentTabId}`)
return { ok: true }
})
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
tools: [notifyUser],
context: {
currentTabId: 'settings',
toast: (message) => window.alert(message),
},
})
export function Chat() {
const { sendMessage } = useChat(chatOptions)
return <button onClick={() => sendMessage('Update my tab')}>Send</button>
}Use client context for local values only. The server does not receive them.
Client-to-Server Handoff#
To send a value from the client to the server, use forwardedProps. On the server, validate it, then copy it into the server context.
On the client, put the value in forwardedProps. Unlike context, it goes to the server:
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
export function Chat() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
forwardedProps: {
tenantId: 'tenant_456',
},
})
return <button onClick={() => sendMessage('Hello')}>Send</button>
}On the server, read forwardedProps and build the context:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type AppContext = {
userId: string
tenantId: string
}
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
// Replace these lines with your auth check and your allowlist of tenants.
const userId = 'user_123'
const defaultTenantId = 'tenant_default'
const tenantId =
typeof params.forwardedProps.tenantId === 'string'
? params.forwardedProps.tenantId
: defaultTenantId
const context: AppContext = { userId, tenantId }
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
context,
})
return toServerSentEventsResponse(stream)
}Warning
The client controls forwardedProps. Validate and allowlist every field before you put it in the server context.
Advanced#
How the type checks work#
The tools and middleware decide what the context must contain. The context object alone does not decide this. In this call, the tool needs UserContext and the middleware needs TenantContext, so context must have both:
import { chat, toolDefinition } from '@tanstack/ai'
import type { ChatMiddleware } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type UserContext = { userId: string }
type TenantContext = { tenantId: string }
const currentUserTool = toolDefinition({
name: 'current_user',
description: 'Read the current user',
}).server<UserContext>((_input, ctx) => {
return { userId: ctx.context.userId }
})
const tenantMiddleware: ChatMiddleware<TenantContext> = {
name: 'tenant',
onStart(ctx) {
console.log(ctx.context.tenantId)
},
}
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Who am I?' }],
threadId: 'thread-1',
tools: [currentUserTool],
middleware: [tenantMiddleware],
context: {
userId: 'user_123',
tenantId: 'tenant_456',
},
})If you remove tenantId, TypeScript reports an error, because tenantMiddleware needs it.
More rules:
- Untyped tools and middleware still work. They get unknown context and do not make context required.
- The inference also works for tools and middleware that you declare outside the chat() call and pass in as arrays.
- To make the context optional, declare TContext | undefined. When all typed consumers accept undefined, you can leave out context.
- If you pass a context value, it must satisfy every typed consumer.
The same rules on the client#
useChat() checks context against the client tools. This tool declares ClientRuntimeContext & { mode: 'debug' }, so context needs currentTabId and the literal mode: 'debug':
import { toolDefinition } from '@tanstack/ai'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
type ClientRuntimeContext = {
currentTabId: string
}
const inspectClientContext = toolDefinition({
name: 'inspect_client_context',
description: 'Inspect local browser context',
}).client<ClientRuntimeContext & { mode: 'debug' }>((_input, ctx) => {
return {
tabId: ctx.context.currentTabId,
mode: ctx.context.mode,
}
})
export function DebugChat() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
tools: [inspectClientContext],
context: {
currentTabId: 'settings',
mode: 'debug',
},
})
return <button onClick={() => sendMessage('Inspect')}>Send</button>
}AG-UI Context#
AG-UI defines its own RunAgentInput.context field. It holds context entries for agents at the protocol level. chatParamsFromRequest gives you this field as params.aguiContext. It is separate from chat({ context }).
TanStack AI does not copy params.aguiContext into the runtime context. To use these values, validate them and map them yourself:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { buildRuntimeContextFrom } from './context'
import { serverTools } from './tools'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
tools: serverTools,
context: buildRuntimeContextFrom(params.aguiContext),
})
return toServerSentEventsResponse(stream)
}buildRuntimeContextFrom is your own function. It reads the entries you trust and returns your context object.