UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

226 lines (225 loc) 7.95 kB
import { AGUIError } from "@ag-ui/core"; //#region src/utilities/chat-params.ts var KNOWN_PART_TYPES = /* @__PURE__ */ new Set([ "text", "image", "audio", "video", "document", "tool-call", "tool-result", "thinking" ]); function isValidParts(value) { if (!Array.isArray(value)) return false; for (const p of value) { if (!p || typeof p !== "object") return false; const type = p.type; if (typeof type !== "string" || !KNOWN_PART_TYPES.has(type)) return false; } return true; } /** * Keyed by `AGUIRole` so a role added upstream fails to compile here until it * is handled, rather than silently falling through as an unknown role. */ var AGUI_ROLES = { developer: true, system: true, assistant: true, user: true, tool: true, activity: true, reasoning: true }; function isAGUIRole(value) { return typeof value === "string" && value in AGUI_ROLES; } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * Reject the request body, pointing at the migration guide. Mirrors the * message the previous `RunAgentInputSchema.safeParse` failure produced. */ function invalidBody(reason) { throw new AGUIError(`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: ${reason}`); } function requireString(value, at) { if (typeof value !== "string") invalidBody(`${at} must be a string`); return value; } function requireArray(value, at) { if (!Array.isArray(value)) invalidBody(`${at} must be an array`); return value; } /** * Assert one AG-UI `Message`, discriminating on `role` exactly as the upstream * `MessageSchema` discriminated union does. The record view is retained on the * asserted type so callers can still inspect non-AG-UI extras like `parts`. */ function assertAGUIMessage(value, at) { requireString(value.id, `${at}.id`); const role = value.role; if (!isAGUIRole(role)) invalidBody(`${at}.role must be one of ${Object.keys(AGUI_ROLES).join(" | ")}`); switch (role) { case "assistant": if (value.content !== void 0) requireString(value.content, `${at}.content`); if (value.toolCalls !== void 0) requireArray(value.toolCalls, `${at}.toolCalls`); break; case "user": if (typeof value.content !== "string" && !Array.isArray(value.content)) invalidBody(`${at}.content must be a string or an array of content parts`); break; case "tool": requireString(value.content, `${at}.content`); requireString(value.toolCallId, `${at}.toolCallId`); break; case "activity": requireString(value.activityType, `${at}.activityType`); if (!isRecord(value.content)) invalidBody(`${at}.content must be an object`); break; case "developer": case "system": case "reasoning": requireString(value.content, `${at}.content`); break; } } function validateMessage(value, index) { const at = `messages[${index}]`; if (!isRecord(value)) invalidBody(`${at} must be an object`); assertAGUIMessage(value, at); if ("parts" in value && !isValidParts(value.parts)) { const withoutParts = { ...value }; Reflect.deleteProperty(withoutParts, "parts"); return withoutParts; } return value; } function validateTool(value, index) { const at = `tools[${index}]`; if (!isRecord(value)) invalidBody(`${at} must be an object`); return { name: requireString(value.name, `${at}.name`), description: requireString(value.description, `${at}.description`), parameters: value.parameters }; } function validateContext(value, index) { const at = `context[${index}]`; if (!isRecord(value)) invalidBody(`${at} must be an object`); return { description: requireString(value.description, `${at}.description`), value: requireString(value.value, `${at}.value`) }; } function validateResumeEntry(value, index) { const at = `resume[${index}]`; if (!isRecord(value)) invalidBody(`${at} must be an object`); const status = value.status; if (status !== "resolved" && status !== "cancelled") invalidBody(`${at}.status must be "resolved" or "cancelled"`); const entry = { interruptId: requireString(value.interruptId, `${at}.interruptId`), status }; if (value.payload !== void 0) entry.payload = value.payload; return entry; } /** * Parse and validate an HTTP request body as an AG-UI `RunAgentInput`. * * Returns a spread-friendly object whose `messages` field is suitable for * passing directly to `chat({ messages })`. The existing * `convertMessagesToModelMessages` handles AG-UI fan-out dedup and * reasoning/activity/developer-role normalization internally. * * Validated structurally against the AG-UI `RunAgentInput` contract without a * schema library, so this package pulls in no validation runtime of its own. * * @throws An error with a migration-pointing message when the body does * not conform to AG-UI `RunAgentInput`. Surface this as a * 400 Bad Request to the client. */ async function chatParamsFromRequestBody(body) { if (!isRecord(body)) invalidBody("body must be a JSON object"); const threadId = requireString(body.threadId, "threadId"); const runId = requireString(body.runId, "runId"); const parentRunId = body.parentRunId === void 0 ? void 0 : requireString(body.parentRunId, "parentRunId"); const messages = requireArray(body.messages, "messages").map(validateMessage); const tools = requireArray(body.tools, "tools").map(validateTool); const aguiContext = requireArray(body.context, "context").map(validateContext); const resume = body.resume === void 0 ? void 0 : requireArray(body.resume, "resume").map(validateResumeEntry); if (body.forwardedProps !== void 0 && !isRecord(body.forwardedProps)) invalidBody("forwardedProps must be an object"); return { messages, threadId, runId, parentRunId, tools, forwardedProps: body.forwardedProps ?? {}, state: body.state, resume, context: aguiContext, aguiContext }; } /** * Read an HTTP `Request`, parse its JSON body, and validate it as an * AG-UI `RunAgentInput` — collapsing the standard `req.json()` + * `chatParamsFromRequestBody(...)` pair into a single call. * * On a malformed body or invalid AG-UI shape, this **throws a * `Response`** with status 400 and a migration-pointing message in the * body. Frameworks that natively handle thrown `Response` objects * (TanStack Start, SolidStart, Remix, React Router 7) will return the * 400 to the client automatically, so the handler reduces to: * * ```ts * export async function POST(req: Request) { * const params = await chatParamsFromRequest(req) * // ...use params * } * ``` * * In frameworks that do not auto-handle thrown `Response` objects * (Next.js Route Handlers, SvelteKit, Hono, raw Node), wrap the call * with try/catch and return the caught Response yourself, or use * `chatParamsFromRequestBody` directly with your own JSON-parsing. * * @throws {Response} 400 on malformed JSON or invalid AG-UI shape. */ async function chatParamsFromRequest(req) { let body; try { body = await req.json(); } catch (cause) { const res = new Response("Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.", { status: 400 }); res.cause = cause; throw res; } try { return await chatParamsFromRequestBody(body); } catch (cause) { const res = new Response("Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md.", { status: 400 }); res.cause = cause; throw res; } } function mergeAgentTools(serverTools, clientTools) { if (clientTools.length === 0) return serverTools; const seen = new Set(serverTools.map((t) => t.name)); const merged = [...serverTools]; for (const ct of clientTools) { if (seen.has(ct.name)) continue; seen.add(ct.name); merged.push({ name: ct.name, description: ct.description, inputSchema: ct.parameters }); } return merged; } //#endregion export { chatParamsFromRequest, chatParamsFromRequestBody, mergeAgentTools }; //# sourceMappingURL=chat-params.js.map