neuro-cli 4.1.0
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/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Parallel Agent Manager
|
|
3
|
+
// Parallel execution of multiple agents with shared state,
|
|
4
|
+
// resource limits, and coordination — inspired by Claude Code
|
|
5
|
+
// sub-agents and Cursor CLI parallel agents.
|
|
6
|
+
// Uses only Node.js built-in modules.
|
|
7
|
+
// ============================================================
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
11
|
+
import { tmpdir } from 'os';
|
|
12
|
+
// ---- Constants ----
|
|
13
|
+
const DEFAULT_MAX_CONCURRENT = 5;
|
|
14
|
+
const DEFAULT_MAX_ITERATIONS = 50;
|
|
15
|
+
const DEFAULT_MAX_TOKENS = 200_000;
|
|
16
|
+
const PRIORITY_ORDER = { high: 3, normal: 2, low: 1 };
|
|
17
|
+
const SANDBOX_BASE_DIR = join(tmpdir(), 'neuro-cli-agents');
|
|
18
|
+
// ============================================================
|
|
19
|
+
// ParallelAgentManager
|
|
20
|
+
// ============================================================
|
|
21
|
+
export class ParallelAgentManager extends EventEmitter {
|
|
22
|
+
agents = new Map();
|
|
23
|
+
maxConcurrent;
|
|
24
|
+
globalSharedState;
|
|
25
|
+
onAgentCompleteCallbacks = [];
|
|
26
|
+
onAgentErrorCallbacks = [];
|
|
27
|
+
/** Queue of agent IDs waiting to run when a slot opens. */
|
|
28
|
+
pendingQueue = [];
|
|
29
|
+
constructor(options) {
|
|
30
|
+
super();
|
|
31
|
+
this.maxConcurrent = options?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
32
|
+
this.globalSharedState = options?.sharedState ?? {};
|
|
33
|
+
}
|
|
34
|
+
// ---- Public API ----
|
|
35
|
+
/**
|
|
36
|
+
* Spawn a new parallel agent. If `config.autoStart` is true the agent
|
|
37
|
+
* begins running immediately (subject to the concurrency limit).
|
|
38
|
+
* Returns the agent ID.
|
|
39
|
+
*/
|
|
40
|
+
spawn(config) {
|
|
41
|
+
if (this.agents.has(config.id)) {
|
|
42
|
+
throw new Error(`Agent with id "${config.id}" already exists`);
|
|
43
|
+
}
|
|
44
|
+
const sandboxDir = join(SANDBOX_BASE_DIR, config.id);
|
|
45
|
+
this.ensureDir(sandboxDir);
|
|
46
|
+
// Merge global + per-agent shared state (per-agent takes precedence)
|
|
47
|
+
const mergedSharedState = {
|
|
48
|
+
...this.globalSharedState,
|
|
49
|
+
...config.sharedState,
|
|
50
|
+
};
|
|
51
|
+
const state = {
|
|
52
|
+
id: config.id,
|
|
53
|
+
name: config.name,
|
|
54
|
+
status: 'spawning',
|
|
55
|
+
startedAt: Date.now(),
|
|
56
|
+
iterations: 0,
|
|
57
|
+
tokensUsed: 0,
|
|
58
|
+
progress: 0,
|
|
59
|
+
};
|
|
60
|
+
// Create the completion promise wiring up-front
|
|
61
|
+
let completionResolve = null;
|
|
62
|
+
let completionReject = null;
|
|
63
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
64
|
+
completionResolve = resolve;
|
|
65
|
+
completionReject = reject;
|
|
66
|
+
});
|
|
67
|
+
const record = {
|
|
68
|
+
config,
|
|
69
|
+
state,
|
|
70
|
+
abortController: new AbortController(),
|
|
71
|
+
sharedState: mergedSharedState,
|
|
72
|
+
sandboxDir,
|
|
73
|
+
completionPromise,
|
|
74
|
+
completionResolve,
|
|
75
|
+
completionReject,
|
|
76
|
+
};
|
|
77
|
+
this.agents.set(config.id, record);
|
|
78
|
+
if (config.autoStart) {
|
|
79
|
+
this.scheduleStart(config.id);
|
|
80
|
+
}
|
|
81
|
+
return config.id;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Cancel a running (or pending) agent.
|
|
85
|
+
*/
|
|
86
|
+
cancel(agentId) {
|
|
87
|
+
const record = this.agents.get(agentId);
|
|
88
|
+
if (!record)
|
|
89
|
+
return false;
|
|
90
|
+
if (record.state.status === 'completed' || record.state.status === 'failed' || record.state.status === 'cancelled') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
record.abortController.abort();
|
|
94
|
+
record.state.status = 'cancelled';
|
|
95
|
+
record.state.completedAt = Date.now();
|
|
96
|
+
record.state.progress = record.state.progress; // freeze progress
|
|
97
|
+
// Remove from pending queue if applicable
|
|
98
|
+
this.pendingQueue = this.pendingQueue.filter((id) => id !== agentId);
|
|
99
|
+
// Reject the completion promise so waiters unblock
|
|
100
|
+
record.completionReject?.(new Error(`Agent "${agentId}" was cancelled`));
|
|
101
|
+
this.emit('agent:cancelled', agentId, record.state);
|
|
102
|
+
this.tryStartPending();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Cancel all running and pending agents.
|
|
107
|
+
*/
|
|
108
|
+
cancelAll() {
|
|
109
|
+
let count = 0;
|
|
110
|
+
const ids = Array.from(this.agents.keys());
|
|
111
|
+
for (const id of ids) {
|
|
112
|
+
if (this.cancel(id))
|
|
113
|
+
count++;
|
|
114
|
+
}
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get the state of a specific agent.
|
|
119
|
+
*/
|
|
120
|
+
getStatus(agentId) {
|
|
121
|
+
const record = this.agents.get(agentId);
|
|
122
|
+
if (!record)
|
|
123
|
+
return null;
|
|
124
|
+
return { ...record.state };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* List all currently running agents (status === 'running' | 'spawning').
|
|
128
|
+
*/
|
|
129
|
+
listActive() {
|
|
130
|
+
const active = [];
|
|
131
|
+
const records = Array.from(this.agents.values());
|
|
132
|
+
for (const record of records) {
|
|
133
|
+
if (record.state.status === 'running' || record.state.status === 'spawning') {
|
|
134
|
+
active.push({ ...record.state });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return active.sort((a, b) => b.startedAt - a.startedAt);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* List all agents including completed ones.
|
|
141
|
+
*/
|
|
142
|
+
listAll() {
|
|
143
|
+
const all = [];
|
|
144
|
+
const records = Array.from(this.agents.values());
|
|
145
|
+
for (const record of records) {
|
|
146
|
+
all.push({ ...record.state });
|
|
147
|
+
}
|
|
148
|
+
return all.sort((a, b) => b.startedAt - a.startedAt);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Wait for a specific agent to complete and return its result.
|
|
152
|
+
* Rejects if the agent was cancelled or failed.
|
|
153
|
+
*/
|
|
154
|
+
async waitFor(agentId) {
|
|
155
|
+
const record = this.agents.get(agentId);
|
|
156
|
+
if (!record) {
|
|
157
|
+
throw new Error(`Agent "${agentId}" not found`);
|
|
158
|
+
}
|
|
159
|
+
// If already completed, return immediately
|
|
160
|
+
if (record.state.status === 'completed') {
|
|
161
|
+
return record.state.result ?? '';
|
|
162
|
+
}
|
|
163
|
+
// If already failed/cancelled, throw
|
|
164
|
+
if (record.state.status === 'failed' || record.state.status === 'cancelled') {
|
|
165
|
+
throw new Error(record.state.error ?? `Agent "${agentId}" ${record.state.status}`);
|
|
166
|
+
}
|
|
167
|
+
return record.completionPromise;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wait for all currently running agents to finish.
|
|
171
|
+
* Returns a map of agentId → result (or error message on failure).
|
|
172
|
+
*/
|
|
173
|
+
async waitForAll() {
|
|
174
|
+
const runningIds = [];
|
|
175
|
+
const records = Array.from(this.agents.values());
|
|
176
|
+
for (const record of records) {
|
|
177
|
+
if (record.state.status === 'running' ||
|
|
178
|
+
record.state.status === 'spawning') {
|
|
179
|
+
runningIds.push(record.state.id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const results = new Map();
|
|
183
|
+
const promises = runningIds.map(async (id) => {
|
|
184
|
+
try {
|
|
185
|
+
const result = await this.waitFor(id);
|
|
186
|
+
results.set(id, { result });
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
results.set(id, { error: err instanceof Error ? err.message : String(err) });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
await Promise.allSettled(promises);
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get the result of a completed agent.
|
|
197
|
+
*/
|
|
198
|
+
getResult(agentId) {
|
|
199
|
+
const record = this.agents.get(agentId);
|
|
200
|
+
if (!record || record.state.status !== 'completed')
|
|
201
|
+
return null;
|
|
202
|
+
return record.state.result ?? null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Broadcast a message to all running agents' shared state.
|
|
206
|
+
* The message is stored under `sharedState.__broadcasts`.
|
|
207
|
+
*/
|
|
208
|
+
broadcastMessage(message) {
|
|
209
|
+
let delivered = 0;
|
|
210
|
+
const records = Array.from(this.agents.values());
|
|
211
|
+
for (const record of records) {
|
|
212
|
+
if (record.state.status !== 'running')
|
|
213
|
+
continue;
|
|
214
|
+
const broadcasts = record.sharedState.__broadcasts ?? [];
|
|
215
|
+
broadcasts.push(message);
|
|
216
|
+
record.sharedState.__broadcasts = broadcasts;
|
|
217
|
+
delivered++;
|
|
218
|
+
}
|
|
219
|
+
this.emit('broadcast', message, delivered);
|
|
220
|
+
return delivered;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Register a callback for when any agent completes successfully.
|
|
224
|
+
*/
|
|
225
|
+
onAgentComplete(callback) {
|
|
226
|
+
this.onAgentCompleteCallbacks.push(callback);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Register a callback for when any agent encounters an error.
|
|
230
|
+
*/
|
|
231
|
+
onAgentError(callback) {
|
|
232
|
+
this.onAgentErrorCallbacks.push(callback);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get aggregate metrics across all agents.
|
|
236
|
+
*/
|
|
237
|
+
getMetrics() {
|
|
238
|
+
let activeAgents = 0;
|
|
239
|
+
let completedAgents = 0;
|
|
240
|
+
let failedAgents = 0;
|
|
241
|
+
let cancelledAgents = 0;
|
|
242
|
+
let totalDurationMs = 0;
|
|
243
|
+
let totalTokensUsed = 0;
|
|
244
|
+
const allRecords = Array.from(this.agents.values());
|
|
245
|
+
for (const record of allRecords) {
|
|
246
|
+
switch (record.state.status) {
|
|
247
|
+
case 'spawning':
|
|
248
|
+
case 'running':
|
|
249
|
+
activeAgents++;
|
|
250
|
+
break;
|
|
251
|
+
case 'completed':
|
|
252
|
+
completedAgents++;
|
|
253
|
+
break;
|
|
254
|
+
case 'failed':
|
|
255
|
+
failedAgents++;
|
|
256
|
+
break;
|
|
257
|
+
case 'cancelled':
|
|
258
|
+
cancelledAgents++;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
totalTokensUsed += record.state.tokensUsed;
|
|
262
|
+
if (record.state.completedAt) {
|
|
263
|
+
totalDurationMs += record.state.completedAt - record.state.startedAt;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const agentsWithDuration = completedAgents + failedAgents + cancelledAgents;
|
|
267
|
+
return {
|
|
268
|
+
totalAgents: this.agents.size,
|
|
269
|
+
activeAgents,
|
|
270
|
+
completedAgents,
|
|
271
|
+
failedAgents,
|
|
272
|
+
cancelledAgents,
|
|
273
|
+
averageDurationMs: agentsWithDuration > 0 ? Math.round(totalDurationMs / agentsWithDuration) : 0,
|
|
274
|
+
totalTokensUsed,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Read a value from the shared state of a specific agent.
|
|
279
|
+
*/
|
|
280
|
+
getSharedValue(agentId, key) {
|
|
281
|
+
const record = this.agents.get(agentId);
|
|
282
|
+
if (!record)
|
|
283
|
+
return undefined;
|
|
284
|
+
return record.sharedState[key];
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Write a value to the shared state of a specific agent.
|
|
288
|
+
* Also writes to the global shared state so future agents inherit it.
|
|
289
|
+
*/
|
|
290
|
+
setSharedValue(agentId, key, value) {
|
|
291
|
+
const record = this.agents.get(agentId);
|
|
292
|
+
if (!record)
|
|
293
|
+
return false;
|
|
294
|
+
record.sharedState[key] = value;
|
|
295
|
+
this.globalSharedState[key] = value;
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Update the progress of a running agent (0-100).
|
|
300
|
+
*/
|
|
301
|
+
updateProgress(agentId, progress) {
|
|
302
|
+
const record = this.agents.get(agentId);
|
|
303
|
+
if (!record || record.state.status !== 'running')
|
|
304
|
+
return false;
|
|
305
|
+
record.state.progress = Math.max(0, Math.min(100, progress));
|
|
306
|
+
this.emit('agent:progress', agentId, record.state.progress);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Clean up sandbox directories for completed/failed/cancelled agents.
|
|
311
|
+
*/
|
|
312
|
+
cleanup() {
|
|
313
|
+
let cleaned = 0;
|
|
314
|
+
const entries = Array.from(this.agents.entries());
|
|
315
|
+
for (const [id, record] of entries) {
|
|
316
|
+
if (record.state.status === 'completed' ||
|
|
317
|
+
record.state.status === 'failed' ||
|
|
318
|
+
record.state.status === 'cancelled') {
|
|
319
|
+
try {
|
|
320
|
+
if (existsSync(record.sandboxDir)) {
|
|
321
|
+
rmSync(record.sandboxDir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Best-effort cleanup
|
|
326
|
+
}
|
|
327
|
+
cleaned++;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return cleaned;
|
|
331
|
+
}
|
|
332
|
+
// ---- Private Helpers ----
|
|
333
|
+
/**
|
|
334
|
+
* Schedule an agent to start. If we're at the concurrency limit,
|
|
335
|
+
* the agent is placed in the pending queue.
|
|
336
|
+
*/
|
|
337
|
+
scheduleStart(agentId) {
|
|
338
|
+
const record = this.agents.get(agentId);
|
|
339
|
+
if (!record)
|
|
340
|
+
return;
|
|
341
|
+
if (this.activeCount() >= this.maxConcurrent) {
|
|
342
|
+
// Enqueue by priority
|
|
343
|
+
this.pendingQueue.push(agentId);
|
|
344
|
+
this.pendingQueue.sort((a, b) => {
|
|
345
|
+
const ra = this.agents.get(a);
|
|
346
|
+
const rb = this.agents.get(b);
|
|
347
|
+
const pa = ra ? PRIORITY_ORDER[ra.config.priority] ?? 2 : 2;
|
|
348
|
+
const pb = rb ? PRIORITY_ORDER[rb.config.priority] ?? 2 : 2;
|
|
349
|
+
return pb - pa; // higher priority first
|
|
350
|
+
});
|
|
351
|
+
this.emit('agent:queued', agentId);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this.runAgent(agentId);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Actually start running an agent.
|
|
358
|
+
*/
|
|
359
|
+
runAgent(agentId) {
|
|
360
|
+
const record = this.agents.get(agentId);
|
|
361
|
+
if (!record)
|
|
362
|
+
return;
|
|
363
|
+
record.state.status = 'running';
|
|
364
|
+
record.state.startedAt = Date.now();
|
|
365
|
+
this.emit('agent:started', agentId, record.state);
|
|
366
|
+
// Run the agent loop asynchronously
|
|
367
|
+
this.executeAgentLoop(record)
|
|
368
|
+
.then((result) => {
|
|
369
|
+
record.state.status = 'completed';
|
|
370
|
+
record.state.result = result;
|
|
371
|
+
record.state.completedAt = Date.now();
|
|
372
|
+
record.state.progress = 100;
|
|
373
|
+
record.completionResolve?.(result);
|
|
374
|
+
this.fireCompleteCallbacks(agentId, record.state);
|
|
375
|
+
this.emit('agent:completed', agentId, record.state);
|
|
376
|
+
this.tryStartPending();
|
|
377
|
+
})
|
|
378
|
+
.catch((err) => {
|
|
379
|
+
if (record.state.status === 'cancelled') {
|
|
380
|
+
// Already handled by cancel()
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
384
|
+
record.state.status = 'failed';
|
|
385
|
+
record.state.error = errorMsg;
|
|
386
|
+
record.state.completedAt = Date.now();
|
|
387
|
+
record.completionReject?.(err instanceof Error ? err : new Error(errorMsg));
|
|
388
|
+
this.fireErrorCallbacks(agentId, errorMsg, record.state);
|
|
389
|
+
this.emit('agent:error', agentId, errorMsg, record.state);
|
|
390
|
+
this.tryStartPending();
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The core agent execution loop.
|
|
395
|
+
*
|
|
396
|
+
* In a real implementation this would call an LLM, execute tools, etc.
|
|
397
|
+
* Here we simulate the loop with AbortController support, token tracking,
|
|
398
|
+
* iteration limits, and sandboxed file I/O — all using only Node built-ins.
|
|
399
|
+
*/
|
|
400
|
+
async executeAgentLoop(record) {
|
|
401
|
+
const maxIterations = record.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
402
|
+
const maxTokens = record.config.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
403
|
+
const workingDir = record.config.workingDirectory ?? record.sandboxDir;
|
|
404
|
+
const abortSignal = record.abortController.signal;
|
|
405
|
+
const outputLines = [`Agent "${record.config.name}" (${record.config.id}) started.`];
|
|
406
|
+
// Persist initial context to sandbox
|
|
407
|
+
this.persistContext(record);
|
|
408
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
409
|
+
// Check for cancellation
|
|
410
|
+
if (abortSignal.aborted) {
|
|
411
|
+
throw new Error(`Agent "${record.config.id}" was cancelled`);
|
|
412
|
+
}
|
|
413
|
+
// Simulate token usage per iteration
|
|
414
|
+
const estimatedTokens = this.estimateTokens(record.config.prompt, i);
|
|
415
|
+
record.state.tokensUsed += estimatedTokens;
|
|
416
|
+
// Enforce token limit
|
|
417
|
+
if (record.state.tokensUsed >= maxTokens) {
|
|
418
|
+
outputLines.push(`Token limit reached (${maxTokens}). Stopping.`);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
record.state.iterations = i + 1;
|
|
422
|
+
// Update progress
|
|
423
|
+
record.state.progress = Math.min(Math.round(((i + 1) / maxIterations) * 100), 100);
|
|
424
|
+
// Simulate one iteration of work (non-blocking)
|
|
425
|
+
await this.yieldControl();
|
|
426
|
+
// Simulate: write intermediate output to sandbox
|
|
427
|
+
this.writeIterationLog(record, i, outputLines);
|
|
428
|
+
// Check broadcasts in shared state
|
|
429
|
+
const broadcasts = record.sharedState.__broadcasts;
|
|
430
|
+
if (broadcasts && broadcasts.length > 0) {
|
|
431
|
+
outputLines.push(`[broadcast received]: ${broadcasts.join('; ')}`);
|
|
432
|
+
// Agent consumes broadcasts
|
|
433
|
+
record.sharedState.__broadcasts = [];
|
|
434
|
+
}
|
|
435
|
+
// For demonstration, we complete after a few iterations.
|
|
436
|
+
// In production, the agent would decide based on LLM output.
|
|
437
|
+
if (i >= 2) {
|
|
438
|
+
outputLines.push(`Task completed after ${i + 1} iterations.`);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Persist final context
|
|
443
|
+
this.persistContext(record);
|
|
444
|
+
const finalResult = outputLines.join('\n');
|
|
445
|
+
return finalResult;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Yield control to the event loop so other agents can progress.
|
|
449
|
+
* Uses setImmediate for true interleaving.
|
|
450
|
+
*/
|
|
451
|
+
yieldControl() {
|
|
452
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Rough token estimation (heuristic: ~4 chars per token).
|
|
456
|
+
*/
|
|
457
|
+
estimateTokens(prompt, iteration) {
|
|
458
|
+
// Base cost from prompt + incremental cost per iteration
|
|
459
|
+
const baseCost = Math.ceil(prompt.length / 4);
|
|
460
|
+
const iterationCost = 50 + Math.floor(Math.random() * 150); // simulated
|
|
461
|
+
return baseCost * (iteration === 0 ? 1 : 0) + iterationCost;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Persist the agent's context to its sandbox directory.
|
|
465
|
+
*/
|
|
466
|
+
persistContext(record) {
|
|
467
|
+
try {
|
|
468
|
+
const contextPath = join(record.sandboxDir, 'context.json');
|
|
469
|
+
const context = {
|
|
470
|
+
id: record.config.id,
|
|
471
|
+
name: record.config.name,
|
|
472
|
+
prompt: record.config.prompt,
|
|
473
|
+
model: record.config.model,
|
|
474
|
+
sharedState: record.sharedState,
|
|
475
|
+
state: record.state,
|
|
476
|
+
updatedAt: Date.now(),
|
|
477
|
+
};
|
|
478
|
+
writeFileSync(contextPath, JSON.stringify(context, null, 2), 'utf-8');
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// Non-critical — context persistence is best-effort
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Write an iteration log to the sandbox directory.
|
|
486
|
+
*/
|
|
487
|
+
writeIterationLog(record, iteration, outputLines) {
|
|
488
|
+
try {
|
|
489
|
+
const logPath = join(record.sandboxDir, `iter_${iteration}.log`);
|
|
490
|
+
writeFileSync(logPath, JSON.stringify({
|
|
491
|
+
agentId: record.config.id,
|
|
492
|
+
iteration,
|
|
493
|
+
tokensUsed: record.state.tokensUsed,
|
|
494
|
+
progress: record.state.progress,
|
|
495
|
+
lastOutput: outputLines[outputLines.length - 1],
|
|
496
|
+
timestamp: Date.now(),
|
|
497
|
+
}, null, 2), 'utf-8');
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Non-critical
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* When a slot opens, try to start the highest-priority pending agent.
|
|
505
|
+
*/
|
|
506
|
+
tryStartPending() {
|
|
507
|
+
while (this.pendingQueue.length > 0 && this.activeCount() < this.maxConcurrent) {
|
|
508
|
+
const nextId = this.pendingQueue.shift();
|
|
509
|
+
const record = this.agents.get(nextId);
|
|
510
|
+
if (!record || record.state.status === 'cancelled')
|
|
511
|
+
continue;
|
|
512
|
+
this.runAgent(nextId);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Count currently active (spawning + running) agents.
|
|
517
|
+
*/
|
|
518
|
+
activeCount() {
|
|
519
|
+
let count = 0;
|
|
520
|
+
const records = Array.from(this.agents.values());
|
|
521
|
+
for (const record of records) {
|
|
522
|
+
if (record.state.status === 'spawning' || record.state.status === 'running') {
|
|
523
|
+
count++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return count;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Fire all registered onAgentComplete callbacks.
|
|
530
|
+
*/
|
|
531
|
+
fireCompleteCallbacks(agentId, state) {
|
|
532
|
+
for (const cb of this.onAgentCompleteCallbacks) {
|
|
533
|
+
try {
|
|
534
|
+
cb(agentId, state);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// Subscriber errors must not break the manager
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Fire all registered onAgentError callbacks.
|
|
543
|
+
*/
|
|
544
|
+
fireErrorCallbacks(agentId, error, state) {
|
|
545
|
+
for (const cb of this.onAgentErrorCallbacks) {
|
|
546
|
+
try {
|
|
547
|
+
cb(agentId, error, state);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// Subscriber errors must not break the manager
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Ensure a directory exists.
|
|
556
|
+
*/
|
|
557
|
+
ensureDir(dir) {
|
|
558
|
+
if (!existsSync(dir)) {
|
|
559
|
+
mkdirSync(dir, { recursive: true });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
//# sourceMappingURL=parallel-agents.js.map
|