@visorcraft/idlehands 1.2.7 → 1.3.1
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 +222 -136
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +117 -37
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/parser.js +12 -13
- package/dist/anton/parser.js.map +1 -1
- package/dist/anton/prompt.js +116 -49
- package/dist/anton/prompt.js.map +1 -1
- package/dist/anton/reporter.js +29 -2
- package/dist/anton/reporter.js.map +1 -1
- package/dist/anton/session.js +25 -0
- package/dist/anton/session.js.map +1 -1
- package/dist/bot/commands.js +10 -2
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord.js +10 -2
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/telegram.js +20 -6
- package/dist/bot/telegram.js.map +1 -1
- package/dist/cli/commands/anton.js +15 -3
- package/dist/cli/commands/anton.js.map +1 -1
- package/dist/client.js +9 -9
- package/dist/client.js.map +1 -1
- package/dist/config.js +6 -1
- package/dist/config.js.map +1 -1
- package/dist/runtime/executor.js +7 -2
- package/dist/runtime/executor.js.map +1 -1
- package/dist/safety.js +10 -6
- package/dist/safety.js.map +1 -1
- package/dist/sys/snapshot.sh +0 -0
- package/dist/tools.js +29 -19
- package/dist/tools.js.map +1 -1
- package/dist/utils.js +32 -7
- package/dist/utils.js.map +1 -1
- package/dist/vault.js +29 -31
- package/dist/vault.js.map +1 -1
- package/dist/vim.js +21 -12
- package/dist/vim.js.map +1 -1
- package/dist/watchdog.js +11 -2
- package/dist/watchdog.js.map +1 -1
- package/package.json +2 -2
- package/src/sys/snapshot.sh +0 -0
package/dist/agent.js
CHANGED
|
@@ -157,41 +157,41 @@ class AgentLoopBreak extends Error {
|
|
|
157
157
|
this.name = 'AgentLoopBreak';
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
const SYSTEM_PROMPT = `You are a coding agent with filesystem and shell access. Execute the user's request using the provided tools.
|
|
161
|
-
|
|
162
|
-
Rules:
|
|
163
|
-
- Work in the current directory. Use relative paths for all file operations.
|
|
164
|
-
- Do the work directly. Do NOT use spawn_task to delegate the user's primary request — only use it for genuinely independent subtasks that benefit from parallel execution.
|
|
165
|
-
- Never use spawn_task to bypass confirmation/safety restrictions (for example blocked package installs). If a command is blocked, adapt the plan or ask the user for approval mode changes.
|
|
166
|
-
- Read the target file before editing. You need the exact text for search/replace.
|
|
167
|
-
- Use read_file with search=... to jump to relevant code; avoid reading whole files.
|
|
168
|
-
- Never call read_file/read_files/list_dir twice in a row with identical arguments (same path/options). Reuse the previous result instead.
|
|
169
|
-
- Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
|
|
170
|
-
- Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
|
|
171
|
-
- edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\nline B"}
|
|
172
|
-
- apply_patch example: {"patch":"--- a/src/foo.ts\n+++ b/src/foo.ts\n@@ -10,2 +10,2 @@\n-old\n+new","files":["src/foo.ts"]}
|
|
173
|
-
- write_file is for new files or explicit full rewrites only. Existing non-empty files require overwrite=true/force=true.
|
|
174
|
-
- Use insert_file for insertions (prepend/append/line).
|
|
175
|
-
- Use exec to run commands, tests, builds; check results before reporting success.
|
|
176
|
-
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd". Each exec call is a fresh shell; cd does not persist.
|
|
177
|
-
- Batch work: read all files you need, then apply all edits, then verify.
|
|
178
|
-
- Be concise. Report what you changed and why.
|
|
179
|
-
- Do NOT read every file in a directory. Use search_files or exec with grep to locate relevant code first, then read only the files that match.
|
|
180
|
-
- If search_files returns 0 matches, try a broader pattern or use: exec grep -rn "keyword" path/
|
|
181
|
-
- Anton (the autonomous task runner) is ONLY activated when the user explicitly invokes /anton. Never self-activate as Anton or start processing task files on your own.
|
|
182
|
-
|
|
183
|
-
Tool call format:
|
|
184
|
-
- Use tool_calls. Do not write JSON tool invocations in your message text.
|
|
160
|
+
const SYSTEM_PROMPT = `You are a coding agent with filesystem and shell access. Execute the user's request using the provided tools.
|
|
161
|
+
|
|
162
|
+
Rules:
|
|
163
|
+
- Work in the current directory. Use relative paths for all file operations.
|
|
164
|
+
- Do the work directly. Do NOT use spawn_task to delegate the user's primary request — only use it for genuinely independent subtasks that benefit from parallel execution.
|
|
165
|
+
- Never use spawn_task to bypass confirmation/safety restrictions (for example blocked package installs). If a command is blocked, adapt the plan or ask the user for approval mode changes.
|
|
166
|
+
- Read the target file before editing. You need the exact text for search/replace.
|
|
167
|
+
- Use read_file with search=... to jump to relevant code; avoid reading whole files.
|
|
168
|
+
- Never call read_file/read_files/list_dir twice in a row with identical arguments (same path/options). Reuse the previous result instead.
|
|
169
|
+
- Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
|
|
170
|
+
- Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
|
|
171
|
+
- edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\nline B"}
|
|
172
|
+
- apply_patch example: {"patch":"--- a/src/foo.ts\n+++ b/src/foo.ts\n@@ -10,2 +10,2 @@\n-old\n+new","files":["src/foo.ts"]}
|
|
173
|
+
- write_file is for new files or explicit full rewrites only. Existing non-empty files require overwrite=true/force=true.
|
|
174
|
+
- Use insert_file for insertions (prepend/append/line).
|
|
175
|
+
- Use exec to run commands, tests, builds; check results before reporting success.
|
|
176
|
+
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd". Each exec call is a fresh shell; cd does not persist.
|
|
177
|
+
- Batch work: read all files you need, then apply all edits, then verify.
|
|
178
|
+
- Be concise. Report what you changed and why.
|
|
179
|
+
- Do NOT read every file in a directory. Use search_files or exec with grep to locate relevant code first, then read only the files that match.
|
|
180
|
+
- If search_files returns 0 matches, try a broader pattern or use: exec grep -rn "keyword" path/
|
|
181
|
+
- Anton (the autonomous task runner) is ONLY activated when the user explicitly invokes /anton. Never self-activate as Anton or start processing task files on your own.
|
|
182
|
+
|
|
183
|
+
Tool call format:
|
|
184
|
+
- Use tool_calls. Do not write JSON tool invocations in your message text.
|
|
185
185
|
`;
|
|
186
186
|
const MCP_TOOLS_REQUEST_TOKEN = '[[MCP_TOOLS_REQUEST]]';
|
|
187
|
-
const DEFAULT_SUB_AGENT_SYSTEM_PROMPT = `You are a focused coding sub-agent. Execute only the delegated task.
|
|
188
|
-
- Work in the current directory. Use relative paths for all file operations.
|
|
189
|
-
- Read the target file before editing. You need the exact text for search/replace.
|
|
190
|
-
- Keep tool usage tight and efficient.
|
|
191
|
-
- Prefer surgical edits over rewrites.
|
|
192
|
-
- Do NOT create files outside the working directory unless explicitly requested.
|
|
193
|
-
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd".
|
|
194
|
-
- Run verification commands when relevant.
|
|
187
|
+
const DEFAULT_SUB_AGENT_SYSTEM_PROMPT = `You are a focused coding sub-agent. Execute only the delegated task.
|
|
188
|
+
- Work in the current directory. Use relative paths for all file operations.
|
|
189
|
+
- Read the target file before editing. You need the exact text for search/replace.
|
|
190
|
+
- Keep tool usage tight and efficient.
|
|
191
|
+
- Prefer surgical edits over rewrites.
|
|
192
|
+
- Do NOT create files outside the working directory unless explicitly requested.
|
|
193
|
+
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd".
|
|
194
|
+
- Run verification commands when relevant.
|
|
195
195
|
- Return a concise outcome summary.`;
|
|
196
196
|
const DEFAULT_SUB_AGENT_RESULT_TOKEN_CAP = 4000;
|
|
197
197
|
const APPROVAL_MODE_SET = new Set(['plan', 'reject', 'default', 'auto-edit', 'yolo']);
|
|
@@ -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))
|
|
@@ -1125,6 +1136,7 @@ export async function createSession(opts) {
|
|
|
1125
1136
|
dirPinned: cfg.dir_pinned,
|
|
1126
1137
|
repoCandidates: cfg.repo_candidates,
|
|
1127
1138
|
confirm: overrides?.confirmBridge ?? defaultConfirmBridge,
|
|
1139
|
+
maxReadLines: cfg.max_read_lines,
|
|
1128
1140
|
replay,
|
|
1129
1141
|
vault,
|
|
1130
1142
|
lens,
|
|
@@ -1132,6 +1144,41 @@ export async function createSession(opts) {
|
|
|
1132
1144
|
onMutation: overrides?.onMutation ?? ((absPath) => { lastEditedPath = absPath; }),
|
|
1133
1145
|
};
|
|
1134
1146
|
};
|
|
1147
|
+
const buildLspLensSymbolOutput = async (filePathRaw) => {
|
|
1148
|
+
if (!lspManager)
|
|
1149
|
+
return '[lsp] unavailable';
|
|
1150
|
+
const semantic = await lspManager.getSymbols(filePathRaw);
|
|
1151
|
+
if (!lens)
|
|
1152
|
+
return semantic;
|
|
1153
|
+
const cwd = cfg.dir ?? process.cwd();
|
|
1154
|
+
const absPath = filePathRaw.startsWith('/') ? filePathRaw : path.resolve(cwd, filePathRaw);
|
|
1155
|
+
const body = await fs.readFile(absPath, 'utf8').catch(() => '');
|
|
1156
|
+
if (!body)
|
|
1157
|
+
return semantic;
|
|
1158
|
+
const projection = await lens.projectFile(absPath, body).catch(() => '');
|
|
1159
|
+
const structural = extractLensBody(projection);
|
|
1160
|
+
if (!structural)
|
|
1161
|
+
return semantic;
|
|
1162
|
+
return `${semantic}\n\n[lens] Structural skeleton:\n${structural}`;
|
|
1163
|
+
};
|
|
1164
|
+
const dispatchLspTool = async (name, args) => {
|
|
1165
|
+
if (!lspManager)
|
|
1166
|
+
return '[lsp] unavailable';
|
|
1167
|
+
switch (name) {
|
|
1168
|
+
case 'lsp_diagnostics':
|
|
1169
|
+
return lspManager.getDiagnostics(typeof args?.path === 'string' ? args.path : undefined, typeof args?.severity === 'number' ? args.severity : undefined);
|
|
1170
|
+
case 'lsp_symbols':
|
|
1171
|
+
return buildLspLensSymbolOutput(String(args?.path ?? ''));
|
|
1172
|
+
case 'lsp_hover':
|
|
1173
|
+
return lspManager.getHover(String(args?.path ?? ''), Number(args?.line ?? 0), Number(args?.character ?? 0));
|
|
1174
|
+
case 'lsp_definition':
|
|
1175
|
+
return lspManager.getDefinition(String(args?.path ?? ''), Number(args?.line ?? 0), Number(args?.character ?? 0));
|
|
1176
|
+
case 'lsp_references':
|
|
1177
|
+
return lspManager.getReferences(String(args?.path ?? ''), Number(args?.line ?? 0), Number(args?.character ?? 0), typeof args?.max_results === 'number' ? args.max_results : 50);
|
|
1178
|
+
default:
|
|
1179
|
+
throw new Error(`unknown LSP tool: ${name}`);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1135
1182
|
const executePlanStep = async (index) => {
|
|
1136
1183
|
if (!planSteps.length)
|
|
1137
1184
|
return ['No plan steps to execute.'];
|
|
@@ -1154,21 +1201,7 @@ export async function createSession(opts) {
|
|
|
1154
1201
|
content = await runSpawnTaskCore(step.args, { signal: inFlight?.signal });
|
|
1155
1202
|
}
|
|
1156
1203
|
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
|
-
}
|
|
1204
|
+
content = await dispatchLspTool(step.tool, step.args);
|
|
1172
1205
|
}
|
|
1173
1206
|
else if (mcpManager?.hasTool(step.tool)) {
|
|
1174
1207
|
const callArgs = step.args && typeof step.args === 'object' && !Array.isArray(step.args)
|
|
@@ -1230,11 +1263,39 @@ export async function createSession(opts) {
|
|
|
1230
1263
|
};
|
|
1231
1264
|
const buildCompactionSystemNote = (kind, dropped) => {
|
|
1232
1265
|
const prefix = kind === 'auto'
|
|
1233
|
-
? `[auto-compacted: ${dropped} old messages dropped to stay within context budget.]`
|
|
1266
|
+
? `[auto-compacted: ${dropped} old messages dropped to stay within context budget. Continue current task.]`
|
|
1234
1267
|
: `[compacted: ${dropped} messages dropped.]`;
|
|
1235
1268
|
const guidance = compactionVaultGuidance();
|
|
1236
1269
|
return guidance ? `${prefix} ${guidance}` : prefix;
|
|
1237
1270
|
};
|
|
1271
|
+
const buildCompactionSummaryPrompt = (dropped) => {
|
|
1272
|
+
const parts = [];
|
|
1273
|
+
for (const m of dropped) {
|
|
1274
|
+
if (m.role === 'assistant') {
|
|
1275
|
+
const text = typeof m.content === 'string' ? m.content : '';
|
|
1276
|
+
const toolCalls = m.tool_calls;
|
|
1277
|
+
if (toolCalls?.length) {
|
|
1278
|
+
for (const tc of toolCalls) {
|
|
1279
|
+
const args = typeof tc.function?.arguments === 'string'
|
|
1280
|
+
? tc.function.arguments.slice(0, 200)
|
|
1281
|
+
: '';
|
|
1282
|
+
parts.push(`[tool_call: ${tc.function?.name}(${args})]`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (text.trim())
|
|
1286
|
+
parts.push(`[assistant]: ${text.slice(0, 500)}`);
|
|
1287
|
+
}
|
|
1288
|
+
else if (m.role === 'tool') {
|
|
1289
|
+
const content = typeof m.content === 'string' ? m.content : '';
|
|
1290
|
+
parts.push(`[tool_result]: ${content.slice(0, 300)}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
let combined = parts.join('\n');
|
|
1294
|
+
if (combined.length > 4000) {
|
|
1295
|
+
combined = combined.slice(0, 4000) + '\n[...truncated]';
|
|
1296
|
+
}
|
|
1297
|
+
return combined;
|
|
1298
|
+
};
|
|
1238
1299
|
let lastAskInstructionText = '';
|
|
1239
1300
|
let lastCompactionReminderObjective = '';
|
|
1240
1301
|
const injectCompactionReminder = (reason) => {
|
|
@@ -1584,6 +1645,10 @@ export async function createSession(opts) {
|
|
|
1584
1645
|
configuredTopP: cfg.top_p,
|
|
1585
1646
|
baseMaxTokens: BASE_MAX_TOKENS,
|
|
1586
1647
|
}));
|
|
1648
|
+
// Update system prompt for the new model/harness
|
|
1649
|
+
if (messages.length > 0 && messages[0].role === 'system') {
|
|
1650
|
+
messages[0].content = buildEffectiveSystemPrompt();
|
|
1651
|
+
}
|
|
1587
1652
|
emitDetached(hookManager.emit('model_changed', {
|
|
1588
1653
|
previousModel,
|
|
1589
1654
|
nextModel: model,
|
|
@@ -1711,7 +1776,7 @@ export async function createSession(opts) {
|
|
|
1711
1776
|
const spinnerStart = Date.now();
|
|
1712
1777
|
let spinnerIdx = 0;
|
|
1713
1778
|
let spinnerTimer;
|
|
1714
|
-
if (process.stderr.isTTY) {
|
|
1779
|
+
if (process.stderr.isTTY && !process.env.IDLEHANDS_QUIET) {
|
|
1715
1780
|
spinnerTimer = setInterval(() => {
|
|
1716
1781
|
const elapsedSec = Math.floor((Date.now() - spinnerStart) / 1000);
|
|
1717
1782
|
const frame = frames[spinnerIdx % frames.length];
|
|
@@ -1719,13 +1784,14 @@ export async function createSession(opts) {
|
|
|
1719
1784
|
process.stderr.write(`\r${frame} Server unavailable - waiting for reconnect (${elapsedSec}s)...`);
|
|
1720
1785
|
}, 120);
|
|
1721
1786
|
}
|
|
1722
|
-
else {
|
|
1787
|
+
else if (!process.env.IDLEHANDS_QUIET) {
|
|
1723
1788
|
console.warn('[model] Server unavailable - waiting for reconnect...');
|
|
1724
1789
|
}
|
|
1725
1790
|
try {
|
|
1726
1791
|
await client.waitForReady({ timeoutMs: 120_000, pollMs: 2_000 });
|
|
1727
1792
|
fresh = normalizeModelsResponse(await client.models());
|
|
1728
|
-
|
|
1793
|
+
if (!process.env.IDLEHANDS_QUIET)
|
|
1794
|
+
console.warn('[model] Reconnected to server.');
|
|
1729
1795
|
}
|
|
1730
1796
|
catch {
|
|
1731
1797
|
return;
|
|
@@ -1733,7 +1799,8 @@ export async function createSession(opts) {
|
|
|
1733
1799
|
finally {
|
|
1734
1800
|
if (spinnerTimer) {
|
|
1735
1801
|
clearInterval(spinnerTimer);
|
|
1736
|
-
process.stderr.
|
|
1802
|
+
if (process.stderr.isTTY)
|
|
1803
|
+
process.stderr.write('\r\x1b[K');
|
|
1737
1804
|
}
|
|
1738
1805
|
}
|
|
1739
1806
|
}
|
|
@@ -1795,6 +1862,9 @@ export async function createSession(opts) {
|
|
|
1795
1862
|
// best effort
|
|
1796
1863
|
}
|
|
1797
1864
|
};
|
|
1865
|
+
const isReadOnlyToolDynamic = (toolName) => {
|
|
1866
|
+
return isReadOnlyTool(toolName) || LSP_TOOL_NAME_SET.has(toolName) || Boolean(mcpManager?.isToolReadOnly(toolName));
|
|
1867
|
+
};
|
|
1798
1868
|
const emitToolResult = async (result) => {
|
|
1799
1869
|
await hookObj.onToolResult?.(result);
|
|
1800
1870
|
await hookManager.emit('tool_result', { askId, turn: turns, result });
|
|
@@ -1934,6 +2004,8 @@ export async function createSession(opts) {
|
|
|
1934
2004
|
let noToolTurns = 0;
|
|
1935
2005
|
const NO_TOOL_REPROMPT_THRESHOLD = 2;
|
|
1936
2006
|
let repromptUsed = false;
|
|
2007
|
+
let readBudgetWarned = false;
|
|
2008
|
+
let noToolNudgeUsed = false;
|
|
1937
2009
|
// Track blocked command loops by exact reason+command signature.
|
|
1938
2010
|
const blockedExecAttemptsBySig = new Map();
|
|
1939
2011
|
// Cache successful read-only exec observations by exact signature.
|
|
@@ -2056,23 +2128,6 @@ export async function createSession(opts) {
|
|
|
2056
2128
|
const tail = detail ? ` — ${detail}` : '';
|
|
2057
2129
|
hookObj.onToken(`\n[sub-agent #${taskId}] ${status}${tail}\n`);
|
|
2058
2130
|
};
|
|
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
2131
|
const runSpawnTask = async (args) => {
|
|
2077
2132
|
if (delegationForbiddenByUser) {
|
|
2078
2133
|
throw new Error('spawn_task: blocked — user explicitly asked for no delegation/sub-agents in this request. Continue directly in the current session.');
|
|
@@ -2102,7 +2157,7 @@ export async function createSession(opts) {
|
|
|
2102
2157
|
messages: beforeMsgs,
|
|
2103
2158
|
contextWindow,
|
|
2104
2159
|
maxTokens: maxTokens,
|
|
2105
|
-
minTailMessages: 12,
|
|
2160
|
+
minTailMessages: cfg.compact_min_tail ?? 12,
|
|
2106
2161
|
compactAt: cfg.compact_at ?? 0.8,
|
|
2107
2162
|
toolSchemaTokens: estimateToolSchemaTokens(getToolsSchema()),
|
|
2108
2163
|
});
|
|
@@ -2137,13 +2192,43 @@ export async function createSession(opts) {
|
|
|
2137
2192
|
}
|
|
2138
2193
|
}
|
|
2139
2194
|
messages = compacted;
|
|
2140
|
-
// Update current context token count after auto compaction
|
|
2141
|
-
currentContextTokens = estimateTokensFromMessages(compacted);
|
|
2142
2195
|
if (dropped.length) {
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2196
|
+
const droppedTokens = estimateTokensFromMessages(dropped);
|
|
2197
|
+
if (cfg.compact_summary !== false && droppedTokens > 200) {
|
|
2198
|
+
try {
|
|
2199
|
+
const summaryContent = buildCompactionSummaryPrompt(dropped);
|
|
2200
|
+
const summaryMaxTokens = cfg.compact_summary_max_tokens ?? 300;
|
|
2201
|
+
const resp = await client.chat({
|
|
2202
|
+
model,
|
|
2203
|
+
messages: [
|
|
2204
|
+
{ role: 'system', content: 'Summarize this agent session progress concisely. List: files read, key findings, decisions made, current approach. Be terse.' },
|
|
2205
|
+
{ role: 'user', content: summaryContent },
|
|
2206
|
+
],
|
|
2207
|
+
max_tokens: summaryMaxTokens,
|
|
2208
|
+
temperature: 0,
|
|
2209
|
+
responseTimeoutMs: 5_000,
|
|
2210
|
+
});
|
|
2211
|
+
const summary = resp.choices?.[0]?.message?.content ?? '';
|
|
2212
|
+
if (summary.trim()) {
|
|
2213
|
+
messages.push({
|
|
2214
|
+
role: 'system',
|
|
2215
|
+
content: `[Compacted ${dropped.length} messages (~${droppedTokens} tokens). Progress summary:]\n${summary.trim()}\n[Continue from where you left off. Do not repeat completed work.]`,
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
else {
|
|
2219
|
+
messages.push({ role: 'system', content: buildCompactionSystemNote('auto', dropped.length) });
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
catch {
|
|
2223
|
+
messages.push({ role: 'system', content: buildCompactionSystemNote('auto', dropped.length) });
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
messages.push({ role: 'system', content: buildCompactionSystemNote('auto', dropped.length) });
|
|
2228
|
+
}
|
|
2146
2229
|
}
|
|
2230
|
+
// Update token count AFTER injections so downstream reads are accurate
|
|
2231
|
+
currentContextTokens = estimateTokensFromMessages(messages);
|
|
2147
2232
|
const afterTokens = estimateTokensFromMessages(compacted);
|
|
2148
2233
|
return {
|
|
2149
2234
|
beforeMessages: beforeMsgs.length,
|
|
@@ -2178,8 +2263,8 @@ export async function createSession(opts) {
|
|
|
2178
2263
|
let resp;
|
|
2179
2264
|
try {
|
|
2180
2265
|
try {
|
|
2181
|
-
const toolsForTurn = forceToollessRecoveryTurn ? [] : getToolsSchema();
|
|
2182
|
-
const toolChoiceForTurn = forceToollessRecoveryTurn ? 'none' : 'auto';
|
|
2266
|
+
const toolsForTurn = (cfg.no_tools || forceToollessRecoveryTurn) ? [] : getToolsSchema();
|
|
2267
|
+
const toolChoiceForTurn = (cfg.no_tools || forceToollessRecoveryTurn) ? 'none' : 'auto';
|
|
2183
2268
|
resp = await client.chatStream({
|
|
2184
2269
|
model,
|
|
2185
2270
|
messages,
|
|
@@ -2209,8 +2294,7 @@ export async function createSession(opts) {
|
|
|
2209
2294
|
const mode = useHardCompaction ? 'hard' : 'force';
|
|
2210
2295
|
messages.push({
|
|
2211
2296
|
role: 'system',
|
|
2212
|
-
content: `[auto-recovery]
|
|
2213
|
-
`(freed ~${compacted.freedTokens} tokens, dropped ${compacted.droppedMessages} messages). Continue from latest state; do not restart work.`,
|
|
2297
|
+
content: `[auto-recovery] Context overflow. Ran ${mode} compaction (freed ~${compacted.freedTokens} tokens). Continue current work.`,
|
|
2214
2298
|
});
|
|
2215
2299
|
continue;
|
|
2216
2300
|
}
|
|
@@ -2348,7 +2432,7 @@ export async function createSession(opts) {
|
|
|
2348
2432
|
}
|
|
2349
2433
|
messages.push({
|
|
2350
2434
|
role: 'user',
|
|
2351
|
-
content: '[system]
|
|
2435
|
+
content: '[system] Empty response. Call a tool or give final answer.',
|
|
2352
2436
|
});
|
|
2353
2437
|
await emitTurnEnd({
|
|
2354
2438
|
turn: turns,
|
|
@@ -2377,10 +2461,14 @@ export async function createSession(opts) {
|
|
|
2377
2461
|
for (const tc of toolCallsArr) {
|
|
2378
2462
|
const n = tc.function?.name ?? '';
|
|
2379
2463
|
let argCount = 0;
|
|
2380
|
-
|
|
2381
|
-
|
|
2464
|
+
// Extract arg count without full parse if possible
|
|
2465
|
+
const argStr = tc.function?.arguments ?? '{}';
|
|
2466
|
+
if (argStr.length > 2) {
|
|
2467
|
+
try {
|
|
2468
|
+
argCount = Object.keys(parseJsonArgs(argStr)).length;
|
|
2469
|
+
}
|
|
2470
|
+
catch { }
|
|
2382
2471
|
}
|
|
2383
|
-
catch { }
|
|
2384
2472
|
if (!byName.has(n))
|
|
2385
2473
|
byName.set(n, []);
|
|
2386
2474
|
byName.get(n).push({ tc, argCount });
|
|
@@ -2440,9 +2528,7 @@ export async function createSession(opts) {
|
|
|
2440
2528
|
mutationVersion++;
|
|
2441
2529
|
},
|
|
2442
2530
|
});
|
|
2443
|
-
|
|
2444
|
-
return isReadOnlyTool(toolName) || LSP_TOOL_NAME_SET.has(toolName) || Boolean(mcpManager?.isToolReadOnly(toolName));
|
|
2445
|
-
};
|
|
2531
|
+
// Tool-call argument parsing and validation logic
|
|
2446
2532
|
const fileMutationsInTurn = toolCallsArr.filter((tc) => FILE_MUTATION_TOOL_SET.has(tc.function?.name)).length;
|
|
2447
2533
|
if (fileMutationsInTurn >= 3 && isGitDirty(ctx.cwd)) {
|
|
2448
2534
|
const shouldStash = confirmBridge
|
|
@@ -2502,7 +2588,7 @@ export async function createSession(opts) {
|
|
|
2502
2588
|
});
|
|
2503
2589
|
messages.push({
|
|
2504
2590
|
role: 'system',
|
|
2505
|
-
content: `[tool-loop ${warning.level}] ${warning.message}.
|
|
2591
|
+
content: `[tool-loop ${warning.level}] ${warning.message}. Use existing results; move on.`,
|
|
2506
2592
|
});
|
|
2507
2593
|
}
|
|
2508
2594
|
}
|
|
@@ -2591,9 +2677,8 @@ export async function createSession(opts) {
|
|
|
2591
2677
|
const consec = consecutiveCounts.get(sig) ?? 1;
|
|
2592
2678
|
const isReadFileTool = READ_FILE_CACHE_TOOLS.has(toolName);
|
|
2593
2679
|
const hardBreakAt = isReadFileTool ? 6 : 4;
|
|
2594
|
-
// At 3x,
|
|
2680
|
+
// At 3x, first warning
|
|
2595
2681
|
if (consec >= 3) {
|
|
2596
|
-
await injectVaultContext().catch(() => { });
|
|
2597
2682
|
if (consec === 3) {
|
|
2598
2683
|
let warningMsg = null;
|
|
2599
2684
|
if (toolName === 'read_file') {
|
|
@@ -2608,7 +2693,7 @@ export async function createSession(opts) {
|
|
|
2608
2693
|
if (warningMsg) {
|
|
2609
2694
|
messages.push({
|
|
2610
2695
|
role: 'system',
|
|
2611
|
-
content: `${warningMsg}
|
|
2696
|
+
content: `${warningMsg} Content unchanged. Reuse prior result.`,
|
|
2612
2697
|
});
|
|
2613
2698
|
}
|
|
2614
2699
|
}
|
|
@@ -2625,7 +2710,7 @@ export async function createSession(opts) {
|
|
|
2625
2710
|
resourceType = 'directory';
|
|
2626
2711
|
messages.push({
|
|
2627
2712
|
role: 'system',
|
|
2628
|
-
content: `CRITICAL:
|
|
2713
|
+
content: `CRITICAL: ${resourceType} unchanged. Move on NOW.`,
|
|
2629
2714
|
});
|
|
2630
2715
|
}
|
|
2631
2716
|
const argsForSig = sigMetaBySig.get(sig)?.args ?? {};
|
|
@@ -2640,8 +2725,7 @@ export async function createSession(opts) {
|
|
|
2640
2725
|
shouldForceToollessRecovery = true;
|
|
2641
2726
|
messages.push({
|
|
2642
2727
|
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.',
|
|
2728
|
+
content: `[tool-loop critical] ${toolName} repeated ${consec}x unchanged. Tools disabled next turn; use existing results.`,
|
|
2645
2729
|
});
|
|
2646
2730
|
}
|
|
2647
2731
|
continue;
|
|
@@ -2668,8 +2752,7 @@ export async function createSession(opts) {
|
|
|
2668
2752
|
toollessRecoveryUsed = true;
|
|
2669
2753
|
messages.push({
|
|
2670
2754
|
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.',
|
|
2755
|
+
content: '[system] Tool loop detected. Tools disabled. Use existing results for next step.',
|
|
2673
2756
|
});
|
|
2674
2757
|
await emitTurnEnd({
|
|
2675
2758
|
turn: turns,
|
|
@@ -2894,21 +2977,7 @@ export async function createSession(opts) {
|
|
|
2894
2977
|
}
|
|
2895
2978
|
else if (isLspTool && lspManager) {
|
|
2896
2979
|
// 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
|
-
}
|
|
2980
|
+
content = await dispatchLspTool(name, args);
|
|
2912
2981
|
}
|
|
2913
2982
|
else {
|
|
2914
2983
|
if (mcpManager == null) {
|
|
@@ -3201,11 +3270,11 @@ export async function createSession(opts) {
|
|
|
3201
3270
|
}
|
|
3202
3271
|
// ── Escalating cumulative read budget (§ anti-scan guardrails) ──
|
|
3203
3272
|
// Warn zone: append warnings to each read result when approaching the hard cap
|
|
3204
|
-
if (cumulativeReadOnlyCalls > READ_BUDGET_WARN && cumulativeReadOnlyCalls <= READ_BUDGET_HARD) {
|
|
3205
|
-
|
|
3273
|
+
if (!readBudgetWarned && cumulativeReadOnlyCalls > READ_BUDGET_WARN && cumulativeReadOnlyCalls <= READ_BUDGET_HARD) {
|
|
3274
|
+
readBudgetWarned = true;
|
|
3206
3275
|
messages.push({
|
|
3207
3276
|
role: 'user',
|
|
3208
|
-
content: `[
|
|
3277
|
+
content: `[system] Read budget: ${cumulativeReadOnlyCalls}/${READ_BUDGET_HARD}. Use search_files instead of reading files individually.`,
|
|
3209
3278
|
});
|
|
3210
3279
|
}
|
|
3211
3280
|
// One bounded automatic repair attempt for invalid tool args.
|
|
@@ -3248,7 +3317,8 @@ export async function createSession(opts) {
|
|
|
3248
3317
|
const assistantText = visible || content || '';
|
|
3249
3318
|
// Recovery fuse: if the model keeps narrating/planning without tool use,
|
|
3250
3319
|
// nudge it once with the original task. Never resend more than once per ask().
|
|
3251
|
-
|
|
3320
|
+
// Skip this check entirely when no_tools is set — text IS the final answer.
|
|
3321
|
+
if (!cfg.no_tools && looksLikePlanningNarration(assistantText, finishReason)) {
|
|
3252
3322
|
noToolTurns += 1;
|
|
3253
3323
|
messages.push({ role: 'assistant', content: assistantText });
|
|
3254
3324
|
if (noToolTurns >= NO_TOOL_REPROMPT_THRESHOLD) {
|
|
@@ -3256,12 +3326,10 @@ export async function createSession(opts) {
|
|
|
3256
3326
|
repromptUsed = true;
|
|
3257
3327
|
noToolTurns = 0;
|
|
3258
3328
|
const reminder = userContentToText(instruction).trim();
|
|
3259
|
-
const clippedReminder = reminder.length >
|
|
3329
|
+
const clippedReminder = reminder.length > 1600 ? `${reminder.slice(0, 1600)}\n[truncated]` : reminder;
|
|
3260
3330
|
messages.push({
|
|
3261
3331
|
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.`
|
|
3332
|
+
content: `[system] Stuck narrating. Resume with tools.\nTask:\n${clippedReminder}`
|
|
3265
3333
|
});
|
|
3266
3334
|
await emitTurnEnd({
|
|
3267
3335
|
turn: turns,
|
|
@@ -3279,10 +3347,28 @@ export async function createSession(opts) {
|
|
|
3279
3347
|
}
|
|
3280
3348
|
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
3349
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3350
|
+
if (!noToolNudgeUsed) {
|
|
3351
|
+
noToolNudgeUsed = true;
|
|
3352
|
+
messages.push({
|
|
3353
|
+
role: 'user',
|
|
3354
|
+
content: '[system] Use tools now or give final answer.'
|
|
3355
|
+
});
|
|
3356
|
+
await emitTurnEnd({
|
|
3357
|
+
turn: turns,
|
|
3358
|
+
toolCalls,
|
|
3359
|
+
promptTokens: cumulativeUsage.prompt,
|
|
3360
|
+
completionTokens: cumulativeUsage.completion,
|
|
3361
|
+
promptTokensTurn,
|
|
3362
|
+
completionTokensTurn,
|
|
3363
|
+
ttftMs,
|
|
3364
|
+
ttcMs,
|
|
3365
|
+
ppTps,
|
|
3366
|
+
tgTps,
|
|
3367
|
+
});
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
// Nudge already used — fall through to next iteration which will
|
|
3371
|
+
// increment noToolTurns and hit the reprompt threshold.
|
|
3286
3372
|
await emitTurnEnd({
|
|
3287
3373
|
turn: turns,
|
|
3288
3374
|
toolCalls,
|
|
@@ -3402,7 +3488,7 @@ export async function createSession(opts) {
|
|
|
3402
3488
|
get capturePath() {
|
|
3403
3489
|
return capturePath;
|
|
3404
3490
|
},
|
|
3405
|
-
getSystemPrompt: () =>
|
|
3491
|
+
getSystemPrompt: () => messages[0]?.role === 'system' ? String(messages[0].content) : activeSystemPromptBase,
|
|
3406
3492
|
setSystemPrompt,
|
|
3407
3493
|
resetSystemPrompt,
|
|
3408
3494
|
listMcpServers,
|