@robzilla1738/agentswarm 0.2.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/bin/swarm.js +10 -0
  4. package/dist/agent.js +211 -0
  5. package/dist/cli.js +667 -0
  6. package/dist/config.js +289 -0
  7. package/dist/control.js +96 -0
  8. package/dist/deepseek.js +321 -0
  9. package/dist/executor.js +988 -0
  10. package/dist/hub.js +553 -0
  11. package/dist/journal.js +152 -0
  12. package/dist/prompts.js +232 -0
  13. package/dist/providers.js +151 -0
  14. package/dist/run.js +309 -0
  15. package/dist/sandbox.js +505 -0
  16. package/dist/state.js +230 -0
  17. package/dist/terminal.js +298 -0
  18. package/dist/tools.js +491 -0
  19. package/dist/types.js +26 -0
  20. package/dist/util.js +209 -0
  21. package/dist/webtools.js +205 -0
  22. package/package.json +63 -0
  23. package/ui/out/404/index.html +1 -0
  24. package/ui/out/404.html +1 -0
  25. package/ui/out/_next/static/chunks/255-2aa030c9ba2867e3.js +1 -0
  26. package/ui/out/_next/static/chunks/383-289a866b246b41cc.js +1 -0
  27. package/ui/out/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
  28. package/ui/out/_next/static/chunks/619-ba102abea3e3d0e4.js +1 -0
  29. package/ui/out/_next/static/chunks/677-b37981ba0eca75b2.js +1 -0
  30. package/ui/out/_next/static/chunks/app/_not-found/page-2d0982e372f7be41.js +1 -0
  31. package/ui/out/_next/static/chunks/app/layout-37ad32c5fdb26f29.js +1 -0
  32. package/ui/out/_next/static/chunks/app/page-0c9f35bd4aa8e370.js +1 -0
  33. package/ui/out/_next/static/chunks/app/run/page-13dc41a57e34da71.js +1 -0
  34. package/ui/out/_next/static/chunks/app/settings/page-a1763be7f6de888c.js +1 -0
  35. package/ui/out/_next/static/chunks/framework-2c534e0e662575a2.js +1 -0
  36. package/ui/out/_next/static/chunks/main-app-889ed884f8bc78e3.js +1 -0
  37. package/ui/out/_next/static/chunks/main-eb90ae3b35d2fd16.js +1 -0
  38. package/ui/out/_next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  39. package/ui/out/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  40. package/ui/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  41. package/ui/out/_next/static/chunks/webpack-38639c05c96dbeca.js +1 -0
  42. package/ui/out/_next/static/css/82edaa7a5942f894.css +3 -0
  43. package/ui/out/_next/static/eiQeDU9uBHNsBj0CFkp8M/_buildManifest.js +1 -0
  44. package/ui/out/_next/static/eiQeDU9uBHNsBj0CFkp8M/_ssgManifest.js +1 -0
  45. package/ui/out/_next/static/media/0aa834ed78bf6d07-s.woff2 +0 -0
  46. package/ui/out/_next/static/media/438aa629764e75f3-s.woff2 +0 -0
  47. package/ui/out/_next/static/media/4c9affa5bc8f420e-s.p.woff2 +0 -0
  48. package/ui/out/_next/static/media/51251f8b9793cdb3-s.woff2 +0 -0
  49. package/ui/out/_next/static/media/67957d42bae0796d-s.woff2 +0 -0
  50. package/ui/out/_next/static/media/875ae681bfde4580-s.woff2 +0 -0
  51. package/ui/out/_next/static/media/886030b0b59bc5a7-s.woff2 +0 -0
  52. package/ui/out/_next/static/media/939c4f875ee75fbb-s.woff2 +0 -0
  53. package/ui/out/_next/static/media/bb3ef058b751a6ad-s.p.woff2 +0 -0
  54. package/ui/out/_next/static/media/cc978ac5ee68c2b6-s.woff2 +0 -0
  55. package/ui/out/_next/static/media/e857b654a2caa584-s.woff2 +0 -0
  56. package/ui/out/_next/static/media/f911b923c6adde36-s.woff2 +0 -0
  57. package/ui/out/icon.png +0 -0
  58. package/ui/out/index.html +1 -0
  59. package/ui/out/index.txt +22 -0
  60. package/ui/out/run/index.html +1 -0
  61. package/ui/out/run/index.txt +22 -0
  62. package/ui/out/settings/index.html +1 -0
  63. package/ui/out/settings/index.txt +22 -0
  64. package/ui/out/swarm-mark.png +0 -0
@@ -0,0 +1,988 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Executor = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const agent_1 = require("./agent");
40
+ const config_1 = require("./config");
41
+ const control_1 = require("./control");
42
+ const deepseek_1 = require("./deepseek");
43
+ const tools_1 = require("./tools");
44
+ const prompts_1 = require("./prompts");
45
+ const sandbox_1 = require("./sandbox");
46
+ const types_1 = require("./types");
47
+ const util_1 = require("./util");
48
+ const VERIFY_MAX_ATTEMPTS = 2;
49
+ class Executor {
50
+ cfg;
51
+ meta;
52
+ runDirPath;
53
+ journal;
54
+ control;
55
+ ac = new AbortController();
56
+ tasks = new Map();
57
+ taskOrder = [];
58
+ taskCounter = 0;
59
+ inflight = new Map();
60
+ settledSinceUpdate = [];
61
+ notes = [];
62
+ conductorMessages = [];
63
+ spentTokens = 0;
64
+ cost = 0;
65
+ finishing = false;
66
+ finishNotes = "";
67
+ finishReason = "";
68
+ fatal = null;
69
+ lastConductorAction = "none";
70
+ resumed = false;
71
+ sandbox;
72
+ constructor(cfg, meta, journal) {
73
+ this.cfg = cfg;
74
+ this.meta = meta;
75
+ this.runDirPath = (0, config_1.runDir)(meta.id);
76
+ this.journal = journal;
77
+ this.control = new control_1.ControlReader(this.runDirPath);
78
+ (0, util_1.ensureDir)(path.join(this.runDirPath, "artifacts"));
79
+ // "A directory on disk" runs always execute on the host — touching the
80
+ // operator's real files is the entire point of that mode.
81
+ const kind = meta.sandbox ? meta.options.sandboxRuntime ?? "host" : "host";
82
+ this.sandbox = (0, sandbox_1.createSandbox)(kind, { runId: meta.id, hostDir: meta.cwd, cfg });
83
+ }
84
+ cancel() {
85
+ this.finishing = true;
86
+ this.finishReason = "cancelled by operator";
87
+ this.ac.abort();
88
+ }
89
+ /**
90
+ * Seed orchestration state from a reduced journal (resume after an
91
+ * interrupt). Settled tasks keep their results and never re-run; `resets`
92
+ * are tasks that were in flight when the engine died — they go back to
93
+ * pending and re-run from scratch. Token spend and cost carry over so the
94
+ * run-wide budget stays a single honest number.
95
+ */
96
+ seedFromState(state, resets) {
97
+ const reset = new Set(resets);
98
+ for (const t of state.taskList()) {
99
+ const copy = { ...t, deps: [...t.deps], artifacts: [...t.artifacts], agentIds: [...t.agentIds] };
100
+ if (reset.has(copy.id)) {
101
+ copy.status = "pending";
102
+ copy.startedAt = undefined;
103
+ copy.endedAt = undefined;
104
+ }
105
+ this.tasks.set(copy.id, copy);
106
+ this.taskOrder.push(copy.id);
107
+ const n = Number(/^T(\d+)$/.exec(copy.id)?.[1] ?? 0);
108
+ this.taskCounter = Math.max(this.taskCounter, n);
109
+ }
110
+ this.notes = state.notes.map((n) => ({ taskId: n.taskId, key: n.key, text: n.text }));
111
+ this.spentTokens = state.totalUsage.promptTokens + state.totalUsage.completionTokens;
112
+ this.cost = state.cost;
113
+ this.resumed = true;
114
+ }
115
+ setStatus(status, reason) {
116
+ this.journal.append("run.status", { status, reason });
117
+ }
118
+ onUsage = (model, usage) => {
119
+ this.spentTokens += usage.promptTokens + usage.completionTokens;
120
+ this.cost += (0, types_1.usageCost)(usage, this.cfg.pricing[model]);
121
+ this.journal.append("usage", { model, usage, cost: this.cost });
122
+ };
123
+ budgetExceeded() {
124
+ return this.spentTokens >= this.meta.options.maxTokens;
125
+ }
126
+ blackboardDigest(max = 1800) {
127
+ if (!this.notes.length)
128
+ return "";
129
+ const lines = this.notes
130
+ .slice(-40)
131
+ .map((n) => `• ${n.key ? `[${n.key}] ` : ""}${(0, util_1.oneLine)(n.text, 160)}${n.taskId ? ` (${n.taskId})` : ""}`);
132
+ let out = lines.join("\n");
133
+ if (out.length > max)
134
+ out = out.slice(out.length - max);
135
+ return out;
136
+ }
137
+ // ---------------------------------------------------------------- main
138
+ async run() {
139
+ this.setStatus("planning");
140
+ // Preflight: validate auth before doing any work so the operator gets an
141
+ // instant, clear error instead of a phantom "done" run.
142
+ const auth = await (0, deepseek_1.validateAuth)(this.cfg);
143
+ if (auth.status === "invalid") {
144
+ this.fatal = `Provider authentication failed — ${auth.message || "invalid API key"}. Set a valid key in Settings (or: swarm config set apiKey <...>).`;
145
+ this.finishReason = this.fatal;
146
+ this.journal.append("log", { level: "error", msg: this.fatal });
147
+ await this.fail(this.fatal);
148
+ return;
149
+ }
150
+ // Boot the sandbox before any work — a dead Docker daemon or a bad cloud
151
+ // key must fail the run instantly with a clear reason, not mid-mission.
152
+ try {
153
+ await this.sandbox.start((msg) => this.journal.append("log", { level: "info", msg }));
154
+ this.journal.append("log", { level: "info", msg: `sandbox: ${this.sandbox.label}` });
155
+ }
156
+ catch (e) {
157
+ this.fatal = `Sandbox failed to start — ${(0, util_1.errMsg)(e)}`;
158
+ this.finishReason = this.fatal;
159
+ this.journal.append("log", { level: "error", msg: this.fatal });
160
+ await this.fail(this.fatal);
161
+ return;
162
+ }
163
+ // Operator control must land while agents are mid-task, not only when the
164
+ // scheduler wakes up — a Stop click aborts in-flight work within ~1s.
165
+ const controlTimer = setInterval(() => {
166
+ try {
167
+ this.drainControl();
168
+ }
169
+ catch {
170
+ /* control polling must never kill the run */
171
+ }
172
+ }, 750);
173
+ this.conductorMessages = [
174
+ { role: "system", content: (0, prompts_1.conductorSystem)(this.meta) },
175
+ {
176
+ role: "user",
177
+ content: this.resumed
178
+ ? (0, prompts_1.conductorUpdate)({
179
+ blackboard: this.blackboardDigest(),
180
+ nextId: this.nextId(),
181
+ taskTable: (0, prompts_1.taskTable)(this.taskList()),
182
+ budgetLine: (0, prompts_1.budgetLine)({ total: this.spentTokens, cost: this.cost }, this.meta.options.maxTokens),
183
+ extra: "This run was interrupted (engine restart) and has just been RESUMED. " +
184
+ "The task table above is the current truth: completed tasks keep their results; " +
185
+ "tasks that were in flight were reset to pending and will re-run automatically. " +
186
+ "Spawn tasks only if the plan has a gap — otherwise wait.",
187
+ })
188
+ : (0, prompts_1.conductorInitialUpdate)(this.meta, this.nextId()),
189
+ },
190
+ ];
191
+ try {
192
+ await this.conductorTurn();
193
+ this.setStatus("running");
194
+ while (!this.finishing) {
195
+ this.drainControl();
196
+ if (this.finishing)
197
+ break;
198
+ if (this.budgetExceeded()) {
199
+ this.finishing = true;
200
+ this.finishReason = "token budget reached";
201
+ break;
202
+ }
203
+ this.startReadyTasks();
204
+ if (this.inflight.size === 0) {
205
+ const runnable = this.runnableTasks();
206
+ if (runnable.length > 0)
207
+ continue; // loop starts them
208
+ // Nothing running, nothing runnable. Include any reports that
209
+ // settled while the conductor was mid-turn — they must not be lost.
210
+ this.blockStuckTasks();
211
+ const reports = this.drainSettled();
212
+ if (!this.hasOpenWork()) {
213
+ // Everything is terminal. Ask the conductor for a final decision.
214
+ this.appendConductorUpdate("All tasks have settled and no tasks are runnable.", reports);
215
+ await this.conductorTurn();
216
+ if (this.lastConductorAction !== "spawn") {
217
+ this.finishing = true;
218
+ this.finishReason = this.finishReason || "all tasks settled";
219
+ }
220
+ }
221
+ else {
222
+ // Stuck: pending tasks exist but can't run (failed/blocked deps).
223
+ this.appendConductorUpdate("Some tasks cannot run because their dependencies failed or were blocked. Re-plan around them or finish.", reports);
224
+ await this.conductorTurn();
225
+ if (this.lastConductorAction === "wait") {
226
+ this.finishing = true;
227
+ this.finishReason = "stalled: dependencies unmet and conductor chose to wait";
228
+ }
229
+ }
230
+ continue;
231
+ }
232
+ // Tasks are running — wait for at least one to settle.
233
+ await Promise.race([...this.inflight.values()]);
234
+ this.drainControl();
235
+ const reports = this.drainSettled();
236
+ if (reports.length && !this.finishing) {
237
+ this.appendConductorUpdate(undefined, reports);
238
+ await this.conductorTurn();
239
+ }
240
+ }
241
+ }
242
+ catch (e) {
243
+ if (!this.ac.signal.aborted) {
244
+ this.journal.append("log", { level: "error", msg: `executor error: ${(0, util_1.errMsg)(e)}` });
245
+ this.finishReason = this.finishReason || `error: ${(0, util_1.errMsg)(e)}`;
246
+ }
247
+ }
248
+ clearInterval(controlTimer);
249
+ // Drain any still-running tasks before synthesis (cancellation aborts them).
250
+ if (this.inflight.size) {
251
+ await Promise.allSettled([...this.inflight.values()]);
252
+ }
253
+ this.drainSettled();
254
+ await this.synthesize();
255
+ await this.sandbox.destroy().catch(() => {
256
+ /* container/sandbox teardown is best-effort */
257
+ });
258
+ await this.journal.flush();
259
+ }
260
+ // ---------------------------------------------------------------- conductor
261
+ nextId() {
262
+ return this.taskCounter + 1;
263
+ }
264
+ async conductorTurn() {
265
+ if (this.finishing)
266
+ return;
267
+ // Re-bound the history every turn — the nudge loop and tool-result pushes
268
+ // below grow it outside appendConductorUpdate's trim.
269
+ this.trimConductorHistory();
270
+ const tools = [tools_1.SPAWN_TASKS_TOOL, tools_1.WAIT_TOOL, tools_1.FINISH_TOOL];
271
+ for (let attempt = 0; attempt < 3; attempt++) {
272
+ let res;
273
+ try {
274
+ res = await (0, deepseek_1.chat)(this.cfg, {
275
+ model: this.meta.options.conductorModel,
276
+ messages: this.conductorMessages,
277
+ tools,
278
+ // "auto" rather than "required" for cross-provider safety; the prompt
279
+ // mandates a tool call and the no-tool nudge loop below enforces it.
280
+ toolChoice: "auto",
281
+ thinking: this.meta.options.thinking,
282
+ reasoningEffort: this.meta.options.reasoningEffort,
283
+ // Generous: with thinking enabled, reasoning + a large spawn_tasks
284
+ // batch can overflow a small cap and truncate the tool-call JSON.
285
+ maxTokens: 16384,
286
+ signal: this.ac.signal,
287
+ onDelta: () => { },
288
+ });
289
+ }
290
+ catch (e) {
291
+ if (this.ac.signal.aborted)
292
+ return;
293
+ const msg = (0, util_1.errMsg)(e);
294
+ this.journal.append("log", { level: "error", msg: `conductor call failed: ${msg}` });
295
+ if ((0, deepseek_1.isFatalAuthError)(e)) {
296
+ // No point continuing — every call will fail the same way.
297
+ this.fatal = `Provider authentication failed — ${msg}. Set a valid key in Settings.`;
298
+ this.finishing = true;
299
+ this.finishReason = this.fatal;
300
+ }
301
+ // Treat a transient conductor failure as a wait so the loop keeps draining tasks.
302
+ this.lastConductorAction = "wait";
303
+ return;
304
+ }
305
+ this.onUsage(this.meta.options.conductorModel, res.usage);
306
+ if (res.content.trim())
307
+ this.journal.append("conductor.say", { text: (0, util_1.clip)(res.content, 4000) });
308
+ if (res.toolCalls.length === 0) {
309
+ this.conductorMessages.push({ role: "assistant", content: res.content, reasoning_content: res.reasoning });
310
+ this.conductorMessages.push({
311
+ role: "user",
312
+ content: "Respond only by calling spawn_tasks, wait, or finish.",
313
+ });
314
+ continue;
315
+ }
316
+ this.conductorMessages.push({
317
+ role: "assistant",
318
+ content: res.content || null,
319
+ reasoning_content: res.reasoning,
320
+ tool_calls: res.toolCalls,
321
+ });
322
+ let acted = "none";
323
+ for (const call of res.toolCalls) {
324
+ const args = safeArgs(call.function.arguments);
325
+ let toolResult = "ok";
326
+ if (call.function.name === "spawn_tasks") {
327
+ toolResult = this.handleSpawn(args);
328
+ acted = "spawn";
329
+ }
330
+ else if (call.function.name === "finish") {
331
+ this.finishing = true;
332
+ this.finishNotes = String(args.notes ?? "");
333
+ this.finishReason = this.finishReason || "conductor declared mission complete";
334
+ toolResult = "Acknowledged. Synthesizing the final deliverable.";
335
+ acted = "finish";
336
+ }
337
+ else if (call.function.name === "wait") {
338
+ toolResult = "Waiting for running tasks to report.";
339
+ if (acted === "none")
340
+ acted = "wait";
341
+ }
342
+ else {
343
+ toolResult = `unknown tool ${call.function.name}`;
344
+ }
345
+ this.conductorMessages.push({ role: "tool", tool_call_id: call.id, content: toolResult });
346
+ }
347
+ this.lastConductorAction = acted;
348
+ this.journal.append("conductor.action", { kind: acted });
349
+ return;
350
+ }
351
+ // Conductor refused to use tools 3x — default to waiting.
352
+ this.lastConductorAction = "wait";
353
+ }
354
+ handleSpawn(args) {
355
+ const specs = Array.isArray(args.tasks) ? args.tasks : [];
356
+ if (!specs.length)
357
+ return "No tasks provided.";
358
+ const remaining = this.meta.options.maxTasks - this.tasks.size;
359
+ if (remaining <= 0) {
360
+ return `Task cap reached (${this.meta.options.maxTasks}). Consolidate or finish — no more tasks can be created.`;
361
+ }
362
+ const accepted = specs.slice(0, remaining);
363
+ // Pre-assign ids so deps within this batch resolve.
364
+ const batchIds = accepted.map(() => this.allocId());
365
+ const created = [];
366
+ const warnings = [];
367
+ // One wave per spawn batch — computed before inserting, otherwise each
368
+ // task would see its predecessor and claim a new wave of its own.
369
+ const wave = this.currentWave();
370
+ accepted.forEach((spec, i) => {
371
+ const id = batchIds[i];
372
+ // A dep may reference any existing task or an *earlier* task in this
373
+ // batch. Self/later-batch references can never become runnable (cycle)
374
+ // and would silently deadlock the run, so they are dropped loudly.
375
+ const allowed = new Set([...this.tasks.keys(), ...batchIds.slice(0, i)]);
376
+ const deps = [...new Set((spec.deps ?? []).map(String))].filter((d) => {
377
+ if (allowed.has(d))
378
+ return true;
379
+ const idx = batchIds.indexOf(d);
380
+ warnings.push(`${id}: dropped dep "${d}" (${idx >= i ? "same-batch later task — would deadlock" : "unknown task"})`);
381
+ return false;
382
+ });
383
+ const task = {
384
+ id,
385
+ title: (0, util_1.clip)(String(spec.title ?? "task"), 120),
386
+ objective: String(spec.objective ?? spec.title ?? ""),
387
+ role: (spec.role ? String(spec.role) : inferRole(spec)).toLowerCase(),
388
+ deps,
389
+ verify: Boolean(spec.verify) && this.cfg.verification !== "off",
390
+ context: spec.context ? String(spec.context) : undefined,
391
+ status: "pending",
392
+ attempt: 1,
393
+ wave,
394
+ artifacts: [],
395
+ createdAt: Date.now(),
396
+ agentIds: [],
397
+ };
398
+ this.tasks.set(id, task);
399
+ this.taskOrder.push(id);
400
+ created.push(id);
401
+ this.journal.append("task.created", { task });
402
+ });
403
+ if (specs.length > accepted.length) {
404
+ warnings.push(`only ${accepted.length}/${specs.length} accepted (task cap)`);
405
+ }
406
+ return `Created ${created.join(", ")}.${warnings.length ? " Notes: " + warnings.join("; ") + "." : ""}`;
407
+ }
408
+ allocId() {
409
+ this.taskCounter++;
410
+ return `T${this.taskCounter}`;
411
+ }
412
+ currentWave() {
413
+ let w = 0;
414
+ for (const t of this.tasks.values())
415
+ w = Math.max(w, t.wave);
416
+ return w + 1;
417
+ }
418
+ appendConductorUpdate(extra, reports) {
419
+ const ops = this.consumeOperatorNotes();
420
+ this.conductorMessages.push({
421
+ role: "user",
422
+ content: (0, prompts_1.conductorUpdate)({
423
+ reports: reports?.map(prompts_1.reportBlock),
424
+ operatorNotes: ops,
425
+ blackboard: this.blackboardDigest(),
426
+ nextId: this.nextId(),
427
+ taskTable: (0, prompts_1.taskTable)(this.taskList()),
428
+ budgetLine: (0, prompts_1.budgetLine)({ total: this.spentTokens, cost: this.cost }, this.meta.options.maxTokens),
429
+ extra,
430
+ }),
431
+ });
432
+ // Keep the conductor's own history from growing without bound.
433
+ this.trimConductorHistory();
434
+ }
435
+ trimConductorHistory() {
436
+ const MAX = 60;
437
+ const TRIM_NOTICE = "[Earlier orchestration history was trimmed. Current swarm state is below.]";
438
+ if (this.conductorMessages.length > MAX) {
439
+ const system = this.conductorMessages[0];
440
+ const tail = this.conductorMessages.slice(-(MAX - 2));
441
+ // Don't begin the tail on an orphic tool result.
442
+ while (tail.length && tail[0].role === "tool")
443
+ tail.shift();
444
+ this.conductorMessages = [system, { role: "user", content: TRIM_NOTICE }, ...tail];
445
+ }
446
+ // Count alone doesn't bound size: every update embeds the full task table,
447
+ // so a deep run can blow the model window long before 60 messages. The
448
+ // mission itself lives in the system message and always survives.
449
+ const budget = Math.floor(this.cfg.contextTokenLimit * 0.75);
450
+ if ((0, agent_1.estimateMessages)(this.conductorMessages) <= budget)
451
+ return;
452
+ if (this.conductorMessages[1]?.content !== TRIM_NOTICE) {
453
+ this.conductorMessages.splice(1, 0, { role: "user", content: TRIM_NOTICE });
454
+ }
455
+ while ((0, agent_1.estimateMessages)(this.conductorMessages) > budget && this.conductorMessages.length > 10) {
456
+ this.conductorMessages.splice(2, 1);
457
+ // Never leave tool results whose assistant turn was dropped.
458
+ while (this.conductorMessages[2]?.role === "tool")
459
+ this.conductorMessages.splice(2, 1);
460
+ }
461
+ }
462
+ // ---------------------------------------------------------------- scheduling
463
+ taskList() {
464
+ return this.taskOrder.map((id) => this.tasks.get(id)).filter(Boolean);
465
+ }
466
+ runnableTasks() {
467
+ return this.taskList().filter((t) => t.status === "pending" && t.deps.every((d) => this.tasks.get(d)?.status === "done"));
468
+ }
469
+ hasOpenWork() {
470
+ return this.taskList().some((t) => ["pending", "running", "verifying"].includes(t.status));
471
+ }
472
+ blockStuckTasks() {
473
+ for (const t of this.taskList()) {
474
+ if (t.status !== "pending")
475
+ continue;
476
+ const bad = t.deps.find((d) => {
477
+ const s = this.tasks.get(d)?.status;
478
+ return s === "failed" || s === "blocked";
479
+ });
480
+ if (bad) {
481
+ t.status = "blocked";
482
+ t.error = `dependency ${bad} did not complete`;
483
+ t.endedAt = Date.now();
484
+ this.journal.append("task.status", { taskId: t.id, status: "blocked", attempt: t.attempt, reason: t.error });
485
+ this.settledSinceUpdate.push(t.id);
486
+ }
487
+ }
488
+ }
489
+ startReadyTasks() {
490
+ while (this.inflight.size < this.meta.options.maxWorkers && !this.finishing) {
491
+ const next = this.runnableTasks()[0];
492
+ if (!next)
493
+ break;
494
+ next.status = "running";
495
+ next.startedAt = Date.now();
496
+ this.journal.append("task.status", { taskId: next.id, status: "running", attempt: next.attempt });
497
+ const p = this.runTaskPipeline(next).finally(() => this.inflight.delete(next.id));
498
+ this.inflight.set(next.id, p);
499
+ }
500
+ }
501
+ drainSettled() {
502
+ const ids = this.settledSinceUpdate.splice(0);
503
+ const seen = new Set();
504
+ const out = [];
505
+ for (const id of ids) {
506
+ if (seen.has(id))
507
+ continue;
508
+ seen.add(id);
509
+ const t = this.tasks.get(id);
510
+ if (t)
511
+ out.push(t);
512
+ }
513
+ return out;
514
+ }
515
+ // ---------------------------------------------------------------- task pipeline
516
+ depReportsFor(task) {
517
+ if (!task.deps.length)
518
+ return "";
519
+ return task.deps
520
+ .map((d) => {
521
+ const dep = this.tasks.get(d);
522
+ if (!dep)
523
+ return `(${d}: missing)`;
524
+ return (0, prompts_1.reportBlock)(dep);
525
+ })
526
+ .join("\n\n");
527
+ }
528
+ makeToolCtx(agentId, task) {
529
+ return {
530
+ cfg: this.cfg,
531
+ meta: this.meta,
532
+ runDirPath: this.runDirPath,
533
+ workdir: this.sandbox.workdir,
534
+ sandbox: this.sandbox,
535
+ agentId,
536
+ taskId: task?.id,
537
+ signal: this.ac.signal,
538
+ addNote: (text, key) => {
539
+ this.notes.push({ taskId: task?.id, key, text });
540
+ // Only the recent tail ever feeds digests; without a cap a multi-day
541
+ // run accumulates every note in memory.
542
+ if (this.notes.length > 2000)
543
+ this.notes.splice(0, this.notes.length - 2000);
544
+ this.journal.append("note.added", { taskId: task?.id, agentId, key, text: (0, util_1.clip)(text, 1200) });
545
+ },
546
+ addArtifact: (rel) => {
547
+ if (task && !task.artifacts.includes(rel))
548
+ task.artifacts.push(rel);
549
+ },
550
+ readBlackboard: () => this.blackboardDigest(),
551
+ log: (level, msg) => {
552
+ this.journal.append("log", { level, msg, agentId, taskId: task?.id });
553
+ },
554
+ };
555
+ }
556
+ async runTaskPipeline(task) {
557
+ for (;;) {
558
+ try {
559
+ const outcome = await this.runWorker(task);
560
+ if (this.ac.signal.aborted) {
561
+ this.finalizeTask(task, "failed", "run cancelled");
562
+ return;
563
+ }
564
+ if (outcome === "retry") {
565
+ if (this.finishing || this.budgetExceeded()) {
566
+ this.finalizeTask(task, "failed", task.feedback || task.error || "not retried: run is winding down");
567
+ return;
568
+ }
569
+ if (task.attempt < VERIFY_MAX_ATTEMPTS) {
570
+ task.attempt++;
571
+ task.status = "running";
572
+ this.journal.append("task.status", { taskId: task.id, status: "running", attempt: task.attempt });
573
+ continue;
574
+ }
575
+ this.finalizeTask(task, "failed", task.feedback || task.error || "verification failed after retries");
576
+ return;
577
+ }
578
+ return; // worker already finalized the task
579
+ }
580
+ catch (e) {
581
+ if (this.ac.signal.aborted) {
582
+ this.finalizeTask(task, "failed", "run cancelled");
583
+ return;
584
+ }
585
+ if (task.attempt < VERIFY_MAX_ATTEMPTS && !this.finishing && !this.budgetExceeded()) {
586
+ task.attempt++;
587
+ task.error = (0, util_1.errMsg)(e);
588
+ task.status = "running";
589
+ this.journal.append("task.status", { taskId: task.id, status: "running", attempt: task.attempt, reason: task.error });
590
+ continue;
591
+ }
592
+ this.finalizeTask(task, "failed", `worker error: ${(0, util_1.errMsg)(e)}`);
593
+ return;
594
+ }
595
+ }
596
+ }
597
+ /** Returns "retry" to request another attempt, or "done" when finalized. */
598
+ async runWorker(task) {
599
+ const agentId = (0, util_1.rid)("w");
600
+ task.agentIds.push(agentId);
601
+ const dirListing = this.topListing();
602
+ const system = (0, prompts_1.workerSystem)({
603
+ agentId,
604
+ role: task.role,
605
+ meta: this.meta,
606
+ task,
607
+ maxSteps: this.meta.options.maxStepsPerTask,
608
+ depReports: this.depReportsFor(task),
609
+ blackboard: this.blackboardDigest(),
610
+ operatorNotes: this.peekOperatorNotes(),
611
+ dirListing,
612
+ });
613
+ this.journal.append("agent.spawned", {
614
+ agentId,
615
+ taskId: task.id,
616
+ role: task.role,
617
+ model: this.meta.options.model,
618
+ purpose: task.title,
619
+ });
620
+ const outcome = await (0, agent_1.runAgent)({
621
+ cfg: this.cfg,
622
+ agentId,
623
+ model: this.meta.options.model,
624
+ thinking: this.meta.options.thinking,
625
+ reasoningEffort: this.meta.options.reasoningEffort,
626
+ system,
627
+ kickoff: prompts_1.WORKER_KICKOFF,
628
+ tools: (0, tools_1.workerToolset)(),
629
+ terminal: [tools_1.REPORT_TOOL],
630
+ maxSteps: this.meta.options.maxStepsPerTask,
631
+ signal: this.ac.signal,
632
+ ctx: this.makeToolCtx(agentId, task),
633
+ hooks: this.agentHooks(agentId, task.id),
634
+ stop: this.agentStop,
635
+ });
636
+ this.flushDeltas(agentId);
637
+ this.journal.append("agent.done", { agentId, taskId: task.id, steps: outcome.steps });
638
+ if (this.ac.signal.aborted)
639
+ return "done";
640
+ if (!outcome.terminal) {
641
+ task.error = "worker ended without reporting";
642
+ return "retry";
643
+ }
644
+ const a = outcome.terminal.args;
645
+ const report = String(a.report ?? "(empty report)");
646
+ const reportStatus = a.status === "blocked" ? "blocked" : "done";
647
+ const reportedArtifacts = Array.isArray(a.artifacts) ? a.artifacts.map(String) : [];
648
+ for (const art of reportedArtifacts)
649
+ if (!task.artifacts.includes(art))
650
+ task.artifacts.push(art);
651
+ task.report = report;
652
+ task.reportStatus = reportStatus;
653
+ this.journal.append("task.report", {
654
+ taskId: task.id,
655
+ status: reportStatus,
656
+ report,
657
+ artifacts: task.artifacts,
658
+ });
659
+ if (reportStatus === "blocked") {
660
+ this.finalizeTask(task, "blocked", report);
661
+ return "done";
662
+ }
663
+ if (task.verify && this.cfg.verification !== "off") {
664
+ task.status = "verifying";
665
+ this.journal.append("task.status", { taskId: task.id, status: "verifying", attempt: task.attempt });
666
+ const pass = await this.runVerifier(task);
667
+ if (!pass)
668
+ return "retry";
669
+ }
670
+ this.finalizeTask(task, "done", report);
671
+ return "done";
672
+ }
673
+ async runVerifier(task) {
674
+ const agentId = (0, util_1.rid)("v");
675
+ task.agentIds.push(agentId);
676
+ this.journal.append("agent.spawned", {
677
+ agentId,
678
+ taskId: task.id,
679
+ role: "verifier",
680
+ model: this.meta.options.model,
681
+ purpose: `verify ${task.id}`,
682
+ });
683
+ const outcome = await (0, agent_1.runAgent)({
684
+ cfg: this.cfg,
685
+ agentId,
686
+ model: this.meta.options.model,
687
+ thinking: this.meta.options.thinking,
688
+ reasoningEffort: this.meta.options.reasoningEffort,
689
+ system: (0, prompts_1.verifierSystem)(this.meta, task),
690
+ kickoff: prompts_1.VERIFIER_KICKOFF,
691
+ tools: (0, tools_1.verifierToolset)(),
692
+ terminal: [tools_1.VERDICT_TOOL],
693
+ maxSteps: Math.min(14, this.meta.options.maxStepsPerTask),
694
+ signal: this.ac.signal,
695
+ ctx: this.makeToolCtx(agentId, task),
696
+ hooks: this.agentHooks(agentId, task.id),
697
+ stop: this.agentStop,
698
+ });
699
+ this.flushDeltas(agentId);
700
+ this.journal.append("agent.done", { agentId, taskId: task.id, steps: outcome.steps });
701
+ if (this.ac.signal.aborted)
702
+ return true;
703
+ const v = (outcome.terminal?.args ?? {});
704
+ const strict = this.cfg.verification === "strict";
705
+ // No verdict returned: in strict mode fail closed, otherwise accept.
706
+ const pass = outcome.terminal ? Boolean(v.pass) : !strict;
707
+ const feedback = String(v.feedback ?? (outcome.terminal ? "" : "verifier produced no verdict"));
708
+ task.feedback = feedback;
709
+ this.journal.append("verify.result", { taskId: task.id, pass, feedback });
710
+ return pass;
711
+ }
712
+ finalizeTask(task, status, reason) {
713
+ task.status = status;
714
+ task.endedAt = Date.now();
715
+ if (reason && status !== "done")
716
+ task.error = reason;
717
+ this.journal.append("task.status", { taskId: task.id, status, attempt: task.attempt, reason });
718
+ this.settledSinceUpdate.push(task.id);
719
+ }
720
+ topListing() {
721
+ // Remote sandboxes own their filesystem; a host listing would be a lie.
722
+ if (!this.sandbox.localFs)
723
+ return "";
724
+ try {
725
+ const entries = fs
726
+ .readdirSync(this.meta.cwd, { withFileTypes: true })
727
+ .filter((e) => !e.name.startsWith(".") && e.name !== "node_modules")
728
+ .slice(0, 40)
729
+ .map((e) => (e.isDirectory() ? e.name + "/" : e.name));
730
+ return entries.join(" ");
731
+ }
732
+ catch {
733
+ return "";
734
+ }
735
+ }
736
+ // ---------------------------------------------------------------- agent hooks → journal
737
+ /** Ends in-flight agents gracefully when the run must wind down. */
738
+ agentStop = () => {
739
+ if (this.budgetExceeded())
740
+ return "The run's token budget is exhausted.";
741
+ if (this.finishing)
742
+ return "The run is finishing.";
743
+ return null;
744
+ };
745
+ /**
746
+ * Streaming deltas arrive one small chunk at a time; writing each as its own
747
+ * journal line bloats events.jsonl enormously on long runs. Coalesce per
748
+ * agent+channel and flush on size, on a short timer, and before any event
749
+ * that must order after the text (tool calls, agent.done).
750
+ */
751
+ deltaBuf = new Map();
752
+ deltaTimer = null;
753
+ queueDelta(agentId, taskId, channel, text) {
754
+ const key = `${agentId}:${channel}`;
755
+ const buf = this.deltaBuf.get(key);
756
+ if (buf)
757
+ buf.text += text;
758
+ else
759
+ this.deltaBuf.set(key, { agentId, taskId, channel, text });
760
+ if (this.deltaBuf.get(key).text.length >= 480) {
761
+ this.flushDeltas(agentId);
762
+ }
763
+ else if (!this.deltaTimer) {
764
+ this.deltaTimer = setTimeout(() => this.flushDeltas(), 200);
765
+ }
766
+ }
767
+ flushDeltas(onlyAgent) {
768
+ if (!onlyAgent && this.deltaTimer) {
769
+ clearTimeout(this.deltaTimer);
770
+ this.deltaTimer = null;
771
+ }
772
+ for (const [key, buf] of [...this.deltaBuf]) {
773
+ if (onlyAgent && buf.agentId !== onlyAgent)
774
+ continue;
775
+ this.deltaBuf.delete(key);
776
+ this.journal.append("agent.delta", {
777
+ agentId: buf.agentId,
778
+ taskId: buf.taskId,
779
+ channel: buf.channel,
780
+ text: buf.text,
781
+ });
782
+ }
783
+ }
784
+ agentHooks(agentId, taskId) {
785
+ return {
786
+ onDelta: (channel, text) => {
787
+ this.queueDelta(agentId, taskId, channel, text);
788
+ },
789
+ onToolCall: (callId, name, args) => {
790
+ this.flushDeltas(agentId);
791
+ this.journal.append("tool.call", { agentId, taskId, callId, name, args });
792
+ },
793
+ onToolResult: (callId, name, ok, summary) => {
794
+ this.journal.append("tool.result", { agentId, taskId, callId, name, ok, summary });
795
+ },
796
+ onUsage: this.onUsage,
797
+ onLog: (level, msg) => {
798
+ this.journal.append("log", { level, msg });
799
+ },
800
+ };
801
+ }
802
+ // ---------------------------------------------------------------- operator control
803
+ operatorQueue = [];
804
+ drainControl() {
805
+ for (const msg of this.control.poll()) {
806
+ if (msg.kind === "cancel") {
807
+ this.journal.append("operator.note", { text: "⛔ Cancel requested by operator." });
808
+ this.cancel();
809
+ }
810
+ else if (msg.kind === "note" && msg.text) {
811
+ this.operatorQueue.push(msg.text);
812
+ this.journal.append("operator.note", { text: msg.text });
813
+ }
814
+ }
815
+ }
816
+ peekOperatorNotes() {
817
+ return [...this.operatorQueue];
818
+ }
819
+ consumeOperatorNotes() {
820
+ const out = [...this.operatorQueue];
821
+ this.operatorQueue = [];
822
+ for (let i = 0; i < out.length; i++)
823
+ this.journal.append("operator.note.consumed", {});
824
+ return out;
825
+ }
826
+ // ---------------------------------------------------------------- synthesis
827
+ /** Write the final report file, set terminal status, emit run.final, flush. */
828
+ async writeFinal(status, reason, reportMarkdown, summary) {
829
+ this.flushDeltas();
830
+ const reportPath = path.join(this.runDirPath, "artifacts", "final-report.md");
831
+ (0, util_1.ensureDir)(path.dirname(reportPath));
832
+ fs.writeFileSync(reportPath, reportMarkdown, "utf8");
833
+ this.setStatus(status, reason);
834
+ this.journal.append("run.final", { summary, reportPath, reason, status });
835
+ await this.journal.flush();
836
+ }
837
+ /** Terminate the run as failed without any further model calls. */
838
+ async fail(reason) {
839
+ const md = [
840
+ `# Run failed`,
841
+ ``,
842
+ `**Mission:** ${this.meta.mission}`,
843
+ ``,
844
+ `**What happened:** ${reason}`,
845
+ ``,
846
+ this.fatal && /auth/i.test(this.fatal)
847
+ ? `## Fix\n1. Get a key at https://platform.deepseek.com\n2. Settings → paste your real DeepSeek key (it should look like \`sk-\` + ~32 characters), or run \`swarm config set apiKey <sk-...>\`\n3. Launch the mission again.`
848
+ : `## Next steps\nReview the error above and retry.`,
849
+ ].join("\n");
850
+ await this.writeFinal("failed", reason, md, reason);
851
+ }
852
+ async synthesize() {
853
+ // Fatal (e.g. bad API key): don't attempt an LLM synth that will just fail
854
+ // again — fail loudly with a clear, actionable report.
855
+ if (this.fatal) {
856
+ await this.fail(this.fatal);
857
+ return;
858
+ }
859
+ this.setStatus("synthesizing", this.finishReason);
860
+ const tasks = this.taskList();
861
+ const reports = tasks.length
862
+ ? tasks.map(prompts_1.reportBlock).join("\n\n")
863
+ : "(no tasks were completed)";
864
+ const artifactList = this.listArtifacts().join("\n") || "(none)";
865
+ const agentId = (0, util_1.rid)("synth");
866
+ let summary = "";
867
+ let reportMarkdown = "";
868
+ try {
869
+ const outcome = await (0, agent_1.runAgent)({
870
+ cfg: this.cfg,
871
+ agentId,
872
+ model: this.meta.options.conductorModel,
873
+ thinking: this.meta.options.thinking,
874
+ reasoningEffort: this.meta.options.reasoningEffort,
875
+ system: (0, prompts_1.synthSystem)({
876
+ meta: this.meta,
877
+ finishNotes: this.finishNotes,
878
+ reports: (0, util_1.truncateMiddle)(reports, 120_000, "chars"),
879
+ blackboard: this.blackboardDigest(4000),
880
+ artifactList,
881
+ reason: this.finishReason || "completed",
882
+ }),
883
+ kickoff: prompts_1.SYNTH_KICKOFF,
884
+ tools: (0, tools_1.synthToolset)(),
885
+ terminal: [tools_1.SUBMIT_FINAL_TOOL],
886
+ maxSteps: 12,
887
+ maxTokensOut: 16384,
888
+ signal: new AbortController().signal, // synthesis should finish even if run was cancelled
889
+ ctx: this.makeToolCtx(agentId, null),
890
+ hooks: this.agentHooks(agentId, ""),
891
+ });
892
+ const a = (outcome.terminal?.args ?? {});
893
+ reportMarkdown = String(a.report_markdown ?? outcome.finalText ?? "");
894
+ summary = String(a.summary ?? "");
895
+ }
896
+ catch (e) {
897
+ this.journal.append("log", { level: "error", msg: `synthesis failed: ${(0, util_1.errMsg)(e)}` });
898
+ }
899
+ if (!reportMarkdown.trim()) {
900
+ reportMarkdown = this.fallbackReport(tasks);
901
+ summary = summary || "Synthesis unavailable; assembled a fallback report from task results.";
902
+ }
903
+ // Truthful terminal status: failed if the conductor produced no tasks, or
904
+ // every task it produced failed/blocked.
905
+ const cancelled = this.finishReason.includes("cancel");
906
+ const anyDone = tasks.some((t) => t.status === "done");
907
+ const noWork = this.taskCounter === 0;
908
+ const allFailed = tasks.length > 0 && !anyDone;
909
+ let status = "done";
910
+ let reason = this.finishReason;
911
+ if (cancelled) {
912
+ status = "cancelled";
913
+ }
914
+ else if (noWork) {
915
+ status = "failed";
916
+ reason = "The conductor produced no tasks (it may have failed to respond). Check the activity log.";
917
+ }
918
+ else if (allFailed) {
919
+ status = "failed";
920
+ reason = `All ${tasks.length} task(s) failed or were blocked.`;
921
+ }
922
+ await this.writeFinal(status, reason, reportMarkdown, summary || (0, util_1.clip)(reportMarkdown, 600));
923
+ }
924
+ fallbackReport(tasks) {
925
+ const lines = [`# ${this.meta.mission}`, ``, `_Run ${this.meta.id} — ${this.finishReason}_`, ``];
926
+ for (const t of tasks) {
927
+ lines.push(`## ${t.id} ${t.title} (${t.status})`);
928
+ lines.push(t.report || t.error || "(no output)");
929
+ if (t.artifacts.length)
930
+ lines.push(`Artifacts: ${t.artifacts.join(", ")}`);
931
+ lines.push("");
932
+ }
933
+ return lines.join("\n");
934
+ }
935
+ listArtifacts() {
936
+ const dir = path.join(this.runDirPath, "artifacts");
937
+ const out = [];
938
+ const walk = (d, prefix) => {
939
+ let entries;
940
+ try {
941
+ entries = fs.readdirSync(d, { withFileTypes: true });
942
+ }
943
+ catch {
944
+ return;
945
+ }
946
+ for (const e of entries) {
947
+ if (e.isDirectory())
948
+ walk(path.join(d, e.name), prefix + e.name + "/");
949
+ else {
950
+ let size = 0;
951
+ try {
952
+ size = fs.statSync(path.join(d, e.name)).size;
953
+ }
954
+ catch { /* race */ }
955
+ out.push(`${prefix}${e.name} (${size}b)`);
956
+ }
957
+ }
958
+ };
959
+ walk(dir, "");
960
+ return out;
961
+ }
962
+ }
963
+ exports.Executor = Executor;
964
+ function inferRole(spec) {
965
+ const s = (spec.title + " " + spec.objective).toLowerCase();
966
+ if (/\b(test|verify|review|audit|check)\b/.test(s))
967
+ return "reviewer";
968
+ if (/\b(research|investigate|find|search|gather)\b/.test(s))
969
+ return "researcher";
970
+ if (/\b(write|draft|document|summary|report)\b/.test(s))
971
+ return "writer";
972
+ if (/\b(data|csv|dataset|scrape|parse|clean)\b/.test(s))
973
+ return "data-wrangler";
974
+ if (/\b(analy|compare|evaluate|benchmark)\b/.test(s))
975
+ return "analyst";
976
+ if (/\b(code|implement|build|fix|refactor|api|function|component)\b/.test(s))
977
+ return "coder";
978
+ return "generalist";
979
+ }
980
+ function safeArgs(s) {
981
+ try {
982
+ const v = JSON.parse(s);
983
+ return v && typeof v === "object" ? v : {};
984
+ }
985
+ catch {
986
+ return {};
987
+ }
988
+ }