@@ -116,13 +116,56 @@ async function bareAgentStreamChatCompletions(opts) {
|
||||
if (j.type === 'response.output_text.delta' && typeof j.delta === 'string') {
|
||||
onEvent({ type: 'delta_content', content: j.delta })
|
||||
}
|
||||
if (j.type === 'response.output_item.added') {
|
||||
const item =
|
||||
j.item && typeof j.item === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (j.item)
|
||||
: null
|
||||
if (
|
||||
item &&
|
||||
(item.type === 'function_call' || item.type === 'custom_tool_call')
|
||||
) {
|
||||
const idx =
|
||||
typeof j.output_index === 'number' ? j.output_index : 0
|
||||
const name = typeof item.name === 'string' ? item.name : ''
|
||||
const args =
|
||||
typeof item.arguments === 'string' ? item.arguments : ''
|
||||
const id =
|
||||
typeof item.call_id === 'string'
|
||||
? item.call_id
|
||||
: typeof item.id === 'string'
|
||||
? item.id
|
||||
: ''
|
||||
if (name || args) {
|
||||
onEvent({
|
||||
type: 'delta_tool_calls',
|
||||
tool_calls: [
|
||||
{
|
||||
index: idx,
|
||||
id,
|
||||
function: { name, arguments: args }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
j.type === 'response.function_call_arguments.delta' &&
|
||||
typeof j.delta === 'string'
|
||||
) {
|
||||
const idx = typeof j.output_index === 'number' ? j.output_index : 0
|
||||
const name = typeof j.name === 'string' ? j.name : ''
|
||||
const id = typeof j.item_id === 'string' ? j.item_id : ''
|
||||
onEvent({
|
||||
type: 'delta_tool_calls',
|
||||
tool_calls: [{ index: 0, function: { arguments: j.delta } }]
|
||||
tool_calls: [
|
||||
{
|
||||
index: idx,
|
||||
id,
|
||||
function: { name, arguments: j.delta }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -139,6 +182,10 @@ async function bareAgentStreamChatCompletions(opts) {
|
||||
typeof ch0?.finish_reason === 'string' ? ch0.finish_reason : ''
|
||||
const usage =
|
||||
typeof j.usage === 'object' && j.usage ? j.usage : undefined
|
||||
const message =
|
||||
ch0 && typeof ch0.message === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (ch0.message)
|
||||
: null
|
||||
|
||||
if (usage) {
|
||||
onEvent({ type: 'usage', usage })
|
||||
@@ -191,6 +238,23 @@ async function bareAgentStreamChatCompletions(opts) {
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const msgTools = message.tool_calls
|
||||
if (Array.isArray(msgTools) && msgTools.length) {
|
||||
onEvent({
|
||||
type: 'delta_tool_calls',
|
||||
tool_calls: msgTools
|
||||
})
|
||||
}
|
||||
const legacy = message.function_call
|
||||
if (legacy && typeof legacy === 'object') {
|
||||
onEvent({
|
||||
type: 'delta_tool_calls',
|
||||
tool_calls: [{ index: 0, function: legacy }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (finishReason)
|
||||
onEvent({
|
||||
type: 'finish_reason',
|
||||
|
||||
@@ -111,8 +111,76 @@ function bareAgentQvacGetProfile(id) {
|
||||
return BARE_AGENT_QVAC_PROFILES[key] || BARE_AGENT_QVAC_PROFILES.recommended
|
||||
}
|
||||
|
||||
const BARE_AGENT_QVAC_PROP_TYPES = Object.freeze([
|
||||
'string',
|
||||
'number',
|
||||
'integer',
|
||||
'boolean',
|
||||
'object',
|
||||
'array'
|
||||
])
|
||||
|
||||
/**
|
||||
* Flatten OpenAI nested tool defs → QVAC flat shape.
|
||||
* QVAC toolSchema only allows { type, description?, enum? } per property.
|
||||
* Nested items/properties/defaults make the whole tools array fail validation
|
||||
* and the SDK then runs the completion with tools omitted.
|
||||
* @param {unknown} params
|
||||
* @returns {{ type: 'object', properties: Record<string, unknown>, required?: string[] }}
|
||||
*/
|
||||
function bareAgentSanitizeQvacToolParameters(params) {
|
||||
const src =
|
||||
params && typeof params === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (params)
|
||||
: {}
|
||||
const rawProps =
|
||||
src.properties && typeof src.properties === 'object' && !Array.isArray(src.properties)
|
||||
? /** @type {Record<string, unknown>} */ (src.properties)
|
||||
: {}
|
||||
/** @type {Record<string, unknown>} */
|
||||
const properties = {}
|
||||
for (const key of Object.keys(rawProps)) {
|
||||
const v = rawProps[key]
|
||||
const prop = v && typeof v === 'object' && !Array.isArray(v)
|
||||
? /** @type {Record<string, unknown>} */ (v)
|
||||
: {}
|
||||
let type = typeof prop.type === 'string' ? prop.type : 'string'
|
||||
if (BARE_AGENT_QVAC_PROP_TYPES.indexOf(type) === -1) type = 'string'
|
||||
/** @type {Record<string, unknown>} */
|
||||
const next = { type: type }
|
||||
if (typeof prop.description === 'string' && prop.description) {
|
||||
next.description = prop.description
|
||||
}
|
||||
if (Array.isArray(prop.enum)) {
|
||||
const en = prop.enum.filter(function (x) {
|
||||
return (
|
||||
typeof x === 'string' ||
|
||||
typeof x === 'number' ||
|
||||
typeof x === 'boolean' ||
|
||||
x === null
|
||||
)
|
||||
})
|
||||
if (en.length) next.enum = en
|
||||
}
|
||||
properties[key] = next
|
||||
}
|
||||
/** @type {{ type: 'object', properties: Record<string, unknown>, required?: string[] }} */
|
||||
const out = { type: 'object', properties: properties }
|
||||
if (Array.isArray(src.required)) {
|
||||
const req = src.required
|
||||
.map(function (x) {
|
||||
return String(x || '')
|
||||
})
|
||||
.filter(function (k) {
|
||||
return k && Object.prototype.hasOwnProperty.call(properties, k)
|
||||
})
|
||||
if (req.length) out.required = req
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten OpenAI nested tool defs → QVAC flat shape, then strip property
|
||||
* fields the QVAC SDK schema rejects (items, nested properties, default, …).
|
||||
* @param {unknown[]} tools
|
||||
* @returns {unknown[]}
|
||||
*/
|
||||
@@ -125,30 +193,118 @@ function bareAgentFlattenToolsForQvac(tools) {
|
||||
const o = /** @type {Record<string, unknown>} */ (t)
|
||||
if (o.function && typeof o.function === 'object') {
|
||||
const fn = /** @type {Record<string, unknown>} */ (o.function)
|
||||
const name = typeof fn.name === 'string' ? fn.name : ''
|
||||
if (!name) continue
|
||||
out.push({
|
||||
type: 'function',
|
||||
name: typeof fn.name === 'string' ? fn.name : '',
|
||||
name: name,
|
||||
description: typeof fn.description === 'string' ? fn.description : '',
|
||||
parameters:
|
||||
fn.parameters && typeof fn.parameters === 'object'
|
||||
? fn.parameters
|
||||
: { type: 'object', properties: {} }
|
||||
parameters: bareAgentSanitizeQvacToolParameters(fn.parameters)
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (typeof o.name === 'string') {
|
||||
if (typeof o.name === 'string' && o.name) {
|
||||
out.push({
|
||||
type: 'function',
|
||||
name: o.name,
|
||||
description: typeof o.description === 'string' ? o.description : '',
|
||||
parameters:
|
||||
o.parameters && typeof o.parameters === 'object'
|
||||
? o.parameters
|
||||
: { type: 'object', properties: {} }
|
||||
parameters: bareAgentSanitizeQvacToolParameters(o.parameters)
|
||||
})
|
||||
}
|
||||
}
|
||||
return out.filter((x) => x && typeof x === 'object' && /** @type {any} */ (x).name)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* QVAC completion history is `{ role, content: string }` only. OpenAI-style
|
||||
* `content: null` + `tool_calls` fails request validation and the turn
|
||||
* never reaches the model (so the harness looks like it "stopped using tools").
|
||||
* @param {unknown[]} history
|
||||
* @returns {{ role: string, content: string }[]}
|
||||
*/
|
||||
function bareAgentSanitizeHistoryForQvac(history) {
|
||||
if (!Array.isArray(history)) return []
|
||||
/** @type {{ role: string, content: string }[]} */
|
||||
const out = []
|
||||
for (const raw of history) {
|
||||
if (!raw || typeof raw !== 'object') continue
|
||||
const m = /** @type {Record<string, unknown>} */ (raw)
|
||||
const role = typeof m.role === 'string' && m.role ? m.role : 'user'
|
||||
let content = ''
|
||||
if (typeof m.content === 'string') content = m.content
|
||||
else if (Array.isArray(m.content)) {
|
||||
const parts = []
|
||||
for (const part of m.content) {
|
||||
if (typeof part === 'string') parts.push(part)
|
||||
else if (part && typeof part === 'object') {
|
||||
const o = /** @type {Record<string, unknown>} */ (part)
|
||||
if (typeof o.text === 'string') parts.push(o.text)
|
||||
else if (typeof o.content === 'string') parts.push(o.content)
|
||||
}
|
||||
}
|
||||
content = parts.join('')
|
||||
} else if (m.content != null && typeof m.content !== 'object') {
|
||||
content = String(m.content)
|
||||
}
|
||||
if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
||||
const chunks = []
|
||||
for (const tc of m.tool_calls) {
|
||||
if (!tc || typeof tc !== 'object') continue
|
||||
const o = /** @type {Record<string, unknown>} */ (tc)
|
||||
const fn =
|
||||
o.function && typeof o.function === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (o.function)
|
||||
: o
|
||||
const name = typeof fn.name === 'string' ? fn.name : ''
|
||||
if (!name) continue
|
||||
let args = fn.arguments
|
||||
if (args != null && typeof args !== 'string') {
|
||||
try {
|
||||
args = JSON.stringify(args)
|
||||
} catch {
|
||||
args = '{}'
|
||||
}
|
||||
}
|
||||
if (typeof args !== 'string' || !args) args = '{}'
|
||||
let argsObj = args
|
||||
try {
|
||||
const parsed = JSON.parse(args)
|
||||
argsObj = JSON.stringify(parsed && typeof parsed === 'object' ? parsed : {})
|
||||
} catch {
|
||||
argsObj = '{}'
|
||||
}
|
||||
chunks.push(
|
||||
'<tool_call>{"name":' +
|
||||
JSON.stringify(name) +
|
||||
',"arguments":' +
|
||||
argsObj +
|
||||
'}</tool_call>'
|
||||
)
|
||||
}
|
||||
if (chunks.length) {
|
||||
const serialized = chunks.join('\n')
|
||||
content = content ? content + '\n' + serialized : serialized
|
||||
}
|
||||
}
|
||||
out.push({ role: role, content: content })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Match QVAC `detectToolDialectFromName` so we do not override auto-detect
|
||||
* with the wrong parser (e.g. LFM/DeepSeek).
|
||||
* @param {string} [modelId]
|
||||
* @returns {'hermes'|'qwen35'|'gemma4'|'harmony'|'pythonic'|'dsml'}
|
||||
*/
|
||||
function bareAgentQvacDetectToolDialect(modelId) {
|
||||
const tag = String(modelId || '').toLowerCase()
|
||||
if (/qwen3[._-]?[56](?![a-z0-9])/.test(tag)) return 'qwen35'
|
||||
if (/gemma[-_]?4(?=[^a-z0-9]|$)/.test(tag)) return 'gemma4'
|
||||
if (/gpt[_-]?oss/.test(tag)) return 'harmony'
|
||||
if (/deepseek[-_. ]?v(?:4|3[._-]?2)(?![0-9])/.test(tag)) return 'dsml'
|
||||
if (/lfm[_-]?\d/.test(tag)) return 'pythonic'
|
||||
return 'hermes'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -431,6 +431,91 @@ function bareAgentFinalizeToolCalls(acc) {
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover tool calls the provider streamed as plain text (Hermes / Qwen XML)
|
||||
* instead of `delta_tool_calls`. Today's models often do this when the SDK
|
||||
* drops tools or picks the wrong dialect.
|
||||
* @param {string} text
|
||||
* @returns {{ index: number, id: string, function: { name: string, arguments: string } }[]}
|
||||
*/
|
||||
function bareAgentExtractToolCallsFromText(text) {
|
||||
const src = String(text || '')
|
||||
if (!src) return []
|
||||
/** @type {{ index: number, id: string, function: { name: string, arguments: string } }[]} */
|
||||
const out = []
|
||||
let idx = 0
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} args
|
||||
*/
|
||||
function push(name, args) {
|
||||
const n = String(name || '').trim()
|
||||
if (!n) return
|
||||
let a = String(args == null ? '' : args).trim()
|
||||
if (!a) a = '{}'
|
||||
else if (a[0] !== '{' && a[0] !== '[') {
|
||||
try {
|
||||
JSON.parse(a)
|
||||
} catch {
|
||||
a = JSON.stringify({ value: a })
|
||||
}
|
||||
}
|
||||
out.push({
|
||||
index: idx,
|
||||
id: 'text_call_' + idx,
|
||||
function: { name: n, arguments: a }
|
||||
})
|
||||
idx += 1
|
||||
}
|
||||
|
||||
const hermes = /<tool_call>\s*([\s\S]*?)<\/tool_call>/gi
|
||||
let m
|
||||
while ((m = hermes.exec(src))) {
|
||||
const inner = String(m[1] || '').trim()
|
||||
const fnXml = /^<function=([^>]+)>([\s\S]*?)<\/function>$/i.exec(inner)
|
||||
if (fnXml) {
|
||||
const name = fnXml[1]
|
||||
/** @type {Record<string, string>} */
|
||||
const args = {}
|
||||
const paramRe = /<parameter=([^>]+)>([\s\S]*?)<\/parameter>/gi
|
||||
let pm
|
||||
while ((pm = paramRe.exec(fnXml[2] || ''))) {
|
||||
args[String(pm[1] || '').trim()] = String(pm[2] || '')
|
||||
}
|
||||
push(name, JSON.stringify(args))
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const j = JSON.parse(inner)
|
||||
if (j && typeof j === 'object' && typeof j.name === 'string') {
|
||||
const args =
|
||||
j.arguments != null
|
||||
? typeof j.arguments === 'string'
|
||||
? j.arguments
|
||||
: JSON.stringify(j.arguments)
|
||||
: '{}'
|
||||
push(j.name, args)
|
||||
continue
|
||||
}
|
||||
} catch {
|
||||
/* not JSON */
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function bareAgentStripToolCallsFromText(text) {
|
||||
return String(text || '')
|
||||
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} cfg
|
||||
*/
|
||||
@@ -1931,6 +2016,23 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
/** @type {unknown} */
|
||||
let usageOut = null
|
||||
let finishReason = ''
|
||||
let recoveredFromText = false
|
||||
|
||||
function recoverToolsFromAssistantText() {
|
||||
if (recoveredFromText) return
|
||||
recoveredFromText = true
|
||||
if (typeof bareAgentExtractToolCallsFromText !== 'function') return
|
||||
const extracted = bareAgentExtractToolCallsFromText(assistantContent)
|
||||
if (!toolAcc.size && extracted.length) {
|
||||
for (const tc of extracted) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||||
appendProgress(
|
||||
'recovered_tool_calls_from_text n=' + String(extracted.length)
|
||||
)
|
||||
}
|
||||
if (extracted.length) {
|
||||
assistantContent = bareAgentStripToolCallsFromText(assistantContent)
|
||||
}
|
||||
}
|
||||
const hideThinkEnv = String(envBag.BARE_OS_AGENT_HIDE_THINK || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@@ -2259,13 +2361,21 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
)
|
||||
}
|
||||
}
|
||||
const qvacHistory =
|
||||
typeof bareAgentSanitizeHistoryForQvac === 'function'
|
||||
? bareAgentSanitizeHistoryForQvac(messages)
|
||||
: messages
|
||||
await ctx.bareOsQvacComplete({
|
||||
history: messages,
|
||||
history: qvacHistory,
|
||||
tools: qvacToolsForTurn,
|
||||
stream: Boolean(configRef.current.stream !== false),
|
||||
captureThinking: !hideThink || reasoningSettings.enabled,
|
||||
modelSrc,
|
||||
toolsEnabled: true,
|
||||
toolDialect:
|
||||
typeof bareAgentQvacDetectToolDialect === 'function'
|
||||
? bareAgentQvacDetectToolDialect(modelSrc)
|
||||
: undefined,
|
||||
ctxSize,
|
||||
device: deviceOpts.device,
|
||||
mainGpu: deviceOpts.mainGpu,
|
||||
@@ -2277,6 +2387,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
clearStatusLine()
|
||||
thinkSplit.flush()
|
||||
sealThinkIfNeeded()
|
||||
recoverToolsFromAssistantText()
|
||||
paintReplyMarkdown()
|
||||
try {
|
||||
detachThinkKeys()
|
||||
@@ -2327,6 +2438,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
clearStatusLine()
|
||||
thinkSplit.flush()
|
||||
sealThinkIfNeeded()
|
||||
recoverToolsFromAssistantText()
|
||||
paintReplyMarkdown()
|
||||
try {
|
||||
detachThinkKeys()
|
||||
@@ -2351,7 +2463,18 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
break
|
||||
}
|
||||
|
||||
const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||||
let toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||||
if (!toolCallsArr.length && typeof bareAgentExtractToolCallsFromText === 'function') {
|
||||
const extracted = bareAgentExtractToolCallsFromText(assistantContent)
|
||||
if (extracted.length) {
|
||||
for (const tc of extracted) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||||
toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||||
assistantContent = bareAgentStripToolCallsFromText(assistantContent)
|
||||
appendProgress(
|
||||
'recovered_tool_calls_from_text n=' + String(toolCallsArr.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
const hasTools = toolCallsArr.length > 0
|
||||
if (!hasTools && finishReason === 'tool_calls') {
|
||||
appendProgress(
|
||||
@@ -2363,7 +2486,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
/** @type {Record<string, unknown>} */
|
||||
const assistantMsg = {
|
||||
role: 'assistant',
|
||||
content: assistantContent || null,
|
||||
content: assistantContent || '',
|
||||
tool_calls: hasTools ? toolCallsArr : undefined
|
||||
}
|
||||
messages.push(assistantMsg)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
|
||||
"scripts": {
|
||||
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
|
||||
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/agent-models.test.mjs && node ./test/agent-live-model.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
|
||||
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/agent-tool-calls.test.mjs && node ./test/agent-models.test.mjs && node ./test/agent-live-model.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
|
||||
"perf:baretop": "node ./test/baretop-perf-baseline.test.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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