본문으로 건너뛰기

AG-UI 클라이언트-서버 규격 준수로 마이그레이션

요약: @tanstack/ai@tanstack/ai-client를 함께 업그레이드합니다. useChat의 메시지, 사고 과정, 도구, 승인은 계속 작동합니다. HTTP wire에는 호환성이 깨지는 0.x 변경이 적용됩니다. 추가 값은 metadata.tanstack에 저장되며 메시지에는 parts가 없습니다. Wire 메시지는 content, toolCalls, fan-out된 role: "tool" / role: "reasoning" 행을 사용합니다. 레거시 body 클라이언트 옵션과 data wire 필드는 지원 중단 브리지로 계속 작동합니다.

변경 사항

@tanstack/ai-client는 이제 AG-UI 0.0.52 RunAgentInput 요청 본문을 POST합니다. 이전 필드(messages, data)도 새 AG-UI 필드와 함께 전송되므로 기존 서버와 클라이언트는 코드 변경 없이 계속 작동합니다.

이전 wire 형태

{
"messages": [...],
"data": {...}
}

새 wire 형태(지원 중단 브리지 포함)

{
"threadId": "thread-...",
"runId": "run-...",
"state": {},
"messages": [...],
"tools": [...],
"context": [],
"forwardedProps": {...},
"data": {...}
}

forwardedPropsdata는 같은 내용을 전달합니다. 새 서버는 forwardedProps를 읽어야 하며, data를 읽는 레거시 서버는 변경 없이 계속 작동합니다. data 필드는 향후 메이저 릴리스에서 제거됩니다.

wire의 messages 배열은 AG-UI 사양을 따릅니다. 기준 메시지는 content, toolCalls, metadata를 사용하며 parts를 포함하지 않습니다. 도구 결과와 사고 과정은 추가 { role: "tool" }, { role: "reasoning" } 메시지로 전송됩니다.

AG-UI 이벤트 및 메시지 추가 값

이는 실제 동작 변경입니다. @tanstack/ai@tanstack/ai-client를 함께 업그레이드합니다.

Wire 이벤트는 사양 필드를 유지하고 metadata를 추가합니다. TanStack 추가 값은 metadata.tanstack 아래에 저장됩니다. 공개 StreamChunk는 사양 필드만 포함합니다.

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 === "RUN_FINISHED") {
console.log(chunk.usage);
console.log(chunk.metadata?.tanstack?.finishReason);
}
}

이는 다음을 의미합니다.

  • Wire 이벤트. 사양 필드는 최상위에 유지됩니다. 추가 TanStack 필드는 metadata.tanstack에 들어갑니다. 사용자 지정 서버는 이벤트 메타데이터를 참조합니다.
  • Wire 메시지. content, toolCalls, fan-out된 role: "tool" / role: "reasoning"을 사용합니다. wire에는 parts 필드가 없습니다.
  • chat() 청크. 프로세스 내 chat()은 계속 toolName, TOOL_CALL_END.input, TanStack TokenUsage(promptTokens)를 생성합니다. SSE/HTTP wire는 usage를 사양 배열(inputTokens)로 변환합니다.
  • 사용량. 미들웨어 onUsage는 계속 TanStack TokenUsage(promptTokens, completionTokens)를 받습니다.

for await 분기는 스트리밍, onUsage미들웨어를 참조합니다.

하위 호환성 및 지원 중단 일정

이 릴리스에서는 세 가지 호환성 브리지를 도입합니다.

영역이전이후(지원 중단 예정, 계속 작동)권장
클라이언트 옵션 (useChat, ChatClient)body: { ... }body: { ... }forwardedProps: { ... }
서버 와이어 필드body.data.Xbody.data.X (forwardedProps의 미러로 생성됨)body.forwardedProps.X 또는 params.forwardedProps.X(chatParamsFromRequest 사용)
서버 chat() 옵션conversationIdconversationId (계속 허용됨)threadId (또는 chatParamsFromRequest에 의존)

세 브리지는 모두 다음 메이저 릴리스에서 제거됩니다. 그때까지는 이전 방식과 새 방식을 자유롭게 함께 사용할 수 있습니다. bodyforwardedProps를 모두 useChat에 전달하면 병합되며, 키가 충돌할 때는 forwardedProps가 우선합니다.

자동 codemod

jscodeshift codemod를 클라이언트 측 이름 변경에 사용할 수 있습니다. 코드베이스에서 실행하면 useChat({ body }), new ChatClient({ body }), updateOptions({ body }), Svelte의 updateBody(...), chat({ conversationId })를 한 번에 정식 이름으로 변경합니다.

npx jscodeshift \
--parser=tsx \
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
"src/**/*.{ts,tsx}"

변경 사항을 먼저 미리 보려면 --dry --print를 추가합니다. 이 codemod는 import source를 기준으로 동작하므로 @tanstack/ai* 패키지를 import하지 않는 파일은 변경하지 않습니다. 전체 변환 목록, 충돌 처리 규칙, 제한 사항은 codemods/ag-ui-compliance/README.md를 참조합니다.

서버 측 body.data.X 변경은 자동화되지 않습니다. 특정 body.data.foo 읽기가 TanStack AI 라우트 핸들러에 속하는지 관련 없는 코드에 속하는지 구문 codemod로 안정적으로 판별할 수 없습니다. 아래 Tier 2 / Tier 3 레시피를 사용해 직접 마이그레이션합니다.

conversationIdthreadId

conversationId는 AG-UI 이전에 "클라이언트와 서버 devtools 이벤트를 연관시키는 데 사용하는 이 대화의 안정적인 식별자"를 뜻하던 이름입니다. AG-UI의 threadId는 표준 이름을 사용하는 동일한 개념입니다. 이제 API 전체에서 conversationIdthreadId의 지원 중단 예정 별칭입니다. 어느 이름을 전달해도 같은 내부 값으로 확인됩니다.

wire에서 변경된 사항: 클라이언트는 더 이상 forwardedProps.conversationId를 자동으로 전송하지 않습니다. 이제 AG-UI 최상위 threadId 필드만 전송합니다. useChat({ forwardedProps: { conversationId } })(또는 레거시 body)를 명시적으로 설정하면 해당 값은 변경 없이 전달됩니다.

서버 코드에 미치는 영향:

  • conversationId를 참조하지 않는 서버 코드는 영향을 받지 않습니다. chat({ conversationId })를 생략하면 런타임이 요청마다 안정적인 threadId를 자동 생성하고 devtools 이벤트 연관에 사용합니다.
  • chat({ conversationId: 'foo' })는 계속 작동합니다. conversationId는 이제 threadId의 지원 중단 예정 별칭이며 내부적으로 확인됩니다. 코드 변경은 필요하지 않습니다.
  • chat({ threadId: 'foo' })가 정식 형식입니다. 새 코드에서는 이를 우선 사용합니다. 둘 다 전달하면 threadId가 우선합니다.
  • TextOptions.conversationId는 JSDoc에서 @deprecated입니다. 향후 메이저 릴리스에서 제거됩니다.

확인해야 할 실제 동작 변경 한 가지. 서버가 body.forwardedProps?.conversationId(또는 레거시 body.data?.conversationId)를 읽어 chat({ conversationId })에 전달하면 업그레이드된 @tanstack/ai-client에서는 값이 undefined가 됩니다. 클라이언트가 더 이상 conversationId를 자동 전송하지 않기 때문입니다. 자동 생성된 threadId는 단일 요청 내부의 devtools 연관을 유지하지만, 요청 간 threadId 안정성은 클라이언트가 자체 threadId를 전송하는지에 달려 있습니다(ChatClient는 전송합니다. params.threadId로 노출되는 AG-UI 최상위 threadId를 참조합니다). 이전 식별자를 복원하려면 서버가 params.threadId를 읽어 chat({ threadId: params.threadId })에 전달하거나, 요청 간 안정성이 필요하지 않다면 자동 대체에 의존합니다.

사용자 지정 미들웨어: ChatMiddlewareContext는 이제 ctx.threadId(정식)와 ctx.conversationId(ctx.threadId와 항상 같은 지원 중단 예정 별칭)를 모두 노출합니다. 새 미들웨어는 ctx.threadId를 읽어야 하며, ctx.conversationId를 읽는 기존 미들웨어도 계속 작동합니다.

// Before — explicit conversationId plumbing
const params = await chatParamsFromRequest(req)
chat({
messages: params.messages,
conversationId: params.forwardedProps.conversationId, // ← auto-emitted by old client
})

// After — drop the plumbing entirely
const params = await chatParamsFromRequest(req)
chat({ messages: params.messages })
// devtools correlation auto-uses the resolved threadId

서버 엔드포인트 업그레이드 — 티어 선택

업그레이드는 선택 사항입니다. 사용하는 기능에 맞는 티어를 선택합니다. 대부분의 서버는 Tier 1에 해당하므로 코드를 변경할 필요가 없습니다.

Tier 1 — 최소(대부분의 서버는 변경 없음)

body.messages를 계속 읽어 그대로 전달합니다. chat()은 혼합된 UIMessage | ModelMessage 배열을 허용하고 AG-UI 메시지 형태의 모든 특수 동작을 내부에서 처리합니다. fan-out 도구 중복 제거, 다음 어시스턴트에 reasoning 행을 사고 과정으로 연결, activity 삭제, developersystem으로 축약을 수행합니다.

import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { serverTools } from './tools'

export async function POST(req: Request) {
const body = await req.json()
const provider = body.data?.provider // ← still works (legacy mirror)
// or, equivalently and recommended:
// const provider = body.forwardedProps?.provider

const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: body.messages, // AG-UI mixed shape — works directly
tools: serverTools,
})
return toServerSentEventsResponse(stream)
}

기존 엔드포인트가 body.data.X를 읽는다면 그대로 둡니다. 다음 메이저 릴리스까지 wire는 forwardedProps를 정확히 복제한 data 필드를 전송합니다. 편한 시점에 body.forwardedProps.X(또는 Tier 2의 params.forwardedProps.X)로 마이그레이션합니다.

다음 중 하나가 필요할 때 chatParamsFromRequest를 도입합니다.

  • 잘못된 본문에 대한 깔끔한 400 응답(RunAgentInputSchema에 대한 Zod 검증)
  • 클라이언트가 지정한 옵션(provider, model, temperature 등)을 위한 forwardedProps 접근
  • 관찰성, 로깅 또는 downstream 전달을 위한 threadId, runId, parentRunId 같은 AG-UI 메타데이터 접근(제공되지 않으면 런타임이 자동 생성하며, 사용할 필요가 있을 때만 params에서 읽으면 됩니다.)
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { serverTools } from './tools'

export async function POST(req: Request) {
const params = await chatParamsFromRequest(req)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
tools: serverTools,
})
return toServerSentEventsResponse(stream)
}

chatParamsFromRequestreq.json()을 읽고 AG-UI RunAgentInputSchema에 대해 검증합니다. 실패하면 400 Response를 throw하며 TanStack Start, SolidStart, Remix, React Router 7 같은 프레임워크는 이를 클라이언트에 자동으로 반환합니다.

프레임워크 참고: Next.js Route Handlers, SvelteKit, Hono, raw Node는 throw된 Response 객체를 자동으로 처리하지 않습니다. 이 환경에서는 호출을 try/catch로 감싸 잡은 Response를 반환하거나, 자체 오류 처리와 함께 chatParamsFromRequestBody(await req.json())를 직접 사용합니다.

Tier 3 — 선택 사항: 클라이언트가 도구를 알리도록 설정

mergeAgentTools를 사용하면 클라이언트가 요청 페이로드(RunAgentInput.tools)에서 도구를 선언하고 요청별로 서버에 등록할 수 있습니다. 이는 기존 패턴을 편리하게 사용하는 기능일 뿐이며, 마이그레이션 요구 사항은 아닙니다.

서버의 tools 배열에 클라이언트 측 도구를 이미 등록했다면 .server() 구현이 없는 도구도 포함해 해당 패턴은 이전과 정확히 동일하게 작동합니다. 런타임은 execute가 없는 도구를 클라이언트 측으로 취급하고 ClientToolRequest 이벤트를 보냅니다. 등록 방식이 정적 배열인지 mergeAgentTools인지는 중요하지 않습니다.

클라이언트가 도구 공개를 주도하게 하려는 경우에만 이 티어를 도입합니다(예: 세션마다 다른 도구를 표시하고 서버의 정적 레지스트리와 동기화하고 싶지 않은 경우). Tier 2와의 유일한 차이는 tools 줄이며 serverToolsmergeAgentTools(serverTools, params.tools)로 감쌉니다.

import {
chat,
chatParamsFromRequest,
mergeAgentTools,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { serverTools } from './tools'

export async function POST(req: Request) {
const params = await chatParamsFromRequest(req)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
// `mergeAgentTools` returns a plain array — pass it straight to `tools`.
tools: mergeAgentTools(serverTools, params.tools), // ← merges client-declared tools
})
return toServerSentEventsResponse(stream)
}

mergeAgentTools는 클라이언트가 선언한 도구를 서버에서 실행 없는 스텁으로 등록합니다. 모델이 이를 호출하면 런타임이 ClientToolRequest 이벤트를 클라이언트로 보내고, 클라이언트는 등록된 핸들러로 실행한 후 결과를 다시 POST합니다.

보안 — 병합은 클라이언트가 도구 표면의 일부를 정의한다고 신뢰합니다. params.tools는 공격자가 제어할 수 있습니다. 악의적이거나 손상된 클라이언트는 RunAgentInput.tools에 원하는 name / description / parameters를 넣을 수 있습니다. 이를 병합하면 해당 정의가 모델에 공개됩니다. 이름이 충돌하면 서버 도구가 우선하며(클라이언트는 서버 도구의 execute가리거나 탈취할 수 없습니다), 클라이언트 선언 도구는 실행되지 않고 같은 클라이언트로 왕복하여 실행됩니다. 그러나 클라이언트는 도구 이름과 설명을 통해 공개된 도구 표면을 확장하고 모델 컨텍스트에 임의의 텍스트를 주입할 수 있습니다(프롬프트 인젝션 벡터).

안전한 기본값은 도구 정의를 서버의 tools 배열에 정적으로 등록하는 것입니다. 클라이언트 실행 도구도 포함하며 .server()가 없는 정의도 작동합니다. 그리고 mergeAgentTools를 호출하지 않습니다. 그러면 클라이언트가 페이로드에서 선언한 도구는 무시됩니다. 모델에 전달되지 않으므로 호출되거나 실행되지 않습니다. 클라이언트가 도구 공개를 주도하기를 진정으로 원하고 해당 클라이언트를 신뢰할 때만 mergeAgentTools를 사용합니다.

forwardedProps 보안(Tier 2 이상만 해당)

Tier 1을 사용하는 경우 이 섹션을 건너뜁니다. forwardedPropschatParamsFromRequest(또는 chatParamsFromRequestBody)를 선택한 경우에만 노출됩니다.

forwardedProps는 클라이언트가 제어하는 임의의 JSON입니다. 이를 chat({...})에 직접 전개하지 않습니다.

// 🚫 UNSAFE — a client could override `adapter`, `model`, `tools`, system prompts, anything
chat({
adapter: openaiText('gpt-5.5'),
...params,
...params.forwardedProps,
})

항상 전달하려는 특정 필드만 구조 분해합니다.

import {
chat,
chatParamsFromRequest,
mergeAgentTools,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { serverTools } from './tools'

export async function POST(req: Request) {
const params = await chatParamsFromRequest(req)

// ✅ SAFE — explicit allowlist. Sampling params live in modelOptions under
// each provider's native key (OpenAI: temperature / max_output_tokens).
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
tools: mergeAgentTools(serverTools, params.tools),
modelOptions: {
temperature:
typeof params.forwardedProps.temperature === 'number'
? params.forwardedProps.temperature
: undefined,
max_output_tokens:
typeof params.forwardedProps.maxTokens === 'number'
? params.forwardedProps.maxTokens
: undefined,
},
})
return toServerSentEventsResponse(stream)
}

전달된 값을 런타임 컨텍스트에 매핑

TanStack AI의 chat({ context })는 도구와 미들웨어를 위한 타입 지정 런타임 컨텍스트입니다. AG-UI의 RunAgentInput.context와는 별개이며 프로토콜 필드에서 자동으로 채워지지 않습니다.

클라이언트 값를 서버 도구나 미들웨어에서 사용할 수 있어야 한다면 forwardedProps에서 검증한 후 런타임 컨텍스트를 명시적으로 구성합니다.

import {
chat,
chatParamsFromRequest,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { serverTools } from './tools'
import { session, defaultTenantId, req } from './context'

const params = await chatParamsFromRequest(req)

const tenantId =
typeof params.forwardedProps.tenantId === 'string'
? params.forwardedProps.tenantId
: defaultTenantId

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

useChat과 연결 어댑터(fetchServerSentEvents, fetchHttpStream)가 새 wire 형식을 내부적으로 처리합니다. 기존 UIMessage 상태는 변경되지 않습니다. useChat({ tools })에 전달한 도구는 이제 요청 페이로드에서 서버에 자동으로 알려집니다.

useChat / ChatClientbody 옵션은 이제 forwardedProps 사용을 위해 @deprecated입니다. 둘 다 허용되며 같은 wire 필드를 채웁니다. 편한 시점에 마이그레이션합니다.

import { useChat } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'

// Before — still works, but deprecated
useChat({
connection: fetchServerSentEvents('/api/chat'),
body: { provider: 'openai', model: 'gpt-5.5' },
})

// After — recommended
useChat({
connection: fetchServerSentEvents('/api/chat'),
forwardedProps: { provider: 'openai', model: 'gpt-5.5' },
})

부분 마이그레이션 중 둘 다 전달하면 키 충돌 시 forwardedProps가 우선하므로 오래된 body 값이 새 값을 가리지 않습니다.

Svelte에서는 updateBodyupdateForwardedProps로 변경합니다. 레거시 updateBody는 유지되며 @deprecated로 표시됩니다.

선택 사항: 명시적 스레드 제어

ChatClient를 직접 인스턴스화하고 스레드 식별자를 제어하려면 생성자 옵션으로 threadId를 전달합니다.

import { ChatClient } from '@tanstack/ai-client'
import { fetchServerSentEvents } from '@tanstack/ai-client'

const client = new ChatClient({
threadId: 'persistent-thread-from-storage',
connection: fetchServerSentEvents('/api/chat'),
})

threadId를 전달하지 않으면 자동으로 생성되어 ChatClient 인스턴스의 수명 동안 유지됩니다. 전송할 때마다 새로운 runId가 생성됩니다.

도구 병합 의미

  • 이름이 충돌하면 서버 도구가 우선합니다. toolDefinition().server(...)로 서버에 등록된 도구는 항상 서버에서 실행됩니다.
  • 클라이언트 전용 도구는 chat()에서 실행 없는 스텁이 됩니다(mergeAgentTools로 등록한 경우). 런타임은 클라이언트에 ClientToolRequest 이벤트를 보내며, 클라이언트의 등록 핸들러(훅의 tools 배열에 있는 .client(...) 도구)가 로컬에서 실행되고 결과를 POST합니다.
  • 이중 핸들러(양쪽에 모두 있는 경우): 서버가 실행한 다음 스트리밍된 도구 결과 이벤트가 도착하면 chat-client.tsonToolCall이 UI 부수 효과로 클라이언트 핸들러를 호출합니다. 대화에서는 서버 결과가 기준입니다.

외부 AG-UI 서버와 통신

외부 AG-UI 서버로 전송되는 @tanstack/ai-client 요청:

  • ✅ 단일 턴 사용자 메시지가 작동합니다. content가 AG-UI의 content 필드에 복제됩니다.
  • ✅ 서버가 생성한 이벤트가 올바르게 스트리밍되고 렌더링됩니다.
  • ✅ 이전 턴의 도구 결과를 포함한 다중 턴 기록은 외부 서버가 AG-UI fan-out 중복(별도의 {role:'tool',...} 메시지)을 통해 읽습니다.
  • ⚠️ 클라이언트 전용 도구는 AG-UI tools 필드로 전송되며 외부 서버가 실제로 호출하는지는 해당 서버의 도구 호출 로직에 따라 달라집니다.

외부 AG-UI 클라이언트에서 TanStack 서버와 통신

순수 AG-UI RunAgentInput 페이로드(TanStack parts 필드 없음)는 처음부터 끝까지 작동합니다.

  • 도구 메시지는 role: 'tool'ModelMessage 항목으로 전달됩니다.
  • reasoning 메시지는 다음 어시스턴트에 사고 과정으로 연결됩니다. 사양의 encryptedValueThinkingPart.signature가 됩니다.
  • activity 메시지는 삭제됩니다(TanStack에 해당 기능이 없음).
  • developer 메시지는 system 역할로 축약됩니다.

@ag-ui/core 버전 업

@tanstack/ai는 이제 @ag-ui/core@0.1.1-canary.beta.0에 의존합니다. 코드가 AG-UI 타입을 다시 내보내는 @tanstack/ai의 타입을 import한다면 약간의 타입 조정이 필요할 수 있습니다. 자세한 내용은 changeset을 참조합니다.

이제 zod가 자동으로 설치되지 않음

@ag-ui/core는 이전에 zod를 런타임 의존성으로 지정했으므로 @tanstack/ai를 설치할 때마다 zod가 전이적으로 설치되었습니다. 0.1.x부터는 zod를 선택적 peer로 선언하며 @tanstack/ai는 더 이상 어디에서도 zod를 사용하지 않습니다. 이제 패키지에는 스키마 검증 런타임이 전혀 포함되지 않습니다.

chatParamsFromRequest / chatParamsFromRequestBody만 zod를 사용했습니다. 이들은 AG-UI의 RunAgentInputSchema로 요청 본문을 검증했습니다. 이제 동일한 RunAgentInput 계약을 구조적으로 검증합니다. 시그니처, throw되는 타입(AGUIErrorchatParamsFromRequest의 400 Response), 메시지의 parts 전달은 모두 변경되지 않습니다. 눈에 보이는 유일한 차이는 오류가 더 이해하기 쉬워진다는 점이며, 예를 들어 오류가 문제가 된 필드를 표시합니다.

Request body is not a valid AG-UI RunAgentInput. ... Validation errors: messages[1].content must be a string

도구 정의에는 여전히 zod를 완전히 지원하지만 이제 대신 설치해 주지는 않습니다. 프로젝트에서 zod를 선언하지 않고 전이적으로 설치된 사본에 의존했다면 명시적으로 추가합니다.

npm install zod

다른 Standard Schema 라이브러리(ArkType, Valibot)로 도구를 정의한다면 이제 zod를 완전히 제거할 수 있습니다.

범위 외(기존 동작 유지)

  • LLM 제공자에 사고 과정 재생. 이 마이그레이션은 해당 경로를 변경하지 않습니다. 서명된 ThinkingPartModelMessage.thinking으로 변환되어 제공자가 실행한 도구 주변을 포함해 원래 순서로 재생됩니다. 서명되지 않은 사고 과정은 다시 전송되지 않습니다.
  • AG-UI statecontext 필드. chatParamsFromRequestBody의 반환 값에서 stateaguiContext로 노출되며, 하위 호환성을 위해 contextaguiContext의 지원 중단 예정 별칭으로 유지됩니다. 이는 엔드포인트에서 검사하거나 전달할 수 있는 프로토콜 수준 필드입니다. TanStack AI의 타입 지정 런타임 컨텍스트는 별도의 chat({ context }) 옵션이므로, 도구나 미들웨어에서 AG-UI 값을 읽게 하려면 직접 검증하고 매핑합니다.