import { createHash } from "node:crypto"; import { RESERVED_TOOL_NAMES } from "../bridge/reserved.ts"; import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts"; import { planToolDeclarations } from "../prompt/ts-schema.ts"; export interface ToolSnapshot { readonly count: number; readonly declaredTools: readonly EvalSchemaToolInfo[]; readonly omittedNames: readonly string[]; readonly tools: readonly EvalSchemaToolInfo[]; readonly version: string; } export function captureToolSnapshot( listTools: () => readonly EvalSchemaToolInfo[] ): ToolSnapshot { const sorted = [...listTools()] .filter( (tool) => tool.name.length > 0 && !RESERVED_TOOL_NAMES.has(tool.name) ) .sort((left, right) => left.name.localeCompare(right.name)); const tools: EvalSchemaToolInfo[] = []; const seen = new Set(); for (const tool of sorted) { if (seen.has(tool.name)) { continue; } seen.add(tool.name); tools.push(freezeTool(tool)); } const declarationPlan = planToolDeclarations(tools); const declaredNames = new Set(declarationPlan.declaredNames); const declaredTools = tools.filter((tool) => declaredNames.has(tool.name)); return Object.freeze({ count: tools.length, declaredTools: Object.freeze(declaredTools), omittedNames: Object.freeze([...declarationPlan.omittedNames]), tools: Object.freeze(tools), version: toolSnapshotVersion(tools), }); } export const EMPTY_TOOL_SNAPSHOT: ToolSnapshot = Object.freeze({ count: 0, declaredTools: Object.freeze([]), omittedNames: Object.freeze([]), tools: Object.freeze([]), version: toolSnapshotVersion([]), }); function freezeTool(tool: EvalSchemaToolInfo): EvalSchemaToolInfo { return Object.freeze({ name: tool.name, ...(tool.description === undefined ? {} : { description: tool.description }), ...(tool.parameters === undefined ? {} : { parameters: freezeSchema(tool.parameters) }), }); } function freezeSchema(schema: unknown): unknown { try { return freezeRecursively(structuredClone(schema)); } catch { return schema; } } function freezeRecursively(value: unknown): unknown { if (value === null || typeof value !== "object" || Object.isFrozen(value)) { return value; } for (const child of Object.values(value)) { freezeRecursively(child); } return Object.freeze(value); } function toolSnapshotVersion(tools: readonly EvalSchemaToolInfo[]): string { return createHash("sha256") .update( tools.map((tool) => `${tool.name}|${tool.description ?? ""}`).join("\n") ) .digest("hex") .slice(0, 12); }