nex-code 0.3.4 → 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 +34 -12
- 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 -425
- 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/sub-agent.js
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/sub-agent.js — Sub-Agent Runner
|
|
3
|
-
* Spawns parallel sub-agents with their own conversation contexts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { callStream, getActiveProviderName, getActiveModelId, getConfiguredProviders, getProvider, getActiveProvider, parseModelSpec } = require('./providers/registry');
|
|
7
|
-
const { parseToolArgs } = require('./ollama');
|
|
8
|
-
const { filterToolsForModel, getModelTier } = require('./tool-tiers');
|
|
9
|
-
const { trackUsage } = require('./costs');
|
|
10
|
-
const { MultiProgress, C } = require('./ui');
|
|
11
|
-
|
|
12
|
-
const MAX_SUB_ITERATIONS = 15;
|
|
13
|
-
const MAX_PARALLEL_AGENTS = 5;
|
|
14
|
-
const MAX_CHAT_RETRIES = 3;
|
|
15
|
-
|
|
16
|
-
// ─── File Locking ─────────────────────────────────────────────
|
|
17
|
-
// Map<filePath, agentId> — allows same agent to re-lock its own files
|
|
18
|
-
const lockedFiles = new Map();
|
|
19
|
-
|
|
20
|
-
function acquireLock(filePath, agentId) {
|
|
21
|
-
const owner = lockedFiles.get(filePath);
|
|
22
|
-
if (owner && owner !== agentId) return false;
|
|
23
|
-
lockedFiles.set(filePath, agentId);
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function releaseLock(filePath) {
|
|
28
|
-
lockedFiles.delete(filePath);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function clearAllLocks() {
|
|
32
|
-
lockedFiles.clear();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─── Retry Logic ─────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
function isRetryableError(err) {
|
|
38
|
-
const msg = err.message || '';
|
|
39
|
-
const code = err.code || '';
|
|
40
|
-
// Rate limit
|
|
41
|
-
if (msg.includes('429')) return true;
|
|
42
|
-
// Server errors
|
|
43
|
-
if (msg.includes('500') || msg.includes('502') || msg.includes('503') || msg.includes('504')) return true;
|
|
44
|
-
// Network errors
|
|
45
|
-
if (code === 'ECONNRESET' || code === 'ECONNABORTED' || code === 'ETIMEDOUT' || code === 'ECONNREFUSED') return true;
|
|
46
|
-
if (msg.includes('socket disconnected') || msg.includes('TLS') || msg.includes('ECONNRESET')) return true;
|
|
47
|
-
if (msg.includes('fetch failed') || msg.includes('ETIMEDOUT') || msg.includes('ENOTFOUND')) return true;
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function callWithRetry(messages, tools, options) {
|
|
52
|
-
let lastError;
|
|
53
|
-
for (let attempt = 0; attempt <= MAX_CHAT_RETRIES; attempt++) {
|
|
54
|
-
try {
|
|
55
|
-
// Use callStream (stream:true) — more reliable than callChat (stream:false)
|
|
56
|
-
// with Ollama Cloud. Silently collect the full response via no-op onToken.
|
|
57
|
-
return await callStream(messages, tools, { ...options, onToken: () => {} });
|
|
58
|
-
} catch (err) {
|
|
59
|
-
lastError = err;
|
|
60
|
-
if (attempt < MAX_CHAT_RETRIES && isRetryableError(err)) {
|
|
61
|
-
const delay = Math.min(2000 * Math.pow(2, attempt), 15000);
|
|
62
|
-
await new Promise(r => setTimeout(r, delay).unref());
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
throw err;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
throw lastError;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Tools that sub-agents should NOT have access to
|
|
72
|
-
const EXCLUDED_TOOLS = new Set(['ask_user', 'task_list', 'spawn_agents']);
|
|
73
|
-
|
|
74
|
-
// Tools that need file locking
|
|
75
|
-
const WRITE_TOOLS = new Set(['write_file', 'edit_file', 'patch_file']);
|
|
76
|
-
|
|
77
|
-
// ─── Task Classification + Model Routing ──────────────────────
|
|
78
|
-
|
|
79
|
-
const FAST_PATTERNS = /\b(read|summarize|search|find|list|check|count|inspect|scan)\b/i;
|
|
80
|
-
const HEAVY_PATTERNS = /\b(refactor|rewrite|implement|create|architect|design|generate|migrate)\b/i;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Classify a task description into a complexity tier.
|
|
84
|
-
* @param {string} taskDesc
|
|
85
|
-
* @returns {'essential'|'standard'|'full'}
|
|
86
|
-
*/
|
|
87
|
-
function classifyTask(taskDesc) {
|
|
88
|
-
if (HEAVY_PATTERNS.test(taskDesc)) return 'full';
|
|
89
|
-
if (FAST_PATTERNS.test(taskDesc)) return 'essential';
|
|
90
|
-
return 'standard';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Pick the best available model at a target tier.
|
|
95
|
-
* Strongly prefers the active provider — only uses others if active has no match.
|
|
96
|
-
* Skips the 'local' provider unless it is the active provider (local may not be running).
|
|
97
|
-
* @param {string} targetTier
|
|
98
|
-
* @returns {{ provider: string, model: string }|null}
|
|
99
|
-
*/
|
|
100
|
-
function pickModelForTier(targetTier) {
|
|
101
|
-
const configured = getConfiguredProviders();
|
|
102
|
-
const activeProv = getActiveProviderName();
|
|
103
|
-
|
|
104
|
-
// First pass: only the active provider
|
|
105
|
-
const activeEntry = configured.find(p => p.name === activeProv);
|
|
106
|
-
if (activeEntry) {
|
|
107
|
-
for (const m of activeEntry.models) {
|
|
108
|
-
if (getModelTier(m.id, activeEntry.name) === targetTier) {
|
|
109
|
-
return { provider: activeEntry.name, model: m.id };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Second pass: other providers (skip 'local' — it claims isConfigured but may not be running)
|
|
115
|
-
for (const p of configured) {
|
|
116
|
-
if (p.name === activeProv || p.name === 'local') continue;
|
|
117
|
-
for (const m of p.models) {
|
|
118
|
-
if (getModelTier(m.id, p.name) === targetTier) {
|
|
119
|
-
return { provider: p.name, model: m.id };
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Resolve the model for a sub-agent: explicit override or auto-routing.
|
|
129
|
-
* @param {{ task: string, model?: string }} agentDef
|
|
130
|
-
* @returns {{ provider: string|null, model: string|null, tier: string|null }}
|
|
131
|
-
*/
|
|
132
|
-
function resolveSubAgentModel(agentDef) {
|
|
133
|
-
// Explicit LLM override: parse "provider:model" format
|
|
134
|
-
if (agentDef.model) {
|
|
135
|
-
const { provider, model } = parseModelSpec(agentDef.model);
|
|
136
|
-
const prov = provider ? getProvider(provider) : getActiveProvider();
|
|
137
|
-
const provName = provider || getActiveProviderName();
|
|
138
|
-
if (prov && prov.isConfigured() && (prov.getModel(model) || provName === 'local')) {
|
|
139
|
-
const tier = getModelTier(model, provName);
|
|
140
|
-
return { provider: provName, model, tier };
|
|
141
|
-
}
|
|
142
|
-
// Invalid spec → fall through to auto-routing
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Auto-routing: always use the active model (most reliable for tool calling)
|
|
146
|
-
// but apply the task's complexity tier for tool filtering (simpler tasks get fewer tools)
|
|
147
|
-
const targetTier = classifyTask(agentDef.task);
|
|
148
|
-
const activeProviderName = getActiveProviderName();
|
|
149
|
-
const activeModel = getActiveModelId();
|
|
150
|
-
|
|
151
|
-
if (activeProviderName && activeModel) {
|
|
152
|
-
return { provider: activeProviderName, model: activeModel, tier: targetTier };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Ultimate fallback: use global defaults
|
|
156
|
-
return { provider: null, model: null, tier: null };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Run a single sub-agent to completion.
|
|
161
|
-
* @param {{ task: string, context?: string, max_iterations?: number }} agentDef
|
|
162
|
-
* @param {{ onUpdate?: (status: string) => void }} callbacks
|
|
163
|
-
* @returns {{ task: string, status: 'done'|'failed', result: string, toolsUsed: string[], tokensUsed: { input: number, output: number } }}
|
|
164
|
-
*/
|
|
165
|
-
async function runSubAgent(agentDef, callbacks = {}) {
|
|
166
|
-
const maxIter = Math.min(agentDef.max_iterations || 10, MAX_SUB_ITERATIONS);
|
|
167
|
-
const agentId = `sub-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
168
|
-
const toolsUsed = [];
|
|
169
|
-
const tokensUsed = { input: 0, output: 0 };
|
|
170
|
-
const locksHeld = new Set();
|
|
171
|
-
|
|
172
|
-
const systemPrompt = `You are a focused sub-agent. Complete this specific task efficiently.
|
|
173
|
-
|
|
174
|
-
TASK: ${agentDef.task}
|
|
175
|
-
${agentDef.context ? `\nCONTEXT: ${agentDef.context}` : ''}
|
|
176
|
-
|
|
177
|
-
WORKING DIRECTORY: ${process.cwd()}
|
|
178
|
-
|
|
179
|
-
RULES:
|
|
180
|
-
- Focus only on your assigned task. Be concise and efficient.
|
|
181
|
-
- When done, respond with a clear summary of what you did and the result.
|
|
182
|
-
- Do not ask questions — make reasonable decisions.
|
|
183
|
-
- Use relative paths when possible.
|
|
184
|
-
|
|
185
|
-
TOOL STRATEGY:
|
|
186
|
-
- Use read_file to read files (not bash cat). Use edit_file/patch_file to modify (not bash sed).
|
|
187
|
-
- Use glob to find files by name. Use grep to search contents. Only use bash for shell operations.
|
|
188
|
-
- ALWAYS read a file with read_file before editing it. edit_file old_text must match exactly.
|
|
189
|
-
|
|
190
|
-
ERROR RECOVERY:
|
|
191
|
-
- If edit_file fails with "old_text not found": read the file again, compare, and retry with exact text.
|
|
192
|
-
- If bash fails: read the error, fix the root cause, then retry.
|
|
193
|
-
- After 2 failed attempts at the same operation, summarize the issue and stop.`;
|
|
194
|
-
|
|
195
|
-
const messages = [{ role: 'system', content: systemPrompt }];
|
|
196
|
-
messages.push({ role: 'user', content: agentDef.task });
|
|
197
|
-
|
|
198
|
-
// Resolve model routing
|
|
199
|
-
const routing = resolveSubAgentModel(agentDef);
|
|
200
|
-
const agentProvider = routing.provider;
|
|
201
|
-
const agentModel = routing.model;
|
|
202
|
-
const agentTier = routing.tier;
|
|
203
|
-
|
|
204
|
-
// Lazy require to avoid circular dependency (tools.js ↔ sub-agent.js)
|
|
205
|
-
const { TOOL_DEFINITIONS, executeTool } = require('./tools');
|
|
206
|
-
|
|
207
|
-
// Filter tools: exclude interactive/meta tools, apply tier override
|
|
208
|
-
const availableTools = filterToolsForModel(
|
|
209
|
-
TOOL_DEFINITIONS.filter(t => !EXCLUDED_TOOLS.has(t.function.name)),
|
|
210
|
-
agentTier
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// Build options for streaming call (callStream uses active provider automatically)
|
|
214
|
-
const chatOptions = {};
|
|
215
|
-
if (agentModel) chatOptions.model = agentModel;
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
for (let i = 0; i < maxIter; i++) {
|
|
219
|
-
const result = await callWithRetry(messages, availableTools, chatOptions);
|
|
220
|
-
|
|
221
|
-
// Guard against null/undefined responses
|
|
222
|
-
if (!result || typeof result !== 'object') {
|
|
223
|
-
throw new Error('Empty or invalid response from provider');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Track tokens
|
|
227
|
-
if (result.usage) {
|
|
228
|
-
const inputT = result.usage.prompt_tokens || 0;
|
|
229
|
-
const outputT = result.usage.completion_tokens || 0;
|
|
230
|
-
tokensUsed.input += inputT;
|
|
231
|
-
tokensUsed.output += outputT;
|
|
232
|
-
const trackProvider = agentProvider || getActiveProviderName();
|
|
233
|
-
const trackModel = agentModel || getActiveModelId();
|
|
234
|
-
trackUsage(trackProvider, trackModel, inputT, outputT);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const content = result.content || '';
|
|
238
|
-
const tool_calls = result.tool_calls;
|
|
239
|
-
|
|
240
|
-
// Build assistant message
|
|
241
|
-
const assistantMsg = { role: 'assistant', content: content || '' };
|
|
242
|
-
if (tool_calls && tool_calls.length > 0) {
|
|
243
|
-
assistantMsg.tool_calls = tool_calls;
|
|
244
|
-
}
|
|
245
|
-
messages.push(assistantMsg);
|
|
246
|
-
|
|
247
|
-
// No tool calls → agent is done
|
|
248
|
-
if (!tool_calls || tool_calls.length === 0) {
|
|
249
|
-
// Release all locks
|
|
250
|
-
for (const fp of locksHeld) releaseLock(fp);
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
task: agentDef.task,
|
|
254
|
-
status: 'done',
|
|
255
|
-
result: content || '(no response)',
|
|
256
|
-
toolsUsed,
|
|
257
|
-
tokensUsed,
|
|
258
|
-
modelSpec: agentProvider && agentModel ? `${agentProvider}:${agentModel}` : null,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Execute tool calls sequentially within sub-agent
|
|
263
|
-
for (const tc of tool_calls) {
|
|
264
|
-
const fnName = tc.function.name;
|
|
265
|
-
const args = parseToolArgs(tc.function.arguments);
|
|
266
|
-
const callId = tc.id || `sub-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
267
|
-
|
|
268
|
-
if (!args) {
|
|
269
|
-
messages.push({
|
|
270
|
-
role: 'tool',
|
|
271
|
-
content: `ERROR: Malformed tool arguments for ${fnName}`,
|
|
272
|
-
tool_call_id: callId,
|
|
273
|
-
});
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// File locking for write tools
|
|
278
|
-
if (WRITE_TOOLS.has(fnName) && args.path) {
|
|
279
|
-
const path = require('path');
|
|
280
|
-
const fp = path.isAbsolute(args.path) ? args.path : path.resolve(process.cwd(), args.path);
|
|
281
|
-
if (!acquireLock(fp, agentId)) {
|
|
282
|
-
messages.push({
|
|
283
|
-
role: 'tool',
|
|
284
|
-
content: `ERROR: File '${args.path}' is locked by another sub-agent. Try a different approach or skip this file.`,
|
|
285
|
-
tool_call_id: callId,
|
|
286
|
-
});
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
locksHeld.add(fp);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
toolsUsed.push(fnName);
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
const toolResult = await executeTool(fnName, args, { autoConfirm: true, silent: true });
|
|
296
|
-
const safeResult = String(toolResult ?? '');
|
|
297
|
-
const truncated = safeResult.length > 20000
|
|
298
|
-
? safeResult.substring(0, 20000) + `\n...(truncated)`
|
|
299
|
-
: safeResult;
|
|
300
|
-
|
|
301
|
-
messages.push({ role: 'tool', content: truncated, tool_call_id: callId });
|
|
302
|
-
} catch (err) {
|
|
303
|
-
messages.push({
|
|
304
|
-
role: 'tool',
|
|
305
|
-
content: `ERROR: ${err.message}`,
|
|
306
|
-
tool_call_id: callId,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (callbacks.onUpdate) {
|
|
312
|
-
callbacks.onUpdate(`step ${i + 1}/${maxIter}`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Max iterations reached
|
|
317
|
-
for (const fp of locksHeld) releaseLock(fp);
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
task: agentDef.task,
|
|
321
|
-
status: 'done',
|
|
322
|
-
result: messages[messages.length - 1]?.content || '(max iterations reached)',
|
|
323
|
-
toolsUsed,
|
|
324
|
-
tokensUsed,
|
|
325
|
-
modelSpec: agentProvider && agentModel ? `${agentProvider}:${agentModel}` : null,
|
|
326
|
-
};
|
|
327
|
-
} catch (err) {
|
|
328
|
-
// Release locks on error
|
|
329
|
-
for (const fp of locksHeld) releaseLock(fp);
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
task: agentDef.task,
|
|
333
|
-
status: 'failed',
|
|
334
|
-
result: `Error: ${err.message}`,
|
|
335
|
-
toolsUsed,
|
|
336
|
-
tokensUsed,
|
|
337
|
-
modelSpec: agentProvider && agentModel ? `${agentProvider}:${agentModel}` : null,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Execute spawn_agents tool: run multiple sub-agents in parallel.
|
|
344
|
-
* @param {{ agents: Array<{ task: string, context?: string, max_iterations?: number }> }} args
|
|
345
|
-
* @returns {string} Formatted results for the parent LLM
|
|
346
|
-
*/
|
|
347
|
-
async function executeSpawnAgents(args) {
|
|
348
|
-
// Normalize: LLMs may use "prompt"/"description"/"name" instead of "task"
|
|
349
|
-
// Strip "model" — LLMs hallucinate model names (e.g. "llama3") that cause 404s.
|
|
350
|
-
// Sub-agents always use the active model.
|
|
351
|
-
const agents = (args.agents || []).map(a => {
|
|
352
|
-
const agent = {
|
|
353
|
-
...a,
|
|
354
|
-
task: a.task || a.prompt || a.description || a.name || '(no task)',
|
|
355
|
-
};
|
|
356
|
-
delete agent.model;
|
|
357
|
-
return agent;
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
if (agents.length === 0) return 'ERROR: No agents specified';
|
|
361
|
-
if (agents.length > MAX_PARALLEL_AGENTS) {
|
|
362
|
-
return `ERROR: Max ${MAX_PARALLEL_AGENTS} parallel agents allowed, got ${agents.length}`;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const labels = agents.map((a, i) => {
|
|
366
|
-
const task = String(a.task || '');
|
|
367
|
-
return `Agent ${i + 1}: ${task.substring(0, 50)}${task.length > 50 ? '...' : ''}`;
|
|
368
|
-
});
|
|
369
|
-
const progress = new MultiProgress(labels);
|
|
370
|
-
progress.start();
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
const promises = agents.map((agentDef, idx) =>
|
|
374
|
-
runSubAgent(agentDef, {
|
|
375
|
-
onUpdate: () => {}, // progress is already showing spinner
|
|
376
|
-
}).then(result => {
|
|
377
|
-
progress.update(idx, result.status === 'done' ? 'done' : 'error');
|
|
378
|
-
return result;
|
|
379
|
-
}).catch(err => {
|
|
380
|
-
progress.update(idx, 'error');
|
|
381
|
-
return {
|
|
382
|
-
task: agentDef.task,
|
|
383
|
-
status: 'failed',
|
|
384
|
-
result: `Error: ${err.message}`,
|
|
385
|
-
toolsUsed: [],
|
|
386
|
-
tokensUsed: { input: 0, output: 0 },
|
|
387
|
-
};
|
|
388
|
-
})
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
const results = await Promise.all(promises);
|
|
392
|
-
progress.stop();
|
|
393
|
-
|
|
394
|
-
// Clear all locks after all agents finish
|
|
395
|
-
clearAllLocks();
|
|
396
|
-
|
|
397
|
-
// Format results for the parent LLM
|
|
398
|
-
const lines = ['Sub-agent results:', ''];
|
|
399
|
-
let totalInput = 0;
|
|
400
|
-
let totalOutput = 0;
|
|
401
|
-
|
|
402
|
-
for (let i = 0; i < results.length; i++) {
|
|
403
|
-
const r = results[i];
|
|
404
|
-
const statusIcon = r.status === 'done' ? '✓' : '✗';
|
|
405
|
-
const modelLabel = r.modelSpec ? ` [${r.modelSpec}]` : '';
|
|
406
|
-
lines.push(`${statusIcon} Agent ${i + 1}${modelLabel}: ${r.task}`);
|
|
407
|
-
lines.push(` Status: ${r.status}`);
|
|
408
|
-
lines.push(` Tools used: ${r.toolsUsed.length > 0 ? r.toolsUsed.join(', ') : 'none'}`);
|
|
409
|
-
lines.push(` Result: ${r.result}`);
|
|
410
|
-
lines.push('');
|
|
411
|
-
totalInput += r.tokensUsed.input;
|
|
412
|
-
totalOutput += r.tokensUsed.output;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
lines.push(`Total sub-agent tokens: ${totalInput} input + ${totalOutput} output`);
|
|
416
|
-
|
|
417
|
-
return lines.join('\n');
|
|
418
|
-
} catch (err) {
|
|
419
|
-
progress.stop();
|
|
420
|
-
clearAllLocks();
|
|
421
|
-
return `ERROR: Sub-agent execution failed: ${err.message}`;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
module.exports = { runSubAgent, executeSpawnAgents, clearAllLocks, classifyTask, pickModelForTier, resolveSubAgentModel, isRetryableError, callWithRetry };
|
package/cli/tasks.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/tasks.js — Task List Management
|
|
3
|
-
* Create, update, and render task lists for complex multi-step operations.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { C } = require('./ui');
|
|
7
|
-
|
|
8
|
-
// Active task list state
|
|
9
|
-
let taskListName = '';
|
|
10
|
-
let tasks = [];
|
|
11
|
-
let taskIdCounter = 0;
|
|
12
|
-
|
|
13
|
-
// onChange callback for live display integration
|
|
14
|
-
let _onChange = null;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Register a callback fired on task list changes.
|
|
18
|
-
* @param {function|null} fn - Callback: fn(event, data) where event is 'create'|'update'|'clear'
|
|
19
|
-
*/
|
|
20
|
-
function setOnChange(fn) {
|
|
21
|
-
_onChange = fn;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create a new task list with a name and array of task descriptions.
|
|
26
|
-
* @param {string} name - Name/title for the task list
|
|
27
|
-
* @param {Array<{description: string, depends_on?: string[]}>} taskDefs
|
|
28
|
-
* @returns {Array<object>} Created tasks
|
|
29
|
-
*/
|
|
30
|
-
function createTasks(name, taskDefs) {
|
|
31
|
-
taskListName = name;
|
|
32
|
-
tasks = [];
|
|
33
|
-
taskIdCounter = 0;
|
|
34
|
-
|
|
35
|
-
for (const def of taskDefs) {
|
|
36
|
-
taskIdCounter++;
|
|
37
|
-
const id = `t${taskIdCounter}`;
|
|
38
|
-
tasks.push({
|
|
39
|
-
id,
|
|
40
|
-
description: def.description || def.title || def.name || def.task || `Task ${taskIdCounter}`,
|
|
41
|
-
status: 'pending',
|
|
42
|
-
dependsOn: def.depends_on || [],
|
|
43
|
-
result: null,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const snapshot = tasks.map(t => ({ ...t }));
|
|
48
|
-
if (_onChange) _onChange('create', { name, tasks: snapshot });
|
|
49
|
-
return snapshot;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Update a task's status and optionally set its result.
|
|
54
|
-
* @param {string} id - Task ID (e.g. 't1')
|
|
55
|
-
* @param {string} status - 'in_progress' | 'done' | 'failed'
|
|
56
|
-
* @param {string} [result] - Summary of the result
|
|
57
|
-
* @returns {object|null} Updated task or null if not found
|
|
58
|
-
*/
|
|
59
|
-
function updateTask(id, status, result) {
|
|
60
|
-
const task = tasks.find(t => t.id === id);
|
|
61
|
-
if (!task) return null;
|
|
62
|
-
|
|
63
|
-
task.status = status;
|
|
64
|
-
if (result !== undefined) task.result = result;
|
|
65
|
-
|
|
66
|
-
if (_onChange) _onChange('update', { id, status, result });
|
|
67
|
-
return { ...task };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get the current task list.
|
|
72
|
-
* @returns {{ name: string, tasks: Array<object> }}
|
|
73
|
-
*/
|
|
74
|
-
function getTaskList() {
|
|
75
|
-
return {
|
|
76
|
-
name: taskListName,
|
|
77
|
-
tasks: tasks.map(t => ({ ...t })),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Clear all tasks.
|
|
83
|
-
*/
|
|
84
|
-
function clearTasks() {
|
|
85
|
-
taskListName = '';
|
|
86
|
-
tasks = [];
|
|
87
|
-
taskIdCounter = 0;
|
|
88
|
-
if (_onChange) _onChange('clear', {});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get tasks that are ready to run (pending + all dependencies done).
|
|
93
|
-
* @returns {Array<object>}
|
|
94
|
-
*/
|
|
95
|
-
function getReadyTasks() {
|
|
96
|
-
return tasks.filter(t => {
|
|
97
|
-
if (t.status !== 'pending') return false;
|
|
98
|
-
if (t.dependsOn.length === 0) return true;
|
|
99
|
-
return t.dependsOn.every(depId => {
|
|
100
|
-
const dep = tasks.find(d => d.id === depId);
|
|
101
|
-
return dep && dep.status === 'done';
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Render the task list for terminal display.
|
|
108
|
-
* @returns {string}
|
|
109
|
-
*/
|
|
110
|
-
function renderTaskList() {
|
|
111
|
-
if (tasks.length === 0) return `${C.dim}No active tasks${C.reset}`;
|
|
112
|
-
|
|
113
|
-
const lines = [];
|
|
114
|
-
|
|
115
|
-
if (taskListName) {
|
|
116
|
-
lines.push(` ${C.bold}${C.cyan}Tasks: ${taskListName}${C.reset}`);
|
|
117
|
-
lines.push(` ${C.dim}${'─'.repeat(40)}${C.reset}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
for (const t of tasks) {
|
|
121
|
-
let icon, color;
|
|
122
|
-
switch (t.status) {
|
|
123
|
-
case 'done':
|
|
124
|
-
icon = '✓';
|
|
125
|
-
color = C.green;
|
|
126
|
-
break;
|
|
127
|
-
case 'in_progress':
|
|
128
|
-
icon = '→';
|
|
129
|
-
color = C.cyan;
|
|
130
|
-
break;
|
|
131
|
-
case 'failed':
|
|
132
|
-
icon = '✗';
|
|
133
|
-
color = C.red;
|
|
134
|
-
break;
|
|
135
|
-
default:
|
|
136
|
-
icon = '·';
|
|
137
|
-
color = C.dim;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const deps = t.dependsOn.length > 0 ? ` ${C.dim}(after: ${t.dependsOn.join(', ')})${C.reset}` : '';
|
|
141
|
-
const status = `[${t.status}]`;
|
|
142
|
-
const desc = t.description.length > 50 ? t.description.substring(0, 47) + '...' : t.description;
|
|
143
|
-
|
|
144
|
-
lines.push(` ${color}${icon}${C.reset} ${C.bold}${t.id}${C.reset} ${desc.padEnd(40)} ${color}${status}${C.reset}${deps}`);
|
|
145
|
-
|
|
146
|
-
if (t.result && t.status === 'done') {
|
|
147
|
-
const shortResult = t.result.length > 60 ? t.result.substring(0, 57) + '...' : t.result;
|
|
148
|
-
lines.push(` ${C.dim}→ ${shortResult}${C.reset}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Summary
|
|
153
|
-
const done = tasks.filter(t => t.status === 'done').length;
|
|
154
|
-
const failed = tasks.filter(t => t.status === 'failed').length;
|
|
155
|
-
const total = tasks.length;
|
|
156
|
-
lines.push(` ${C.dim}${'─'.repeat(40)}${C.reset}`);
|
|
157
|
-
lines.push(` ${C.dim}${done}/${total} done${failed > 0 ? `, ${failed} failed` : ''}${C.reset}`);
|
|
158
|
-
|
|
159
|
-
return lines.join('\n');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Check if there are active (non-done/failed) tasks.
|
|
164
|
-
* @returns {boolean}
|
|
165
|
-
*/
|
|
166
|
-
function hasActiveTasks() {
|
|
167
|
-
return tasks.length > 0 && tasks.some(t => t.status === 'pending' || t.status === 'in_progress');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
module.exports = {
|
|
171
|
-
createTasks,
|
|
172
|
-
updateTask,
|
|
173
|
-
getTaskList,
|
|
174
|
-
clearTasks,
|
|
175
|
-
getReadyTasks,
|
|
176
|
-
renderTaskList,
|
|
177
|
-
setOnChange,
|
|
178
|
-
hasActiveTasks,
|
|
179
|
-
};
|