@tanstack/ai 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/activities/chat/index.d.ts +12 -3
- package/dist/esm/activities/chat/index.js +10 -5
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +26 -2
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.js +1 -1
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +12 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +5 -0
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +29 -8
- package/dist/esm/utilities/ag-ui-wire.d.ts +44 -0
- package/dist/esm/utilities/ag-ui-wire.js +96 -0
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -0
- package/dist/esm/utilities/chat-params.d.ts +80 -0
- package/dist/esm/utilities/chat-params.js +96 -0
- package/dist/esm/utilities/chat-params.js.map +1 -0
- package/package.json +3 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +46 -3
- package/src/activities/chat/index.ts +33 -9
- package/src/activities/chat/messages.ts +44 -4
- package/src/activities/chat/middleware/compose.ts +1 -1
- package/src/activities/chat/middleware/types.ts +12 -1
- package/src/activities/chat/tools/schema-converter.ts +14 -0
- package/src/index.ts +11 -0
- package/src/types.ts +39 -8
- package/src/utilities/ag-ui-wire.ts +182 -0
- package/src/utilities/chat-params.ts +199 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ag-ui-wire.js","sources":["../../../src/utilities/ag-ui-wire.ts"],"sourcesContent":["import type { ContentPart, MessagePart, TextPart, UIMessage } from '../types'\n\ntype AGUITextInputContent = { type: 'text'; text: string }\ntype AGUIInputContent =\n | AGUITextInputContent\n | (ContentPart & { type: 'image' | 'audio' | 'video' | 'document' })\n\ntype AGUIToolCallMirror = {\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n}\n\ntype AGUIToolMessage = {\n role: 'tool'\n id: string\n toolCallId: string\n content: string\n error?: string\n}\n\ntype AGUIReasoningMessage = {\n role: 'reasoning'\n id: string\n content: string\n}\n\ntype WireAnchorMessage = UIMessage & {\n content?: string | Array<AGUIInputContent>\n toolCalls?: Array<AGUIToolCallMirror>\n}\n\nexport type WireMessage =\n | WireAnchorMessage\n | AGUIToolMessage\n | AGUIReasoningMessage\n\n/**\n * Serialize TanStack `UIMessage`s into the AG-UI `RunAgentInput.messages`\n * wire shape. Each anchor (system/user/assistant) carries the canonical\n * `parts` array verbatim plus AG-UI mirror fields (`content`, `toolCalls`)\n * so AG-UI Zod parsing succeeds. Tool results and thinking parts on\n * assistant messages are additionally emitted as fan-out\n * `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict\n * AG-UI server consumers.\n */\nexport function uiMessagesToWire(\n messages: Array<UIMessage>,\n): Array<WireMessage> {\n const wire: Array<WireMessage> = []\n\n for (const msg of messages) {\n // Defensive: if parts is missing (ModelMessage-shaped input), pass through as-is.\n // UIMessage always has parts; ModelMessage uses content directly.\n const parts: ReadonlyArray<MessagePart> =\n (msg.parts as ReadonlyArray<MessagePart> | undefined) ?? []\n\n if (msg.role === 'system') {\n wire.push({\n ...msg,\n content:\n parts.length > 0\n ? collectText(parts)\n : ((msg as unknown as { content?: string }).content ?? ''),\n })\n continue\n }\n\n if (msg.role === 'user') {\n wire.push({\n ...msg,\n content:\n parts.length > 0\n ? collectUserContent(parts)\n : ((msg as unknown as { content?: string }).content ?? ''),\n })\n continue\n }\n\n // assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs\n for (const part of parts) {\n if (part.type === 'thinking') {\n wire.push({\n role: 'reasoning',\n id: deriveReasoningId(msg.id, part),\n content: part.content,\n })\n }\n }\n\n const text = collectText(parts)\n const toolCalls = collectToolCalls(parts)\n wire.push({\n ...msg,\n ...(text !== '' && { content: text }),\n ...(toolCalls && { toolCalls }),\n })\n\n for (const part of parts) {\n if (part.type === 'tool-result') {\n wire.push({\n role: 'tool',\n id: deriveToolMessageId(part.toolCallId),\n toolCallId: part.toolCallId,\n content: part.content,\n ...(part.error !== undefined && { error: part.error }),\n })\n }\n }\n }\n\n return wire\n}\n\nfunction collectText(parts: ReadonlyArray<MessagePart>): string {\n return parts\n .filter((p): p is TextPart => p.type === 'text')\n .map((p) => p.content)\n .join('')\n}\n\nfunction collectUserContent(\n parts: ReadonlyArray<MessagePart>,\n): string | Array<AGUIInputContent> {\n const hasMultimodal = parts.some(\n (p) =>\n p.type === 'image' ||\n p.type === 'audio' ||\n p.type === 'video' ||\n p.type === 'document',\n )\n if (!hasMultimodal) {\n return collectText(parts)\n }\n const out: Array<AGUIInputContent> = []\n for (const p of parts) {\n if (p.type === 'text') {\n out.push({ type: 'text', text: p.content })\n } else if (\n p.type === 'image' ||\n p.type === 'audio' ||\n p.type === 'video' ||\n p.type === 'document'\n ) {\n out.push(p as AGUIInputContent)\n }\n }\n return out\n}\n\nfunction collectToolCalls(\n parts: ReadonlyArray<MessagePart>,\n): Array<AGUIToolCallMirror> | undefined {\n const calls: Array<AGUIToolCallMirror> = []\n for (const p of parts) {\n if (p.type === 'tool-call') {\n calls.push({\n id: p.id,\n type: 'function',\n function: { name: p.name, arguments: p.arguments },\n })\n }\n }\n return calls.length > 0 ? calls : undefined\n}\n\nfunction deriveReasoningId(messageId: string, part: MessagePart): string {\n return `${messageId}-reasoning-${(part as { id?: string }).id ?? hashContent((part as { content: string }).content)}`\n}\n\nfunction deriveToolMessageId(toolCallId: string): string {\n return `tool-${toolCallId}`\n}\n\nfunction hashContent(s: string): string {\n // Cheap deterministic id suffix; collisions are tolerable since\n // reasoning ids only matter for AG-UI server consumers, not for our\n // own server's dedup logic (which keys on toolCallId, not reasoning id).\n let h = 0\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0\n return Math.abs(h).toString(36)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Context as AGUIContext } from '@ag-ui/core';
|
|
2
|
+
import { JSONSchema, ModelMessage, Tool, UIMessage } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.
|
|
5
|
+
*
|
|
6
|
+
* Returns a spread-friendly object whose `messages` field is suitable for
|
|
7
|
+
* passing directly to `chat({ messages })`. The existing
|
|
8
|
+
* `convertMessagesToModelMessages` handles AG-UI fan-out dedup and
|
|
9
|
+
* reasoning/activity/developer-role normalization internally.
|
|
10
|
+
*
|
|
11
|
+
* @throws An error with a migration-pointing message when the body does
|
|
12
|
+
* not conform to AG-UI 0.0.52 `RunAgentInputSchema`. Surface this as a
|
|
13
|
+
* 400 Bad Request to the client.
|
|
14
|
+
*/
|
|
15
|
+
export declare function chatParamsFromRequestBody(body: unknown): Promise<{
|
|
16
|
+
messages: Array<UIMessage | ModelMessage>;
|
|
17
|
+
threadId: string;
|
|
18
|
+
runId: string;
|
|
19
|
+
parentRunId?: string;
|
|
20
|
+
tools: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
parameters: JSONSchema;
|
|
24
|
+
}>;
|
|
25
|
+
forwardedProps: Record<string, unknown>;
|
|
26
|
+
state: unknown;
|
|
27
|
+
context: Array<AGUIContext>;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Read an HTTP `Request`, parse its JSON body, and validate it as an
|
|
31
|
+
* AG-UI `RunAgentInput` — collapsing the standard `req.json()` +
|
|
32
|
+
* `chatParamsFromRequestBody(...)` pair into a single call.
|
|
33
|
+
*
|
|
34
|
+
* On a malformed body or invalid AG-UI shape, this **throws a
|
|
35
|
+
* `Response`** with status 400 and a migration-pointing message in the
|
|
36
|
+
* body. Frameworks that natively handle thrown `Response` objects
|
|
37
|
+
* (TanStack Start, SolidStart, Remix, React Router 7) will return the
|
|
38
|
+
* 400 to the client automatically, so the handler reduces to:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* export async function POST(req: Request) {
|
|
42
|
+
* const params = await chatParamsFromRequest(req)
|
|
43
|
+
* // ...use params
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* In frameworks that do not auto-handle thrown `Response` objects
|
|
48
|
+
* (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call
|
|
49
|
+
* with try/catch and return the caught Response yourself, or use
|
|
50
|
+
* `chatParamsFromRequestBody` directly with your own JSON-parsing.
|
|
51
|
+
*
|
|
52
|
+
* @throws {Response} 400 on malformed JSON or invalid AG-UI shape.
|
|
53
|
+
*/
|
|
54
|
+
export declare function chatParamsFromRequest(req: Request): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>>;
|
|
55
|
+
/**
|
|
56
|
+
* Merge a server-side tool array with the AG-UI client-declared tools
|
|
57
|
+
* received in the request body.
|
|
58
|
+
*
|
|
59
|
+
* Rules:
|
|
60
|
+
* - Server tools win on name collision. The client's declaration is
|
|
61
|
+
* ignored if the server already has a tool with that name. The client's
|
|
62
|
+
* UI-side handler still fires when the streamed tool-result event comes
|
|
63
|
+
* through (see `chat-client.ts` `onToolCall`), giving the
|
|
64
|
+
* "after server execution the client also handles" semantic for free.
|
|
65
|
+
* - Client-only tools (name not in `serverTools`) become no-execute
|
|
66
|
+
* entries: the runtime's existing `ClientToolRequest` path handles
|
|
67
|
+
* them — server emits a tool-call request, client executes via its
|
|
68
|
+
* registered handler, client posts back the result.
|
|
69
|
+
*
|
|
70
|
+
* @param serverTools - The server's tool array (e.g. from
|
|
71
|
+
* `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.
|
|
72
|
+
* @param clientTools - The `tools` array received from
|
|
73
|
+
* `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.
|
|
74
|
+
* @returns A merged array suitable for `chat({ tools })`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function mergeAgentTools(serverTools: ReadonlyArray<Tool>, clientTools: ReadonlyArray<{
|
|
77
|
+
name: string;
|
|
78
|
+
description: string;
|
|
79
|
+
parameters: JSONSchema;
|
|
80
|
+
}>): Array<Tool>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { RunAgentInputSchema, AGUIError } from "@ag-ui/core";
|
|
2
|
+
const KNOWN_PART_TYPES = /* @__PURE__ */ new Set([
|
|
3
|
+
"text",
|
|
4
|
+
"image",
|
|
5
|
+
"audio",
|
|
6
|
+
"video",
|
|
7
|
+
"document",
|
|
8
|
+
"tool-call",
|
|
9
|
+
"tool-result",
|
|
10
|
+
"thinking"
|
|
11
|
+
]);
|
|
12
|
+
function isValidParts(value) {
|
|
13
|
+
if (!Array.isArray(value)) return false;
|
|
14
|
+
for (const p of value) {
|
|
15
|
+
if (!p || typeof p !== "object") return false;
|
|
16
|
+
const type = p.type;
|
|
17
|
+
if (typeof type !== "string" || !KNOWN_PART_TYPES.has(type)) return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
function chatParamsFromRequestBody(body) {
|
|
22
|
+
const parseResult = RunAgentInputSchema.safeParse(body);
|
|
23
|
+
if (!parseResult.success) {
|
|
24
|
+
return Promise.reject(
|
|
25
|
+
new AGUIError(
|
|
26
|
+
`Request body is not a valid AG-UI RunAgentInput. If you're upgrading from a previous @tanstack/ai-client release, see docs/migration/ag-ui-compliance.md. Validation errors: ${parseResult.error.message}`
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const parsed = parseResult.data;
|
|
31
|
+
const rawMessages = body.messages ?? [];
|
|
32
|
+
const messages = parsed.messages.map((m, i) => {
|
|
33
|
+
const raw = rawMessages[i];
|
|
34
|
+
if (raw && typeof raw === "object" && "parts" in raw && isValidParts(raw.parts)) {
|
|
35
|
+
return { ...m, parts: raw.parts };
|
|
36
|
+
}
|
|
37
|
+
return m;
|
|
38
|
+
});
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
messages,
|
|
41
|
+
threadId: parsed.threadId,
|
|
42
|
+
runId: parsed.runId,
|
|
43
|
+
parentRunId: parsed.parentRunId,
|
|
44
|
+
tools: parsed.tools,
|
|
45
|
+
forwardedProps: parsed.forwardedProps ?? {},
|
|
46
|
+
state: parsed.state,
|
|
47
|
+
context: parsed.context
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function chatParamsFromRequest(req) {
|
|
51
|
+
let body;
|
|
52
|
+
try {
|
|
53
|
+
body = await req.json();
|
|
54
|
+
} catch (cause) {
|
|
55
|
+
const res = new Response(
|
|
56
|
+
"Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.",
|
|
57
|
+
{ status: 400 }
|
|
58
|
+
);
|
|
59
|
+
res.cause = cause;
|
|
60
|
+
throw res;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await chatParamsFromRequestBody(body);
|
|
64
|
+
} catch (cause) {
|
|
65
|
+
const res = new Response(
|
|
66
|
+
"Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.",
|
|
67
|
+
{ status: 400 }
|
|
68
|
+
);
|
|
69
|
+
res.cause = cause;
|
|
70
|
+
throw res;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function mergeAgentTools(serverTools, clientTools) {
|
|
74
|
+
const seen = new Set(serverTools.map((t) => t.name));
|
|
75
|
+
const merged = [...serverTools];
|
|
76
|
+
for (const ct of clientTools) {
|
|
77
|
+
if (seen.has(ct.name)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
seen.add(ct.name);
|
|
81
|
+
merged.push({
|
|
82
|
+
name: ct.name,
|
|
83
|
+
description: ct.description,
|
|
84
|
+
inputSchema: ct.parameters
|
|
85
|
+
// No `execute` — runtime treats this as a client-side tool and
|
|
86
|
+
// emits ClientToolRequest events.
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return merged;
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
chatParamsFromRequest,
|
|
93
|
+
chatParamsFromRequestBody,
|
|
94
|
+
mergeAgentTools
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=chat-params.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-params.js","sources":["../../../src/utilities/chat-params.ts"],"sourcesContent":["import { AGUIError, RunAgentInputSchema } from '@ag-ui/core'\nimport type { Context as AGUIContext } from '@ag-ui/core'\nimport type { JSONSchema, ModelMessage, Tool, UIMessage } from '../types'\n\nconst KNOWN_PART_TYPES = new Set([\n 'text',\n 'image',\n 'audio',\n 'video',\n 'document',\n 'tool-call',\n 'tool-result',\n 'thinking',\n])\n\nfunction isValidParts(value: unknown): value is Array<{ type: string }> {\n if (!Array.isArray(value)) return false\n for (const p of value) {\n if (!p || typeof p !== 'object') return false\n const type = (p as { type?: unknown }).type\n if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false\n }\n return true\n}\n\n/**\n * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`.\n *\n * Returns a spread-friendly object whose `messages` field is suitable for\n * passing directly to `chat({ messages })`. The existing\n * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and\n * reasoning/activity/developer-role normalization internally.\n *\n * @throws An error with a migration-pointing message when the body does\n * not conform to AG-UI 0.0.52 `RunAgentInputSchema`. Surface this as a\n * 400 Bad Request to the client.\n */\nexport function chatParamsFromRequestBody(body: unknown): Promise<{\n messages: Array<UIMessage | ModelMessage>\n threadId: string\n runId: string\n parentRunId?: string\n tools: Array<{ name: string; description: string; parameters: JSONSchema }>\n forwardedProps: Record<string, unknown>\n state: unknown\n context: Array<AGUIContext>\n}> {\n const parseResult = RunAgentInputSchema.safeParse(body)\n if (!parseResult.success) {\n return Promise.reject(\n new AGUIError(\n `Request body is not a valid AG-UI RunAgentInput. ` +\n `If you're upgrading from a previous @tanstack/ai-client release, ` +\n `see docs/migration/ag-ui-compliance.md. ` +\n `Validation errors: ${parseResult.error.message}`,\n ),\n )\n }\n\n const parsed = parseResult.data\n\n // AG-UI Zod uses `.strip()` so extra fields like `parts` on messages are\n // dropped during parse. We re-attach them from the original body so the\n // existing UIMessage path inside `chat()` can use them directly.\n const rawMessages =\n (body as { messages?: Array<Record<string, unknown>> }).messages ?? []\n const messages = parsed.messages.map((m, i) => {\n const raw = rawMessages[i]\n if (\n raw &&\n typeof raw === 'object' &&\n 'parts' in raw &&\n isValidParts(raw.parts)\n ) {\n return { ...m, parts: raw.parts } as UIMessage | ModelMessage\n }\n return m as ModelMessage\n })\n\n return Promise.resolve({\n messages,\n threadId: parsed.threadId,\n runId: parsed.runId,\n parentRunId: parsed.parentRunId,\n tools: parsed.tools as Array<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n forwardedProps: (parsed.forwardedProps ?? {}) as Record<string, unknown>,\n state: parsed.state,\n context: parsed.context,\n })\n}\n\n/**\n * Read an HTTP `Request`, parse its JSON body, and validate it as an\n * AG-UI `RunAgentInput` — collapsing the standard `req.json()` +\n * `chatParamsFromRequestBody(...)` pair into a single call.\n *\n * On a malformed body or invalid AG-UI shape, this **throws a\n * `Response`** with status 400 and a migration-pointing message in the\n * body. Frameworks that natively handle thrown `Response` objects\n * (TanStack Start, SolidStart, Remix, React Router 7) will return the\n * 400 to the client automatically, so the handler reduces to:\n *\n * ```ts\n * export async function POST(req: Request) {\n * const params = await chatParamsFromRequest(req)\n * // ...use params\n * }\n * ```\n *\n * In frameworks that do not auto-handle thrown `Response` objects\n * (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call\n * with try/catch and return the caught Response yourself, or use\n * `chatParamsFromRequestBody` directly with your own JSON-parsing.\n *\n * @throws {Response} 400 on malformed JSON or invalid AG-UI shape.\n */\nexport async function chatParamsFromRequest(\n req: Request,\n): Promise<Awaited<ReturnType<typeof chatParamsFromRequestBody>>> {\n let body: unknown\n try {\n body = await req.json()\n } catch (cause) {\n // Preserve the underlying error on the thrown Response for\n // server-side observability without leaking it to the client.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as unknown as { cause?: unknown }).cause = cause\n throw res\n }\n try {\n return await chatParamsFromRequestBody(body)\n } catch (cause) {\n // Generic public message — avoid echoing Zod paths (which can contain\n // user payload fragments) or internal validator strings to the client.\n // The original AGUIError is attached as `cause` so server logs can\n // surface it without exposing it to remote callers.\n const res = new Response(\n 'Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.',\n { status: 400 },\n )\n ;(res as unknown as { cause?: unknown }).cause = cause\n throw res\n }\n}\n\n/**\n * Merge a server-side tool array with the AG-UI client-declared tools\n * received in the request body.\n *\n * Rules:\n * - Server tools win on name collision. The client's declaration is\n * ignored if the server already has a tool with that name. The client's\n * UI-side handler still fires when the streamed tool-result event comes\n * through (see `chat-client.ts` `onToolCall`), giving the\n * \"after server execution the client also handles\" semantic for free.\n * - Client-only tools (name not in `serverTools`) become no-execute\n * entries: the runtime's existing `ClientToolRequest` path handles\n * them — server emits a tool-call request, client executes via its\n * registered handler, client posts back the result.\n *\n * @param serverTools - The server's tool array (e.g. from\n * `[myToolDef.server(...)]`). Pass directly to `chat({ tools })`.\n * @param clientTools - The `tools` array received from\n * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.\n * @returns A merged array suitable for `chat({ tools })`.\n */\nexport function mergeAgentTools(\n serverTools: ReadonlyArray<Tool>,\n clientTools: ReadonlyArray<{\n name: string\n description: string\n parameters: JSONSchema\n }>,\n): Array<Tool> {\n const seen = new Set(serverTools.map((t) => t.name))\n const merged: Array<Tool> = [...serverTools]\n for (const ct of clientTools) {\n if (seen.has(ct.name)) {\n // Server wins on name collision.\n continue\n }\n seen.add(ct.name)\n merged.push({\n name: ct.name,\n description: ct.description,\n inputSchema: ct.parameters,\n // No `execute` — runtime treats this as a client-side tool and\n // emits ClientToolRequest events.\n } as Tool)\n }\n return merged\n}\n"],"names":[],"mappings":";AAIA,MAAM,uCAAuB,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,aAAa,OAAkD;AACtE,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,KAAK,OAAO,MAAM,SAAU,QAAO;AACxC,UAAM,OAAQ,EAAyB;AACvC,QAAI,OAAO,SAAS,YAAY,CAAC,iBAAiB,IAAI,IAAI,EAAG,QAAO;AAAA,EACtE;AACA,SAAO;AACT;AAcO,SAAS,0BAA0B,MASvC;AACD,QAAM,cAAc,oBAAoB,UAAU,IAAI;AACtD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,QAAQ;AAAA,MACb,IAAI;AAAA,QACF,gLAGwB,YAAY,MAAM,OAAO;AAAA,MAAA;AAAA,IACnD;AAAA,EAEJ;AAEA,QAAM,SAAS,YAAY;AAK3B,QAAM,cACH,KAAuD,YAAY,CAAA;AACtE,QAAM,WAAW,OAAO,SAAS,IAAI,CAAC,GAAG,MAAM;AAC7C,UAAM,MAAM,YAAY,CAAC;AACzB,QACE,OACA,OAAO,QAAQ,YACf,WAAW,OACX,aAAa,IAAI,KAAK,GACtB;AACA,aAAO,EAAE,GAAG,GAAG,OAAO,IAAI,MAAA;AAAA,IAC5B;AACA,WAAO;AAAA,EACT,CAAC;AAED,SAAO,QAAQ,QAAQ;AAAA,IACrB;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,OAAO,OAAO;AAAA,IACd,aAAa,OAAO;AAAA,IACpB,OAAO,OAAO;AAAA,IAKd,gBAAiB,OAAO,kBAAkB,CAAA;AAAA,IAC1C,OAAO,OAAO;AAAA,IACd,SAAS,OAAO;AAAA,EAAA,CACjB;AACH;AA2BA,eAAsB,sBACpB,KACgE;AAChE,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAA;AAAA,EACnB,SAAS,OAAO;AAGd,UAAM,MAAM,IAAI;AAAA,MACd;AAAA,MACA,EAAE,QAAQ,IAAA;AAAA,IAAI;AAEd,QAAuC,QAAQ;AACjD,UAAM;AAAA,EACR;AACA,MAAI;AACF,WAAO,MAAM,0BAA0B,IAAI;AAAA,EAC7C,SAAS,OAAO;AAKd,UAAM,MAAM,IAAI;AAAA,MACd;AAAA,MACA,EAAE,QAAQ,IAAA;AAAA,IAAI;AAEd,QAAuC,QAAQ;AACjD,UAAM;AAAA,EACR;AACF;AAuBO,SAAS,gBACd,aACA,aAKa;AACb,QAAM,OAAO,IAAI,IAAI,YAAY,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AACnD,QAAM,SAAsB,CAAC,GAAG,WAAW;AAC3C,aAAW,MAAM,aAAa;AAC5B,QAAI,KAAK,IAAI,GAAG,IAAI,GAAG;AAErB;AAAA,IACF;AACA,SAAK,IAAI,GAAG,IAAI;AAChB,WAAO,KAAK;AAAA,MACV,MAAM,GAAG;AAAA,MACT,aAAa,GAAG;AAAA,MAChB,aAAa,GAAG;AAAA;AAAA;AAAA,IAAA,CAGT;AAAA,EACX;AACA,SAAO;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Core TanStack AI library - Open source AI SDK",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"tanstack-intent"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@ag-ui/core": "0.0.
|
|
56
|
+
"@ag-ui/core": "^0.0.52",
|
|
57
57
|
"partial-json": "^0.1.7",
|
|
58
|
-
"@tanstack/ai-event-client": "0.3.
|
|
58
|
+
"@tanstack/ai-event-client": "0.3.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -39,6 +39,47 @@ export async function POST(request: Request) {
|
|
|
39
39
|
typed AG-UI event (discriminated union on `type`). The `toServerSentEventsResponse()`
|
|
40
40
|
helper encodes that iterable into an SSE-formatted `Response` with correct headers.
|
|
41
41
|
|
|
42
|
+
## Setup — Receiving AG-UI RunAgentInput on the Server
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import {
|
|
46
|
+
chat,
|
|
47
|
+
chatParamsFromRequestBody,
|
|
48
|
+
mergeAgentTools,
|
|
49
|
+
toServerSentEventsResponse,
|
|
50
|
+
} from '@tanstack/ai'
|
|
51
|
+
import { openaiText } from '@tanstack/ai-openai/adapters'
|
|
52
|
+
import { serverTools } from './tools'
|
|
53
|
+
|
|
54
|
+
export async function POST(req: Request) {
|
|
55
|
+
let params
|
|
56
|
+
try {
|
|
57
|
+
params = await chatParamsFromRequestBody(await req.json())
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return new Response(
|
|
60
|
+
error instanceof Error ? error.message : 'Bad request',
|
|
61
|
+
{ status: 400 },
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const stream = chat({
|
|
66
|
+
adapter: openaiText('gpt-4o'),
|
|
67
|
+
messages: params.messages,
|
|
68
|
+
tools: mergeAgentTools(serverTools, params.tools),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return toServerSentEventsResponse(stream)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`chatParamsFromRequestBody` validates the body against `RunAgentInputSchema` from `@ag-ui/core`. `mergeAgentTools` merges the server's tool registry with client-declared tools (server wins on collision; client-only tools become no-execute stubs that flow through the runtime's `ClientToolRequest` path).
|
|
76
|
+
|
|
77
|
+
`params.messages` is a mixed array of TanStack `UIMessage` anchors (with `parts`) and AG-UI fan-out duplicates (`{role:'tool',...}`, `{role:'reasoning',...}`). The existing `convertMessagesToModelMessages` (called inside `chat()`) handles dedup automatically.
|
|
78
|
+
|
|
79
|
+
**Wire shape (POST body):** AG-UI `RunAgentInput` — `{threadId, runId, parentRunId?, state, messages, tools, context, forwardedProps}`. The `messages` array carries TanStack `UIMessage` anchors with their canonical `parts` plus AG-UI mirror fields (`content`, `toolCalls`) inline; tool results and thinking parts are additionally emitted as fan-out `{role:'tool',...}` and `{role:'reasoning',...}` entries.
|
|
80
|
+
|
|
81
|
+
**`forwardedProps` security:** Don't spread it directly into `chat()` — clients could override `adapter`, `model`, `tools`, etc. Always allowlist specific fields.
|
|
82
|
+
|
|
42
83
|
## Core Patterns
|
|
43
84
|
|
|
44
85
|
### 1. SSE Format — toServerSentEventsStream / toServerSentEventsResponse
|
|
@@ -223,9 +264,11 @@ Source: docs/protocol/chunk-definitions.md
|
|
|
223
264
|
|
|
224
265
|
## Tension
|
|
225
266
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
267
|
+
RESOLVED: TanStack AI is fully AG-UI compliant on both axes (server→client events
|
|
268
|
+
AND client→server `RunAgentInput`). The wire format carries TanStack `UIMessage`
|
|
269
|
+
anchors with their parts intact alongside AG-UI fan-out messages, so strict AG-UI
|
|
270
|
+
servers see role-based messages while TanStack-aware servers read parts directly
|
|
271
|
+
without transformation. See `docs/migration/ag-ui-compliance.md` for details.
|
|
229
272
|
|
|
230
273
|
## Cross-References
|
|
231
274
|
|
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
ToolCallArgsEvent,
|
|
49
49
|
ToolCallEndEvent,
|
|
50
50
|
ToolCallStartEvent,
|
|
51
|
+
UIMessage,
|
|
51
52
|
} from '../../types'
|
|
52
53
|
import type {
|
|
53
54
|
ChatMiddleware,
|
|
@@ -85,12 +86,21 @@ export interface TextActivityOptions<
|
|
|
85
86
|
> {
|
|
86
87
|
/** The text adapter to use (created by a provider function like openaiText('gpt-4o')) */
|
|
87
88
|
adapter: TAdapter
|
|
88
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Conversation messages. Accepts:
|
|
91
|
+
* - `ConstrainedModelMessage` — content types constrained by the adapter's input modalities.
|
|
92
|
+
* - `ModelMessage` — unconstrained model message (e.g., forwarded from an AG-UI wire payload).
|
|
93
|
+
* - `UIMessage` — parts-based UI representation; converted internally via `convertMessagesToModelMessages`.
|
|
94
|
+
*
|
|
95
|
+
* The three shapes can be mixed in a single array (e.g., when forwarding a wire payload that includes both anchor UIMessages and AG-UI fan-out ModelMessages).
|
|
96
|
+
*/
|
|
89
97
|
messages?: Array<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
| UIMessage
|
|
99
|
+
| ModelMessage
|
|
100
|
+
| ConstrainedModelMessage<{
|
|
101
|
+
inputModalities: TAdapter['~types']['inputModalities']
|
|
102
|
+
messageMetadataByModality: TAdapter['~types']['messageMetadataByModality']
|
|
103
|
+
}>
|
|
94
104
|
>
|
|
95
105
|
/** System prompts to prepend to the conversation */
|
|
96
106
|
systemPrompts?: TextOptions['systemPrompts']
|
|
@@ -128,6 +138,8 @@ export interface TextActivityOptions<
|
|
|
128
138
|
threadId?: TextOptions['threadId']
|
|
129
139
|
/** Run ID override for AG-UI protocol. Auto-generated by adapter if not provided. */
|
|
130
140
|
runId?: TextOptions['runId']
|
|
141
|
+
/** Parent run ID for AG-UI protocol nested run correlation. */
|
|
142
|
+
parentRunId?: TextOptions['parentRunId']
|
|
131
143
|
/**
|
|
132
144
|
* Optional Standard Schema for structured output.
|
|
133
145
|
* When provided, the activity will:
|
|
@@ -313,6 +325,7 @@ class TextEngine<
|
|
|
313
325
|
// AG-UI protocol IDs
|
|
314
326
|
private threadId: string
|
|
315
327
|
private runIdOverride?: string
|
|
328
|
+
private parentRunIdOverride?: string
|
|
316
329
|
|
|
317
330
|
// Middleware support
|
|
318
331
|
private readonly middlewareRunner: MiddlewareRunner
|
|
@@ -364,8 +377,15 @@ class TextEngine<
|
|
|
364
377
|
? { signal: config.params.abortController.signal }
|
|
365
378
|
: undefined
|
|
366
379
|
this.effectiveSignal = config.params.abortController?.signal
|
|
367
|
-
|
|
380
|
+
// `conversationId` is the legacy alias of `threadId` — accept it
|
|
381
|
+
// as a fallback so `chat({ conversationId })` keeps working, with
|
|
382
|
+
// explicit `threadId` winning when both are set.
|
|
383
|
+
this.threadId =
|
|
384
|
+
config.params.threadId ||
|
|
385
|
+
config.params.conversationId ||
|
|
386
|
+
this.createId('thread')
|
|
368
387
|
this.runIdOverride = config.params.runId
|
|
388
|
+
this.parentRunIdOverride = config.params.parentRunId
|
|
369
389
|
|
|
370
390
|
// Initialize middleware — devtools first, strip-to-spec always last.
|
|
371
391
|
// handleStreamChunk processes raw chunks BEFORE middleware, so internal
|
|
@@ -381,7 +401,10 @@ class TextEngine<
|
|
|
381
401
|
this.middlewareCtx = {
|
|
382
402
|
requestId: this.requestId,
|
|
383
403
|
streamId: this.streamId,
|
|
384
|
-
|
|
404
|
+
threadId: this.threadId,
|
|
405
|
+
// Legacy alias kept on the ctx so middleware that reads
|
|
406
|
+
// `ctx.conversationId` keeps working. Always equals `threadId`.
|
|
407
|
+
conversationId: this.threadId,
|
|
385
408
|
phase: 'init' as ChatMiddlewarePhase,
|
|
386
409
|
iteration: 0,
|
|
387
410
|
chunkIndex: 0,
|
|
@@ -429,7 +452,7 @@ class TextEngine<
|
|
|
429
452
|
async *run(): AsyncGenerator<StreamChunk> {
|
|
430
453
|
this.beforeRun()
|
|
431
454
|
this.logger.agentLoop('run started', {
|
|
432
|
-
|
|
455
|
+
threadId: this.middlewareCtx.threadId,
|
|
433
456
|
})
|
|
434
457
|
|
|
435
458
|
try {
|
|
@@ -508,7 +531,7 @@ class TextEngine<
|
|
|
508
531
|
// Genuine error — call onError
|
|
509
532
|
this.logger.errors('chat run failed', {
|
|
510
533
|
error,
|
|
511
|
-
|
|
534
|
+
threadId: this.middlewareCtx.threadId,
|
|
512
535
|
})
|
|
513
536
|
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
514
537
|
error,
|
|
@@ -634,6 +657,7 @@ class TextEngine<
|
|
|
634
657
|
logger: this.logger,
|
|
635
658
|
threadId: this.threadId,
|
|
636
659
|
runId: this.runIdOverride,
|
|
660
|
+
parentRunId: this.parentRunIdOverride,
|
|
637
661
|
})) {
|
|
638
662
|
if (this.isCancelled()) {
|
|
639
663
|
break
|
|
@@ -63,15 +63,55 @@ function getTextContent(content: string | null | Array<ContentPart>): string {
|
|
|
63
63
|
export function convertMessagesToModelMessages(
|
|
64
64
|
messages: Array<UIMessage | ModelMessage>,
|
|
65
65
|
): Array<ModelMessage> {
|
|
66
|
+
// Pre-pass: collect toolCallIds already represented in anchor UIMessage parts.
|
|
67
|
+
// Fan-out tool messages whose toolCallId matches an anchored ToolResultPart
|
|
68
|
+
// are AG-UI duplicates and must be dropped to avoid double-feeding the LLM.
|
|
69
|
+
const anchoredToolCallIds = new Set<string>()
|
|
70
|
+
for (const msg of messages) {
|
|
71
|
+
if ('parts' in msg) {
|
|
72
|
+
for (const part of msg.parts) {
|
|
73
|
+
if (part.type === 'tool-result') {
|
|
74
|
+
anchoredToolCallIds.add(part.toolCallId)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
const modelMessages: Array<ModelMessage> = []
|
|
67
81
|
for (const msg of messages) {
|
|
68
82
|
if ('parts' in msg) {
|
|
69
|
-
// UIMessage
|
|
83
|
+
// UIMessage anchor — existing fan-out path
|
|
70
84
|
modelMessages.push(...uiMessageToModelMessages(msg))
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const role = (msg as { role: string }).role
|
|
89
|
+
|
|
90
|
+
// AG-UI tool fan-out duplicate — drop if anchor already covers it
|
|
91
|
+
if (
|
|
92
|
+
role === 'tool' &&
|
|
93
|
+
msg.toolCallId &&
|
|
94
|
+
anchoredToolCallIds.has(msg.toolCallId)
|
|
95
|
+
) {
|
|
96
|
+
continue
|
|
74
97
|
}
|
|
98
|
+
|
|
99
|
+
// AG-UI reasoning and activity — no ModelMessage equivalent today
|
|
100
|
+
if (role === 'reasoning' || role === 'activity') {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// AG-UI developer — collapse to system
|
|
105
|
+
if (role === 'developer') {
|
|
106
|
+
modelMessages.push({
|
|
107
|
+
role: 'system' as ModelMessage['role'],
|
|
108
|
+
content: (msg as { content: string }).content,
|
|
109
|
+
} as ModelMessage)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Already a ModelMessage (user, assistant, system, tool with no anchor) — pass through
|
|
114
|
+
modelMessages.push(msg)
|
|
75
115
|
}
|
|
76
116
|
return modelMessages
|
|
77
117
|
}
|
|
@@ -28,7 +28,18 @@ export interface ChatMiddlewareContext {
|
|
|
28
28
|
requestId: string
|
|
29
29
|
/** Unique identifier for this stream */
|
|
30
30
|
streamId: string
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* AG-UI thread identifier — a stable per-conversation ID used to
|
|
33
|
+
* correlate client and server devtools events. Resolves to the
|
|
34
|
+
* caller-provided `threadId` (or legacy `conversationId`), or an
|
|
35
|
+
* auto-generated value when neither is supplied.
|
|
36
|
+
*/
|
|
37
|
+
threadId: string
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use `threadId` instead. Retained as an alias of
|
|
40
|
+
* `threadId` so middleware written before the AG-UI rename keeps
|
|
41
|
+
* working unchanged. Will be removed in a future major release.
|
|
42
|
+
*/
|
|
32
43
|
conversationId?: string
|
|
33
44
|
/** Current lifecycle phase */
|
|
34
45
|
phase: ChatMiddlewarePhase
|
|
@@ -254,6 +254,20 @@ export function convertSchemaToJsonSchema(
|
|
|
254
254
|
return result as JSONSchema
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
|
|
258
|
+
// expose a `~standard.jsonSchema` converter. These would otherwise fall
|
|
259
|
+
// through to the JSONSchema pass-through below and ship `{ '~standard': … }`
|
|
260
|
+
// straight to the LLM provider, producing an opaque downstream error. Fail
|
|
261
|
+
// fast with actionable guidance instead.
|
|
262
|
+
if (isStandardSchema(schema)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'Schema is a Standard Schema validator but does not expose a JSON Schema ' +
|
|
265
|
+
'converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, ' +
|
|
266
|
+
'or wrap a Valibot schema with `toStandardJsonSchema()` from ' +
|
|
267
|
+
'`@valibot/to-json-schema` before passing it as `outputSchema`.',
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
257
271
|
// If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
|
|
258
272
|
// Still apply structured output transformation if requested
|
|
259
273
|
|
package/src/index.ts
CHANGED
|
@@ -168,6 +168,17 @@ export type {
|
|
|
168
168
|
JSONParser,
|
|
169
169
|
} from './activities/chat/stream/index'
|
|
170
170
|
|
|
171
|
+
// Chat utilities
|
|
172
|
+
export {
|
|
173
|
+
chatParamsFromRequest,
|
|
174
|
+
chatParamsFromRequestBody,
|
|
175
|
+
mergeAgentTools,
|
|
176
|
+
} from './utilities/chat-params'
|
|
177
|
+
|
|
178
|
+
// AG-UI wire serialization (used internally by @tanstack/ai-client)
|
|
179
|
+
export { uiMessagesToWire } from './utilities/ag-ui-wire'
|
|
180
|
+
export type { WireMessage } from './utilities/ag-ui-wire'
|
|
181
|
+
|
|
171
182
|
// Adapter extension utilities
|
|
172
183
|
export { createModel, extendAdapter } from './extend-adapter'
|
|
173
184
|
export type { ExtendedModelDef } from './extend-adapter'
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
StandardJSONSchemaV1,
|
|
3
|
+
StandardSchemaV1,
|
|
4
|
+
} from '@standard-schema/spec'
|
|
2
5
|
import type { InternalLogger } from './logger/internal-logger'
|
|
3
6
|
import type {
|
|
4
7
|
BaseEvent as AGUIBaseEvent,
|
|
@@ -91,25 +94,42 @@ export interface JSONSchema {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
/**
|
|
94
|
-
* Union type for schema input - can be any Standard
|
|
97
|
+
* Union type for schema input - can be any Standard Schema compliant validator,
|
|
98
|
+
* any Standard JSON Schema compliant schema, or a plain JSONSchema object.
|
|
95
99
|
*
|
|
96
|
-
* Standard JSON Schema compliant libraries
|
|
100
|
+
* Standard JSON Schema compliant libraries (carry the JSON-schema converter):
|
|
97
101
|
* - Zod v4.2+ (natively supports StandardJSONSchemaV1)
|
|
98
102
|
* - ArkType v2.1.28+ (natively supports StandardJSONSchemaV1)
|
|
99
103
|
* - Valibot v1.2+ (via `toStandardJsonSchema()` from `@valibot/to-json-schema`)
|
|
100
104
|
*
|
|
105
|
+
* StandardSchemaV1 covers libraries whose published types only expose the
|
|
106
|
+
* validator surface — Zod's core `$ZodType['~standard']` is currently typed
|
|
107
|
+
* as `StandardSchemaV1.Props` even though the runtime attaches the
|
|
108
|
+
* `jsonSchema` converter, so this branch is what makes `InferSchemaType`
|
|
109
|
+
* recover the inferred type for callers using `z.ZodType<T>`.
|
|
110
|
+
*
|
|
101
111
|
* @see https://standardschema.dev/json-schema
|
|
102
112
|
*/
|
|
103
113
|
|
|
104
|
-
export type SchemaInput =
|
|
114
|
+
export type SchemaInput =
|
|
115
|
+
| StandardJSONSchemaV1<any, any>
|
|
116
|
+
| StandardSchemaV1<any, any>
|
|
117
|
+
| JSONSchema
|
|
105
118
|
|
|
106
119
|
/**
|
|
107
120
|
* Infer the TypeScript type from a schema.
|
|
108
121
|
* For Standard JSON Schema compliant schemas, extracts the input type.
|
|
109
|
-
* For
|
|
122
|
+
* For Standard Schema validators (e.g. Zod's `~standard` surface), extracts
|
|
123
|
+
* the input type from the `StandardSchemaV1` shape.
|
|
124
|
+
* For plain JSONSchema, returns `unknown` since we can't infer types from
|
|
125
|
+
* JSON Schema at compile time.
|
|
110
126
|
*/
|
|
111
127
|
export type InferSchemaType<T> =
|
|
112
|
-
T extends StandardJSONSchemaV1<infer TInput, unknown>
|
|
128
|
+
T extends StandardJSONSchemaV1<infer TInput, unknown>
|
|
129
|
+
? TInput
|
|
130
|
+
: T extends StandardSchemaV1<infer TInput, unknown>
|
|
131
|
+
? TInput
|
|
132
|
+
: unknown
|
|
113
133
|
|
|
114
134
|
export interface ToolCall<TMetadata = unknown> {
|
|
115
135
|
id: string
|
|
@@ -729,8 +749,14 @@ export interface TextOptions<
|
|
|
729
749
|
*/
|
|
730
750
|
outputSchema?: SchemaInput
|
|
731
751
|
/**
|
|
732
|
-
*
|
|
733
|
-
*
|
|
752
|
+
* @deprecated Use `threadId` instead. `conversationId` is the legacy
|
|
753
|
+
* pre-AG-UI name for the same concept (a stable per-conversation
|
|
754
|
+
* identifier used to correlate client/server devtools events). When
|
|
755
|
+
* `conversationId` is omitted, the runtime falls back to `threadId`
|
|
756
|
+
* automatically, so most callers can simply pass `threadId` (or rely
|
|
757
|
+
* on `chatParamsFromRequest`, which surfaces it on `params`).
|
|
758
|
+
*
|
|
759
|
+
* Will be removed in a future major release.
|
|
734
760
|
*/
|
|
735
761
|
conversationId?: string
|
|
736
762
|
/**
|
|
@@ -766,6 +792,11 @@ export interface TextOptions<
|
|
|
766
792
|
* If not provided, a unique ID will be generated.
|
|
767
793
|
*/
|
|
768
794
|
runId?: string
|
|
795
|
+
/**
|
|
796
|
+
* Parent run ID for AG-UI protocol nested run correlation.
|
|
797
|
+
* Surfaced for observability/middleware; not consumed by the LLM call.
|
|
798
|
+
*/
|
|
799
|
+
parentRunId?: string
|
|
769
800
|
}
|
|
770
801
|
|
|
771
802
|
// ============================================================================
|