사용자 지정 이벤트 레퍼런스
chat() 스트림을 읽다가 CUSTOM 이벤트를 만났습니다. 다음 중 하나일 수 있습니다.
sandbox.file.diff, Code Mode 진행 이벤트,
structured-output.complete일 수 있습니다. 각 기능 페이지는 해당 이벤트를
맥락 속에서 설명합니다. 이 페이지는 TanStack AI 자체가 발생시키는 모든
CUSTOM 이벤트를 하나의 표로 정리하고, 헬퍼 함수나 캐스트 없이 일반적인
if만으로 어느 이벤트든 읽을 수 있게 하는 타입 메커니즘을 설명합니다.
타입: ChatStream
chat()은 기본적으로 ChatStream을 반환합니다(outputSchema가 없고 stream이 명시적으로 false가 아닌 경우).
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import type { ChatStream } from "@tanstack/ai";
const stream: ChatStream = chat({
adapter: openaiText("gpt-5.5"),
messages: [{ role: "user", content: "Hello" }],
});
ChatStream은 다음과 같이 정의됩니다.
type ChatStream = AsyncIterable<Exclude<StreamChunk, CustomEvent> | KnownCustomEvent>
StreamChunk(원시 AG-UI 이벤트 유니온)에는 CUSTOM 형태의 멤버가 정확히
하나 있습니다. name: string과 value: any를 사용하는 제네릭
CustomEvent 인터페이스입니다. 이 멤버를 유니온에 그대로 두면 이 단일
any가 모든 타입 좁히기를 오염시킵니다. 두 멤버가 병합된 뒤에는
TypeScript가 제네릭 멤버와 특정 멤버를 구분할 수 없으므로,
if (chunk.type === 'CUSTOM' && chunk.name === 'sandbox.file')를 사용해도
chunk.value는 여전히 any 타입입니다. ChatStream은 두 단계로 이 문제를
해결합니다. Exclude<StreamChunk, CustomEvent>가 제네릭 멤버를 제거하고,
| KnownCustomEvent가 TanStack AI가 실제로 발생시키는 모든 이벤트의
판별 유니온을 다시 추가합니다. 각 이벤트에는 리터럴 name과 구체적인
value가 있습니다. 그 결과 CUSTOM 이벤트도 다른 이벤트와 동일하게
타입이 좁혀지는 스트림이 됩니다.
이벤트 읽기: 일반적인 타입 좁히기 패턴
chunk.type === 'CUSTOM'을 확인한 다음 chunk.name을 리터럴 문자열과
비교합니다. 이것이 클라이언트 측 API의 전부입니다. 가져와야 할
isCustomEvent 또는 isSandboxEvent 가드는 없습니다.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.5"),
messages: [{ role: "user", content: "Hello" }],
});
for await (const chunk of stream) {
if (chunk.type === "CUSTOM" && chunk.name === "sandbox.file.diff") {
console.log(chunk.value.path, chunk.value.diff); // typed, no helper, no cast
} else if (chunk.type === "CUSTOM" && chunk.name === "structured-output.complete") {
console.log(chunk.value.object); // typed, no helper, no cast
}
}
전체 분류
아래의 모든 인터페이스는 기본 CustomEvent(type: 'CUSTOM' 및 선택적인
model?)를 확장하며, 리터럴 name과 구체적인 value를 가집니다. 모든
인터페이스는 KnownCustomEvent로 유니온되며, 각각의 개별 인터페이스와
함께 @tanstack/ai에서 내보냅니다.
| 인터페이스 | name | value | 발생 시점 |
|---|---|---|---|
SandboxFileCustomEvent | sandbox.file | { type: 'create' | 'change' | 'delete'; path: string; timestamp: number } | 활성 샌드박스에서 파일이 생성/변경/삭제될 때마다 |
SandboxFileDiffEvent | sandbox.file.diff | { path: string; diff: string } | 파일이 변경될 때마다. fileEvents: { diff: true }로 옵트인해야 합니다. |
FileChangedEvent | file.changed | { path: string; diff: string } | 하네스 어댑터(Grok Build, Claude Code, …)에서 실행이 완료된 후 한 번 |
SessionIdEvent | `${string}.session-id` | { sessionId: string } | 하네스 어댑터의 샌드박스 내 세션이 생성되거나 재개될 때 한 번 |
CodeModeExecutionStartedEvent | code_mode:execution_started | { timestamp: number; codeLength: number } | Code Mode에서 샌드박스 실행이 시작될 때 |
CodeModeConsoleEvent | code_mode:console | { level: 'log' | 'warn' | 'error' | 'info'; message: string; timestamp: number } | Code Mode에서 샌드박스 내부의 console.* 호출마다 |
CodeModeExternalCallEvent | code_mode:external_call | { function: string; args: unknown; timestamp: number } | Code Mode에서 바인딩된 external_* 함수가 실행되기 전에 |
CodeModeExternalResultEvent | code_mode:external_result | { function: string; result: unknown; duration: number } | Code Mode에서 external_* 호출이 성공한 후 |
CodeModeExternalErrorEvent | code_mode:external_error | { function: string; error: string; duration: number } | Code Mode에서 external_* 호출이 예외를 발생시킬 때 |
CodeModeSnippetCallEvent | code_mode:snippet_call | { snippet: string; input: unknown; timestamp: number } | 스니펫을 사용하는 Code Mode에서 스니펫이 실행되기 전에 |
CodeModeSnippetResultEvent | code_mode:snippet_result | { snippet: string; result: unknown; duration: number; timestamp: number } | 스니펫을 사용하는 Code Mode에서 스니펫 실행이 성공한 후 |
CodeModeSnippetErrorEvent | code_mode:snippet_error | { snippet: string; error: string; duration: number; timestamp: number } | 스니펫을 사용하는 Code Mode에서 스니펫이 예외를 발생시킬 때 |
SnippetRegisteredEvent | snippet:registered | { id: string; name: string; description: string; timestamp: number } | 스니펫이 도구 레지스트리에 등록될 때 |
StructuredOutputStartEvent | structured-output.start | { messageId: string } | chat({ outputSchema, stream: true }), 구조화된 메시지마다 한 번 |
StructuredOutputCompleteEvent<T> | structured-output.complete | { object: T; raw: string; reasoning?: string } | 구조화된 출력 스트리밍에서 검증된 객체와 함께 한 번 |
ApprovalRequestedEvent | approval-requested | { toolCallId: string; toolName: string; input: unknown; approval: { id: string; needsApproval: true } } | 서버 도구에 승인이 필요할 때 실행이 일시 중지됩니다. 도구 승인 흐름을 참조하세요. |
ToolInputAvailableEvent | tool-input-available | { toolCallId: string; toolName: string; input: unknown } | 클라이언트 도구가 호출되면 실행이 일시 중지됩니다. 클라이언트 도구를 참조하세요. |
UIResourceEvent | ui-resource | { resource; serverId?: string; toolCallId: string; toolName: string; meta?: Record<string, unknown> } | MCP 도구가 ui:// 리소스를 반환할 때(MCP Apps) |
이 이벤트는 CUSTOM으로 유지됩니다
하네스 *.session-id 이벤트와 structured-output.start / structured-output.complete은
CUSTOM으로 유지됩니다. RUN_FINISHED의 필드가 아닙니다.
이 페이지의 다른 이벤트와 동일한 chunk.type === "CUSTOM" && chunk.name === "..."
분기에서 읽습니다. 전체 이벤트는 구조화된 출력 스트리밍을
참조하세요.
자체 사용자 지정 이벤트는 이 유니온에 포함되지 않습니다
도구와 chat 미들웨어는 emitCustomEvent를 통해 애플리케이션이 정의한
이벤트를 발생시킬 수 있습니다.
서버 도구는 실행 컨텍스트에서 emitCustomEvent를 받습니다.
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const importRows = toolDefinition({
name: "importRows",
description: "Import rows into the dataset, reporting progress as it runs",
inputSchema: z.object({ rows: z.array(z.string()) }),
}).server(async ({ rows }, context) => {
for (let i = 0; i < rows.length; i++) {
context?.emitCustomEvent("my-app:progress", {
done: i + 1,
total: rows.length,
});
}
return { imported: rows.length };
});
Chat 미들웨어는 ChatMiddlewareContext에서 동일한 헬퍼를 호출합니다. 엔진은
훅이 아직 실행 중일 때 청크를 yield하므로, 실행 시간이 긴 onConfig도 작업이
완료되기 전에 started를 보낼 수 있습니다.
import { type ChatMiddleware } from "@tanstack/ai";
async function prepare() {
await new Promise<void>((resolve) => {
setTimeout(resolve, 1);
});
}
const progress: ChatMiddleware = {
name: "progress",
async onConfig(ctx) {
if (ctx.phase !== "beforeModel") return;
ctx.emitCustomEvent("my-app:progress", { step: "prepare" });
await prepare();
ctx.emitCustomEvent("my-app:progress", { step: "ready" });
},
};
이 이벤트는 기본 제공 이벤트와 정확히 동일하게 전송됩니다. CUSTOM 청크
형태와 런타임 동작도 동일합니다. 그러나 'my-app:progress'는
KnownCustomEvent의 리터럴 이름 중 하나가 아니므로 ChatStream 타입에서
의도적으로 제외됩니다. 이는 StructuredOutputStream이 이미 선택한 것과
동일한 트레이드오프입니다. 제네릭 대체 멤버를 포함하면 스트림의 다른 모든
이벤트에 value: any 오염이 다시 발생하기 때문입니다.
자체 이벤트의 value를 읽으려면 해당 분기에서 ChatStream의 좁혀진
유니온에 의존하지 말고, 대신 스트림에 더 넓은 StreamChunk 타입을
지정합니다. 이 경우 제네릭 CUSTOM 멤버의 value: any에 캐스트가
필요하지 않습니다.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import type { StreamChunk } from "@tanstack/ai";
import { importRows } from "./tools";
const stream: AsyncIterable<StreamChunk> = chat({
adapter: openaiText("gpt-5.5"),
messages: [{ role: "user", content: "Import these rows" }],
tools: [importRows],
});
for await (const chunk of stream) {
if (chunk.type === "CUSTOM" && chunk.name === "my-app:progress") {
console.log(chunk.value.done, chunk.value.total); // value: any — your event, your shape
}
}
어느 방식이든 이벤트는 런타임에 계속 도착합니다. 이는 TypeScript가 작성할
수 있도록 허용하는 내용만 변경합니다. 완전한 타입 안전성으로 TanStack AI
자체 이벤트를 읽을 때는 ChatStream이 적절한 기본값이며, 자체 이벤트를
읽는 분기에서는 StreamChunk로 대체합니다.
관련 항목
- 이벤트 메타데이터 — 사용자 지정 AG-UI 서버가 전송해야
useChat이finishReason과 모델을 가져올 수 있는metadata.tanstack필드입니다. - 샌드박스 이벤트 — 이 표의 샌드박스 및 하네스 관련 행을 맥락과 함께 설명하며,
sandbox.file.diff의 옵트인도 다룹니다. - 관측 가능성 —
sandbox.file.diff를 뒷받침하는 서버 측 훅 접근자(before()/after()/diff())입니다. - UI에 Code Mode 표시 —
code_mode:*이벤트를 실시간으로 렌더링합니다. - 스트리밍 UI —
structured-output.complete을 처음부터 끝까지 읽습니다. - 스트리밍 — 이 유니온이 확장하는 표준 AG-UI
StreamChunk수명 주기입니다.