+1
-1
@@ -10,7 +10,7 @@ On a **new chat**, recall who you are talking to before you speak:
|
||||
1. Use `USER.md` if it has their name.
|
||||
2. If the name is missing, `read_file` `USER.md` and `MEMORY.md` (and today’s `memory/YYYY-MM-DD.md` if present). Then greet them by name.
|
||||
3. If those files still have no name, ask once and write `USER.md`.
|
||||
4. Follow `PERSONA.md` for user-authored acting notes. Settings is the source of truth for that file.
|
||||
4. Follow acting instructions from Settings when present. Those live in the system prompt, not as a second identity.
|
||||
|
||||
Do not invent a name. Skills: only the catalog is inlined. `read_file` the matching `SKILL.md` when a task fits.
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ Each chat session you wake up fresh. These workspace files _are_ you:
|
||||
- `IDENTITY.md`: name and face
|
||||
- `AGENTS.md`: how you operate
|
||||
- `USER.md`: who you’re helping
|
||||
- `PERSONA.md`: extra acting notes from Settings
|
||||
- `PERSONA.md`: copy of Settings acting notes; the live copy lives in the system prompt
|
||||
- `MEMORY.md` and `memory/YYYY-MM-DD.md`: what you’ve learned
|
||||
|
||||
Read them. Update them when something durable happens. That is how you persist.
|
||||
|
||||
Vendored
+19
-2
@@ -4,6 +4,7 @@
|
||||
|
||||
const engine = require('../lib/qvac.js');
|
||||
const catalog = require('../lib/catalog.js');
|
||||
const toolParse = require('../lib/tool-parse.js');
|
||||
const sessions = require('./sessions.js');
|
||||
const tools = require('./tools.js');
|
||||
const sandbox = require('./sandbox.js');
|
||||
@@ -232,7 +233,7 @@ function resolvePlanDecision(sessionId, decision) {
|
||||
|
||||
function buildToolDefs(session, payload, tracker) {
|
||||
const hostWorkspace = session.hostWorkspace !== false;
|
||||
return tools
|
||||
const defs = tools
|
||||
.defs({
|
||||
planMode: planMode.isActive(tracker),
|
||||
webFetch: payload && payload.webFetch,
|
||||
@@ -240,6 +241,7 @@ function buildToolDefs(session, payload, tracker) {
|
||||
builtinTools: session.builtinTools,
|
||||
})
|
||||
.concat(customTools.defs(session.id));
|
||||
return catalog.filterToolsForModel(defs, session.model);
|
||||
}
|
||||
|
||||
function refreshSystem(session, sys) {
|
||||
@@ -394,7 +396,7 @@ async function runTurn(ctx) {
|
||||
if (session.goal && goalMod.isActive(session.goal)) {
|
||||
extraSys = [extraSys, goalMod.plannerAddendum(session.goal)].filter(Boolean).join('\n\n');
|
||||
}
|
||||
const sys = prompts.assemble({
|
||||
const sysBase = prompts.assemble({
|
||||
cwd: hostWorkspace ? cwd : session.workspace || cwd,
|
||||
hostWorkspace,
|
||||
extra: extraSys,
|
||||
@@ -410,6 +412,9 @@ async function runTurn(ctx) {
|
||||
}
|
||||
: null,
|
||||
});
|
||||
const sys = catalog.isCompactToolModel(session.model)
|
||||
? sysBase + '\n\n' + toolParse.FORMAT_REMINDER
|
||||
: sysBase;
|
||||
const sidecars = [];
|
||||
if (hostWorkspace) {
|
||||
if (!voice) {
|
||||
@@ -737,6 +742,18 @@ async function runTurn(ctx) {
|
||||
);
|
||||
|
||||
let calls = (result && result.toolCalls) || [];
|
||||
if (!calls.length && result) {
|
||||
const recovered = toolParse.recover({
|
||||
text: result.text,
|
||||
thinking: result.thinking,
|
||||
tools: toolDefs,
|
||||
existing: calls,
|
||||
});
|
||||
calls = recovered.calls;
|
||||
if (recovered.text != null) result.text = recovered.text;
|
||||
} else if (result && result.text) {
|
||||
result.text = toolParse.stripToolMarkup(result.text);
|
||||
}
|
||||
if (budget.answerOnly) calls = [];
|
||||
if ((result && result.text) || calls.length) {
|
||||
if (result.text) lastText = result.text;
|
||||
|
||||
+11
-2
@@ -52,12 +52,21 @@ function loadWorkspaceRules(fsRead, cwd) {
|
||||
return loadWorkspaceFiles(fsRead, cwd, ['AGENTS.md', '.agent-harness/AGENTS.md']);
|
||||
}
|
||||
|
||||
function voiceWorkspaceFiles(extra) {
|
||||
const acting = /\n## Acting\n/.test(String(extra || ''));
|
||||
return VOICE_WORKSPACE_FILES.filter((name) => {
|
||||
if (name === 'PERSONA.md') return false;
|
||||
if (acting && (name === 'SOUL.md' || name === 'IDENTITY.md')) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function assemble({ cwd, extra, fsRead, hostWorkspace, personality }) {
|
||||
if (personality === 'voice') {
|
||||
const parts = [];
|
||||
if (extra) parts.push(String(extra));
|
||||
if (cwd) parts.push('Current workspace: ' + cwd);
|
||||
const files = fsRead ? loadWorkspaceFiles(fsRead, cwd, VOICE_WORKSPACE_FILES) : '';
|
||||
const files = fsRead ? loadWorkspaceFiles(fsRead, cwd, voiceWorkspaceFiles(extra)) : '';
|
||||
if (files) parts.push(files);
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
@@ -69,4 +78,4 @@ function assemble({ cwd, extra, fsRead, hostWorkspace, personality }) {
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
module.exports = { DEFAULT_SYSTEM, PAGE_SYSTEM, assemble, loadWorkspaceRules, VOICE_WORKSPACE_FILES };
|
||||
module.exports = { DEFAULT_SYSTEM, PAGE_SYSTEM, assemble, loadWorkspaceRules, VOICE_WORKSPACE_FILES, voiceWorkspaceFiles };
|
||||
|
||||
Vendored
+60
@@ -9,6 +9,7 @@ const CATALOG = [
|
||||
constant: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M',
|
||||
name: 'Qwen3.5 0.8B',
|
||||
tools: true,
|
||||
compactTools: true,
|
||||
vision: true,
|
||||
mmproj: 'MMPROJ_QWEN3_5_0_8B_MULTIMODAL_Q8_0',
|
||||
minRamGb: 4,
|
||||
@@ -20,6 +21,7 @@ const CATALOG = [
|
||||
constant: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M',
|
||||
name: 'Qwen3.5 2B',
|
||||
tools: true,
|
||||
compactTools: true,
|
||||
vision: true,
|
||||
mmproj: 'MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0',
|
||||
minRamGb: 6,
|
||||
@@ -149,6 +151,7 @@ const CATALOG = [
|
||||
constant: 'QWEN3_1_7B_INST_Q4',
|
||||
name: 'Qwen3 1.7B',
|
||||
tools: true,
|
||||
compactTools: true,
|
||||
vision: false,
|
||||
minRamGb: 8,
|
||||
approxDownloadGb: 1.2,
|
||||
@@ -275,6 +278,60 @@ function toolDialectFor(idOrConstant) {
|
||||
return 'hermes';
|
||||
}
|
||||
|
||||
function isCompactToolModel(idOrConstant) {
|
||||
const e = findCatalogEntry(idOrConstant);
|
||||
if (e && e.compactTools) return true;
|
||||
const id = String((e && e.id) || idOrConstant || '').toLowerCase();
|
||||
return /qwen3\.5-0\.8b|qwen3\.5-2b|qwen3-1\.7b/.test(id);
|
||||
}
|
||||
|
||||
const COMPACT_TOOL_ALLOW = [
|
||||
'read_file',
|
||||
'write_file',
|
||||
'search_replace',
|
||||
'list_dir',
|
||||
'grep',
|
||||
'run_terminal_cmd',
|
||||
'web_search',
|
||||
'google_search',
|
||||
'fetch_page',
|
||||
'web_fetch',
|
||||
'wiki_search',
|
||||
'hn_search',
|
||||
'code_search',
|
||||
'jarvis_status',
|
||||
'cu_status',
|
||||
'cu_observe',
|
||||
'cu_find',
|
||||
'cu_click',
|
||||
'cu_type',
|
||||
'cu_key',
|
||||
'fs_search',
|
||||
'fs_read',
|
||||
'fs_write',
|
||||
'app_list',
|
||||
'memory_recall',
|
||||
'memory_remember',
|
||||
'memory_search',
|
||||
'memory_get',
|
||||
'memory_write',
|
||||
'capability_status',
|
||||
'qvac_runtime_state',
|
||||
'qvac_system_resources',
|
||||
];
|
||||
|
||||
function compactToolAllowlist() {
|
||||
return new Set(COMPACT_TOOL_ALLOW);
|
||||
}
|
||||
|
||||
function filterToolsForModel(defs, idOrConstant) {
|
||||
const list = Array.isArray(defs) ? defs : [];
|
||||
if (!isCompactToolModel(idOrConstant)) return list;
|
||||
const allow = compactToolAllowlist();
|
||||
const filtered = list.filter((t) => t && allow.has(t.name));
|
||||
return filtered.length ? filtered : list;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CATALOG,
|
||||
FALLBACK_LLM_IDS,
|
||||
@@ -286,5 +343,8 @@ module.exports = {
|
||||
mmprojConstant,
|
||||
mmprojCandidates,
|
||||
toolDialectFor,
|
||||
isCompactToolModel,
|
||||
compactToolAllowlist,
|
||||
filterToolsForModel,
|
||||
catalogLabel,
|
||||
};
|
||||
|
||||
Vendored
+24
-3
@@ -10,6 +10,7 @@ const device = require('./device.js');
|
||||
const events = require('./events.js');
|
||||
const paths = require('./paths.js');
|
||||
const completeWatch = require('./complete-watch.js');
|
||||
const toolParse = require('./tool-parse.js');
|
||||
|
||||
let sdk = null;
|
||||
let initError = null;
|
||||
@@ -154,11 +155,22 @@ function toTools(tools) {
|
||||
return {
|
||||
type: 'function',
|
||||
name: t.function.name,
|
||||
description: t.function.description,
|
||||
parameters: t.function.parameters,
|
||||
description: t.function.description || t.function.name,
|
||||
parameters: t.function.parameters && t.function.parameters.type === 'object'
|
||||
? t.function.parameters
|
||||
: { type: 'object', properties: {} },
|
||||
};
|
||||
}
|
||||
return t;
|
||||
if (!t || !t.name) return t;
|
||||
const parameters = t.parameters && t.parameters.type === 'object'
|
||||
? t.parameters
|
||||
: { type: 'object', properties: (t.parameters && t.parameters.properties) || {} };
|
||||
return {
|
||||
type: 'function',
|
||||
name: t.name,
|
||||
description: t.description || t.name,
|
||||
parameters,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -455,6 +467,15 @@ async function complete(opts, onEvent) {
|
||||
if (run.stats) stats = await run.stats;
|
||||
} catch (_) {}
|
||||
}
|
||||
if (tools && tools.length) {
|
||||
const recovered = toolParse.recover({ text, thinking, tools, existing: toolCalls });
|
||||
if (!toolCalls.length && recovered.calls.length) {
|
||||
for (const call of recovered.calls) toolCalls.push(call);
|
||||
}
|
||||
text = recovered.text;
|
||||
} else {
|
||||
text = toolParse.stripToolMarkup(text);
|
||||
}
|
||||
return {
|
||||
text,
|
||||
thinking,
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Recover Qwen3.5 / Hermes tool calls that QVAC's stream framer misses.
|
||||
*
|
||||
* Compact Qwen3.5 models often emit <tool_call> inside <think>…</think>.
|
||||
* The SDK thinking framer swallows that XML, so the agent never runs a tool.
|
||||
* No Bare imports — unit-testable on Node.
|
||||
*/
|
||||
|
||||
const FORMAT_REMINDER =
|
||||
'When you need a tool, close thinking first, then emit this XML (not JSON, not a spoken plan):\n' +
|
||||
'<tool_call>\n<function=TOOL_NAME>\n<parameter=ARG>value</parameter>\n</function>\n</tool_call>';
|
||||
|
||||
const ALIASES = {
|
||||
search: 'web_search',
|
||||
websearch: 'web_search',
|
||||
google: 'google_search',
|
||||
googlesearch: 'google_search',
|
||||
fetch: 'web_fetch',
|
||||
webfetch: 'web_fetch',
|
||||
fetchpage: 'fetch_page',
|
||||
open_url: 'fetch_page',
|
||||
openurl: 'fetch_page',
|
||||
shell: 'run_terminal_cmd',
|
||||
bash: 'run_terminal_cmd',
|
||||
terminal: 'run_terminal_cmd',
|
||||
cmd: 'run_terminal_cmd',
|
||||
read: 'read_file',
|
||||
cat: 'read_file',
|
||||
write: 'write_file',
|
||||
ls: 'list_dir',
|
||||
status: 'jarvis_status',
|
||||
};
|
||||
|
||||
function knownNames(tools) {
|
||||
const names = new Set();
|
||||
for (const t of tools || []) {
|
||||
if (t && t.name) names.add(String(t.name));
|
||||
if (t && t.function && t.function.name) names.add(String(t.function.name));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function remapName(name, names) {
|
||||
const raw = String(name || '').trim();
|
||||
if (!raw) return raw;
|
||||
if (names.has(raw)) return raw;
|
||||
const folded = raw.toLowerCase().replace(/[\s-]+/g, '_');
|
||||
if (names.has(folded)) return folded;
|
||||
const alias = ALIASES[folded];
|
||||
if (alias && (names.size === 0 || names.has(alias))) return alias;
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parseJsonArgs(value) {
|
||||
if (value == null) return {};
|
||||
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
if (typeof value !== 'string') return { value };
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
||||
return { value: parsed };
|
||||
} catch (_) {
|
||||
return { value: trimmed };
|
||||
}
|
||||
}
|
||||
|
||||
function parseQwenXmlInner(inner) {
|
||||
const calls = [];
|
||||
const fnRe = /<function\s*=\s*([^>\s]+)\s*>([\s\S]*?)<\/function>/gi;
|
||||
let fn;
|
||||
while ((fn = fnRe.exec(inner)) !== null) {
|
||||
const args = {};
|
||||
const paramRe = /<parameter\s*=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/gi;
|
||||
let pm;
|
||||
while ((pm = paramRe.exec(fn[2])) !== null) args[pm[1].trim()] = String(pm[2]).trim();
|
||||
if (!Object.keys(args).length) {
|
||||
const lineRe = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*(.+?)\s*$/gm;
|
||||
let line;
|
||||
while ((line = lineRe.exec(fn[2])) !== null) args[line[1]] = line[2].trim();
|
||||
}
|
||||
calls.push({ name: fn[1].trim(), arguments: args });
|
||||
}
|
||||
if (calls.length) return calls;
|
||||
const openFn = /<function\s*=\s*([^>\s]+)\s*>([\s\S]*)$/i.exec(inner);
|
||||
if (openFn) {
|
||||
const args = {};
|
||||
const paramRe = /<parameter\s*=\s*([^>\s]+)\s*>([\s\S]*?)(?:<\/parameter>|$)/gi;
|
||||
let pm;
|
||||
while ((pm = paramRe.exec(openFn[2])) !== null) args[pm[1].trim()] = String(pm[2]).replace(/<\/parameter>\s*$/i, '').trim();
|
||||
return [{ name: openFn[1].trim(), arguments: args }];
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
function parseHermesInner(inner) {
|
||||
const trimmed = String(inner || '').trim();
|
||||
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
const list = Array.isArray(parsed) ? parsed : [parsed];
|
||||
return list
|
||||
.map((obj) => {
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
const name = obj.name || (obj.function && obj.function.name);
|
||||
if (!name) return null;
|
||||
const args = obj.arguments != null ? obj.arguments : obj.parameters != null ? obj.parameters : obj.args != null ? obj.args : (obj.function && obj.function.arguments);
|
||||
return { name: String(name), arguments: parseJsonArgs(args) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function extractFrames(text) {
|
||||
const src = String(text || '');
|
||||
const frames = [];
|
||||
const re = /<tool_call>([\s\S]*?)<\/tool_call>/gi;
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) frames.push(m[1]);
|
||||
if (!frames.length) {
|
||||
const open = src.search(/<tool_call>/i);
|
||||
if (open >= 0) frames.push(src.slice(open + '<tool_call>'.length));
|
||||
}
|
||||
if (!frames.length && /<function\s*=/i.test(src)) frames.push(src);
|
||||
return frames;
|
||||
}
|
||||
|
||||
function extractCalls(text, tools) {
|
||||
const names = knownNames(tools);
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const frame of extractFrames(text)) {
|
||||
let parsed = parseQwenXmlInner(frame);
|
||||
if (!parsed.length) parsed = parseHermesInner(frame);
|
||||
for (const call of parsed) {
|
||||
const name = remapName(call.name, names);
|
||||
const args = call.arguments && typeof call.arguments === 'object' ? call.arguments : {};
|
||||
const key = name + ':' + JSON.stringify(args);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ name, arguments: args });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function stripToolMarkup(text) {
|
||||
return String(text || '')
|
||||
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
||||
.replace(/<function\s*=[^>]*>[\s\S]*?<\/function>/gi, '')
|
||||
.replace(/<tool_call>[\s\S]*$/gi, '')
|
||||
.replace(/[ \t]+\n/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function recover({ text, thinking, tools, existing } = {}) {
|
||||
const spoken = stripToolMarkup(text);
|
||||
const have = Array.isArray(existing) && existing.length;
|
||||
const calls = have ? existing.slice() : extractCalls([thinking, text].filter(Boolean).join('\n'), tools);
|
||||
return { calls, text: spoken };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FORMAT_REMINDER,
|
||||
ALIASES,
|
||||
extractCalls,
|
||||
stripToolMarkup,
|
||||
recover,
|
||||
remapName,
|
||||
};
|
||||
Vendored
+61
@@ -31,6 +31,22 @@ function testCatalog() {
|
||||
assert.strictEqual(catalog.findCatalogEntry('qwen3-8b').vision, false);
|
||||
assert.strictEqual(catalog.toolDialectFor('gemma4-4b'), 'gemma4');
|
||||
assert.strictEqual(catalog.toolDialectFor('qwen3-8b'), 'hermes');
|
||||
assert.strictEqual(catalog.toolDialectFor('qwen3.5-2b'), 'qwen35');
|
||||
assert.strictEqual(catalog.toolDialectFor('qwen3.5-0.8b'), 'qwen35');
|
||||
assert.strictEqual(catalog.toolDialectFor('QWEN3_5_2B_MULTIMODAL_Q4_K_M'), 'qwen35');
|
||||
assert.strictEqual(catalog.findCatalogEntry('qwen3.5-2b').tools, true);
|
||||
assert.strictEqual(catalog.findCatalogEntry('qwen3.5-2b').compactTools, true);
|
||||
assert.strictEqual(catalog.findCatalogEntry('qwen3.5-0.8b').tools, true);
|
||||
assert.strictEqual(catalog.findCatalogEntry('qwen3.5-0.8b').compactTools, true);
|
||||
assert.strictEqual(catalog.isCompactToolModel('qwen3.5-2b'), true);
|
||||
assert.strictEqual(catalog.isCompactToolModel('qwen3.5-0.8b'), true);
|
||||
assert.strictEqual(catalog.isCompactToolModel('qwen3.5-4b'), false);
|
||||
const tiny = catalog.filterToolsForModel(
|
||||
[{ name: 'web_search' }, { name: 'qvac_capability' }, { name: 'cu_drag' }],
|
||||
'qwen3.5-0.8b',
|
||||
);
|
||||
assert.deepStrictEqual(tiny.map((t) => t.name), ['web_search']);
|
||||
assert.strictEqual(catalog.filterToolsForModel([{ name: 'cu_drag' }], 'qwen3.5-4b')[0].name, 'cu_drag');
|
||||
assert.ok(/~5 GB/.test(catalog.catalogLabel(catalog.findCatalogEntry('qwen3-8b'))));
|
||||
assert.ok(catalog.FALLBACK_LLM_IDS.indexOf('gemma4-4b') >= 0);
|
||||
const listed = catalog.listCatalog().find((m) => m.id === 'gemma4-2b');
|
||||
@@ -250,7 +266,52 @@ function testDevicePrefersGpu() {
|
||||
assert.strictEqual(device.backendLabel(norm).backend, 'vulkan');
|
||||
}
|
||||
|
||||
function testToolParse() {
|
||||
const parse = require('../lib/tool-parse.js');
|
||||
const tools = [
|
||||
{ name: 'web_search', parameters: { type: 'object', properties: { query: { type: 'string' } } } },
|
||||
{ name: 'run_terminal_cmd', parameters: { type: 'object', properties: { command: { type: 'string' } } } },
|
||||
];
|
||||
const xml = [
|
||||
'<think>',
|
||||
'I should look that up.',
|
||||
'<tool_call>',
|
||||
'<function=web_search>',
|
||||
'<parameter=query>weather in Paris</parameter>',
|
||||
'</function>',
|
||||
'</tool_call>',
|
||||
'</think>',
|
||||
].join('\n');
|
||||
const nested = parse.extractCalls(xml, tools);
|
||||
assert.strictEqual(nested.length, 1);
|
||||
assert.strictEqual(nested[0].name, 'web_search');
|
||||
assert.strictEqual(nested[0].arguments.query, 'weather in Paris');
|
||||
|
||||
const hermes = parse.extractCalls(
|
||||
'<think><tool_call>{"name":"web_search","arguments":{"query":"news"}}</tool_call></think>',
|
||||
tools
|
||||
);
|
||||
assert.strictEqual(hermes[0].name, 'web_search');
|
||||
assert.strictEqual(hermes[0].arguments.query, 'news');
|
||||
|
||||
const aliased = parse.extractCalls(
|
||||
'<tool_call><function=search><parameter=query>headlines</parameter></function></tool_call>',
|
||||
tools
|
||||
);
|
||||
assert.strictEqual(aliased[0].name, 'web_search');
|
||||
|
||||
const recovered = parse.recover({
|
||||
thinking: xml,
|
||||
text: 'I will search now.\n<tool_call><function=web_search><parameter=query>x</parameter></function></tool_call>',
|
||||
tools,
|
||||
});
|
||||
assert.strictEqual(recovered.calls[0].name, 'web_search');
|
||||
assert.strictEqual(recovered.text, 'I will search now.');
|
||||
assert.ok(parse.FORMAT_REMINDER.indexOf('<function=TOOL_NAME>') >= 0);
|
||||
}
|
||||
|
||||
testCatalog();
|
||||
testToolParse();
|
||||
testCompaction();
|
||||
testSearchReplace();
|
||||
testToolSet();
|
||||
|
||||
Reference in New Issue
Block a user