@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
JavaScript
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