@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
243 lines (242 loc) • 9.96 kB
JavaScript
import { tanstackMetadata } from "./merge-metadata.js";
import { normalizeToolResult } from "./tool-result.js";
import { coerceCreatedAt, modelMessageToUIMessage } from "../activities/chat/messages.js";
//#region src/utilities/ag-ui-wire.ts
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function rebuiltToolMetadata(metadata, createdAt, id, content, anchorOwnsUiResources = false) {
const source = isRecord(metadata) ? metadata : {};
const tanstack = isRecord(source.tanstack) ? { ...source.tanstack } : {};
if (anchorOwnsUiResources) delete tanstack.uiResources;
const date = coerceCreatedAt(createdAt);
const toolResult = {
...id !== void 0 ? { id } : {},
...date && { createdAt: date.toISOString() },
...Array.isArray(content) && { content }
};
const result = {
...source,
tanstack: {
...tanstack,
toolResult
}
};
return Object.keys(result).length ? result : void 0;
}
/**
* Serialize TanStack `UIMessage`s and `ModelMessage`s into the AG-UI
* `RunAgentInput.messages` wire shape. Anchors are spec-only (`id`, `role`,
* `name`, `content`, `toolCalls`, `metadata`). Tool results and thinking parts
* on assistant messages are additionally emitted as fan-out
* `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict AG-UI
* server consumers. Set `includeSnapshotStructuredOutput` to retain complete
* structured-output metadata for UI snapshots.
*/
function uiMessagesToWire(messages, options) {
const wire = [];
const usedWireIds = new Set(messages.flatMap((message) => "id" in message && message.id && message.role !== "tool" ? [message.id] : []));
const includeSnapshotStructuredOutput = options?.includeSnapshotStructuredOutput ?? false;
const assistantIds = /* @__PURE__ */ new Set();
for (const msg of messages) if (msg.role === "assistant" && msg.id !== void 0) assistantIds.add(msg.id);
for (const msg of messages) {
if (!("parts" in msg) && msg.role === "tool" && msg.toolCallId) {
const id = uniqueToolWireId(toolWireId(msg.id, msg.toolCallId, assistantIds), usedWireIds);
const metadata = rebuiltToolMetadata(msg.metadata, msg.createdAt, msg.id, msg.content);
wire.push({
role: "tool",
id,
...msg.name !== void 0 && { name: msg.name },
toolCallId: msg.toolCallId,
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
...msg.error !== void 0 && { error: msg.error },
...metadata !== void 0 && { metadata }
});
continue;
}
const uiMessage = "parts" in msg ? msg : modelMessageToUIMessage(msg, msg.id);
const parts = uiMessage.parts;
usedWireIds.add(uiMessage.id);
if (msg.role === "system") {
wire.push(toAnchor(uiMessage, "system", { content: parts.length > 0 ? collectText(parts) : msg.content ?? "" }, parts, includeSnapshotStructuredOutput));
continue;
}
if (msg.role === "user") {
wire.push(toAnchor(uiMessage, "user", { content: parts.length > 0 ? collectUserContent(parts) : msg.content ?? "" }, parts, includeSnapshotStructuredOutput));
continue;
}
for (const part of parts) if (part.type === "thinking") {
const reasoning = {
role: "reasoning",
id: uniqueWireId(deriveReasoningId(uiMessage.id, part), usedWireIds),
content: part.content
};
if (part.signature) reasoning.encryptedValue = part.signature;
wire.push(reasoning);
}
const text = collectText(parts);
const toolCalls = collectToolCalls(parts);
wire.push(toAnchor(uiMessage, "assistant", {
...text !== "" && { content: text },
...toolCalls && { toolCalls }
}, parts, includeSnapshotStructuredOutput));
const explicitToolResults = new Set(parts.flatMap((part) => part.type === "tool-result" ? [part.toolCallId] : []));
for (const part of parts) if (part.type === "tool-result") {
const id = uniqueToolWireId(part.id ?? deriveToolMessageId(part.toolCallId), usedWireIds);
const metadata = rebuiltToolMetadata(part.metadata, part.createdAt, part.id, part.content, true);
wire.push({
role: "tool",
id,
toolCallId: part.toolCallId,
...part.name !== void 0 && { name: part.name },
content: typeof part.content === "string" ? part.content : JSON.stringify(part.content),
...part.error !== void 0 && { error: part.error },
...metadata !== void 0 && { metadata }
});
} else if (part.type === "tool-call") {
const approved = part.approval?.approved;
if (explicitToolResults.has(part.id) || part.output === void 0 && (part.state !== "approval-responded" || approved === void 0)) continue;
const result = part.output !== void 0 ? normalizeToolResult(part.output) : JSON.stringify({
approved,
...approved && { pendingExecution: true },
message: approved ? "User approved this action" : "User denied this action"
});
const content = typeof result === "string" ? result : JSON.stringify(result);
wire.push({
role: "tool",
id: uniqueToolWireId(deriveToolMessageId(part.id), usedWireIds),
toolCallId: part.id,
content,
metadata: rebuiltToolMetadata(void 0, void 0, void 0, result)
});
}
}
return wire;
}
function toAnchor(msg, role, extras, parts, includeSnapshotStructuredOutput) {
const metadata = messageMetadata(msg, parts, includeSnapshotStructuredOutput);
const base = {
id: msg.id,
...msg.name !== void 0 && { name: msg.name },
...metadata !== void 0 && { metadata }
};
if (role === "system") return {
...base,
role,
content: String(extras.content ?? "")
};
if (role === "user") return {
...base,
role,
content: extras.content ?? ""
};
return {
...base,
role,
...typeof extras.content === "string" && { content: extras.content },
...extras.toolCalls !== void 0 && { toolCalls: extras.toolCalls }
};
}
function messageMetadata(msg, parts, includeSnapshotStructuredOutput) {
const base = { ...msg.metadata ?? {} };
const previousTanstack = tanstackMetadata(msg);
const tanstack = {};
if (previousTanstack?.model !== void 0) tanstack.model = previousTanstack.model;
if (previousTanstack?.signature !== void 0) tanstack.signature = previousTanstack.signature;
const createdAt = coerceCreatedAt(msg.createdAt);
if (createdAt !== void 0) tanstack.createdAt = createdAt.toISOString();
const structuredOutput = serializedStructuredOutput(parts, includeSnapshotStructuredOutput);
if (structuredOutput) tanstack.structuredOutput = structuredOutput;
const toolCallMetadata = {};
for (const part of parts) if (part.type === "tool-call" && part.metadata !== void 0) toolCallMetadata[part.id] = part.metadata;
if (Object.keys(toolCallMetadata).length > 0) tanstack.toolCallMetadata = toolCallMetadata;
const uiResources = parts.filter((p) => p.type === "ui-resource");
if (uiResources.length > 0) tanstack.uiResources = uiResources;
if (Object.keys(tanstack).length > 0) base.tanstack = tanstack;
else delete base.tanstack;
return Object.keys(base).length > 0 ? base : void 0;
}
function serializedStructuredOutput(parts, includeSnapshotStructuredOutput) {
for (const p of parts) if (p.type === "structured-output" && (includeSnapshotStructuredOutput || p.status !== "complete")) return structuredOutputMetadata(p, includeSnapshotStructuredOutput);
}
function structuredOutputMetadata(part, includeSnapshotStructuredOutput) {
return {
status: part.status,
raw: part.raw,
...includeSnapshotStructuredOutput && part.partial !== void 0 ? { partial: part.partial } : {},
...includeSnapshotStructuredOutput && part.data !== void 0 ? { data: part.data } : {},
...includeSnapshotStructuredOutput && part.reasoning ? { reasoning: part.reasoning } : {},
...part.errorMessage !== void 0 && { errorMessage: part.errorMessage }
};
}
function collectText(parts) {
const out = [];
for (const p of parts) if (p.type === "text") out.push(p.content);
else if (p.type === "structured-output" && p.status === "complete" && p.raw !== "") out.push(p.raw);
return out.join("");
}
function collectUserContent(parts) {
if (!parts.some((p) => p.type === "image" || p.type === "audio" || p.type === "video" || p.type === "document")) return collectText(parts);
const out = [];
for (const p of parts) if (p.type === "text") out.push({
type: "text",
text: p.content
});
else if (p.type === "image" || p.type === "audio" || p.type === "video" || p.type === "document") out.push(p);
return out;
}
function thoughtSignatureFromMetadata(metadata) {
if (metadata == null || typeof metadata !== "object" || Array.isArray(metadata)) return;
if (!("thoughtSignature" in metadata)) return void 0;
const value = metadata.thoughtSignature;
return typeof value === "string" && value !== "" ? value : void 0;
}
function collectToolCalls(parts) {
const calls = [];
for (const p of parts) if (p.type === "tool-call") {
const encryptedValue = thoughtSignatureFromMetadata(p.metadata);
calls.push({
id: p.id,
type: "function",
function: {
name: p.name,
arguments: p.arguments
},
...encryptedValue !== void 0 ? { encryptedValue } : {}
});
}
return calls.length > 0 ? calls : void 0;
}
function deriveReasoningId(messageId, part) {
return `${messageId}-reasoning-${part.id ?? hashContent(part.content)}`;
}
function deriveToolMessageId(toolCallId) {
return `tool-${toolCallId}`;
}
function uniqueToolWireId(id, used) {
return uniqueWireId(id, used);
}
function uniqueWireId(id, used) {
if (!used.has(id)) {
used.add(id);
return id;
}
let suffix = 2;
while (used.has(`${id}-${suffix}`)) suffix++;
const unique = `${id}-${suffix}`;
used.add(unique);
return unique;
}
function toolWireId(id, toolCallId, assistantIds) {
const derived = deriveToolMessageId(toolCallId);
if (id === void 0 || assistantIds.has(id)) return derived;
return id;
}
function hashContent(s) {
let h = 0;
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
return Math.abs(h).toString(36);
}
//#endregion
export { uiMessagesToWire };
//# sourceMappingURL=ag-ui-wire.js.map