Vendored
+85
-32
@@ -15,6 +15,7 @@ const toolSet = require('./tool-set.js');
|
||||
const planMode = require('./plan-mode.js');
|
||||
const todos = require('./todos.js');
|
||||
const stationarity = require('./stationarity.js');
|
||||
const toolBudget = require('./tool-budget.js');
|
||||
const goalMod = require('./goal.js');
|
||||
const truncate = require('./truncate.js');
|
||||
const sr = require('./search-replace.js');
|
||||
@@ -160,7 +161,18 @@ function resolvePermission(jobId, toolCallId, decision) {
|
||||
if (fn) {
|
||||
pendingPerms.delete(key);
|
||||
fn(decision);
|
||||
return true;
|
||||
}
|
||||
const keys = Array.from(pendingPerms.keys());
|
||||
for (const k of keys) {
|
||||
if (k === toolCallId || k.endsWith(':' + toolCallId)) {
|
||||
const resolve = pendingPerms.get(k);
|
||||
pendingPerms.delete(k);
|
||||
if (resolve) resolve(decision);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function waitCustomResult(jobId, toolCallId) {
|
||||
@@ -260,11 +272,15 @@ function applyPlanWrite(session, name, args) {
|
||||
return { ok: true, path: file, bytes: text.length };
|
||||
}
|
||||
|
||||
function sidecarMessages(session, tracker) {
|
||||
function sidecarMessages(session, tracker, budget) {
|
||||
const extra = [];
|
||||
if (budget && budget.answerOnly) {
|
||||
extra.push({ role: 'user', content: toolBudget.answerNowMessage() });
|
||||
}
|
||||
if (tracker && tracker.pendingCompactReminder) {
|
||||
extra.push({ role: 'user', content: compaction.compactReminder() });
|
||||
const cont = compaction.autoContinue(session.history);
|
||||
const voice = !!(budget && budget.voice);
|
||||
extra.push({ role: 'user', content: compaction.compactReminder({ voice }) });
|
||||
const cont = compaction.autoContinue(session.history, { voice });
|
||||
if (cont) extra.push(cont);
|
||||
tracker.pendingCompactReminder = false;
|
||||
}
|
||||
@@ -368,6 +384,7 @@ async function runTurn(ctx) {
|
||||
|
||||
await ensureModel(session.model);
|
||||
|
||||
const voice = (payload && payload.voice === true) || origin === 'jarvis-qvac';
|
||||
let extraSys = payload && payload.system;
|
||||
if (session.goal && goalMod.isActive(session.goal)) {
|
||||
extraSys = [extraSys, goalMod.plannerAddendum(session.goal)].filter(Boolean).join('\n\n');
|
||||
@@ -376,20 +393,24 @@ async function runTurn(ctx) {
|
||||
cwd: hostWorkspace ? cwd : session.workspace || cwd,
|
||||
hostWorkspace,
|
||||
extra: extraSys,
|
||||
fsRead: hostWorkspace
|
||||
? (c, r) => {
|
||||
try {
|
||||
return fsRead(c, r);
|
||||
} catch (_) {
|
||||
return '';
|
||||
personality: voice ? 'voice' : undefined,
|
||||
fsRead:
|
||||
hostWorkspace && !voice
|
||||
? (c, r) => {
|
||||
try {
|
||||
return fsRead(c, r);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
: null,
|
||||
});
|
||||
const sidecars = [];
|
||||
if (hostWorkspace) {
|
||||
const gitText = await gitSidecar.gitStatusSb(cwd, { hostWorkspace: true, run: tools.runShell });
|
||||
if (gitText) sidecars.push(gitText);
|
||||
if (!voice) {
|
||||
const gitText = await gitSidecar.gitStatusSb(cwd, { hostWorkspace: true, run: tools.runShell });
|
||||
if (gitText) sidecars.push(gitText);
|
||||
}
|
||||
const memText = memory.injectBlock(origin, userText || '');
|
||||
if (memText) sidecars.push(memText);
|
||||
}
|
||||
@@ -416,6 +437,7 @@ async function runTurn(ctx) {
|
||||
|
||||
const cancelled = () => live.get(session.id) && live.get(session.id).cancelled;
|
||||
const stuck = stationarity.create();
|
||||
const budget = toolBudget.fromPayload(payload, origin);
|
||||
let lastText = '';
|
||||
let goalNudges = 0;
|
||||
|
||||
@@ -557,29 +579,39 @@ async function runTurn(ctx) {
|
||||
}
|
||||
|
||||
try {
|
||||
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
||||
for (let turn = 0; turn < budget.maxTurns; turn++) {
|
||||
if (cancelled()) return endTurn(emit, session, jobId, tracker, { reason: 'cancelled', turns: turn });
|
||||
const toolDefs = buildToolDefs(session, payload, tracker);
|
||||
if (turn === budget.maxTurns - 1) toolBudget.forceAnswer(budget);
|
||||
const toolDefs = budget.answerOnly ? [] : buildToolDefs(session, payload, tracker);
|
||||
ctx.planMode = planMode.isActive(tracker);
|
||||
const ctxSize = loadedCtxSize();
|
||||
const beforeLen = session.history.length;
|
||||
const beforeUsage = compaction.usage(session.history, toolDefs, ctxSize);
|
||||
emitLive(emit, session.id, jobId, Object.assign({ type: 'context' }, beforeUsage));
|
||||
if (compaction.shouldCompact(session.history, toolDefs, ctxSize)) {
|
||||
const useLlm = !budget.voice || compaction.nonSystemCount(session.history) >= 8;
|
||||
emitUpdate(emit, session.id, jobId, {
|
||||
type: 'compaction',
|
||||
status: 'start',
|
||||
method: 'llm',
|
||||
method: useLlm ? 'llm' : 'heuristic',
|
||||
used: beforeUsage.used,
|
||||
limit: beforeUsage.limit,
|
||||
pct: beforeUsage.pct,
|
||||
threshold: beforeUsage.threshold,
|
||||
});
|
||||
session.history = await compaction.compactWithLlm(session.history, {
|
||||
const compactOpts = {
|
||||
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, 0),
|
||||
tools: toolDefs,
|
||||
complete: (opts) => engine.complete(Object.assign({}, opts, { desktopVision: false })),
|
||||
});
|
||||
voice: !!budget.voice,
|
||||
};
|
||||
session.history = useLlm
|
||||
? await compaction.compactWithLlm(
|
||||
session.history,
|
||||
Object.assign({}, compactOpts, {
|
||||
complete: (opts) => engine.complete(Object.assign({}, opts, { desktopVision: false })),
|
||||
})
|
||||
)
|
||||
: compaction.compact(session.history, compactOpts);
|
||||
sessions.replaceHistory(session.id, session.history);
|
||||
tracker.pendingCompactReminder = true;
|
||||
if (hostWorkspace) {
|
||||
@@ -593,7 +625,7 @@ async function runTurn(ctx) {
|
||||
emitUpdate(emit, session.id, jobId, {
|
||||
type: 'compaction',
|
||||
status: 'done',
|
||||
method: 'llm',
|
||||
method: useLlm ? 'llm' : 'heuristic',
|
||||
used: afterUsage.used,
|
||||
limit: afterUsage.limit,
|
||||
pct: afterUsage.pct,
|
||||
@@ -605,6 +637,7 @@ async function runTurn(ctx) {
|
||||
session.history = compaction.compact(session.history, {
|
||||
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, 0),
|
||||
tools: toolDefs,
|
||||
voice: !!budget.voice,
|
||||
});
|
||||
if (session.history.length !== beforeLen) {
|
||||
sessions.replaceHistory(session.id, session.history);
|
||||
@@ -624,7 +657,7 @@ async function runTurn(ctx) {
|
||||
}
|
||||
|
||||
emitUpdate(emit, session.id, jobId, { type: 'turn', turn });
|
||||
let streamBase = compaction.usage(session.history.concat(sidecarMessages(session, tracker)), toolDefs, ctxSize);
|
||||
let streamBase = compaction.usage(session.history.concat(sidecarMessages(session, tracker, budget)), toolDefs, ctxSize);
|
||||
emitLive(emit, session.id, jobId, Object.assign({ type: 'context' }, streamBase));
|
||||
let streamChars = 0;
|
||||
function liveUsed() {
|
||||
@@ -636,7 +669,7 @@ async function runTurn(ctx) {
|
||||
}
|
||||
let result;
|
||||
for (let overflowTry = 0; overflowTry < 4; overflowTry++) {
|
||||
const history = session.history.concat(sidecarMessages(session, tracker));
|
||||
const history = session.history.concat(sidecarMessages(session, tracker, budget));
|
||||
streamBase = compaction.usage(history, toolDefs, ctxSize);
|
||||
streamChars = 0;
|
||||
try {
|
||||
@@ -681,6 +714,7 @@ async function runTurn(ctx) {
|
||||
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, overflowTry + 1),
|
||||
tools: toolDefs,
|
||||
aggressive: true,
|
||||
voice: !!budget.voice,
|
||||
});
|
||||
sessions.replaceHistory(session.id, session.history);
|
||||
tracker.pendingCompactReminder = true;
|
||||
@@ -694,15 +728,17 @@ async function runTurn(ctx) {
|
||||
Object.assign({ type: 'context' }, usageFromStats(result && result.stats, liveUsed(), ctxSize))
|
||||
);
|
||||
|
||||
if (result.text) {
|
||||
lastText = result.text;
|
||||
pushHistory(session, { role: 'assistant', content: result.text });
|
||||
let calls = (result && result.toolCalls) || [];
|
||||
if (budget.answerOnly) calls = [];
|
||||
if ((result && result.text) || calls.length) {
|
||||
if (result.text) lastText = result.text;
|
||||
const assistant = { role: 'assistant', content: result.text || '' };
|
||||
if (calls.length) assistant.tool_calls = calls;
|
||||
pushHistory(session, assistant);
|
||||
}
|
||||
|
||||
const calls = result.toolCalls || [];
|
||||
if (!calls.length) {
|
||||
const goalActive = goalMod.isActive(session.goal);
|
||||
if (goalActive && goalNudges < MAX_GOAL_NUDGES) {
|
||||
if (goalActive && goalNudges < MAX_GOAL_NUDGES && !budget.answerOnly) {
|
||||
goalNudges += 1;
|
||||
pushHistory(session, { role: 'user', content: goalMod.continuation(session.goal) });
|
||||
continue;
|
||||
@@ -710,7 +746,7 @@ async function runTurn(ctx) {
|
||||
return endTurn(emit, session, jobId, tracker, {
|
||||
type: 'end',
|
||||
reason: 'stop',
|
||||
text: result.text || lastText || '',
|
||||
text: (result && result.text) || lastText || toolBudget.lastToolText(session.history) || '',
|
||||
turns: turn + 1,
|
||||
});
|
||||
}
|
||||
@@ -738,6 +774,13 @@ async function runTurn(ctx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name === 'run_terminal_cmd' && toolBudget.shouldSkipShell(budget)) {
|
||||
const skipped = toolBudget.skipShellMessage();
|
||||
pushHistory(session, { role: 'tool', name, content: skipped, tool_call_id: toolCallId });
|
||||
emitUpdate(emit, session.id, jobId, { type: 'tool_result', toolCallId, name, result: skipped });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sandbox.needsPermission(name, mode) && !planMode.isPlanFilePath(args.path, tracker.planPath)) {
|
||||
let remembered = null;
|
||||
try {
|
||||
@@ -760,7 +803,7 @@ async function runTurn(ctx) {
|
||||
let decision = await waitPermission(jobId, { toolCallId });
|
||||
if (decision === 'always') {
|
||||
try {
|
||||
permStore.remember(name, args, 'allow');
|
||||
permStore.rememberAlways(name);
|
||||
} catch (_) {}
|
||||
decision = 'allow';
|
||||
}
|
||||
@@ -774,6 +817,7 @@ async function runTurn(ctx) {
|
||||
}
|
||||
|
||||
prepared.push({ name, args, toolCallId });
|
||||
if (name === 'run_terminal_cmd') toolBudget.markShell(budget);
|
||||
}
|
||||
|
||||
let stopEarly = null;
|
||||
@@ -795,19 +839,28 @@ async function runTurn(ctx) {
|
||||
}
|
||||
if (stopEarly) break;
|
||||
}
|
||||
if (prepared.length) toolBudget.markToolRound(budget);
|
||||
if (stopEarly) {
|
||||
return endTurn(emit, session, jobId, tracker, stopEarly);
|
||||
}
|
||||
|
||||
if (stuckNow) {
|
||||
return endTurn(emit, session, jobId, tracker, { reason: 'stuck', text: lastText, turns: turn + 1 });
|
||||
return endTurn(emit, session, jobId, tracker, {
|
||||
reason: 'stuck',
|
||||
text: lastText || toolBudget.lastToolText(session.history),
|
||||
turns: turn + 1,
|
||||
});
|
||||
}
|
||||
if (stationarity.shouldNudge(stuck)) {
|
||||
stationarity.markNudged(stuck);
|
||||
pushHistory(session, { role: 'user', content: stationarity.nudgeText() });
|
||||
}
|
||||
}
|
||||
return endTurn(emit, session, jobId, tracker, { reason: 'max_turns', text: lastText, turns: MAX_TURNS });
|
||||
return endTurn(emit, session, jobId, tracker, {
|
||||
reason: 'max_turns',
|
||||
text: lastText || toolBudget.lastToolText(session.history),
|
||||
turns: budget.maxTurns,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err && err.message === 'cancelled') {
|
||||
return endTurn(emit, session, jobId, tracker, { reason: 'cancelled', text: lastText });
|
||||
|
||||
Reference in New Issue
Block a user