'use strict'; import test from 'node:test'; import assert from 'node:assert/strict'; import { LLMOrbiter, defineTool } from '../src/index.js'; class MockProvider { constructor(responses) { this.responses = [...responses]; this.calls = []; } async chatCompletions(args) { this.calls.push(args); const next = this.responses.shift(); if (!next) throw new Error('MockProvider ran out of mock responses'); return next; } } test('LLMOrbiter loop continues when provider returns no response (no tool calls and no content)', async () => { let toolInvoked = false; const testTool = defineTool({ name: 'test_tool', description: 'Test tool', parameters: { type: 'object', properties: {} }, invoke: async () => { toolInvoked = true; return { ok: true }; }, }); // Sequence: // 1. Tool call // 2. No response (no tool calls, empty content) -> should continue loop! // 3. Final content const mock = new MockProvider([ { tool_calls: [ { id: 'call_1', type: 'function', function: { name: 'test_tool', arguments: '{}' }, }, ], content: null, }, { tool_calls: undefined, content: '', }, { tool_calls: undefined, content: 'Task completed successfully.', }, ]); const orbiter = new LLMOrbiter(mock); const result = await orbiter.run({ prompt: 'Do something', tools: [testTool], maxIterations: 10, }); assert.equal(toolInvoked, true, 'Tool should have been invoked'); assert.equal(result.iterations, 3, 'Should have taken 3 iterations (tool call, no response, final answer)'); assert.equal(result.content, 'Task completed successfully.'); assert.equal(mock.calls.length, 3, 'Provider should have been called 3 times'); }); test('LLMOrbiter stops at maxIterations if provider continually returns no response', async () => { const mock = new MockProvider([ { tool_calls: undefined, content: '' }, { tool_calls: undefined, content: null }, { tool_calls: undefined, content: ' ' }, ]); const orbiter = new LLMOrbiter(mock); const result = await orbiter.run({ prompt: 'Hello', tools: [], maxIterations: 3, }); assert.equal(result.iterations, 3, 'Should have iterated up to maxIterations'); assert.equal(result.content, null, 'Should return content null when no response was ever given'); }); test('LLMOrbiter finishes immediately in 1 iteration when content is returned with no tool calls', async () => { const mock = new MockProvider([ { tool_calls: undefined, content: 'Hello there!' }, ]); const orbiter = new LLMOrbiter(mock); const result = await orbiter.run({ prompt: 'Hi', tools: [], maxIterations: 10, }); assert.equal(result.iterations, 1); assert.equal(result.content, 'Hello there!'); }); test('LLMOrbiter calls onToolResult before onUsage in a tool-calling iteration', async () => { const events = []; const testTool = defineTool({ name: 'test_tool', description: 'Test tool', parameters: { type: 'object', properties: {} }, invoke: async () => { return { ok: true, data: 42 }; }, }); const mock = new MockProvider([ { tool_calls: [ { id: 'call_1', type: 'function', function: { name: 'test_tool', arguments: '{}' }, }, ], content: null, usage: { prompt_tokens: 10, completion_tokens: 5 }, }, { tool_calls: undefined, content: 'All done.', usage: { prompt_tokens: 20, completion_tokens: 8 }, }, ]); const orbiter = new LLMOrbiter(mock, { onToolCall: ({ name }) => events.push(`tool_call:${name}`), onToolResult: ({ name }) => events.push(`tool_result:${name}`), onUsage: (u) => events.push(`usage:${u.input}:${u.output}`), }); const result = await orbiter.run({ prompt: 'Run tool', tools: [testTool], }); assert.equal(result.content, 'All done.'); assert.deepEqual(events, [ 'tool_call:test_tool', 'tool_result:test_tool', 'usage:10:5', 'usage:20:8', ]); });