본문으로 건너뛰기

런타임 컨텍스트

런타임 컨텍스트는 도구 구현과 미들웨어에 전달하는 애플리케이션 상태입니다. 인증된 사용자, 데이터베이스 클라이언트, 테넌시, 기능 플래그, 감사 로거 또는 브라우저 서비스와 같은 요청 범위 또는 클라이언트 로컬 의존성에 사용합니다.

런타임 컨텍스트는 프롬프트 컨텍스트가 아니며 AG-UI의 RunAgentInput.context 필드도 아닙니다. 모델로 자동 전송되지 않습니다.

타입 안전성이 작동하는 방식

런타임 컨텍스트는 이를 소비하는 코드의 관점에서 검사됩니다. 도구와 미들웨어는 필요한 컨텍스트 형태를 선언하고, chat(), ChatClient, 프레임워크 훅은 전달한 context 값이 해당 요구 사항을 충족하는지 검사합니다.

기준이 되는 선언은 다음과 같습니다.

  • 서버 도구의 경우 toolDefinition(...).server<TContext>(...)입니다.
  • 클라이언트 도구의 경우 toolDefinition(...).client<TContext>(...)입니다.
  • 미들웨어의 경우 ChatMiddleware<TContext>입니다.

즉, 컨텍스트 값은 런타임에 제공하는 구현 세부 사항이고 도구와 미들웨어는 계약입니다. TanStack AI는 호출에 포함된 타입 지정 도구와 미들웨어에서 필요한 컨텍스트를 추론하고, 해당 요구 사항을 병합한 뒤 결과와 context 옵션을 대조합니다.

import { chat, toServerSentEventsResponse, toolDefinition, 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);
},
};

export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [currentUserTool],
middleware: [tenantMiddleware],
context: {
userId: "user_123",
tenantId: "tenant_456",
},
});
return toServerSentEventsResponse(stream);
}

이 예제에서 도구는 UserContext를, 미들웨어는 TenantContext를 요구하므로 context 값은 두 요구 사항을 모두 충족해야 합니다. tenantId를 제거하면 tenantMiddleware가 이를 필요로 한다고 선언했기 때문에 TypeScript가 오류를 보고합니다.

이는 의도된 동작입니다. context 객체만으로 어떤 도구와 미들웨어가 읽을 수 있는지를 결정해서는 안 됩니다. 소비자가 요구 사항을 정의하고 호출 위치가 해당 요구 사항을 제공했음을 증명합니다. 타입이 지정되지 않은 도구와 미들웨어도 계속 작동하며, unknown 컨텍스트를 받고 context 옵션을 강제하지 않습니다.

이 추론은 재사용 가능한 도구나 미들웨어를 chat() 호출 외부에서 선언하고 배열로 전달하는 경우에도 작동합니다. 소비자는 TContext | undefined를 선언하여 선택적 런타임 컨텍스트를 사용할 수 있으며, 타입이 지정된 모든 소비자가 undefined를 허용하면 context 옵션을 생략할 수 있습니다. 컨텍스트 값을 제공하는 경우에도 모든 타입 지정 소비자를 충족해야 합니다.

클라이언트에도 동일한 규칙이 적용됩니다.

import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { toolDefinition } from "@tanstack/ai";

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

useChat({
connection: fetchServerSentEvents("/api/chat"),
tools: [inspectClientContext],
context: {
currentTabId: "settings",
mode: "debug",
},
});

클라이언트 도구가 ClientRuntimeContext & { mode: "debug" }를 선언하므로 useChat()에는 currentTabId와 리터럴 mode: "debug"를 모두 포함한 context 값이 필요합니다.

서버 런타임 컨텍스트

컨텍스트 타입을 한 번 정의하고 서버 도구와 미들웨어에서 사용한 다음, 이에 맞는 context 값을 chat()에 전달합니다.

import {
chat,
toServerSentEventsResponse,
toolDefinition,
type ChatMiddleware,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
import { requireUser, db } from "./auth";

type AppContext = {
userId: string;
tenantId: string;
db: {
notes: {
findMany(args: { userId: string; tenantId: string }): Promise<Array<{ title: string }>>;
};
};
};

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 ctx.context.db.notes.findMany({
userId: ctx.context.userId,
tenantId: ctx.context.tenantId,
});
});

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 } = await request.json();
const user = await requireUser(request);

const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [listNotes],
middleware: [auditMiddleware],
context: {
userId: user.id,
tenantId: user.tenantId,
db,
},
});

return toServerSentEventsResponse(stream);
}

chat() 호출의 도구나 미들웨어 중 하나라도 구체적인 컨텍스트 타입을 선언하면 TypeScript가 해당 타입에 맞춰 context 값을 검사합니다. 기존의 타입이 지정되지 않은 도구와 미들웨어도 계속 작동하며, 해당 도구와 미들웨어의 ctx.context 타입은 unknown으로 유지됩니다.

클라이언트 런타임 컨텍스트

클라이언트 런타임 컨텍스트는 ChatClient와 프레임워크 훅에 로컬입니다. 클라이언트 도구 구현에 전달되며 서버로 직렬화되지 않습니다.

import { createChatClientOptions } from "@tanstack/ai-client";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { toolDefinition } from "@tanstack/ai";

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"),
tools: [notifyUser],
context: {
currentTabId: "settings",
toast: (message) => window.alert(message),
},
});

const chat = useChat(chatOptions);

클라이언트 컨텍스트는 로컬 의존성에만 사용합니다. 서버가 받을 것으로 기대하는 값을 여기에 넣지 마세요.

클라이언트에서 서버로 전달

직렬화 가능한 클라이언트 데이터를 서버로 보내려면 forwardedProps를 사용하고, 라우트에서 이를 검증한 뒤 서버 런타임 컨텍스트에 명시적으로 매핑합니다.

import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { toolDefinition } from "@tanstack/ai";

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

// Client
useChat({
connection: fetchServerSentEvents("/api/chat"),
tools: [notifyUser],
forwardedProps: {
tenantId: "tenant_456",
},
context: {
currentTabId: "settings",
toast: (message) => window.alert(message),
},
});
// Server
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { requireUser } from "./auth";
import { serverTools } from "./tools";

type AppContext = {
userId: string;
tenantId: string;
};

export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
const user = await requireUser(request);

const tenantId =
typeof params.forwardedProps.tenantId === "string"
? params.forwardedProps.tenantId
: user.defaultTenantId;

const stream = chat({
adapter: openaiText("gpt-5.5"),
messages: params.messages,
tools: serverTools,
context: {
userId: user.id,
tenantId,
} satisfies AppContext,
});

return toServerSentEventsResponse(stream);
}

forwardedProps를 클라이언트가 제어하는 입력으로 취급합니다. 서버 런타임 컨텍스트를 구성하기 전에 모든 필드를 검증하고 허용 목록에 등록해야 합니다.

AG-UI 컨텍스트

AG-UI는 상호 운용 가능한 에이전트를 위한 프로토콜 수준 컨텍스트 항목으로 보통 사용하는 RunAgentInput.context도 정의합니다. TanStack AI는 chatParamsFromRequest를 통해 이 필드를 노출하지만 chat({ context })와는 별개입니다.

TanStack AI는 AG-UI의 params.aguiContext를 런타임 컨텍스트로 자동 복사하지 않습니다. AG-UI 컨텍스트 값을 사용하려면 직접 검증하고 매핑해야 합니다. params.context는 이전 버전과의 호환성을 위해 유지되는 params.aguiContext의 더 이상 권장되지 않는 별칭입니다.

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-5.5"),
messages: params.messages,
tools: serverTools,
context: buildRuntimeContextFrom(params.aguiContext),
});

return toServerSentEventsResponse(stream);
}