@@ -10,12 +10,44 @@ function loadAgentQvacHelpers() {
|
||||
vm.createContext(sandbox)
|
||||
vm.runInContext(
|
||||
QVAC_SRC +
|
||||
'\n;this.__exports = { bareAgentFlattenToolsForQvac, bareAgentQvacGetProfile, bareAgentQvacProfileList, bareAgentResolveBackend, bareAgentQvacBridgeAvailable, bareAgentQvacResolveCtxSize, bareAgentQvacResolveDeviceOpts, bareAgentQvacModelCardCtxSize, bareAgentSanitizeConfigForBackend, bareAgentRestGetProvider, bareAgentRestProviderList, bareAgentIsQvacModelId, bareAgentMaskSecretPreview, BARE_AGENT_QVAC_CTX_QWEN3, BARE_AGENT_QVAC_CTX_LLAMA32_1B }',
|
||||
'\n;this.__exports = { bareAgentFlattenToolsForQvac, bareAgentSanitizeHistoryForQvac, bareAgentQvacDetectToolDialect, bareAgentQvacGetProfile, bareAgentQvacProfileList, bareAgentResolveBackend, bareAgentQvacBridgeAvailable, bareAgentQvacResolveCtxSize, bareAgentQvacResolveDeviceOpts, bareAgentQvacModelCardCtxSize, bareAgentSanitizeConfigForBackend, bareAgentRestGetProvider, bareAgentRestProviderList, bareAgentIsQvacModelId, bareAgentMaskSecretPreview, BARE_AGENT_QVAC_CTX_QWEN3, BARE_AGENT_QVAC_CTX_LLAMA32_1B }',
|
||||
sandbox
|
||||
)
|
||||
return sandbox.__exports
|
||||
}
|
||||
|
||||
test('full agent tool list flattens to QVAC-safe property schemas', async (t) => {
|
||||
const defs = readFileSync(
|
||||
new URL('../lib/agent/agent-tool-definitions.js', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
const sandbox = { console }
|
||||
vm.createContext(sandbox)
|
||||
vm.runInContext(
|
||||
QVAC_SRC +
|
||||
'\n' +
|
||||
defs +
|
||||
'\n;this.__exports = { bareAgentFlattenToolsForQvac, bareAgentToolDefinitions }',
|
||||
sandbox
|
||||
)
|
||||
const { bareAgentFlattenToolsForQvac, bareAgentToolDefinitions } = sandbox.__exports
|
||||
const flat = bareAgentFlattenToolsForQvac(bareAgentToolDefinitions())
|
||||
t.ok(flat.length > 20)
|
||||
const allowed = { type: 1, description: 1, enum: 1 }
|
||||
for (const tool of flat) {
|
||||
t.ok(tool.name, 'tool missing name')
|
||||
t.is(tool.type, 'function')
|
||||
t.ok(tool.parameters && tool.parameters.type === 'object')
|
||||
const props = tool.parameters.properties || {}
|
||||
for (const key of Object.keys(props)) {
|
||||
const prop = props[key]
|
||||
for (const pk of Object.keys(prop)) {
|
||||
t.ok(allowed[pk], tool.name + '.' + key + ' has forbidden key ' + pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('flatten OpenAI nested tools for QVAC', async (t) => {
|
||||
const { bareAgentFlattenToolsForQvac } = loadAgentQvacHelpers()
|
||||
const flat = bareAgentFlattenToolsForQvac([
|
||||
@@ -35,6 +67,88 @@ test('flatten OpenAI nested tools for QVAC', async (t) => {
|
||||
t.is(flat[0].function, undefined)
|
||||
})
|
||||
|
||||
test('flatten strips nested JSON-schema fields QVAC rejects', async (t) => {
|
||||
const { bareAgentFlattenToolsForQvac } = loadAgentQvacHelpers()
|
||||
const flat = bareAgentFlattenToolsForQvac([
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'todo_write',
|
||||
description: 'todos',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
todos: {
|
||||
type: 'array',
|
||||
description: 'items',
|
||||
items: { type: 'object', properties: { id: { type: 'string' } } }
|
||||
},
|
||||
merge: { type: 'boolean', default: true }
|
||||
},
|
||||
required: ['todos', 'missing']
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
t.is(flat.length, 1)
|
||||
const todos = flat[0].parameters.properties.todos
|
||||
t.is(todos.type, 'array')
|
||||
t.is(todos.description, 'items')
|
||||
t.absent(todos.items)
|
||||
t.absent(flat[0].parameters.properties.merge.default)
|
||||
t.alike(flat[0].parameters.required, ['todos'])
|
||||
})
|
||||
|
||||
test('sanitize QVAC history stringifies tool_calls and never sends null content', async (t) => {
|
||||
const { bareAgentSanitizeHistoryForQvac } = loadAgentQvacHelpers()
|
||||
const out = bareAgentSanitizeHistoryForQvac([
|
||||
{ role: 'system', content: 'sys' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'c1',
|
||||
function: { name: 'read_file', arguments: '{"path":"/x"}' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{ role: 'tool', tool_call_id: 'c1', content: 'ok' }
|
||||
])
|
||||
t.is(out.length, 3)
|
||||
t.is(out[1].role, 'assistant')
|
||||
t.ok(out[1].content.includes('<tool_call>'))
|
||||
t.ok(out[1].content.includes('read_file'))
|
||||
t.absent('tool_calls' in out[1])
|
||||
t.is(out[2].content, 'ok')
|
||||
})
|
||||
|
||||
test('sanitize QVAC history keeps prose and serializes tool_calls', async (t) => {
|
||||
const { bareAgentSanitizeHistoryForQvac } = loadAgentQvacHelpers()
|
||||
const out = bareAgentSanitizeHistoryForQvac([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Working.',
|
||||
tool_calls: [
|
||||
{ function: { name: 'read_file', arguments: '{"path":"/x"}' } }
|
||||
]
|
||||
}
|
||||
])
|
||||
t.ok(out[0].content.includes('Working.'))
|
||||
t.ok(out[0].content.includes('<tool_call>'))
|
||||
t.ok(out[0].content.includes('read_file'))
|
||||
})
|
||||
|
||||
test('detect QVAC tool dialect from model id', async (t) => {
|
||||
const { bareAgentQvacDetectToolDialect } = loadAgentQvacHelpers()
|
||||
t.is(bareAgentQvacDetectToolDialect('QWEN3_1_7B_INST_Q4'), 'hermes')
|
||||
t.is(bareAgentQvacDetectToolDialect('QWEN3_5_4B_MULTIMODAL_Q4_K_M'), 'qwen35')
|
||||
t.is(bareAgentQvacDetectToolDialect('GEMMA4_2B_MULTIMODAL_Q4_K_M'), 'gemma4')
|
||||
t.is(bareAgentQvacDetectToolDialect('GPT_OSS_20B_INST_Q4_K_M'), 'harmony')
|
||||
t.is(bareAgentQvacDetectToolDialect('LFM2_1_2B_Q4_K_M'), 'pythonic')
|
||||
t.is(bareAgentQvacDetectToolDialect('DEEPSEEK_V3_2_CHAT'), 'dsml')
|
||||
})
|
||||
|
||||
test('qvac profiles include recommended + lite', async (t) => {
|
||||
const { bareAgentQvacProfileList, bareAgentQvacGetProfile } = loadAgentQvacHelpers()
|
||||
const list = bareAgentQvacProfileList()
|
||||
|
||||
@@ -55,6 +55,28 @@ test('finalize drops duplicate name+args when ids differ', () => {
|
||||
assert.equal(out.length, 1)
|
||||
})
|
||||
|
||||
test('extract hermes <tool_call> JSON from assistant text', () => {
|
||||
const { bareAgentExtractToolCallsFromText, bareAgentStripToolCallsFromText } =
|
||||
loadToolCallFns()
|
||||
const text =
|
||||
'Working.\n<tool_call>{"name":"read_file","arguments":{"path":"/home/guest/x"}}</tool_call>'
|
||||
const out = bareAgentExtractToolCallsFromText(text)
|
||||
assert.equal(out.length, 1)
|
||||
assert.equal(out[0].function.name, 'read_file')
|
||||
assert.ok(String(out[0].function.arguments).includes('/home/guest/x'))
|
||||
assert.equal(bareAgentStripToolCallsFromText(text), 'Working.')
|
||||
})
|
||||
|
||||
test('extract qwen3.5 XML tool_call from assistant text', () => {
|
||||
const { bareAgentExtractToolCallsFromText } = loadToolCallFns()
|
||||
const text =
|
||||
'<tool_call><function=list_directory><parameter=path>/home</parameter></function></tool_call>'
|
||||
const out = bareAgentExtractToolCallsFromText(text)
|
||||
assert.equal(out.length, 1)
|
||||
assert.equal(out[0].function.name, 'list_directory')
|
||||
assert.equal(JSON.parse(out[0].function.arguments).path, '/home')
|
||||
})
|
||||
|
||||
test('finalize keeps same tool with different args', () => {
|
||||
const { bareAgentMergeToolCallDelta, bareAgentFinalizeToolCalls } = loadToolCallFns()
|
||||
const acc = new Map()
|
||||
|
||||
Reference in New Issue
Block a user