import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { profile } from '../daemon/model-profiles.js'; const require = createRequire(import.meta.url); const catalog = require('../vendor/agent-harness/lib/catalog.js'); const toolParse = require('../vendor/agent-harness/lib/tool-parse.js'); function qvacTransport(history) { return history.map((message) => ({ role: message.role, content: message.content })); } test('Qwen tool history replays calls through the QVAC role/content transport', () => { const history = [ { role: 'assistant', content: '', tool_calls: [{ id: 'c1', name: 'jarvis_status', arguments: { component: 'speaker' } }] }, { role: 'tool', tool_call_id: 'c1', content: '{"code":"SPEAKER-7429"}' }, { role: 'assistant', content: 'Checking.', tool_calls: [{ type: 'function', function: { name: 'lookup', arguments: '{"count":2,"enabled":false}' } }] }, ]; const prepared = toolParse.prepareToolHistory(history, 'qwen35'); const xml = toolParse.qwen35ToolCallXml({ name: 'jarvis_status', arguments: { component: 'speaker' } }); assert.match(xml, /^\n\n\nspeaker\n<\/parameter>\n<\/function>\n<\/tool_call>$/); assert.match(prepared[0].content, //); assert.match(prepared[0].content, /\nspeaker\n<\/parameter>/); assert.match(prepared[2].content, /\nfalse\n<\/parameter>/); assert.equal(history[0].content, ''); assert.equal(prepared[1], history[1]); assert.deepEqual(toolParse.prepareToolHistory(prepared, 'qwen35'), prepared); assert.equal(toolParse.prepareToolHistory(history, 'hermes'), history); const transported = qvacTransport(prepared); assert.deepEqual(Object.keys(transported[0]).sort(), ['content', 'role']); const recovered = toolParse.extractCalls(transported[0].content, [{ name: 'jarvis_status' }]); assert.equal(recovered.length, 1); assert.equal(recovered[0].name, 'jarvis_status'); assert.equal(recovered[0].arguments.component, 'speaker'); assert.equal(transported[1].role, 'tool'); assert.equal(transported[1].content, '{"code":"SPEAKER-7429"}'); assert.match(transported[2].content, /[\s\S]*/); }); test('Qwen3.5 0.8B and 2B use the qwen35 tool dialect and compact-tool reminder', () => { assert.equal(profile('laptop-4gb-mm').model, 'qwen3.5-0.8b'); assert.equal(profile('laptop-8gb-mm').model, 'qwen3.5-2b'); for (const id of ['qwen3.5-0.8b', 'qwen3.5-2b']) { assert.equal(catalog.toolDialectFor(id), 'qwen35'); assert.equal(catalog.findCatalogEntry(id).tools, true); assert.equal(catalog.isCompactToolModel(id), true); assert.equal(catalog.findCatalogEntry(id).ctxSize, 16384); } assert.match(toolParse.FORMAT_REMINDER, //); assert.deepEqual(catalog.compactGenerationParams('qwen3.5-0.8b'), { temp: 0.55 }); assert.deepEqual(catalog.compactGenerationParams('qwen3.5-2b'), { temp: 0.55 }); assert.deepEqual(catalog.compactGenerationParams('qwen3.5-4b'), {}); assert.equal(catalog.reasoningBudgetForModel('qwen3.5-0.8b'), 128); assert.equal(catalog.reasoningBudgetForModel('qwen3.5-2b'), 192); assert.equal(catalog.reasoningBudgetForModel('qwen3.5-4b'), 320); assert.equal(catalog.reasoningBudgetForModel('qwen3.5-9b'), 448); assert.equal(catalog.reasoningBudgetForModel('gemma4-4b'), null); const compactGen = catalog.generationParamsForModel('qwen3.5-0.8b', { predict: 512, seed: 7 }); assert.equal(compactGen.temp, 0.55); assert.equal(compactGen.reasoning_budget, 128); assert.equal(compactGen.repeat_penalty, 1.15); assert.equal(compactGen.predict, 512); assert.equal(compactGen.seed, 7); const largeGen = catalog.generationParamsForModel('qwen3.5-4b', { predict: 512 }); assert.equal(largeGen.reasoning_budget, 320); assert.equal(largeGen.predict, 512); assert.equal(largeGen.repeat_penalty, 1.15); const tiny = catalog.filterToolsForModel( [{ name: 'web_search' }, { name: 'todo_write' }, { name: 'qvac_capability' }, { name: 'cu_drag' }], 'qwen3.5-0.8b', ); assert.deepEqual(tiny.map((t) => t.name), ['web_search', 'todo_write']); }); test('Qwen3.5 compact models recover tool calls nested in think tags', () => { const tools = [{ name: 'web_search' }, { name: 'web_fetch' }]; const thinking = ` Need a current answer. Ubuntu 26.04 release `; const recovered = toolParse.recover({ thinking, text: '', tools }); assert.equal(recovered.calls.length, 1); assert.equal(recovered.calls[0].name, 'web_search'); assert.equal(recovered.calls[0].arguments.query, 'Ubuntu 26.04 release'); }); test('SDK thinking events use text and recover think tags from content', () => { const events = require('../vendor/agent-harness/lib/events.js'); const sdkThink = events.normalizeCompletionEvent({ type: 'thinkingDelta', seq: 1, text: 'Considering the lookup.' }); assert.equal(sdkThink.type, 'thinkingDelta'); assert.equal(sdkThink.delta, 'Considering the lookup.'); const first = events.expandThinkEvents({ type: 'contentDelta', delta: 'Hello reason' }, { inThink: false, carry: '' }); assert.deepEqual(first.events, [ { type: 'contentDelta', delta: 'Hello ' }, { type: 'thinkingDelta', delta: 'reason' }, ]); assert.equal(first.state.inThink, true); const next = events.expandThinkEvents({ type: 'contentDelta', delta: 'ingAnswer' }, first.state); assert.deepEqual(next.events, [ { type: 'thinkingDelta', delta: 'ing' }, { type: 'contentDelta', delta: 'Answer' }, ]); assert.equal(next.state.inThink, false); }); test('click and type aliases recover as computer-use tools', () => { const tools = [{ name: 'cu_click' }, { name: 'cu_type' }, { name: 'cu_observe' }]; const calls = toolParse.extractCalls( '\nr4\n', tools, ); assert.equal(calls[0].name, 'cu_click'); assert.equal(calls[0].arguments.ref, 'r4'); const typed = toolParse.extractCalls( '\nhi\n', tools, ); assert.equal(typed[0].name, 'cu_type'); });