import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { buildOpenApiInstructionsWithTools, runToolEnabledOpenApiChatLoop, } from "../src/openapi-chat-tool-loop.js"; const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), ".."); const openApiChatSource = readFileSync(resolve(desktopRoot, "src/openapi-chat.ts"), "utf8"); const openApiChatPromptFlowsSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-prompt-flows.ts"), "utf8"); const openApiChatToolLoopSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-tool-loop.ts"), "utf8"); assert.match(openApiChatSource, /from "\.\/openapi-chat-prompt-flows(?:\.js)?"/, "openapi-chat must import the extracted prompt-flow seam."); assert.match(openApiChatPromptFlowsSource, /from "\.\/openapi-chat-tool-loop(?:\.js)?"/, "openapi-chat prompt flows must import the extracted tool loop seam."); assert.match(openApiChatToolLoopSource, /export async function runToolEnabledOpenApiChatLoop/, "openapi-chat tool loop seam must export the async tool runner."); assert.match(openApiChatToolLoopSource, /export function buildOpenApiInstructionsWithTools/, "openapi-chat tool loop seam must export tool-aware instruction shaping."); assert.equal( buildOpenApiInstructionsWithTools("Base instructions", [ { type: "function", function: { name: "memory.save", description: "Persist a note", parameters: { type: "object" }, }, }, ]), "Base instructions\n\nYou have access to the following tools: memory.save. When you need to use a tool, respond with a tool call. The system will execute it and return the result. Do not make up tool results.", "tool-aware instructions must append the available tool names and execution rule.", ); const requestBodies: Array> = []; const toolCalls: Array<{ name: string; args: unknown }> = []; const responses = [ new Response(JSON.stringify({ id: "chatcmpl_tool", choices: [ { message: { content: "Checking memory", tool_calls: [ { id: "tool_1", type: "function", function: { name: "memory.save", arguments: "{\"note\":\"Remember the milk\"}", }, }, ], }, }, ], })), new Response(JSON.stringify({ id: "chatcmpl_final", choices: [ { message: { content: "Saved it for later.", }, }, ], })), ]; const result = await runToolEnabledOpenApiChatLoop({ endpoint: "https://provider.example/v1/responses", credential: "sk-test", model: "gpt-test", prompt: "Remember the milk", systemContent: "You are FamiliarOS.", history: [], maxHistoryEntries: 24, maxToolCallIterations: 5, maxResponseOutputTokens: 256, tools: [ { type: "function", function: { name: "memory.save", description: "Persist a note", parameters: { type: "object" }, }, }, ], manager: { async callTool(name: string, args: unknown) { toolCalls.push({ name, args }); return "stored"; }, }, async fetchImpl(_input, init) { assert.ok(init?.body, "tool loop must send a request body."); requestBodies.push(JSON.parse(String(init.body)) as Record); const next = responses.shift(); assert.ok(next, "tool loop test ran out of stubbed responses."); return next; }, }); assert.equal(result.finalText, "Saved it for later."); assert.equal(result.responseId, "chatcmpl_final"); assert.equal(result.iterationCount, 2); assert.equal(result.endpoint, "https://provider.example/v1/chat/completions"); assert.equal(toolCalls.length, 1); assert.deepEqual(toolCalls[0], { name: "memory.save", args: { note: "Remember the milk" }, }); assert.equal(requestBodies.length, 2); assert.equal(requestBodies[0].tool_choice, "auto"); assert.equal( (requestBodies[0].tools as Array<{ function: { name: string } }>)[0]?.function.name, "memory.save", "tool loop must forward the OpenAI tool definition payload.", ); assert.deepEqual( result.history, [ { role: "system", content: "You are FamiliarOS." }, { role: "user", content: "Remember the milk" }, { role: "assistant", content: "Checking memory", tool_calls: [ { id: "tool_1", type: "function", function: { name: "memory.save", arguments: "{\"note\":\"Remember the milk\"}", }, }, ], }, { role: "tool", tool_call_id: "tool_1", content: "stored", }, { role: "assistant", content: "Saved it for later.", }, ], "tool loop must preserve the system/user/tool-call/tool-result/final-assistant history sequence.", ); console.error("OpenAPI chat tool loop validation passed.");