@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
177 lines (176 loc) • 6.32 kB
JavaScript
import { tanstackMetadata } from "./merge-metadata.js";
import { isProviderExecutedToolCall } from "./provider-executed.js";
import { buildBlockOrder, orderedAssistantBlocks } from "./block-order.js";
//#region src/utilities/replay-messages.ts
/** The pi shortHash algorithm, over UTF-16 code units. */
function hashToolCallId(id) {
let h1 = 3735928559;
let h2 = 1103547991;
for (let index = 0; index < id.length; index++) {
const ch = id.charCodeAt(index);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
}
function isForeign(message, target) {
const source = tanstackMetadata(message.metadata)?.source;
return target !== void 0 && source !== void 0 && (source.provider !== target.provider || source.api !== target.api || source.model !== target.model);
}
/** A string input that does not parse. Some adapters keep an object here. */
function isCutInput(input) {
if (typeof input !== "string") return false;
try {
JSON.parse(input || "{}");
return false;
} catch {
return true;
}
}
function isFailed(message) {
const stop = tanstackMetadata(message.metadata)?.stopReason;
return message.role === "assistant" && (stop === "error" || stop === "aborted");
}
/** Build a provider request without changing stored history. An absent target only cleans history. */
function transformMessagesForReplay(original, target, toolIdRule) {
const usedIds = /* @__PURE__ */ new Set();
for (const message of original) if (!isForeign(message, target) && !isFailed(message)) for (const call of message.toolCalls ?? []) usedIds.add(call.id);
const idMap = /* @__PURE__ */ new Map();
const failedCallBatch = /* @__PURE__ */ new Map();
const messages = [];
let pending = [];
const answered = /* @__PURE__ */ new Set();
let turnId;
const closePending = () => {
for (const call of pending) if (!answered.has(call.id) && !isProviderExecutedToolCall(call)) messages.push({
role: "tool",
toolCallId: call.id,
name: call.function.name,
content: "No result provided",
error: "No result provided"
});
pending = [];
answered.clear();
turnId = void 0;
};
const mapCall = (call, foreign, source) => {
let id = call.id;
if (foreign && toolIdRule) {
let attempt = 0;
do {
if (attempt === 1e3) throw new Error(`Tool ID rule could not allocate a unique ID for ${call.id}`);
id = toolIdRule(call.id, {
foreign,
source,
attempt: attempt++
});
} while (usedIds.has(id));
usedIds.add(id);
}
idMap.set(call.id, id);
if (!foreign && id === call.id) return call;
const metadata = call.metadata;
if (metadata !== null && typeof metadata === "object" && !Array.isArray(metadata)) {
const rest = Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "thoughtSignature"));
return {
...call,
id,
metadata: rest
};
}
return id === call.id ? call : {
...call,
id
};
};
for (const message of original) {
const nextSegment = message.role === "assistant" && answered.size === 0 && turnId !== void 0 && message.id !== void 0 && (message.id === turnId || message.id.startsWith(`${turnId}-segment-`));
if (message.role !== "tool" && !nextSegment) closePending();
if (message.role === "assistant") for (const call of message.toolCalls ?? []) failedCallBatch.set(call.id, isFailed(message));
if (isFailed(message)) continue;
if (message.role === "tool") {
if (message.toolCallId !== void 0 && failedCallBatch.get(message.toolCallId)) continue;
const id = message.toolCallId !== void 0 ? idMap.get(message.toolCallId) : void 0;
const mapped = id !== void 0 && id !== message.toolCallId ? {
...message,
toolCallId: id
} : message;
if (mapped.toolCallId !== void 0) answered.add(mapped.toolCallId);
messages.push(mapped);
continue;
}
if (message.role !== "assistant") {
messages.push(message);
continue;
}
const foreign = isForeign(message, target);
let mapped = message;
if (foreign) {
const defaultBlocks = [
...(message.thinking ?? []).map((thinking) => ({
type: "thinking",
thinking
})),
...typeof message.content === "string" ? [{
type: "text",
text: message.content
}] : [],
...(message.toolCalls ?? []).map((toolCall) => ({
type: "tool-call",
toolCall
}))
];
const blocks = orderedAssistantBlocks(message) ?? defaultBlocks;
const entries = [];
const calls = [];
let content = "";
for (const block of blocks) if (block.type === "tool-call") {
const call = mapCall(block.toolCall, true, tanstackMetadata(message.metadata)?.source);
calls.push(call);
entries.push({
type: "tool-call",
id: call.id
});
} else {
const text = block.type === "text" ? block.text : block.thinking.redacted ? "" : block.thinking.content.trim() === "" ? "" : block.thinking.content;
if (text) {
content += text;
entries.push({
type: "text",
text
});
}
}
const { thinking: _thinking, blockOrder: _order, ...rest } = message;
mapped = {
...rest,
content: typeof message.content === "string" || message.content === null ? content || null : content ? [{
type: "text",
content
}, ...message.content] : message.content,
...message.toolCalls ? { toolCalls: calls } : {},
...buildBlockOrder(entries) ? { blockOrder: buildBlockOrder(entries) } : {}
};
} else for (const call of message.toolCalls ?? []) idMap.set(call.id, call.id);
if (tanstackMetadata(message.metadata)?.finishReason === "length" && mapped.toolCalls?.some((call) => isCutInput(call.function.arguments))) mapped = {
...mapped,
toolCalls: mapped.toolCalls.map((call) => isCutInput(call.function.arguments) ? {
...call,
function: {
...call.function,
arguments: "{}"
}
} : call)
};
pending = [...pending, ...mapped.toolCalls ?? []];
turnId ??= message.id;
messages.push(mapped);
}
closePending();
return { messages };
}
//#endregion
export { hashToolCallId, transformMessagesForReplay };
//# sourceMappingURL=replay-messages.js.map