Updates
Rolling release / release (push) Successful in 6m52s

This commit is contained in:
2026-09-12 10:43:01 -04:00
parent f26204505e
commit b8c60e4326
14 changed files with 701 additions and 89 deletions
+17 -5
View File
@@ -70,9 +70,13 @@ function loadedCtxSize() {
return (loaded && loaded.ctxSize) || 8192;
}
function toolResultCap() {
function toolResultCap(budget) {
const ctx = loadedCtxSize();
return Math.min(8000, Math.max(1200, Math.floor(ctx * 0.35)));
const voice = !!(budget && budget.voice);
const max = voice ? 2000 : 8000;
const ratio = voice ? 0.1 : 0.35;
const min = voice ? 600 : 1200;
return Math.min(max, Math.max(min, Math.floor(ctx * ratio)));
}
function emitCompactDone(emit, session, jobId, toolDefs, ctxSize, beforeUsage, method) {
@@ -440,6 +444,7 @@ async function runTurn(ctx) {
const budget = toolBudget.fromPayload(payload, origin);
let lastText = '';
let goalNudges = 0;
let toolNudges = 0;
async function runOneTool(item, turn) {
const name = item.name;
@@ -535,7 +540,7 @@ async function runTurn(ctx) {
if (out && out.type === 'goal_blocked') {
session.goal.status = 'blocked';
emitUpdate(emit, session.id, jobId, { type: 'goal_update', goal: goalMod.snapshot(session.goal) });
const rendered = truncate.renderToolResult(out, toolResultCap());
const rendered = truncate.renderToolResult(out, toolResultCap(budget));
pushHistory(session, { role: 'tool', name, content: rendered, tool_call_id: toolCallId });
emitUpdate(emit, session.id, jobId, { type: 'tool_result', toolCallId, name, result: rendered.slice(0, 4000) });
return { reason: 'goal_blocked', text: out.blocked_reason || lastText, turns: turn + 1 };
@@ -545,7 +550,7 @@ async function runTurn(ctx) {
const skipVerify = payload && payload.verify === false;
const verdict = skipVerify ? { achieved: true, gaps: [] } : await verifyGoal(session, tracker, lastText);
if (verdict.achieved) {
const rendered = truncate.renderToolResult({ ok: true, achieved: true }, toolResultCap());
const rendered = truncate.renderToolResult({ ok: true, achieved: true }, toolResultCap(budget));
pushHistory(session, { role: 'tool', name, content: rendered, tool_call_id: toolCallId });
emitUpdate(emit, session.id, jobId, { type: 'tool_result', toolCallId, name, result: rendered });
emitUpdate(emit, session.id, jobId, { type: 'goal_update', goal: goalMod.snapshot(session.goal) });
@@ -563,7 +568,7 @@ async function runTurn(ctx) {
} catch (err) {
out = { error: err.message };
}
const rendered = truncate.renderToolResult(out, toolResultCap());
const rendered = truncate.renderToolResult(out, toolResultCap(budget));
pushHistory(session, { role: 'tool', name, content: rendered, tool_call_id: toolCallId });
emitUpdate(
emit,
@@ -679,6 +684,8 @@ async function runTurn(ctx) {
tools: toolDefs,
toolDialect: catalog.toolDialectFor(session.model),
desktopVision: payload && payload.desktopVision === false ? false : undefined,
timeoutMs: budget.completeTimeoutMs,
idleMs: budget.completeIdleMs,
},
(ev) => {
if (ev.type === 'contentDelta') {
@@ -743,6 +750,11 @@ async function runTurn(ctx) {
pushHistory(session, { role: 'user', content: goalMod.continuation(session.goal) });
continue;
}
if (toolNudges < 2 && toolBudget.shouldNudgeToolCall([result && result.text, result && result.thinking].filter(Boolean).join('\n'), budget)) {
toolNudges += 1;
pushHistory(session, { role: 'user', content: toolBudget.continueToolMessage() });
continue;
}
return endTurn(emit, session, jobId, tracker, {
type: 'end',
reason: 'stop',
+2 -1
View File
@@ -1,7 +1,8 @@
/** Permission + shell policy with no Bare imports (unit-testable on Node). */
const WRITE_TOOLS = new Set(['search_replace', 'write_file', 'run_terminal_cmd', 'use_tool']);
const ASK_TOOLS = new Set(['run_terminal_cmd', 'web_fetch', 'web_search', 'use_tool']);
const ASK_TOOLS = new Set(['run_terminal_cmd', 'use_tool']);
// web_fetch and web_search are public reads; they do not prompt.
const SHELL_ALLOW = new Set([
'git', 'rg', 'grep', 'ls', 'cat', 'head', 'tail', 'pwd', 'echo', 'node', 'npm', 'npx',
'python3', 'python', 'cargo', 'go', 'make', 'bare', 'wc', 'sort', 'uniq', 'find', 'sed', 'awk',
+18
View File
@@ -19,6 +19,8 @@ function fromPayload(payload, origin) {
maxTurns: num(payload.maxTurns, voice ? 6 : 24),
maxShellCalls: unlimitedShell ? 0 : num(payload.maxShellCalls, voice ? 1 : 0),
maxToolRounds: num(payload.maxToolRounds, voice ? 4 : 0),
completeTimeoutMs: voice ? 45000 : 0,
completeIdleMs: voice ? 10000 : 0,
shellCalls: 0,
toolRounds: 0,
answerOnly: false,
@@ -57,6 +59,20 @@ function answerNowMessage() {
return 'You have tool results. Reply to the user in one to three sentences. Do not call more tools.';
}
const UNFINISHED_TOOL =
/\b(let me|i(?:'m| am) going to|i(?:'ll| will)|i should|need to)\b[\s\S]{0,160}\b(fetch|search|look(?:ing)? up|check|open|read|run|call|use)\b/i;
function shouldNudgeToolCall(text, budget) {
if (!budget || !budget.voice || budget.answerOnly) return false;
const blob = String(text || '');
if (UNFINISHED_TOOL.test(blob)) return true;
return /\b(search(?:ing)? again|fetch (?:a |the |that )|look(?:ing)? that up)\b/i.test(blob);
}
function continueToolMessage() {
return 'Thinking is not an answer. Call the tool now with a concrete query or URL, then speak the result. Do not describe the next step.';
}
function lastToolText(history, maxChars) {
const list = Array.isArray(history) ? history : [];
for (let i = list.length - 1; i >= 0; i--) {
@@ -78,5 +94,7 @@ module.exports = {
forceAnswer,
skipShellMessage,
answerNowMessage,
shouldNudgeToolCall,
continueToolMessage,
lastToolText,
};
+143 -20
View File
@@ -239,28 +239,137 @@ function defs(opts) {
return toolSet.filterBuiltinSchemas(SCHEMAS, opts);
}
async function webSearch(query) {
const net = require('../lib/net.js');
const url = 'https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query);
net.assertPublicHttpUrl(url);
const res = await fetch(url, { headers: { 'user-agent': 'agent-harness/0.1' } });
const text = await res.text();
const hits = [];
const re = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = re.exec(text)) && hits.length < 8) {
hits.push({ url: m[1], title: m[2].replace(/<[^>]+>/g, '').trim() });
}
return hits;
const WEB_TIMEOUT_MS = 12000;
const BROWSER_UA =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
function abortError(timeoutMs) {
const err = new Error('timed out after ' + timeoutMs + 'ms');
err.name = 'AbortError';
return err;
}
async function webFetch(url) {
function fetchWithTimeout(url, opts, timeoutMs) {
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
const headers = Object.assign({ 'user-agent': BROWSER_UA }, (opts && opts.headers) || {});
const controller = typeof AbortController === 'function' ? new AbortController() : null;
let timer;
const init = Object.assign({}, opts || {}, { headers });
if (controller) init.signal = controller.signal;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
try {
if (controller) controller.abort();
} catch (_) {}
reject(abortError(ms));
}, ms);
});
const pending = fetch(url, init);
pending.catch(() => {});
return Promise.race([pending, timeout]).finally(() => {
if (timer) clearTimeout(timer);
});
}
function readBodyWithTimeout(res, timeoutMs) {
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
if (!res || typeof res.text !== 'function') return Promise.resolve('');
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(abortError(ms)), ms);
});
const pending = res.text();
pending.catch(() => {});
return Promise.race([pending, timeout]).finally(() => {
if (timer) clearTimeout(timer);
});
}
function decodeSearchUrl(href) {
let raw = String(href || '').replace(/&amp;/g, '&').trim();
if (!raw) return raw;
if (raw.startsWith('//')) raw = 'https:' + raw;
try {
const u = new URL(raw);
const host = u.hostname.replace(/^www\./, '');
if (host === 'duckduckgo.com') {
const uddg = u.searchParams.get('uddg');
if (uddg) {
let dest = String(uddg);
try {
dest = decodeURIComponent(dest);
} catch (_) {}
dest = dest.replace(/&amp;/g, '&');
if (dest.startsWith('//')) dest = 'https:' + dest;
return dest;
}
}
return u.toString();
} catch (_) {
return raw;
}
}
async function webSearch(query, timeoutMs) {
const net = require('../lib/net.js');
net.assertHttpUrl(url);
const res = await fetch(url, { headers: { 'user-agent': 'agent-harness/0.1' } });
let text = await res.text();
text = truncate.truncateWithMarker(text, 80000);
return { status: res.status, url: String(res.url || url), text };
const url = 'https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query);
try {
net.assertPublicHttpUrl(url);
} catch (err) {
return { error: String(err && err.message || err), url };
}
try {
const res = await fetchWithTimeout(url, {}, timeoutMs);
if (res.status >= 400) {
return { error: 'HTTP ' + res.status, url: String(res.url || url), status: res.status };
}
const text = await readBodyWithTimeout(res, timeoutMs);
const hits = [];
const re = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = re.exec(text)) && hits.length < 8) {
hits.push({ url: decodeSearchUrl(m[1]), title: m[2].replace(/<[^>]+>/g, '').trim() });
}
return hits;
} catch (err) {
return { error: String(err && err.message || err), url };
}
}
function htmlToText(html) {
const raw = String(html || '');
if (!/<(?:html|body|div|p|script|head)\b/i.test(raw) && !/<!DOCTYPE/i.test(raw)) return raw;
return raw
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/\s+/g, ' ')
.trim();
}
async function webFetch(url, timeoutMs) {
const net = require('../lib/net.js');
try {
net.assertHttpUrl(url);
} catch (err) {
return { error: String(err && err.message || err), url: String(url || '') };
}
try {
const res = await fetchWithTimeout(url, {}, timeoutMs);
let text = htmlToText(await readBodyWithTimeout(res, timeoutMs));
text = truncate.truncateWithMarker(text, 12000);
const href = String(res.url || url);
if (res.status >= 400) {
return { error: 'HTTP ' + res.status, url: href, status: res.status, text };
}
return { status: res.status, url: href, text };
} catch (err) {
return { error: String(err && err.message || err), url: String(url) };
}
}
async function execute(ctx, name, args) {
@@ -425,4 +534,18 @@ function ensureParent(abs) {
} catch (_) {}
}
module.exports = { defs, execute, SCHEMAS, HOST_WORKSPACE_TOOLS: toolSet.HOST_WORKSPACE_TOOLS, runShell, formatShellResult };
module.exports = {
defs,
execute,
SCHEMAS,
HOST_WORKSPACE_TOOLS: toolSet.HOST_WORKSPACE_TOOLS,
runShell,
formatShellResult,
webFetch,
webSearch,
decodeSearchUrl,
htmlToText,
fetchWithTimeout,
WEB_TIMEOUT_MS,
BROWSER_UA,
};
+44
View File
@@ -0,0 +1,44 @@
/** Wall-clock + idle abort for QVAC completion streams. No Bare imports. */
function attachCompleteWatch(opts) {
opts = opts || {};
const timeoutMs = Number(opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 0;
const idleMs = Number(opts.idleMs) > 0 ? Number(opts.idleMs) : 0;
const abort = opts.abort;
let timedOut = false;
let wall = null;
let idle = null;
function clear() {
if (wall) clearTimeout(wall);
if (idle) clearTimeout(idle);
wall = null;
idle = null;
}
function fire() {
if (timedOut) return;
timedOut = true;
clear();
try {
if (typeof abort === 'function') abort();
} catch (_) {}
if (typeof opts.onTimeout === 'function') opts.onTimeout();
}
if (timeoutMs) wall = setTimeout(fire, timeoutMs);
function bump() {
if (timedOut || !(idleMs > 0)) return;
if (idle) clearTimeout(idle);
idle = setTimeout(fire, idleMs);
}
return {
bump,
clear,
timedOut: () => timedOut,
};
}
module.exports = { attachCompleteWatch };
+44 -6
View File
@@ -9,6 +9,7 @@ const catalog = require('./catalog.js');
const device = require('./device.js');
const events = require('./events.js');
const paths = require('./paths.js');
const completeWatch = require('./complete-watch.js');
let sdk = null;
let initError = null;
@@ -377,11 +378,29 @@ async function complete(opts, onEvent) {
let text = '';
let thinking = '';
const toolCalls = [];
try {
const abortRun = () => {
try {
if (run && typeof run.abort === 'function') run.abort();
else if (sdk && typeof sdk.abortCompletion === 'function' && requestId) sdk.abortCompletion({ requestId });
} catch (_) {}
};
let settleTimeout;
const timedOutGate = new Promise((resolve) => {
settleTimeout = () => resolve('timeout');
});
const watch = completeWatch.attachCompleteWatch({
timeoutMs: opts && opts.timeoutMs,
idleMs: opts && opts.idleMs,
abort: abortRun,
onTimeout: settleTimeout,
});
const consume = (async () => {
if (run.events && typeof run.events[Symbol.asyncIterator] === 'function') {
for await (const ev of run.events) {
if (watch.timedOut()) return;
const n = events.normalizeCompletionEvent(ev);
if (!n) continue;
watch.bump();
if (n.type === 'contentDelta') {
text += n.delta;
if (onEvent) onEvent(n);
@@ -397,18 +416,22 @@ async function complete(opts, onEvent) {
}
} else if (run.tokenStream) {
for await (const token of run.tokenStream) {
if (watch.timedOut()) return;
watch.bump();
text += token;
if (onEvent) onEvent({ type: 'contentDelta', delta: token });
}
if (run.toolCallStream) {
for await (const evt of run.toolCallStream) {
if (watch.timedOut()) return;
watch.bump();
const call = evt.call || evt;
toolCalls.push(call);
if (onEvent) onEvent({ type: 'toolCall', call });
}
}
}
let stats = null;
if (watch.timedOut()) return;
try {
if (run.final) {
const fin = await run.final;
@@ -419,14 +442,29 @@ async function complete(opts, onEvent) {
toolCalls.length = 0;
for (const c of fin.toolCalls) toolCalls.push(c);
}
stats = fin.stats || null;
}
} else if (run.stats) {
stats = await run.stats;
}
} catch (_) {}
return { text, thinking, toolCalls, stats, requestId, stopReason: 'stop' };
})();
consume.catch(() => {});
try {
await Promise.race([consume, timedOutGate]);
let stats = null;
if (!watch.timedOut()) {
try {
if (run.stats) stats = await run.stats;
} catch (_) {}
}
return {
text,
thinking,
toolCalls,
stats,
requestId,
stopReason: watch.timedOut() ? 'timeout' : 'stop',
};
} finally {
watch.clear();
if (requestId) activeRequests.delete(requestId);
}
}
+49 -1
View File
@@ -20,6 +20,7 @@ const policy = require('../agent/policy.js');
const toolBudget = require('../agent/tool-budget.js');
const paths = require('../lib/paths.js');
const device = require('../lib/device.js');
const tools = require('../agent/tools.js');
function testCatalog() {
assert.strictEqual(catalog.resolveModelConstant('qwen3.5-4b'), 'QWEN3_5_4B_MULTIMODAL_Q4_K_M');
@@ -261,4 +262,51 @@ testTruncateAndPerm();
testPaths();
testQvacWorkerDeps();
testDevicePrefersGpu();
console.log('ok');
testWebFetchTimeout()
.then(() => {
console.log('ok');
})
.catch((err) => {
console.error(err);
process.exitCode = 1;
});
async function testWebFetchTimeout() {
const orig = globalThis.fetch;
globalThis.fetch = () => new Promise(() => {});
const started = Date.now();
try {
const hung = await tools.webFetch('https://example.com/ip', 40);
assert.ok(hung.error);
assert.ok(/timed out/i.test(hung.error));
assert.strictEqual(hung.url, 'https://example.com/ip');
assert.ok(Date.now() - started < 2000);
} finally {
globalThis.fetch = orig;
}
globalThis.fetch = async (url) => ({
status: 200,
url: String(url),
text: async () => '203.0.113.8',
});
try {
const ok = await tools.webFetch('https://ifconfig.me/ip', 200);
assert.strictEqual(ok.status, 200);
assert.strictEqual(ok.url, 'https://ifconfig.me/ip');
assert.strictEqual(ok.text, '203.0.113.8');
assert.ok(!ok.error);
} finally {
globalThis.fetch = orig;
}
globalThis.fetch = async () => ({ status: 503, url: 'https://example.com', text: async () => 'down' });
try {
const failed = await tools.webFetch('https://example.com/status', 200);
assert.ok(failed.error);
assert.strictEqual(failed.status, 503);
assert.strictEqual(failed.url, 'https://example.com');
} finally {
globalThis.fetch = orig;
}
}