본문으로 건너뛰기

옵저버빌리티 (고급)

클라이언트가 읽는 것은 이벤트 스트림입니다. 이 페이지에서는 그 서버 측 부분인 에이전트가 다루는 모든 파일에서 실행되는 훅, 샌드박스 디버그 로깅, 그리고 chat() 실행 외부에서 구동할 수 있는 저수준 감시기를 다룹니다.

파일 이벤트 훅

샌드박스 내부에서 파일이 생성, 변경 또는 삭제되는 이벤트를 수신합니다. 예를 들어 에이전트가 작업하면서 무엇을 편집하는지 감시할 수 있습니다. 감시기는 프로바이더에 종속되지 않습니다. 프로바이더가 지원하는 경우에는 네이티브 OS 감시 (local-process)를 사용하고, 그 외의 모든 경우(Docker 및 기타 exec 전용 프로바이더)에는 추가 종속성이나 이미지 변경 없이 이식 가능한 find 폴링으로 대체합니다.

이러한 훅을 선언하는 위치는 범위가 서로 다른 두 곳입니다.

샌드박스 범위 훅

defineSandbox({ hooks })에 직접 선언합니다. 샌드박스를 공유하는 실행 수와 관계없이 파일 이벤트마다 한 번 실행되며, 샌드박스 자체의 수명 주기 콜백과 함께 동작합니다.

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'),
},
})

실행 범위 훅

미들웨어 내부에서 파일 이벤트를 처리하려면(예: 요청별 감사 로깅) defineChatMiddlewaresandbox 훅 그룹을 사용합니다. 이 훅은 실행마다 동작하며, 각 핸들러는 현재 실행의 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 }),
},
})

두 훅 그룹 모두 서버 측에서 실행되며 스트림과 독립적입니다. 엔진은 훅을 등록했는지 여부와 관계없이 변경마다 하나의 CUSTOM sandbox.file 이벤트를 자동으로 내보내므로, 클라이언트는 추가 미들웨어 없이 동일한 편집에 반응할 수 있습니다.

훅에서 콘텐츠와 diff 읽기

각 훅이 받는 이벤트에는 { type, path, timestamp } 이상의 정보가 있습니다. 파일 콘텐츠에 지연 방식으로 접근할 수 있는 git 기반 접근자도 포함됩니다.

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
}

에이전트가 변경한 내용을 표시하려면 diff()를 사용합니다. 직접 git diff를 구현할 필요가 없습니다.

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}`);
},
},
});

동일한 접근자는 실행 범위 훅에서도 사용할 수 있으며, 이때 e는 두 번째 인수입니다.

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 });
},
},
});

지연 방식의 경로 전용 훅에는 비용이 들지 않습니다. before(), after(), diff()는 필드가 아니라 메서드이므로 호출할 때만 파일을 읽거나 git을 셸로 실행합니다. e.path / e.type만 읽는 훅(위 샌드박스 범위 훅의 전체 수신 로거 등)은 파일 시스템에 접근하거나 프로세스를 생성하지 않습니다.

Git 세션 기준점. onReady에서 샌드박스는 git rev-parse HEAD를 한 번 실행해 세션의 기준 커밋으로 스냅샷합니다(작업공간이 git 저장소가 아니거나 아직 커밋이 없으면 비어 있습니다). 이후 세션의 모든 before()diff() 호출은 동일한 고정 기준점과 비교하므로 onFileChange는 감시기의 마지막 폴링 이후 델타가 아니라 실행 시작 이후 파일의 누적 변경을 항상 보고합니다. after()는 기준점과 관계없이 파일의 현재 디스크 콘텐츠를 읽습니다. 세 접근자는 어느 것도 예외를 발생시키지 않습니다. 삭제된 파일의 after()''이 되고(before()는 여전히 사용 가능), 새 파일의 before()''이 됩니다(after()는 여전히 사용 가능). git이 아닌 작업공간에서는 before()after() 모두 ''이 되며 diff()after()로 만든 합성 추가 패치로 대체됩니다. 단, git이 아닌 작업공간의 delete 이벤트는 합성할 내용이 없으므로 diff()''이 됩니다. git 작업공간에서 에이전트가 방금 만든 파일처럼 git이 아직 추적하지 않는 파일은 이후 편집마다 diff가 비어 있습니다. git diff가 추적되지 않는 파일을 무시하기 때문이므로, 기준점에 파일이 없으면 diff()는 동일한 합성 추가 패치로 대체됩니다. 따라서 에이전트가 만든 파일(계속 편집하는 경우)은 빈 diff를 스트리밍하지 않으며, 기준점과 동일한 추적된 파일은 올바르게 빈 상태를 유지합니다. git에서 무시된 파일(예: .env 또는 자격 증명 파일)은 예외입니다. 파일 이벤트는 여전히 발생해 변경 사실을 알리지만, 콘텐츠가 diff 피드에 노출되지 않도록 diff()''을 반환합니다.

이러한 접근자와 find 폴링 감시기에서 발생하는 모든 git, exec, fs 오류는 ''로 대체되거나 마지막 스냅샷을 유지하지만, 실패를 확인할 수 있도록 조용히 빈 값을 반환하기 전에 먼저 기록됩니다. 실제 이상(실패한 git diff, 읽을 수 없는 파일, 0이 아닌 상태로 종료된 find 폴링, 손실된 git 기준점)은 기본적으로 활성화된 errors 카테고리에, 예상되는 빈 상태(새 파일의 before())는 sandbox 디버그 카테고리에 기록됩니다. 후자를 보려면 debug: { sandbox: true }(디버깅 참조)을 활성화합니다.

파일 감시 비활성화

감시기를 중지하고 샌드박스의 sandbox.file 이벤트를 완전히 억제하려면 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
})

디버깅

샌드박스 내부(감시기 시작 및 중지, 이벤트 디스패치, 수명 주기 전환)를 기록하려면 sandbox 디버그 카테고리를 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
})

저수준: watchWorkspace()

watchWorkspace()는 훅의 기반이 되는 구성 요소입니다. chat() 실행 외부에서 감시기를 사용하려면 이를 이용합니다.

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()
  • 이벤트: 클라이언트가 읽는 CUSTOM 이벤트 스트림입니다.
  • 수명 주기 및 스냅샷: 샌드박스가 생성되고 해제되는 시점입니다.
  • 도구: 도구 호출 청크로 표시되는 브리지된 호스트 도구입니다.