本页还没有译文
本页 25 段还没译成Français,下面显示的是英文原文。Observability (Advanced)
还没翻译译自英文(tanstack.com)本页 25 段都还没译
The events stream is what a client reads. This page is the
server-side half: hooks that fire on every file the agent touches, sandbox
debug logging, and the low-level watcher you can drive outside a chat() run.
File-event hooks
Listen to files being created, changed or deleted inside a sandbox, for
example to watch what the agent edits as it works. The watcher is
provider-agnostic: it uses native OS watching where the provider supports it
(local-process) and falls back to a portable find poll everywhere else (Docker
and other exec-only providers), with no extra dependencies or image changes.
There are two places to declare these hooks, with different scopes.
Sandbox-scoped hooks
Declared directly on defineSandbox({ hooks }). They fire once per file
event, regardless of how many runs share the sandbox, alongside the sandbox's
own lifecycle callbacks:
import { defineSandbox } from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
const repoSandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
hooks: {
// catch-all: fires for every event
onFile: (e) => console.log(`[${e.type}] ${e.path}`),
// type-specific variants
onFileCreate: (e) => console.log('created', e.path),
onFileChange: (e) => console.log('changed', e.path),
onFileDelete: (e) => console.log('deleted', e.path),
// lifecycle
onReady: (handle) => console.log('sandbox ready', handle.id),
onError: (err) => console.error('sandbox error', err),
onDestroy: () => console.log('sandbox destroyed'),
},
})Run-scoped hooks
To handle file events inside a middleware (for example per-request audit
logging), use the sandbox hook group on defineChatMiddleware. These fire
per-run, and each handler receives the current run's ChatMiddlewareContext:
import { defineChatMiddleware } from '@tanstack/ai'
import { db } from './db'
const auditMiddleware = defineChatMiddleware({
name: 'audit',
// ctx is the ChatMiddlewareContext for the current run
sandbox: {
onFile: (ctx, e) => console.log(ctx.runId, e.type, e.path),
onFileCreate: (ctx, e) => db.log({ run: ctx.runId, event: e }),
},
})Both hook groups fire server-side and are independent of the stream: the
engine automatically emits one CUSTOM sandbox.file
event per change regardless of whether you register any hooks, so the client
can react to the same edits without extra middleware.
Reading content and diffs in hooks
The event every hook receives is more than { type, path, timestamp }. It also
carries lazy, git-backed accessors for the file's content:
interface SandboxFileHookEvent {
type: "create" | "change" | "delete";
path: string;
timestamp: number;
before(): Promise<string>; // content at the session baseline ('' if new / non-git)
after(): Promise<string>; // current content ('' if deleted)
diff(): Promise<string>; // unified patch vs the baseline
}Reach for diff() to show what the agent changed. There is no need to hand-roll a
git diff yourself:
import { defineSandbox } from "@tanstack/ai-sandbox";
import { dockerSandbox } from "@tanstack/ai-sandbox-docker";
const repoSandbox = defineSandbox({
id: "repo-agent",
provider: dockerSandbox({ image: "node:22" }),
hooks: {
onFileChange: async (e) => {
const patch = await e.diff();
console.log(`${e.path} changed:\n${patch}`);
},
},
});The same accessors are available on run-scoped hooks, where e is the
second argument:
import { defineChatMiddleware } from "@tanstack/ai";
import { db } from "./db";
const auditMiddleware = defineChatMiddleware({
name: "audit",
sandbox: {
onFileChange: async (ctx, e) => {
const [before, after] = await Promise.all([e.before(), e.after()]);
db.log({ run: ctx.runId, path: e.path, before, after });
},
},
});Lazy, path-only hooks pay nothing. before(), after(), and diff()
are methods, not fields: each one only reads the file or shells out to git
when you call it. A hook that only reads e.path / e.type (like the
catch-all logger in Sandbox-scoped hooks above)
never touches the filesystem or spawns a process.
Git session baseline. At onReady, the sandbox snapshots
git rev-parse HEAD once, as the session's baseline commit (empty if the
workspace isn't a git repo, or has no commits yet). Every before() and
diff() call for the rest of the session diffs against that same fixed
baseline, so onFileChange always reports the file's cumulative change
since the run started, not just the delta since the watcher's last poll.
after() always reads the file's current on-disk content, independent of
the baseline. None of the three accessors throw: a deleted file resolves
after() to '' (it still has before()); a new file resolves before()
to '' (it still has after()); a non-git workspace resolves both
before() and after() to '' and makes diff() fall back to a
synthesized add-patch built from after(), except for a delete event in
a non-git workspace, where there's nothing to synthesize and diff()
resolves to ''. In a git workspace, a file git is not tracking yet (one the
agent just created) and every later edit to it diff empty because
git diff ignores untracked files, so diff() falls back to that same
synthesized add-patch whenever the file is absent at the baseline. A file the
agent creates (and keeps editing) therefore never streams an empty diff, while
a tracked file that's identical to the baseline correctly stays empty.
A git-ignored file (e.g. a .env or a credentials file) is the exception:
the file event still fires so you're notified it changed, but diff() returns
'' so its contents are never surfaced in the diff feed.
Every git, exec and fs failure behind these accessors, and behind the find-poll
watcher, still falls back to '' (or preserves the last snapshot), but is
logged first so a failure is observable rather than a silent empty value:
real anomalies (a failed git diff, an unreadable file, a find poll that
exits non-zero, a lost git baseline) under the errors category (on by
default), and expected-empty conditions (a new file's before()) under the
sandbox debug category. Enable debug: { sandbox: true } (see
Debugging) to see the latter.
Disabling file watching
To stop the watcher and suppress sandbox.file events for a sandbox entirely,
set fileEvents: false:
import { defineSandbox } from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
const sandbox = defineSandbox({
id: 'quiet-agent',
provider: dockerSandbox({ image: 'node:22' }),
fileEvents: false, // watcher not started; no sandbox.file events emitted
})Debugging
To log sandbox internals (watcher start and stop, event dispatch, lifecycle
transitions) pass the sandbox debug category to chat():
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import { withSandbox } from '@tanstack/ai-sandbox'
import { repoSandbox } from './sandbox'
import { messages } from './chat-context'
chat({
threadId: 'thread-1',
adapter: grokBuildText('grok-build'),
messages,
middleware: [withSandbox(repoSandbox)],
debug: { sandbox: true }, // or `debug: true` for all categories
})Low-level: watchWorkspace()
watchWorkspace() is the building block the hooks are built on. Reach for it
when you want the watcher outside a chat() run:
import { watchWorkspace } from '@tanstack/ai-sandbox'
import { repoSandbox } from './sandbox'
const handle = await repoSandbox.ensure({ threadId: 'thread-1', runId: 'run-1' })
const watcher = await watchWorkspace(handle, {
onEvent: (event) => {
// event.type is 'create' | 'change' | 'delete'
console.log(`${event.type} ${event.path}`)
},
ignore: ['.git', 'node_modules'], // default
})
// …do work outside a chat run…
await watcher.stop()Related
- Events: the
CUSTOMevent stream the client reads. - Lifecycle & Snapshots: when sandboxes are created and torn down.
- Tools: bridged host tools that surface as tool-call chunks.