@visorcraft/idlehands 1.2.7 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +122 -98
- package/dist/agent.js.map +1 -1
- package/dist/client.js +9 -9
- package/dist/client.js.map +1 -1
- package/dist/safety.js +10 -6
- package/dist/safety.js.map +1 -1
- package/dist/tools.js +25 -18
- package/dist/tools.js.map +1 -1
- package/dist/utils.js +21 -4
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -844,9 +844,6 @@ export async function createSession(opts) {
|
|
|
844
844
|
catch { }
|
|
845
845
|
}
|
|
846
846
|
}
|
|
847
|
-
if (harness.systemPromptSuffix) {
|
|
848
|
-
sessionMeta += '\n\n' + harness.systemPromptSuffix;
|
|
849
|
-
}
|
|
850
847
|
// Phase 9: sys-eager — inject full system snapshot into first message
|
|
851
848
|
if (cfg.sys_eager && cfg.mode === 'sys') {
|
|
852
849
|
try {
|
|
@@ -857,30 +854,42 @@ export async function createSession(opts) {
|
|
|
857
854
|
console.warn(`[warn] sys-eager snapshot failed: ${e?.message ?? e}`);
|
|
858
855
|
}
|
|
859
856
|
}
|
|
860
|
-
const
|
|
861
|
-
let
|
|
857
|
+
const defaultSystemPromptBase = SYSTEM_PROMPT;
|
|
858
|
+
let activeSystemPromptBase = (cfg.system_prompt_override ?? '').trim() || defaultSystemPromptBase;
|
|
859
|
+
let systemPromptOverridden = (cfg.system_prompt_override ?? '').trim().length > 0;
|
|
860
|
+
const buildEffectiveSystemPrompt = () => {
|
|
861
|
+
let p = activeSystemPromptBase;
|
|
862
|
+
if (harness.systemPromptSuffix && !systemPromptOverridden) {
|
|
863
|
+
p += '\n\n' + harness.systemPromptSuffix;
|
|
864
|
+
}
|
|
865
|
+
return p;
|
|
866
|
+
};
|
|
862
867
|
let messages = [
|
|
863
|
-
{ role: 'system', content:
|
|
868
|
+
{ role: 'system', content: buildEffectiveSystemPrompt() }
|
|
864
869
|
];
|
|
865
870
|
let sessionMetaPending = sessionMeta;
|
|
866
871
|
const setSystemPrompt = (prompt) => {
|
|
867
872
|
const next = String(prompt ?? '').trim();
|
|
868
873
|
if (!next)
|
|
869
874
|
throw new Error('system prompt cannot be empty');
|
|
870
|
-
|
|
875
|
+
activeSystemPromptBase = next;
|
|
876
|
+
systemPromptOverridden = true;
|
|
877
|
+
const effective = buildEffectiveSystemPrompt();
|
|
871
878
|
if (messages.length > 0 && messages[0].role === 'system') {
|
|
872
|
-
messages[0] = { role: 'system', content:
|
|
879
|
+
messages[0] = { role: 'system', content: effective };
|
|
873
880
|
}
|
|
874
881
|
else {
|
|
875
|
-
messages.unshift({ role: 'system', content:
|
|
882
|
+
messages.unshift({ role: 'system', content: effective });
|
|
876
883
|
}
|
|
877
884
|
};
|
|
878
885
|
const resetSystemPrompt = () => {
|
|
879
|
-
|
|
886
|
+
systemPromptOverridden = false;
|
|
887
|
+
setSystemPrompt(defaultSystemPromptBase);
|
|
880
888
|
};
|
|
881
889
|
const reset = () => {
|
|
890
|
+
const effective = buildEffectiveSystemPrompt();
|
|
882
891
|
messages = [
|
|
883
|
-
{ role: 'system', content:
|
|
892
|
+
{ role: 'system', content: effective }
|
|
884
893
|
];
|
|
885
894
|
sessionMetaPending = sessionMeta;
|
|
886
895
|
lastEditedPath = undefined;
|
|
@@ -895,7 +904,9 @@ export async function createSession(opts) {
|
|
|
895
904
|
throw new Error('restore: first message must be system');
|
|
896
905
|
}
|
|
897
906
|
messages = next;
|
|
898
|
-
|
|
907
|
+
activeSystemPromptBase = String(next[0].content ?? defaultSystemPromptBase);
|
|
908
|
+
// Note: we don't force buildEffectiveSystemPrompt() here because the restore
|
|
909
|
+
// data might already have a customized system prompt we want to respect.
|
|
899
910
|
if (mcpManager) {
|
|
900
911
|
const usedMcpTool = next.some((msg) => {
|
|
901
912
|
if (msg?.role !== 'assistant' || !Array.isArray(msg.tool_calls))
|
|
@@ -1132,6 +1143,41 @@ export async function createSession(opts) {
|
|
|
1132
1143
|
onMutation: overrides?.onMutation ?? ((absPath) => { lastEditedPath = absPath; }),
|
|
1133
1144
|
};
|
|
1134
1145
|
};
|
|
1146
|
+
const buildLspLensSymbolOutput = async (filePathRaw) => {
|
|
1147
|
+
if (!lspManager)
|
|
1148
|
+
return '[lsp] unavailable';
|
|
1149
|
+
const semantic = await lspManager.getSymbols(filePathRaw);
|
|
1150
|
+
if (!lens)
|
|
1151
|
+
return semantic;
|
|
1152
|
+
const cwd = cfg.dir ?? process.cwd();
|
|
1153
|
+
const absPath = filePathRaw.startsWith('/') ? filePathRaw : path.resolve(cwd, filePathRaw);
|
|
1154
|
+
const body = await fs.readFile(absPath, 'utf8').catch(() => '');
|
|
1155
|
+
if (!body)
|
|
1156
|
+
return semantic;
|
|
1157
|
+
const projection = await lens.projectFile(absPath, body).catch(() => '');
|
|
1158
|
+
const structural = extractLensBody(projection);
|
|
1159
|
+
if (!structural)
|
|
1160
|
+
return semantic;
|
|
1161
|
+
return `${semantic}\n\n[lens] Structural skeleton:\n${structural}`;
|
|
1162
|
+
};
|
|
1163
|
+
const dispatchLspTool = async (name, args) => {
|
|
1164
|
+
if (!lspManager)
|
|
1165
|
+
return '[lsp] unavailable';
|
|
1166
|
+
switch (name) {
|
|
1167
|
+
case 'lsp_diagnostics':
|
|
1168
|
+
return lspManager.getDiagnostics(typeof args?.path === 'string' ? args.path : undefined, typeof args?.severity === 'number' ? args.severity : undefined);
|
|
1169
|
+
case 'lsp_symbols':
|
|
1170
|
+
return buildLspLensSymbolOutput(String(args?.path ?? ''));
|
|
1171
|
+
case 'lsp_hover':
|
|
1172
|
+
return lspManager.getHover(String(args?.path ?? ''), Number(args?.line ?? 0), Number(args?.character ?? 0));
|
|
1173
|
+
case 'lsp_definition':
|
|
1174
|
+
return lspManager.getDefinition(String(args?.path ?? ''), Number(args?.line ?? 0), Number(args?.character ?? 0));
|
|
1175
|
+
case 'lsp_references':
|
|
1176
|
+
return lspManager.getReferences(String(args?.path ?? ''), Number(args?.line ?? 0), Number(args?.character ?? 0), typeof args?.max_results === 'number' ? args.max_results : 50);
|
|
1177
|
+
default:
|
|
1178
|
+
throw new Error(`unknown LSP tool: ${name}`);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1135
1181
|
const executePlanStep = async (index) => {
|
|
1136
1182
|
if (!planSteps.length)
|
|
1137
1183
|
return ['No plan steps to execute.'];
|
|
@@ -1154,21 +1200,7 @@ export async function createSession(opts) {
|
|
|
1154
1200
|
content = await runSpawnTaskCore(step.args, { signal: inFlight?.signal });
|
|
1155
1201
|
}
|
|
1156
1202
|
else if (LSP_TOOL_NAME_SET.has(step.tool) && lspManager) {
|
|
1157
|
-
|
|
1158
|
-
content = await lspManager.getDiagnostics(typeof step.args?.path === 'string' ? step.args.path : undefined, typeof step.args?.severity === 'number' ? step.args.severity : undefined);
|
|
1159
|
-
}
|
|
1160
|
-
else if (step.tool === 'lsp_symbols') {
|
|
1161
|
-
content = await lspManager.getSymbols(String(step.args?.path ?? ''));
|
|
1162
|
-
}
|
|
1163
|
-
else if (step.tool === 'lsp_hover') {
|
|
1164
|
-
content = await lspManager.getHover(String(step.args?.path ?? ''), Number(step.args?.line ?? 0), Number(step.args?.character ?? 0));
|
|
1165
|
-
}
|
|
1166
|
-
else if (step.tool === 'lsp_definition') {
|
|
1167
|
-
content = await lspManager.getDefinition(String(step.args?.path ?? ''), Number(step.args?.line ?? 0), Number(step.args?.character ?? 0));
|
|
1168
|
-
}
|
|
1169
|
-
else if (step.tool === 'lsp_references') {
|
|
1170
|
-
content = await lspManager.getReferences(String(step.args?.path ?? ''), Number(step.args?.line ?? 0), Number(step.args?.character ?? 0), typeof step.args?.max_results === 'number' ? step.args.max_results : 50);
|
|
1171
|
-
}
|
|
1203
|
+
content = await dispatchLspTool(step.tool, step.args);
|
|
1172
1204
|
}
|
|
1173
1205
|
else if (mcpManager?.hasTool(step.tool)) {
|
|
1174
1206
|
const callArgs = step.args && typeof step.args === 'object' && !Array.isArray(step.args)
|
|
@@ -1230,7 +1262,7 @@ export async function createSession(opts) {
|
|
|
1230
1262
|
};
|
|
1231
1263
|
const buildCompactionSystemNote = (kind, dropped) => {
|
|
1232
1264
|
const prefix = kind === 'auto'
|
|
1233
|
-
? `[auto-compacted: ${dropped} old messages dropped to stay within context budget.]`
|
|
1265
|
+
? `[auto-compacted: ${dropped} old messages dropped to stay within context budget. Continue current task.]`
|
|
1234
1266
|
: `[compacted: ${dropped} messages dropped.]`;
|
|
1235
1267
|
const guidance = compactionVaultGuidance();
|
|
1236
1268
|
return guidance ? `${prefix} ${guidance}` : prefix;
|
|
@@ -1584,6 +1616,10 @@ export async function createSession(opts) {
|
|
|
1584
1616
|
configuredTopP: cfg.top_p,
|
|
1585
1617
|
baseMaxTokens: BASE_MAX_TOKENS,
|
|
1586
1618
|
}));
|
|
1619
|
+
// Update system prompt for the new model/harness
|
|
1620
|
+
if (messages.length > 0 && messages[0].role === 'system') {
|
|
1621
|
+
messages[0].content = buildEffectiveSystemPrompt();
|
|
1622
|
+
}
|
|
1587
1623
|
emitDetached(hookManager.emit('model_changed', {
|
|
1588
1624
|
previousModel,
|
|
1589
1625
|
nextModel: model,
|
|
@@ -1711,7 +1747,7 @@ export async function createSession(opts) {
|
|
|
1711
1747
|
const spinnerStart = Date.now();
|
|
1712
1748
|
let spinnerIdx = 0;
|
|
1713
1749
|
let spinnerTimer;
|
|
1714
|
-
if (process.stderr.isTTY) {
|
|
1750
|
+
if (process.stderr.isTTY && !process.env.IDLEHANDS_QUIET) {
|
|
1715
1751
|
spinnerTimer = setInterval(() => {
|
|
1716
1752
|
const elapsedSec = Math.floor((Date.now() - spinnerStart) / 1000);
|
|
1717
1753
|
const frame = frames[spinnerIdx % frames.length];
|
|
@@ -1719,13 +1755,14 @@ export async function createSession(opts) {
|
|
|
1719
1755
|
process.stderr.write(`\r${frame} Server unavailable - waiting for reconnect (${elapsedSec}s)...`);
|
|
1720
1756
|
}, 120);
|
|
1721
1757
|
}
|
|
1722
|
-
else {
|
|
1758
|
+
else if (!process.env.IDLEHANDS_QUIET) {
|
|
1723
1759
|
console.warn('[model] Server unavailable - waiting for reconnect...');
|
|
1724
1760
|
}
|
|
1725
1761
|
try {
|
|
1726
1762
|
await client.waitForReady({ timeoutMs: 120_000, pollMs: 2_000 });
|
|
1727
1763
|
fresh = normalizeModelsResponse(await client.models());
|
|
1728
|
-
|
|
1764
|
+
if (!process.env.IDLEHANDS_QUIET)
|
|
1765
|
+
console.warn('[model] Reconnected to server.');
|
|
1729
1766
|
}
|
|
1730
1767
|
catch {
|
|
1731
1768
|
return;
|
|
@@ -1733,7 +1770,8 @@ export async function createSession(opts) {
|
|
|
1733
1770
|
finally {
|
|
1734
1771
|
if (spinnerTimer) {
|
|
1735
1772
|
clearInterval(spinnerTimer);
|
|
1736
|
-
process.stderr.
|
|
1773
|
+
if (process.stderr.isTTY)
|
|
1774
|
+
process.stderr.write('\r\x1b[K');
|
|
1737
1775
|
}
|
|
1738
1776
|
}
|
|
1739
1777
|
}
|
|
@@ -1795,6 +1833,9 @@ export async function createSession(opts) {
|
|
|
1795
1833
|
// best effort
|
|
1796
1834
|
}
|
|
1797
1835
|
};
|
|
1836
|
+
const isReadOnlyToolDynamic = (toolName) => {
|
|
1837
|
+
return isReadOnlyTool(toolName) || LSP_TOOL_NAME_SET.has(toolName) || Boolean(mcpManager?.isToolReadOnly(toolName));
|
|
1838
|
+
};
|
|
1798
1839
|
const emitToolResult = async (result) => {
|
|
1799
1840
|
await hookObj.onToolResult?.(result);
|
|
1800
1841
|
await hookManager.emit('tool_result', { askId, turn: turns, result });
|
|
@@ -1934,6 +1975,8 @@ export async function createSession(opts) {
|
|
|
1934
1975
|
let noToolTurns = 0;
|
|
1935
1976
|
const NO_TOOL_REPROMPT_THRESHOLD = 2;
|
|
1936
1977
|
let repromptUsed = false;
|
|
1978
|
+
let readBudgetWarned = false;
|
|
1979
|
+
let noToolNudgeUsed = false;
|
|
1937
1980
|
// Track blocked command loops by exact reason+command signature.
|
|
1938
1981
|
const blockedExecAttemptsBySig = new Map();
|
|
1939
1982
|
// Cache successful read-only exec observations by exact signature.
|
|
@@ -2056,23 +2099,6 @@ export async function createSession(opts) {
|
|
|
2056
2099
|
const tail = detail ? ` — ${detail}` : '';
|
|
2057
2100
|
hookObj.onToken(`\n[sub-agent #${taskId}] ${status}${tail}\n`);
|
|
2058
2101
|
};
|
|
2059
|
-
const buildLspLensSymbolOutput = async (filePathRaw) => {
|
|
2060
|
-
if (!lspManager)
|
|
2061
|
-
return '[lsp] unavailable';
|
|
2062
|
-
const semantic = await lspManager.getSymbols(filePathRaw);
|
|
2063
|
-
if (!lens)
|
|
2064
|
-
return semantic;
|
|
2065
|
-
const cwd = cfg.dir ?? process.cwd();
|
|
2066
|
-
const absPath = filePathRaw.startsWith('/') ? filePathRaw : path.resolve(cwd, filePathRaw);
|
|
2067
|
-
const body = await fs.readFile(absPath, 'utf8').catch(() => '');
|
|
2068
|
-
if (!body)
|
|
2069
|
-
return semantic;
|
|
2070
|
-
const projection = await lens.projectFile(absPath, body).catch(() => '');
|
|
2071
|
-
const structural = extractLensBody(projection);
|
|
2072
|
-
if (!structural)
|
|
2073
|
-
return semantic;
|
|
2074
|
-
return `${semantic}\n\n[lens] Structural skeleton:\n${structural}`;
|
|
2075
|
-
};
|
|
2076
2102
|
const runSpawnTask = async (args) => {
|
|
2077
2103
|
if (delegationForbiddenByUser) {
|
|
2078
2104
|
throw new Error('spawn_task: blocked — user explicitly asked for no delegation/sub-agents in this request. Continue directly in the current session.');
|
|
@@ -2137,13 +2163,11 @@ export async function createSession(opts) {
|
|
|
2137
2163
|
}
|
|
2138
2164
|
}
|
|
2139
2165
|
messages = compacted;
|
|
2140
|
-
// Update current context token count after auto compaction
|
|
2141
|
-
currentContextTokens = estimateTokensFromMessages(compacted);
|
|
2142
2166
|
if (dropped.length) {
|
|
2143
2167
|
messages.push({ role: 'system', content: buildCompactionSystemNote('auto', dropped.length) });
|
|
2144
|
-
await injectVaultContext().catch(() => { });
|
|
2145
|
-
injectCompactionReminder('auto context-budget compaction');
|
|
2146
2168
|
}
|
|
2169
|
+
// Update token count AFTER injections so downstream reads are accurate
|
|
2170
|
+
currentContextTokens = estimateTokensFromMessages(messages);
|
|
2147
2171
|
const afterTokens = estimateTokensFromMessages(compacted);
|
|
2148
2172
|
return {
|
|
2149
2173
|
beforeMessages: beforeMsgs.length,
|
|
@@ -2209,8 +2233,7 @@ export async function createSession(opts) {
|
|
|
2209
2233
|
const mode = useHardCompaction ? 'hard' : 'force';
|
|
2210
2234
|
messages.push({
|
|
2211
2235
|
role: 'system',
|
|
2212
|
-
content: `[auto-recovery]
|
|
2213
|
-
`(freed ~${compacted.freedTokens} tokens, dropped ${compacted.droppedMessages} messages). Continue from latest state; do not restart work.`,
|
|
2236
|
+
content: `[auto-recovery] Context overflow. Ran ${mode} compaction (freed ~${compacted.freedTokens} tokens). Continue current work.`,
|
|
2214
2237
|
});
|
|
2215
2238
|
continue;
|
|
2216
2239
|
}
|
|
@@ -2348,7 +2371,7 @@ export async function createSession(opts) {
|
|
|
2348
2371
|
}
|
|
2349
2372
|
messages.push({
|
|
2350
2373
|
role: 'user',
|
|
2351
|
-
content: '[system]
|
|
2374
|
+
content: '[system] Empty response. Call a tool or give final answer.',
|
|
2352
2375
|
});
|
|
2353
2376
|
await emitTurnEnd({
|
|
2354
2377
|
turn: turns,
|
|
@@ -2377,10 +2400,14 @@ export async function createSession(opts) {
|
|
|
2377
2400
|
for (const tc of toolCallsArr) {
|
|
2378
2401
|
const n = tc.function?.name ?? '';
|
|
2379
2402
|
let argCount = 0;
|
|
2380
|
-
|
|
2381
|
-
|
|
2403
|
+
// Extract arg count without full parse if possible
|
|
2404
|
+
const argStr = tc.function?.arguments ?? '{}';
|
|
2405
|
+
if (argStr.length > 2) {
|
|
2406
|
+
try {
|
|
2407
|
+
argCount = Object.keys(parseJsonArgs(argStr)).length;
|
|
2408
|
+
}
|
|
2409
|
+
catch { }
|
|
2382
2410
|
}
|
|
2383
|
-
catch { }
|
|
2384
2411
|
if (!byName.has(n))
|
|
2385
2412
|
byName.set(n, []);
|
|
2386
2413
|
byName.get(n).push({ tc, argCount });
|
|
@@ -2440,9 +2467,7 @@ export async function createSession(opts) {
|
|
|
2440
2467
|
mutationVersion++;
|
|
2441
2468
|
},
|
|
2442
2469
|
});
|
|
2443
|
-
|
|
2444
|
-
return isReadOnlyTool(toolName) || LSP_TOOL_NAME_SET.has(toolName) || Boolean(mcpManager?.isToolReadOnly(toolName));
|
|
2445
|
-
};
|
|
2470
|
+
// Tool-call argument parsing and validation logic
|
|
2446
2471
|
const fileMutationsInTurn = toolCallsArr.filter((tc) => FILE_MUTATION_TOOL_SET.has(tc.function?.name)).length;
|
|
2447
2472
|
if (fileMutationsInTurn >= 3 && isGitDirty(ctx.cwd)) {
|
|
2448
2473
|
const shouldStash = confirmBridge
|
|
@@ -2502,7 +2527,7 @@ export async function createSession(opts) {
|
|
|
2502
2527
|
});
|
|
2503
2528
|
messages.push({
|
|
2504
2529
|
role: 'system',
|
|
2505
|
-
content: `[tool-loop ${warning.level}] ${warning.message}.
|
|
2530
|
+
content: `[tool-loop ${warning.level}] ${warning.message}. Use existing results; move on.`,
|
|
2506
2531
|
});
|
|
2507
2532
|
}
|
|
2508
2533
|
}
|
|
@@ -2591,9 +2616,8 @@ export async function createSession(opts) {
|
|
|
2591
2616
|
const consec = consecutiveCounts.get(sig) ?? 1;
|
|
2592
2617
|
const isReadFileTool = READ_FILE_CACHE_TOOLS.has(toolName);
|
|
2593
2618
|
const hardBreakAt = isReadFileTool ? 6 : 4;
|
|
2594
|
-
// At 3x,
|
|
2619
|
+
// At 3x, first warning
|
|
2595
2620
|
if (consec >= 3) {
|
|
2596
|
-
await injectVaultContext().catch(() => { });
|
|
2597
2621
|
if (consec === 3) {
|
|
2598
2622
|
let warningMsg = null;
|
|
2599
2623
|
if (toolName === 'read_file') {
|
|
@@ -2608,7 +2632,7 @@ export async function createSession(opts) {
|
|
|
2608
2632
|
if (warningMsg) {
|
|
2609
2633
|
messages.push({
|
|
2610
2634
|
role: 'system',
|
|
2611
|
-
content: `${warningMsg}
|
|
2635
|
+
content: `${warningMsg} Content unchanged. Reuse prior result.`,
|
|
2612
2636
|
});
|
|
2613
2637
|
}
|
|
2614
2638
|
}
|
|
@@ -2625,7 +2649,7 @@ export async function createSession(opts) {
|
|
|
2625
2649
|
resourceType = 'directory';
|
|
2626
2650
|
messages.push({
|
|
2627
2651
|
role: 'system',
|
|
2628
|
-
content: `CRITICAL:
|
|
2652
|
+
content: `CRITICAL: ${resourceType} unchanged. Move on NOW.`,
|
|
2629
2653
|
});
|
|
2630
2654
|
}
|
|
2631
2655
|
const argsForSig = sigMetaBySig.get(sig)?.args ?? {};
|
|
@@ -2640,8 +2664,7 @@ export async function createSession(opts) {
|
|
|
2640
2664
|
shouldForceToollessRecovery = true;
|
|
2641
2665
|
messages.push({
|
|
2642
2666
|
role: 'system',
|
|
2643
|
-
content: `[tool-loop critical] ${toolName} repeated ${consec}x
|
|
2644
|
-
'Next turn will run with tools disabled so you must use existing results and provide a concrete next step/final response.',
|
|
2667
|
+
content: `[tool-loop critical] ${toolName} repeated ${consec}x unchanged. Tools disabled next turn; use existing results.`,
|
|
2645
2668
|
});
|
|
2646
2669
|
}
|
|
2647
2670
|
continue;
|
|
@@ -2668,8 +2691,7 @@ export async function createSession(opts) {
|
|
|
2668
2691
|
toollessRecoveryUsed = true;
|
|
2669
2692
|
messages.push({
|
|
2670
2693
|
role: 'user',
|
|
2671
|
-
content: '[system]
|
|
2672
|
-
'Use already available tool results to provide a concrete next step or final response; do not request more tools.',
|
|
2694
|
+
content: '[system] Tool loop detected. Tools disabled. Use existing results for next step.',
|
|
2673
2695
|
});
|
|
2674
2696
|
await emitTurnEnd({
|
|
2675
2697
|
turn: turns,
|
|
@@ -2894,21 +2916,7 @@ export async function createSession(opts) {
|
|
|
2894
2916
|
}
|
|
2895
2917
|
else if (isLspTool && lspManager) {
|
|
2896
2918
|
// LSP tool dispatch
|
|
2897
|
-
|
|
2898
|
-
content = await lspManager.getDiagnostics(typeof args.path === 'string' ? args.path : undefined, typeof args.severity === 'number' ? args.severity : undefined);
|
|
2899
|
-
}
|
|
2900
|
-
else if (name === 'lsp_symbols') {
|
|
2901
|
-
content = await buildLspLensSymbolOutput(String(args.path ?? ''));
|
|
2902
|
-
}
|
|
2903
|
-
else if (name === 'lsp_hover') {
|
|
2904
|
-
content = await lspManager.getHover(String(args.path ?? ''), Number(args.line ?? 0), Number(args.character ?? 0));
|
|
2905
|
-
}
|
|
2906
|
-
else if (name === 'lsp_definition') {
|
|
2907
|
-
content = await lspManager.getDefinition(String(args.path ?? ''), Number(args.line ?? 0), Number(args.character ?? 0));
|
|
2908
|
-
}
|
|
2909
|
-
else if (name === 'lsp_references') {
|
|
2910
|
-
content = await lspManager.getReferences(String(args.path ?? ''), Number(args.line ?? 0), Number(args.character ?? 0), typeof args.max_results === 'number' ? args.max_results : 50);
|
|
2911
|
-
}
|
|
2919
|
+
content = await dispatchLspTool(name, args);
|
|
2912
2920
|
}
|
|
2913
2921
|
else {
|
|
2914
2922
|
if (mcpManager == null) {
|
|
@@ -3201,11 +3209,11 @@ export async function createSession(opts) {
|
|
|
3201
3209
|
}
|
|
3202
3210
|
// ── Escalating cumulative read budget (§ anti-scan guardrails) ──
|
|
3203
3211
|
// Warn zone: append warnings to each read result when approaching the hard cap
|
|
3204
|
-
if (cumulativeReadOnlyCalls > READ_BUDGET_WARN && cumulativeReadOnlyCalls <= READ_BUDGET_HARD) {
|
|
3205
|
-
|
|
3212
|
+
if (!readBudgetWarned && cumulativeReadOnlyCalls > READ_BUDGET_WARN && cumulativeReadOnlyCalls <= READ_BUDGET_HARD) {
|
|
3213
|
+
readBudgetWarned = true;
|
|
3206
3214
|
messages.push({
|
|
3207
3215
|
role: 'user',
|
|
3208
|
-
content: `[
|
|
3216
|
+
content: `[system] Read budget: ${cumulativeReadOnlyCalls}/${READ_BUDGET_HARD}. Use search_files instead of reading files individually.`,
|
|
3209
3217
|
});
|
|
3210
3218
|
}
|
|
3211
3219
|
// One bounded automatic repair attempt for invalid tool args.
|
|
@@ -3256,12 +3264,10 @@ export async function createSession(opts) {
|
|
|
3256
3264
|
repromptUsed = true;
|
|
3257
3265
|
noToolTurns = 0;
|
|
3258
3266
|
const reminder = userContentToText(instruction).trim();
|
|
3259
|
-
const clippedReminder = reminder.length >
|
|
3267
|
+
const clippedReminder = reminder.length > 1600 ? `${reminder.slice(0, 1600)}\n[truncated]` : reminder;
|
|
3260
3268
|
messages.push({
|
|
3261
3269
|
role: 'user',
|
|
3262
|
-
content: `[system]
|
|
3263
|
-
`Original task:\n${clippedReminder}\n\n` +
|
|
3264
|
-
`Call the needed tools directly. If everything is truly complete, provide the final answer.`
|
|
3270
|
+
content: `[system] Stuck narrating. Resume with tools.\nTask:\n${clippedReminder}`
|
|
3265
3271
|
});
|
|
3266
3272
|
await emitTurnEnd({
|
|
3267
3273
|
turn: turns,
|
|
@@ -3279,10 +3285,28 @@ export async function createSession(opts) {
|
|
|
3279
3285
|
}
|
|
3280
3286
|
throw new Error(`no-tool loop detected: model produced planning/narration without tool calls for ${NO_TOOL_REPROMPT_THRESHOLD} turns even after one recovery reprompt`);
|
|
3281
3287
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3288
|
+
if (!noToolNudgeUsed) {
|
|
3289
|
+
noToolNudgeUsed = true;
|
|
3290
|
+
messages.push({
|
|
3291
|
+
role: 'user',
|
|
3292
|
+
content: '[system] Use tools now or give final answer.'
|
|
3293
|
+
});
|
|
3294
|
+
await emitTurnEnd({
|
|
3295
|
+
turn: turns,
|
|
3296
|
+
toolCalls,
|
|
3297
|
+
promptTokens: cumulativeUsage.prompt,
|
|
3298
|
+
completionTokens: cumulativeUsage.completion,
|
|
3299
|
+
promptTokensTurn,
|
|
3300
|
+
completionTokensTurn,
|
|
3301
|
+
ttftMs,
|
|
3302
|
+
ttcMs,
|
|
3303
|
+
ppTps,
|
|
3304
|
+
tgTps,
|
|
3305
|
+
});
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
// Nudge already used — fall through to next iteration which will
|
|
3309
|
+
// increment noToolTurns and hit the reprompt threshold.
|
|
3286
3310
|
await emitTurnEnd({
|
|
3287
3311
|
turn: turns,
|
|
3288
3312
|
toolCalls,
|
|
@@ -3402,7 +3426,7 @@ export async function createSession(opts) {
|
|
|
3402
3426
|
get capturePath() {
|
|
3403
3427
|
return capturePath;
|
|
3404
3428
|
},
|
|
3405
|
-
getSystemPrompt: () =>
|
|
3429
|
+
getSystemPrompt: () => messages[0]?.role === 'system' ? String(messages[0].content) : activeSystemPromptBase,
|
|
3406
3430
|
setSystemPrompt,
|
|
3407
3431
|
resetSystemPrompt,
|
|
3408
3432
|
listMcpServers,
|