nex-code 0.3.5 → 0.3.7
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/README.md +23 -1
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -441
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
package/cli/agent.js
DELETED
|
@@ -1,835 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/agent.js — Agentic Loop + Conversation State
|
|
3
|
-
* Hybrid: chat + tool-use in a single conversation.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { C, Spinner, TaskProgress, formatToolCall, formatResult, formatToolSummary, setActiveTaskProgress } = require('./ui');
|
|
7
|
-
const { callStream } = require('./providers/registry');
|
|
8
|
-
const { parseToolArgs } = require('./ollama');
|
|
9
|
-
const { TOOL_DEFINITIONS, executeTool } = require('./tools');
|
|
10
|
-
const { gatherProjectContext } = require('./context');
|
|
11
|
-
const { fitToContext, forceCompress, getUsage } = require('./context-engine');
|
|
12
|
-
const { autoSave } = require('./session');
|
|
13
|
-
const { getMemoryContext } = require('./memory');
|
|
14
|
-
const { checkPermission, setPermission, savePermissions } = require('./permissions');
|
|
15
|
-
const { confirm, setAllowAlwaysHandler } = require('./safety');
|
|
16
|
-
const { isPlanMode, getPlanModePrompt } = require('./planner');
|
|
17
|
-
const { StreamRenderer } = require('./render');
|
|
18
|
-
const { runHooks } = require('./hooks');
|
|
19
|
-
const { routeMCPCall, getMCPToolDefinitions } = require('./mcp');
|
|
20
|
-
const { getSkillInstructions, getSkillToolDefinitions, routeSkillCall } = require('./skills');
|
|
21
|
-
const { trackUsage } = require('./costs');
|
|
22
|
-
const { validateToolArgs } = require('./tool-validator');
|
|
23
|
-
const { filterToolsForModel, getModelTier, PROVIDER_DEFAULT_TIER } = require('./tool-tiers');
|
|
24
|
-
const { getConfiguredProviders } = require('./providers/registry');
|
|
25
|
-
|
|
26
|
-
let MAX_ITERATIONS = 50;
|
|
27
|
-
function setMaxIterations(n) { if (Number.isFinite(n) && n > 0) MAX_ITERATIONS = n; }
|
|
28
|
-
|
|
29
|
-
// Abort signal getter — set by cli/index.js to avoid circular dependency
|
|
30
|
-
let _getAbortSignal = () => null;
|
|
31
|
-
function setAbortSignalGetter(fn) { _getAbortSignal = fn; }
|
|
32
|
-
|
|
33
|
-
// Tools that can safely run in parallel (read-only, no side effects)
|
|
34
|
-
const PARALLEL_SAFE = new Set([
|
|
35
|
-
'read_file', 'list_directory', 'search_files', 'glob', 'grep',
|
|
36
|
-
'web_fetch', 'web_search', 'git_status', 'git_diff', 'git_log',
|
|
37
|
-
]);
|
|
38
|
-
const MAX_RATE_LIMIT_RETRIES = 5;
|
|
39
|
-
const MAX_NETWORK_RETRIES = 3;
|
|
40
|
-
const MAX_STALE_RETRIES = 2;
|
|
41
|
-
const STALE_WARN_MS = 60000; // Warn after 60s without tokens
|
|
42
|
-
const STALE_ABORT_MS = 120000; // Abort after 120s without tokens
|
|
43
|
-
const CWD = process.cwd();
|
|
44
|
-
|
|
45
|
-
// Wire up "a" (always allow) from confirm dialog → permission system
|
|
46
|
-
setAllowAlwaysHandler((toolName) => {
|
|
47
|
-
setPermission(toolName, 'allow');
|
|
48
|
-
savePermissions();
|
|
49
|
-
console.log(`${C.green} ✓ ${toolName}: always allow${C.reset}`);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// ─── Tool Call Helpers ────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Prepare a tool call: parse args, validate, check permissions.
|
|
56
|
-
* Returns an object ready for execution.
|
|
57
|
-
*/
|
|
58
|
-
async function prepareToolCall(tc) {
|
|
59
|
-
const fnName = tc.function.name;
|
|
60
|
-
const args = parseToolArgs(tc.function.arguments);
|
|
61
|
-
const callId = tc.id || `cli-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
62
|
-
|
|
63
|
-
// Malformed args
|
|
64
|
-
if (!args) {
|
|
65
|
-
const allToolDefs = [...TOOL_DEFINITIONS, ...getSkillToolDefinitions(), ...getMCPToolDefinitions()];
|
|
66
|
-
const toolDef = allToolDefs.find(t => t.function.name === fnName);
|
|
67
|
-
const schema = toolDef ? JSON.stringify(toolDef.function.parameters, null, 2) : 'unknown';
|
|
68
|
-
console.log(`${C.yellow} ⚠ ${fnName}: malformed arguments, sending schema hint${C.reset}`);
|
|
69
|
-
return {
|
|
70
|
-
callId, fnName, args: null, canExecute: false,
|
|
71
|
-
errorResult: {
|
|
72
|
-
role: 'tool',
|
|
73
|
-
content: `ERROR: Malformed tool arguments. Could not parse your arguments as JSON.\n` +
|
|
74
|
-
`Raw input: ${typeof tc.function.arguments === 'string' ? tc.function.arguments.substring(0, 200) : 'N/A'}\n\n` +
|
|
75
|
-
`Expected JSON schema for "${fnName}":\n${schema}\n\n` +
|
|
76
|
-
`Please retry the tool call with valid JSON arguments matching this schema.`,
|
|
77
|
-
tool_call_id: callId,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Validate
|
|
83
|
-
const validation = validateToolArgs(fnName, args);
|
|
84
|
-
if (!validation.valid) {
|
|
85
|
-
console.log(`${C.yellow} ⚠ ${fnName}: ${validation.error.split('\n')[0]}${C.reset}`);
|
|
86
|
-
return {
|
|
87
|
-
callId, fnName, args, canExecute: false,
|
|
88
|
-
errorResult: { role: 'tool', content: validation.error, tool_call_id: callId },
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const finalArgs = validation.corrected || args;
|
|
93
|
-
|
|
94
|
-
// Log validator corrections so user/LLM can see auto-fixes
|
|
95
|
-
if (validation.corrected) {
|
|
96
|
-
const orig = Object.keys(args);
|
|
97
|
-
const fixed = Object.keys(validation.corrected);
|
|
98
|
-
const renamed = orig.filter(k => !fixed.includes(k));
|
|
99
|
-
if (renamed.length) {
|
|
100
|
-
console.log(`${C.dim} ✓ ${fnName}: corrected args (${renamed.join(', ')})${C.reset}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Permission check
|
|
105
|
-
const perm = checkPermission(fnName);
|
|
106
|
-
if (perm === 'deny') {
|
|
107
|
-
console.log(`${C.red} ✗ ${fnName}: denied by permissions${C.reset}`);
|
|
108
|
-
return {
|
|
109
|
-
callId, fnName, args: finalArgs, canExecute: false,
|
|
110
|
-
errorResult: { role: 'tool', content: `DENIED: Tool '${fnName}' is blocked by permissions`, tool_call_id: callId },
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
if (perm === 'ask') {
|
|
114
|
-
const ok = await confirm(` Allow ${fnName}?`, { toolName: fnName });
|
|
115
|
-
if (!ok) {
|
|
116
|
-
return {
|
|
117
|
-
callId, fnName, args: finalArgs, canExecute: false,
|
|
118
|
-
errorResult: { role: 'tool', content: `CANCELLED: User declined ${fnName}`, tool_call_id: callId },
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { callId, fnName, args: finalArgs, canExecute: true, errorResult: null };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Execute a single prepared tool call through the routing chain.
|
|
128
|
-
*/
|
|
129
|
-
async function executeToolRouted(fnName, args, options = {}) {
|
|
130
|
-
const skillResult = await routeSkillCall(fnName, args);
|
|
131
|
-
if (skillResult !== null) return skillResult;
|
|
132
|
-
const mcpResult = await routeMCPCall(fnName, args);
|
|
133
|
-
if (mcpResult !== null) return mcpResult;
|
|
134
|
-
return executeTool(fnName, args, options);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Short arg preview for spinner labels.
|
|
139
|
-
*/
|
|
140
|
-
function _argPreview(name, args) {
|
|
141
|
-
switch (name) {
|
|
142
|
-
case 'read_file': case 'write_file': case 'edit_file':
|
|
143
|
-
case 'patch_file': case 'list_directory':
|
|
144
|
-
return args.path || '';
|
|
145
|
-
case 'bash':
|
|
146
|
-
return (args.command || '').substring(0, 60);
|
|
147
|
-
case 'grep': case 'search_files': case 'glob':
|
|
148
|
-
return args.pattern || '';
|
|
149
|
-
case 'web_fetch':
|
|
150
|
-
return (args.url || '').substring(0, 50);
|
|
151
|
-
case 'web_search':
|
|
152
|
-
return (args.query || '').substring(0, 40);
|
|
153
|
-
default:
|
|
154
|
-
return '';
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Execute a single prepared tool and return { msg, summary }.
|
|
160
|
-
* @param {boolean} quiet - suppress formatToolCall/formatResult output
|
|
161
|
-
*/
|
|
162
|
-
async function executeSingleTool(prep, quiet = false) {
|
|
163
|
-
if (!quiet) {
|
|
164
|
-
console.log(formatToolCall(prep.fnName, prep.args));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
runHooks('pre-tool', { tool_name: prep.fnName });
|
|
168
|
-
|
|
169
|
-
const toolResult = await executeToolRouted(prep.fnName, prep.args, { silent: true });
|
|
170
|
-
const safeResult = String(toolResult ?? '');
|
|
171
|
-
const truncated = safeResult.length > 50000
|
|
172
|
-
? safeResult.substring(0, 50000) + `\n...(truncated ${safeResult.length - 50000} chars)`
|
|
173
|
-
: safeResult;
|
|
174
|
-
|
|
175
|
-
if (!quiet) {
|
|
176
|
-
console.log(formatResult(truncated));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
runHooks('post-tool', { tool_name: prep.fnName });
|
|
180
|
-
|
|
181
|
-
const firstLine = truncated.split('\n')[0];
|
|
182
|
-
const isError = firstLine.startsWith('ERROR') || firstLine.includes('CANCELLED') || firstLine.includes('BLOCKED')
|
|
183
|
-
|| (prep.fnName === 'spawn_agents' && !/✓ Agent/.test(truncated) && /✗ Agent/.test(truncated));
|
|
184
|
-
const summary = formatToolSummary(prep.fnName, prep.args, truncated, isError);
|
|
185
|
-
const msg = { role: 'tool', content: truncated, tool_call_id: prep.callId };
|
|
186
|
-
return { msg, summary };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Execute prepared tool calls with parallel batching.
|
|
191
|
-
* Consecutive PARALLEL_SAFE tools run via Promise.all.
|
|
192
|
-
* @param {boolean} quiet - use spinner + compact summaries instead of verbose output
|
|
193
|
-
*/
|
|
194
|
-
async function executeBatch(prepared, quiet = false, options = {}) {
|
|
195
|
-
const results = new Array(prepared.length);
|
|
196
|
-
const summaries = [];
|
|
197
|
-
let batch = [];
|
|
198
|
-
|
|
199
|
-
// Quiet mode: show a single spinner for all tools
|
|
200
|
-
let spinner = null;
|
|
201
|
-
if (quiet && !options.skipSpinner) {
|
|
202
|
-
const execTools = prepared.filter(p => p.canExecute);
|
|
203
|
-
if (execTools.length > 0) {
|
|
204
|
-
let label;
|
|
205
|
-
if (execTools.length === 1) {
|
|
206
|
-
const p = execTools[0];
|
|
207
|
-
label = `▸ ${p.fnName} ${_argPreview(p.fnName, p.args)}`;
|
|
208
|
-
} else {
|
|
209
|
-
const names = execTools.map(p => p.fnName).join(', ');
|
|
210
|
-
label = `▸ ${execTools.length} tools: ${names.length > 60 ? names.substring(0, 57) + '...' : names}`;
|
|
211
|
-
}
|
|
212
|
-
spinner = new Spinner(label);
|
|
213
|
-
spinner.start();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function flushBatch() {
|
|
218
|
-
if (batch.length === 0) return;
|
|
219
|
-
if (batch.length === 1) {
|
|
220
|
-
const idx = batch[0];
|
|
221
|
-
const { msg, summary } = await executeSingleTool(prepared[idx], quiet);
|
|
222
|
-
results[idx] = msg;
|
|
223
|
-
summaries.push(summary);
|
|
224
|
-
} else {
|
|
225
|
-
const promises = batch.map(idx => executeSingleTool(prepared[idx], quiet));
|
|
226
|
-
const batchResults = await Promise.all(promises);
|
|
227
|
-
for (let j = 0; j < batch.length; j++) {
|
|
228
|
-
results[batch[j]] = batchResults[j].msg;
|
|
229
|
-
summaries.push(batchResults[j].summary);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
batch = [];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
for (let i = 0; i < prepared.length; i++) {
|
|
236
|
-
const prep = prepared[i];
|
|
237
|
-
|
|
238
|
-
if (!prep.canExecute) {
|
|
239
|
-
await flushBatch();
|
|
240
|
-
results[i] = prep.errorResult;
|
|
241
|
-
summaries.push(formatToolSummary(prep.fnName, prep.args || {}, prep.errorResult.content, true));
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (PARALLEL_SAFE.has(prep.fnName)) {
|
|
246
|
-
batch.push(i);
|
|
247
|
-
} else {
|
|
248
|
-
await flushBatch();
|
|
249
|
-
// spawn_agents manages its own display (MultiProgress) — stop outer spinner
|
|
250
|
-
if (prep.fnName === 'spawn_agents' && spinner) {
|
|
251
|
-
spinner.stop();
|
|
252
|
-
spinner = null;
|
|
253
|
-
}
|
|
254
|
-
const { msg, summary } = await executeSingleTool(prep, quiet);
|
|
255
|
-
results[i] = msg;
|
|
256
|
-
summaries.push(summary);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
await flushBatch();
|
|
261
|
-
|
|
262
|
-
// Stop spinner and print compact summaries
|
|
263
|
-
if (spinner) spinner.stop();
|
|
264
|
-
if (quiet && summaries.length > 0 && !options.skipSummaries) {
|
|
265
|
-
for (const s of summaries) console.log(s);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return results;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Persistent conversation state
|
|
272
|
-
let conversationMessages = [];
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Build dynamic model routing guide for spawn_agents.
|
|
276
|
-
* Only shown when 2+ models are available across configured providers.
|
|
277
|
-
*/
|
|
278
|
-
function _buildModelRoutingGuide() {
|
|
279
|
-
try {
|
|
280
|
-
const configured = getConfiguredProviders();
|
|
281
|
-
const allModels = configured.flatMap(p =>
|
|
282
|
-
p.models.map(m => ({
|
|
283
|
-
spec: `${p.name}:${m.id}`,
|
|
284
|
-
tier: getModelTier(m.id, p.name),
|
|
285
|
-
name: m.name,
|
|
286
|
-
}))
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
if (allModels.length < 2) return '';
|
|
290
|
-
|
|
291
|
-
const tierLabels = {
|
|
292
|
-
full: 'complex tasks (refactor, implement, generate)',
|
|
293
|
-
standard: 'regular tasks (edit, fix, analyze)',
|
|
294
|
-
essential: 'simple tasks (read, search, list)',
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
let guide = '\n# Sub-Agent Model Routing\n\n';
|
|
298
|
-
guide += 'Sub-agents auto-select models by task complexity. Override with `model: "provider:model"` in agent definition.\n\n';
|
|
299
|
-
guide += '| Model | Tier | Auto-assigned for |\n|---|---|---|\n';
|
|
300
|
-
for (const m of allModels) {
|
|
301
|
-
guide += `| ${m.spec} | ${m.tier} | ${tierLabels[m.tier] || m.tier} |\n`;
|
|
302
|
-
}
|
|
303
|
-
return guide;
|
|
304
|
-
} catch {
|
|
305
|
-
return '';
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function buildSystemPrompt() {
|
|
310
|
-
const projectContext = gatherProjectContext(CWD);
|
|
311
|
-
|
|
312
|
-
const memoryContext = getMemoryContext();
|
|
313
|
-
const skillInstructions = getSkillInstructions();
|
|
314
|
-
const planPrompt = isPlanMode() ? getPlanModePrompt() : '';
|
|
315
|
-
|
|
316
|
-
return `You are Nex Code, an expert coding assistant. You help with programming tasks by reading, writing, and editing files, running commands, and answering questions.
|
|
317
|
-
|
|
318
|
-
WORKING DIRECTORY: ${CWD}
|
|
319
|
-
All relative paths resolve from this directory.
|
|
320
|
-
|
|
321
|
-
PROJECT CONTEXT:
|
|
322
|
-
${projectContext}
|
|
323
|
-
${memoryContext ? `\n${memoryContext}\n` : ''}${skillInstructions ? `\n${skillInstructions}\n` : ''}${planPrompt ? `\n${planPrompt}\n` : ''}
|
|
324
|
-
# Core Behavior
|
|
325
|
-
|
|
326
|
-
- You can use tools OR respond with text. For simple questions, answer directly.
|
|
327
|
-
- For coding tasks, use tools to read files, make changes, run tests, etc.
|
|
328
|
-
- Be concise but complete. Keep responses focused while ensuring the user gets the information they asked for.
|
|
329
|
-
- When referencing code, include file:line (e.g. src/app.js:42) so the user can navigate.
|
|
330
|
-
- Do not make up file paths or URLs. Use tools to discover them.
|
|
331
|
-
|
|
332
|
-
# Response Quality (Critical)
|
|
333
|
-
|
|
334
|
-
⚠ CRITICAL: The user CANNOT see tool output. They only see your text + 1-line summaries like "✓ bash ssh ... → ok".
|
|
335
|
-
If you run tools but write NO text → the user sees NOTHING useful. This is the #1 quality failure.
|
|
336
|
-
|
|
337
|
-
MANDATORY RULE: After ANY tool call that gathers information (bash, read_file, grep, ssh commands, etc.), you MUST write a text response summarizing the findings. NEVER end your response with only tool calls and no text.
|
|
338
|
-
|
|
339
|
-
- Use markdown formatting: **bold** for key points, headers for sections, bullet lists for multiple items, \`code\` for identifiers. The terminal renders markdown with syntax highlighting.
|
|
340
|
-
- Structure longer responses with headers (## Section) so the user can scan quickly.
|
|
341
|
-
|
|
342
|
-
Response patterns by request type:
|
|
343
|
-
- **Questions / analysis / "status" / "explain" / "what is"**: Gather data with tools, then respond with a clear, structured summary. NEVER just run tools and stop.
|
|
344
|
-
- **Coding tasks (implement, fix, refactor)**: Brief confirmation of what you'll do, then use tools. After changes, summarize what you did and any important details.
|
|
345
|
-
- **Simple questions ("what does X do?")**: Answer directly without tools when you have enough context.
|
|
346
|
-
- **Ambiguous requests**: When a request is vague or could be interpreted multiple ways (e.g. "optimize this", "improve performance", "fix the issues", "refactor this"), ALWAYS ask clarifying questions first using ask_user. Do NOT guess scope or intent. Ask about: which specific area, what the expected outcome is, any constraints. Only proceed after the user clarifies.
|
|
347
|
-
- **Server/SSH commands**: After running remote commands, ALWAYS present the results: service status, log errors, findings.
|
|
348
|
-
|
|
349
|
-
After completing multi-step tasks, suggest logical next steps (e.g. "You can run npm test to verify" or "Consider committing with /commit").
|
|
350
|
-
|
|
351
|
-
# Doing Tasks
|
|
352
|
-
|
|
353
|
-
- For non-trivial tasks, briefly state your approach before starting (1 sentence). This helps the user know what to expect.
|
|
354
|
-
- ALWAYS read code before modifying it. Never propose changes to code you haven't read.
|
|
355
|
-
- Prefer edit_file for targeted changes over write_file for full rewrites.
|
|
356
|
-
- Do not create new files unless absolutely necessary. Edit existing files instead.
|
|
357
|
-
- Use relative paths when possible.
|
|
358
|
-
- When blocked, try alternative approaches rather than retrying the same thing.
|
|
359
|
-
- Keep solutions simple. Only change what's directly requested or clearly necessary.
|
|
360
|
-
- Don't add features, refactoring, or "improvements" beyond what was asked.
|
|
361
|
-
- Don't add error handling for impossible scenarios. Only validate at system boundaries.
|
|
362
|
-
- Don't add docstrings/comments to code you didn't change.
|
|
363
|
-
- Don't create helpers or abstractions for one-time operations.
|
|
364
|
-
- Three similar lines of code is better than a premature abstraction.
|
|
365
|
-
- After completing work, give a brief summary of what was done and any important details. Don't just silently finish.
|
|
366
|
-
|
|
367
|
-
# Tool Strategy
|
|
368
|
-
|
|
369
|
-
- Use the RIGHT tool for the job:
|
|
370
|
-
- read_file to read files (not bash cat/head/tail)
|
|
371
|
-
- edit_file or patch_file to modify files (not bash sed/awk)
|
|
372
|
-
- glob to find files by name pattern (not bash find/ls)
|
|
373
|
-
- grep or search_files to search file contents (not bash grep)
|
|
374
|
-
- list_directory for directory structure (not bash ls/tree)
|
|
375
|
-
- Only use bash for actual shell operations: running tests, installing packages, git commands, build tools.
|
|
376
|
-
- Call multiple tools in parallel when they're independent (e.g. reading multiple files at once).
|
|
377
|
-
- For complex tasks with 3+ steps, create a task list with task_list first.
|
|
378
|
-
- Use spawn_agents for 2+ independent tasks that can run simultaneously.
|
|
379
|
-
- Good for: reading multiple files, analyzing separate modules.
|
|
380
|
-
- Bad for: tasks that depend on each other or modify the same file.
|
|
381
|
-
- Max 5 parallel agents.
|
|
382
|
-
${_buildModelRoutingGuide()}
|
|
383
|
-
|
|
384
|
-
# Edit Reliability (Critical)
|
|
385
|
-
|
|
386
|
-
- edit_file's old_text must match the file content EXACTLY — including whitespace, indentation, and newlines.
|
|
387
|
-
- Always read the file first (read_file) before editing to see the exact current content.
|
|
388
|
-
- If old_text is not found, the edit fails. Common causes:
|
|
389
|
-
- Indentation mismatch (tabs vs spaces, wrong level)
|
|
390
|
-
- Invisible characters or trailing whitespace
|
|
391
|
-
- Content changed since last read — read again before retrying.
|
|
392
|
-
- For multiple changes to the same file, prefer patch_file (single operation, atomic).
|
|
393
|
-
- Never guess file content. Always read first, then edit with the exact text you saw.
|
|
394
|
-
|
|
395
|
-
# Error Recovery
|
|
396
|
-
|
|
397
|
-
When a tool call returns ERROR:
|
|
398
|
-
- edit_file/patch_file "old_text not found": Read the file again with read_file. Compare your old_text with the actual content. The most common cause is stale content — the file changed since you last read it.
|
|
399
|
-
- bash non-zero exit: Read the error output. Fix the root cause (missing dependency, wrong path, syntax error) rather than retrying the same command.
|
|
400
|
-
- "File not found": Use glob or list_directory to find the correct path. Do not guess.
|
|
401
|
-
- After 2 failed attempts at the same operation, stop and explain the issue to the user.
|
|
402
|
-
|
|
403
|
-
# Git Workflow
|
|
404
|
-
|
|
405
|
-
- Before committing, review changes with git_diff. Write messages that explain WHY, not WHAT.
|
|
406
|
-
- Stage specific files rather than git add -A to avoid committing unrelated changes.
|
|
407
|
-
- Use conventional commits: type(scope): description (feat, fix, refactor, docs, test, chore).
|
|
408
|
-
- Branch naming: feat/, fix/, refactor/, docs/ prefixes with kebab-case.
|
|
409
|
-
- NEVER force-push, skip hooks (--no-verify), or amend published commits without explicit permission.
|
|
410
|
-
- When asked to commit: review diff, propose message, wait for approval, then execute.
|
|
411
|
-
|
|
412
|
-
# Safety & Reversibility
|
|
413
|
-
|
|
414
|
-
- Consider reversibility before acting. File reads and searches are safe. File writes and bash commands may not be.
|
|
415
|
-
- For hard-to-reverse actions (deleting files, force-pushing, dropping tables), confirm with the user first.
|
|
416
|
-
- NEVER read .env files, credentials, or SSH keys.
|
|
417
|
-
- NEVER run destructive commands (rm -rf /, mkfs, dd, etc.).
|
|
418
|
-
- Dangerous commands (git push, npm publish, sudo, rm -rf) require user confirmation.
|
|
419
|
-
- Prefer creating new git commits over amending. Never force-push without explicit permission.
|
|
420
|
-
- If you encounter unexpected state (unfamiliar files, branches), investigate before modifying.`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function clearConversation() {
|
|
424
|
-
conversationMessages = [];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function getConversationLength() {
|
|
428
|
-
return conversationMessages.length;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function getConversationMessages() {
|
|
432
|
-
return conversationMessages;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function setConversationMessages(messages) {
|
|
436
|
-
conversationMessages = messages;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Print résumé + follow-up suggestions after the agent loop.
|
|
441
|
-
* Only shown for multi-step responses (totalSteps >= 1).
|
|
442
|
-
*/
|
|
443
|
-
function _printResume(totalSteps, toolCounts, filesModified, filesRead, startTime) {
|
|
444
|
-
if (totalSteps < 1) return;
|
|
445
|
-
|
|
446
|
-
const totalTools = [...toolCounts.values()].reduce((a, b) => a + b, 0);
|
|
447
|
-
let resume = `── ${totalSteps} ${totalSteps === 1 ? 'step' : 'steps'} · ${totalTools} ${totalTools === 1 ? 'tool' : 'tools'}`;
|
|
448
|
-
if (filesModified.size > 0) {
|
|
449
|
-
resume += ` · ${filesModified.size} ${filesModified.size === 1 ? 'file' : 'files'} modified`;
|
|
450
|
-
}
|
|
451
|
-
if (startTime) {
|
|
452
|
-
const elapsed = Date.now() - startTime;
|
|
453
|
-
const secs = Math.round(elapsed / 1000);
|
|
454
|
-
resume += secs >= 60 ? ` · ${Math.floor(secs / 60)}m ${secs % 60}s` : ` · ${secs}s`;
|
|
455
|
-
}
|
|
456
|
-
resume += ' ──';
|
|
457
|
-
console.log(`\n${C.dim} ${resume}${C.reset}`);
|
|
458
|
-
|
|
459
|
-
// Follow-up suggestions based on what happened
|
|
460
|
-
if (filesModified.size > 0) {
|
|
461
|
-
console.log(`${C.dim} 💡 /diff · /commit · /undo${C.reset}`);
|
|
462
|
-
} else if (filesRead.size > 0 && totalSteps >= 2) {
|
|
463
|
-
console.log(`${C.dim} 💡 /save · /clear${C.reset}`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Process a single user input through the agentic loop.
|
|
469
|
-
* Maintains conversation state across calls.
|
|
470
|
-
*/
|
|
471
|
-
async function processInput(userInput) {
|
|
472
|
-
conversationMessages.push({ role: 'user', content: userInput });
|
|
473
|
-
|
|
474
|
-
const { setOnChange } = require('./tasks');
|
|
475
|
-
let taskProgress = null;
|
|
476
|
-
let cumulativeTokens = 0;
|
|
477
|
-
|
|
478
|
-
// Wire task onChange to create/update live task display
|
|
479
|
-
setOnChange((event, data) => {
|
|
480
|
-
if (event === 'create') {
|
|
481
|
-
if (taskProgress) taskProgress.stop();
|
|
482
|
-
taskProgress = new TaskProgress(data.name, data.tasks);
|
|
483
|
-
taskProgress.setStats({ tokens: cumulativeTokens });
|
|
484
|
-
taskProgress.start();
|
|
485
|
-
} else if (event === 'update' && taskProgress) {
|
|
486
|
-
taskProgress.updateTask(data.id, data.status);
|
|
487
|
-
} else if (event === 'clear') {
|
|
488
|
-
if (taskProgress) {
|
|
489
|
-
taskProgress.stop();
|
|
490
|
-
taskProgress = null;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
const systemPrompt = buildSystemPrompt();
|
|
496
|
-
const fullMessages = [{ role: 'system', content: systemPrompt }, ...conversationMessages];
|
|
497
|
-
|
|
498
|
-
// Pre-spinner: visible activity during fitToContext + getUsage (can take 50–5000ms with LLM compacting)
|
|
499
|
-
const preSpinner = new Spinner('Thinking...');
|
|
500
|
-
preSpinner.start();
|
|
501
|
-
|
|
502
|
-
// Context-aware compression: fit messages into context window
|
|
503
|
-
const { messages: fittedMessages, compressed, compacted, tokensRemoved } = await fitToContext(
|
|
504
|
-
fullMessages,
|
|
505
|
-
TOOL_DEFINITIONS
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
// Context budget warning
|
|
509
|
-
const usage = getUsage(fullMessages, TOOL_DEFINITIONS);
|
|
510
|
-
|
|
511
|
-
preSpinner.stop();
|
|
512
|
-
|
|
513
|
-
if (compacted) {
|
|
514
|
-
console.log(`${C.dim} [context compacted — summary (~${tokensRemoved} tokens freed)]${C.reset}`);
|
|
515
|
-
} else if (compressed) {
|
|
516
|
-
const pct = usage.limit > 0 ? Math.round((tokensRemoved / usage.limit) * 100) : 0;
|
|
517
|
-
console.log(`${C.dim} [context compressed — ~${tokensRemoved} tokens freed (${pct}%)]${C.reset}`);
|
|
518
|
-
}
|
|
519
|
-
if (usage.percentage > 85) {
|
|
520
|
-
console.log(`${C.yellow} ⚠ Context ${Math.round(usage.percentage)}% full — consider /clear or /save + start fresh${C.reset}`);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Use fitted messages for the API call, but keep fullMessages reference for appending
|
|
524
|
-
let apiMessages = fittedMessages;
|
|
525
|
-
let rateLimitRetries = 0;
|
|
526
|
-
let networkRetries = 0;
|
|
527
|
-
let staleRetries = 0;
|
|
528
|
-
let contextRetries = 0;
|
|
529
|
-
|
|
530
|
-
// ─── Stats tracking for résumé ───
|
|
531
|
-
let totalSteps = 0;
|
|
532
|
-
const toolCounts = new Map();
|
|
533
|
-
const filesModified = new Set();
|
|
534
|
-
const filesRead = new Set();
|
|
535
|
-
const startTime = Date.now();
|
|
536
|
-
|
|
537
|
-
let i;
|
|
538
|
-
for (i = 0; i < MAX_ITERATIONS; i++) {
|
|
539
|
-
// Check if aborted (Ctrl+C) at start of each iteration
|
|
540
|
-
const loopSignal = _getAbortSignal();
|
|
541
|
-
if (loopSignal?.aborted) break;
|
|
542
|
-
|
|
543
|
-
// Step indicator — deferred, only shown for tool iterations (matches résumé count)
|
|
544
|
-
let stepPrinted = true; // default: no marker (text-only iterations stay silent)
|
|
545
|
-
function printStepIfNeeded() {
|
|
546
|
-
if (!stepPrinted) {
|
|
547
|
-
console.log(`${C.dim} ── step ${totalSteps} ──${C.reset}`);
|
|
548
|
-
stepPrinted = true;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
let spinner = null;
|
|
553
|
-
if (taskProgress && taskProgress.isActive()) {
|
|
554
|
-
// Resume the live task display instead of a plain spinner
|
|
555
|
-
if (taskProgress._paused) taskProgress.resume();
|
|
556
|
-
} else if (!taskProgress) {
|
|
557
|
-
const spinnerText = totalSteps > 0 ? `Thinking... (step ${totalSteps + 1})` : 'Thinking...';
|
|
558
|
-
spinner = new Spinner(spinnerText);
|
|
559
|
-
spinner.start();
|
|
560
|
-
}
|
|
561
|
-
let firstToken = true;
|
|
562
|
-
let streamedText = '';
|
|
563
|
-
const stream = new StreamRenderer();
|
|
564
|
-
|
|
565
|
-
let result;
|
|
566
|
-
// Stale-stream detection: warn/abort if provider stops sending tokens
|
|
567
|
-
let lastTokenTime = Date.now();
|
|
568
|
-
let staleWarned = false;
|
|
569
|
-
const staleAbort = new AbortController();
|
|
570
|
-
const staleTimer = setInterval(() => {
|
|
571
|
-
const elapsed = Date.now() - lastTokenTime;
|
|
572
|
-
if (elapsed >= STALE_ABORT_MS) {
|
|
573
|
-
stream._clearCursorLine();
|
|
574
|
-
console.log(`${C.yellow} ⚠ Stream stale for ${Math.round(elapsed / 1000)}s — aborting and retrying${C.reset}`);
|
|
575
|
-
staleAbort.abort();
|
|
576
|
-
} else if (elapsed >= STALE_WARN_MS && !staleWarned) {
|
|
577
|
-
staleWarned = true;
|
|
578
|
-
stream._clearCursorLine();
|
|
579
|
-
console.log(`${C.yellow} ⚠ No tokens received for ${Math.round(elapsed / 1000)}s — waiting...${C.reset}`);
|
|
580
|
-
}
|
|
581
|
-
}, 5000);
|
|
582
|
-
try {
|
|
583
|
-
const allTools = filterToolsForModel([...TOOL_DEFINITIONS, ...getSkillToolDefinitions(), ...getMCPToolDefinitions()]);
|
|
584
|
-
const userSignal = _getAbortSignal();
|
|
585
|
-
// Combine user abort (Ctrl+C) and stale abort into one signal
|
|
586
|
-
const combinedAbort = new AbortController();
|
|
587
|
-
if (userSignal) userSignal.addEventListener('abort', () => combinedAbort.abort(), { once: true });
|
|
588
|
-
staleAbort.signal.addEventListener('abort', () => combinedAbort.abort(), { once: true });
|
|
589
|
-
|
|
590
|
-
result = await callStream(apiMessages, allTools, {
|
|
591
|
-
signal: combinedAbort.signal,
|
|
592
|
-
onToken: (text) => {
|
|
593
|
-
lastTokenTime = Date.now();
|
|
594
|
-
staleWarned = false;
|
|
595
|
-
if (firstToken) {
|
|
596
|
-
if (taskProgress && !taskProgress._paused) {
|
|
597
|
-
taskProgress.pause();
|
|
598
|
-
} else if (spinner) {
|
|
599
|
-
spinner.stop();
|
|
600
|
-
}
|
|
601
|
-
printStepIfNeeded();
|
|
602
|
-
stream.startCursor();
|
|
603
|
-
firstToken = false;
|
|
604
|
-
}
|
|
605
|
-
streamedText += text;
|
|
606
|
-
stream.push(text);
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
} catch (err) {
|
|
610
|
-
clearInterval(staleTimer);
|
|
611
|
-
if (taskProgress && !taskProgress._paused) taskProgress.pause();
|
|
612
|
-
if (spinner) spinner.stop();
|
|
613
|
-
stream.stopCursor();
|
|
614
|
-
|
|
615
|
-
// Stale abort → retry this iteration (with limit)
|
|
616
|
-
if (staleAbort.signal.aborted && !_getAbortSignal()?.aborted) {
|
|
617
|
-
staleRetries++;
|
|
618
|
-
if (staleRetries > MAX_STALE_RETRIES) {
|
|
619
|
-
console.log(`${C.red} ✗ Stream stale: max retries (${MAX_STALE_RETRIES}) exceeded. The model may be overloaded — try again or switch models.${C.reset}`);
|
|
620
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
621
|
-
setOnChange(null);
|
|
622
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
623
|
-
autoSave(conversationMessages);
|
|
624
|
-
break;
|
|
625
|
-
}
|
|
626
|
-
i--; // Don't count stale timeouts as iterations
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Abort errors (Ctrl+C) — break silently
|
|
631
|
-
if (err.name === 'AbortError' || err.name === 'CanceledError' ||
|
|
632
|
-
err.message?.includes('canceled') || err.message?.includes('aborted')) {
|
|
633
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
634
|
-
setOnChange(null);
|
|
635
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
636
|
-
autoSave(conversationMessages);
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// User-friendly error message (avoid raw stack traces/cryptic codes)
|
|
641
|
-
let userMessage = err.message;
|
|
642
|
-
if (err.code === 'ECONNREFUSED' || err.message.includes('ECONNREFUSED')) {
|
|
643
|
-
userMessage = 'Connection refused — please check your internet connection or API endpoint';
|
|
644
|
-
} else if (err.code === 'ENOTFOUND' || err.message.includes('ENOTFOUND')) {
|
|
645
|
-
userMessage = 'Network error — could not reach the API server. Please check your connection';
|
|
646
|
-
} else if (err.code === 'ETIMEDOUT' || err.message.includes('timeout')) {
|
|
647
|
-
userMessage = 'Request timed out — the API server took too long to respond. Please try again';
|
|
648
|
-
} else if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
|
649
|
-
userMessage = 'Authentication failed — please check your API key in the .env file';
|
|
650
|
-
} else if (err.message.includes('403') || err.message.includes('Forbidden')) {
|
|
651
|
-
userMessage = 'Access denied — your API key may not have permission for this model';
|
|
652
|
-
} else if (err.message.includes('400')) {
|
|
653
|
-
// Check if this is a context-too-long error before generic 400 handling
|
|
654
|
-
const errLower = (err.message || '').toLowerCase();
|
|
655
|
-
const isContextTooLong = errLower.includes('context') || errLower.includes('token') ||
|
|
656
|
-
errLower.includes('length') || errLower.includes('too long') || errLower.includes('too many');
|
|
657
|
-
if (isContextTooLong && contextRetries < 1) {
|
|
658
|
-
contextRetries++;
|
|
659
|
-
console.log(`${C.yellow} ⚠ Context too long — force-compressing and retrying...${C.reset}`);
|
|
660
|
-
const allTools = [...TOOL_DEFINITIONS, ...getSkillToolDefinitions(), ...getMCPToolDefinitions()];
|
|
661
|
-
const { messages: compressedMsgs, tokensRemoved } = forceCompress(apiMessages, allTools);
|
|
662
|
-
apiMessages = compressedMsgs;
|
|
663
|
-
console.log(`${C.dim} [force-compressed — ~${tokensRemoved} tokens freed]${C.reset}`);
|
|
664
|
-
i--;
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
if (isContextTooLong) {
|
|
668
|
-
userMessage = 'Context too long — force compression exhausted. Use /clear to start fresh';
|
|
669
|
-
} else {
|
|
670
|
-
userMessage = 'Bad request — the conversation may be too long or contain unsupported content. Try /clear and retry';
|
|
671
|
-
}
|
|
672
|
-
} else if (err.message.includes('500') || err.message.includes('502') || err.message.includes('503') || err.message.includes('504')) {
|
|
673
|
-
userMessage = 'API server error — the provider is experiencing issues. Please try again in a moment';
|
|
674
|
-
} else if (err.message.includes('fetch failed') || err.message.includes('fetch')) {
|
|
675
|
-
userMessage = 'Network request failed — please check your internet connection';
|
|
676
|
-
}
|
|
677
|
-
console.log(`${C.red} ✗ ${userMessage}${C.reset}`);
|
|
678
|
-
|
|
679
|
-
if (err.message.includes('429')) {
|
|
680
|
-
rateLimitRetries++;
|
|
681
|
-
if (rateLimitRetries > MAX_RATE_LIMIT_RETRIES) {
|
|
682
|
-
console.log(`${C.red} Rate limit: max retries (${MAX_RATE_LIMIT_RETRIES}) exceeded. Try again later or use /budget to check your limits.${C.reset}`);
|
|
683
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
684
|
-
setOnChange(null);
|
|
685
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
686
|
-
autoSave(conversationMessages);
|
|
687
|
-
break;
|
|
688
|
-
}
|
|
689
|
-
const delay = Math.min(10000 * Math.pow(2, rateLimitRetries - 1), 120000);
|
|
690
|
-
const waitSpinner = new Spinner(`Rate limit — waiting ${Math.round(delay / 1000)}s (retry ${rateLimitRetries}/${MAX_RATE_LIMIT_RETRIES})`);
|
|
691
|
-
waitSpinner.start();
|
|
692
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
693
|
-
waitSpinner.stop();
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Network/TLS errors — retry with backoff (don't burn iterations)
|
|
698
|
-
const isNetworkError = err.message.includes('socket disconnected') ||
|
|
699
|
-
err.message.includes('TLS') || err.message.includes('ECONNRESET') ||
|
|
700
|
-
err.message.includes('ECONNABORTED') || err.message.includes('ETIMEDOUT') ||
|
|
701
|
-
err.code === 'ECONNRESET' || err.code === 'ECONNABORTED';
|
|
702
|
-
if (isNetworkError) {
|
|
703
|
-
networkRetries++;
|
|
704
|
-
if (networkRetries > MAX_NETWORK_RETRIES) {
|
|
705
|
-
console.log(`${C.red} Network error: max retries (${MAX_NETWORK_RETRIES}) exceeded. Check your connection and try again.${C.reset}`);
|
|
706
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
707
|
-
setOnChange(null);
|
|
708
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
709
|
-
autoSave(conversationMessages);
|
|
710
|
-
break;
|
|
711
|
-
}
|
|
712
|
-
const delay = Math.min(2000 * Math.pow(2, networkRetries - 1), 30000);
|
|
713
|
-
const waitSpinner = new Spinner(`Network error — retrying in ${Math.round(delay / 1000)}s (${networkRetries}/${MAX_NETWORK_RETRIES})`);
|
|
714
|
-
waitSpinner.start();
|
|
715
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
716
|
-
waitSpinner.stop();
|
|
717
|
-
i--; // Don't count network errors as iterations
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Auto-save on error so conversation isn't lost
|
|
722
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
723
|
-
setOnChange(null);
|
|
724
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
725
|
-
autoSave(conversationMessages);
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
clearInterval(staleTimer);
|
|
730
|
-
|
|
731
|
-
if (firstToken) {
|
|
732
|
-
if (taskProgress && !taskProgress._paused) taskProgress.pause();
|
|
733
|
-
if (spinner) spinner.stop();
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// Reset retry counters on success
|
|
737
|
-
networkRetries = 0;
|
|
738
|
-
staleRetries = 0;
|
|
739
|
-
|
|
740
|
-
// Flush remaining stream buffer
|
|
741
|
-
if (streamedText) {
|
|
742
|
-
stream.flush();
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Track token usage for cost dashboard
|
|
746
|
-
if (result && result.usage) {
|
|
747
|
-
const { getActiveProviderName, getActiveModelId } = require('./providers/registry');
|
|
748
|
-
trackUsage(
|
|
749
|
-
getActiveProviderName(),
|
|
750
|
-
getActiveModelId(),
|
|
751
|
-
result.usage.prompt_tokens || 0,
|
|
752
|
-
result.usage.completion_tokens || 0
|
|
753
|
-
);
|
|
754
|
-
cumulativeTokens += (result.usage.prompt_tokens || 0) + (result.usage.completion_tokens || 0);
|
|
755
|
-
if (taskProgress) taskProgress.setStats({ tokens: cumulativeTokens });
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const { content, tool_calls } = result;
|
|
759
|
-
|
|
760
|
-
// Build assistant message for history
|
|
761
|
-
const assistantMsg = { role: 'assistant', content: content || '' };
|
|
762
|
-
if (tool_calls && tool_calls.length > 0) {
|
|
763
|
-
assistantMsg.tool_calls = tool_calls;
|
|
764
|
-
}
|
|
765
|
-
conversationMessages.push(assistantMsg);
|
|
766
|
-
apiMessages.push(assistantMsg);
|
|
767
|
-
|
|
768
|
-
// No tool calls → response complete (or nudge if empty after tools)
|
|
769
|
-
if (!tool_calls || tool_calls.length === 0) {
|
|
770
|
-
const hasText = (content || '').trim().length > 0 || streamedText.trim().length > 0;
|
|
771
|
-
// If we just ran tools but the LLM produced no text → nudge it to summarize
|
|
772
|
-
if (!hasText && totalSteps > 0 && i < MAX_ITERATIONS - 1) {
|
|
773
|
-
const nudge = { role: 'user', content: '[SYSTEM] You ran tools but produced no visible output. The user CANNOT see tool results — only your text. Please summarize your findings now.' };
|
|
774
|
-
apiMessages.push(nudge);
|
|
775
|
-
continue; // retry — don't count as a new step
|
|
776
|
-
}
|
|
777
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
778
|
-
setOnChange(null);
|
|
779
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
780
|
-
autoSave(conversationMessages);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// ─── Update stats ───
|
|
785
|
-
totalSteps++;
|
|
786
|
-
if (totalSteps > 1) stepPrinted = false; // enable deferred step marker for steps 2+
|
|
787
|
-
for (const tc of tool_calls) {
|
|
788
|
-
const name = tc.function.name;
|
|
789
|
-
toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// ─── Prepare all tool calls (parse, validate, permissions — sequential) ───
|
|
793
|
-
const prepared = [];
|
|
794
|
-
for (const tc of tool_calls) {
|
|
795
|
-
prepared.push(await prepareToolCall(tc));
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// ─── Execute with parallel batching (quiet mode: spinner + compact summaries) ───
|
|
799
|
-
const batchOpts = taskProgress ? { skipSpinner: true, skipSummaries: true } : {};
|
|
800
|
-
if (!batchOpts.skipSummaries) printStepIfNeeded();
|
|
801
|
-
// Resume TaskProgress animation during tool execution so the UI never looks frozen
|
|
802
|
-
if (taskProgress && taskProgress._paused) taskProgress.resume();
|
|
803
|
-
const toolMessages = await executeBatch(prepared, true, batchOpts);
|
|
804
|
-
|
|
805
|
-
// Track modified and read files
|
|
806
|
-
for (let j = 0; j < prepared.length; j++) {
|
|
807
|
-
const prep = prepared[j];
|
|
808
|
-
if (!prep.canExecute) continue;
|
|
809
|
-
const res = toolMessages[j].content;
|
|
810
|
-
const isOk = !res.startsWith('ERROR') && !res.includes('CANCELLED');
|
|
811
|
-
if (isOk && ['write_file', 'edit_file', 'patch_file'].includes(prep.fnName)) {
|
|
812
|
-
if (prep.args && prep.args.path) filesModified.add(prep.args.path);
|
|
813
|
-
}
|
|
814
|
-
if (isOk && prep.fnName === 'read_file') {
|
|
815
|
-
if (prep.args && prep.args.path) filesRead.add(prep.args.path);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
for (const toolMsg of toolMessages) {
|
|
820
|
-
conversationMessages.push(toolMsg);
|
|
821
|
-
apiMessages.push(toolMsg);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Only print résumé + max-iterations warning if the loop actually exhausted (not on break)
|
|
826
|
-
if (i >= MAX_ITERATIONS) {
|
|
827
|
-
if (taskProgress) { taskProgress.stop(); taskProgress = null; }
|
|
828
|
-
setOnChange(null);
|
|
829
|
-
_printResume(totalSteps, toolCounts, filesModified, filesRead, startTime);
|
|
830
|
-
autoSave(conversationMessages);
|
|
831
|
-
console.log(`\n${C.yellow}⚠ Max iterations (${MAX_ITERATIONS}) reached. Try ${C.bold}--max-turns ${MAX_ITERATIONS + 20}${C.reset}${C.yellow} or break into smaller steps.${C.reset}`);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
module.exports = { processInput, clearConversation, getConversationLength, getConversationMessages, setConversationMessages, setAbortSignalGetter, setMaxIterations };
|