UNPKG

@tanstack/ai

Version:

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

169 lines (168 loc) • 5.82 kB
import { tanstackMetadata } from "./merge-metadata.js"; //#region src/utilities/tool-call-arguments.ts function sameJsonValue(left, right) { if (Object.is(left, right)) return true; if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJsonValue(value, right[index])); if (left === null || right === null || typeof left !== "object" || typeof right !== "object" || Array.isArray(left) || Array.isArray(right)) return false; const leftKeys = Object.keys(left); const rightKeys = Object.keys(right); return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && sameJsonValue(Reflect.get(left, key), Reflect.get(right, key))); } function scanJson(raw, parsed) { let cursor = 0; const whitespace = () => { while (raw[cursor] === " " || raw[cursor] === " " || raw[cursor] === "\r" || raw[cursor] === "\n") cursor++; }; const stringToken = () => { const start = cursor++; while (cursor < raw.length) if (raw[cursor] === "\\") cursor += 2; else if (raw[cursor++] === "\"") break; return raw.slice(start, cursor); }; const value = (parsedValue) => { whitespace(); const start = cursor; if (raw[cursor] === "{") { cursor++; whitespace(); const properties = /* @__PURE__ */ new Map(); while (raw[cursor] !== "}") { const keyToken = stringToken(); const key = JSON.parse(keyToken); whitespace(); cursor++; const child = parsedValue !== null && typeof parsedValue === "object" && typeof key === "string" ? Reflect.get(parsedValue, key) : void 0; const childNode = value(child); if (typeof key === "string") properties.set(key, { ...childNode, keyToken }); whitespace(); if (raw[cursor] !== ",") break; cursor++; whitespace(); } cursor++; return { start, end: cursor, value: parsedValue, properties }; } if (raw[cursor] === "[") { cursor++; whitespace(); const items = []; while (raw[cursor] !== "]") { items.push(value(Array.isArray(parsedValue) ? parsedValue[items.length] : void 0)); whitespace(); if (raw[cursor] !== ",") break; cursor++; } cursor++; return { start, end: cursor, value: parsedValue, items }; } if (raw[cursor] === "\"") stringToken(); else while (cursor < raw.length && !/[\s,}\]]/.test(raw[cursor] ?? "")) cursor++; return { start, end: cursor, value: parsedValue }; }; return value(parsed); } function serializeValue(value, raw, node, seen) { if (node && sameJsonValue(node.value, value)) return raw.slice(node.start, node.end); if (typeof value === "number") { if (Object.is(value, -0)) return "-0"; if (value === Infinity) return "1e999"; if (value === -Infinity) return "-1e999"; return JSON.stringify(value); } if (value === null || typeof value !== "object") return JSON.stringify(value); if (seen.has(value)) throw new TypeError("Cannot serialize circular tool input"); seen.add(value); try { if (Array.isArray(value)) return "[" + Array.from(value, (item, index) => serializeValue(item, raw, node?.items?.[index], seen) ?? "null").join(",") + "]"; const properties = []; for (const key of Object.keys(value)) { const serialized = serializeValue(Reflect.get(value, key), raw, node?.properties?.get(key), seen); if (serialized !== void 0) properties.push((node?.properties?.get(key)?.keyToken ?? JSON.stringify(key)) + ":" + serialized); } return "{" + properties.join(",") + "}"; } finally { seen.delete(value); } } /** * Keep `raw` when it is the same JSON value as `input`. When they differ, * write `input` again and keep the raw tokens of the values that did not * change. Empty or missing raw arguments use the input. */ function reconcileToolCallArguments(raw, input) { if (input === void 0) return raw ?? ""; if (raw === void 0) return serializeValue(input, "", void 0, /* @__PURE__ */ new Set()) ?? ""; let parsed; try { parsed = JSON.parse(raw); } catch { if (raw.trim() === "") return serializeValue(input, "", void 0, /* @__PURE__ */ new Set()) ?? raw; return raw; } if (sameJsonValue(parsed, input)) return raw; return serializeValue(input, raw, scanJson(raw, parsed), /* @__PURE__ */ new Set()) ?? raw; } /** * The input of a tool call from its raw arguments: `{}` for an empty string, * `undefined` when the arguments are not complete JSON (for example cut off) * or are `null`. */ function parseToolArguments(args) { if (args.trim() === "") return {}; try { return JSON.parse(args) ?? void 0; } catch { return; } } /** * The final arguments and input of a tool call at TOOL_CALL_END. `streamed` * is the text of its TOOL_CALL_ARGS deltas. The END's raw `args` (the * provider's own string) win over it, and stay as they are when they are the * same JSON value as the END's `input`. Malformed arguments also stay, so the * tool reports the error and the model can repair them. `input` is always * the parse of `args`. */ function finishToolCallArguments(streamed, end) { const metadata = tanstackMetadata(end); const endArgs = end.args ?? metadata?.args; const rawArgs = typeof endArgs === "string" ? endArgs : void 0; const endInput = end.input ?? metadata?.input; const input = endInput !== null && typeof endInput === "object" ? endInput : void 0; let raw = rawArgs ?? streamed; if (rawArgs === void 0 && input !== void 0) try { JSON.parse(raw); } catch { raw = void 0; } let args; try { args = reconcileToolCallArguments(raw, input); } catch { args = raw ?? streamed; } return { args, input: parseToolArguments(args) }; } //#endregion export { finishToolCallArguments, parseToolArguments, reconcileToolCallArguments }; //# sourceMappingURL=tool-call-arguments.js.map