@wrongstack/core 0.32.0 → 0.51.3

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 (45) hide show
  1. package/dist/{agent-bridge-D_XcS2HL.d.ts → agent-bridge-CjbD-i7-.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DpZTLdBe.d.ts → agent-subagent-runner-DfvlBx5N.d.ts} +3 -3
  3. package/dist/{config-BUEGM4JP.d.ts → config-ZRCf7sTu.d.ts} +21 -1
  4. package/dist/coordination/index.d.ts +10 -10
  5. package/dist/coordination/index.js +3310 -3056
  6. package/dist/coordination/index.js.map +1 -1
  7. package/dist/defaults/index.d.ts +13 -13
  8. package/dist/defaults/index.js +1544 -1390
  9. package/dist/defaults/index.js.map +1 -1
  10. package/dist/{events-BrQiweXN.d.ts → events-Bt44ikPN.d.ts} +135 -1
  11. package/dist/execution/index.d.ts +35 -9
  12. package/dist/execution/index.js +61 -28
  13. package/dist/execution/index.js.map +1 -1
  14. package/dist/extension/index.d.ts +3 -3
  15. package/dist/{index-pXJdVLe0.d.ts → index-OzA1XjHL.d.ts} +35 -3
  16. package/dist/{index-ysfO_DlX.d.ts → index-mAWBdLyJ.d.ts} +2 -2
  17. package/dist/index.d.ts +221 -25
  18. package/dist/index.js +1670 -1017
  19. package/dist/index.js.map +1 -1
  20. package/dist/infrastructure/index.d.ts +4 -4
  21. package/dist/infrastructure/index.js +17 -3
  22. package/dist/infrastructure/index.js.map +1 -1
  23. package/dist/kernel/index.d.ts +4 -4
  24. package/dist/kernel/index.js +3 -1
  25. package/dist/kernel/index.js.map +1 -1
  26. package/dist/{mcp-servers-BzB3r7_c.d.ts → mcp-servers-DONdo-XM.d.ts} +1 -1
  27. package/dist/models/index.js +5 -2
  28. package/dist/models/index.js.map +1 -1
  29. package/dist/{multi-agent-C8Z1i__e.d.ts → multi-agent-Ba9Ni2hC.d.ts} +1 -1
  30. package/dist/{multi-agent-coordinator-DOXSgtom.d.ts → multi-agent-coordinator-BuKq0q89.d.ts} +2 -2
  31. package/dist/{null-fleet-bus-DLsUjOyB.d.ts → null-fleet-bus-C0xd73YP.d.ts} +169 -138
  32. package/dist/observability/index.d.ts +1 -1
  33. package/dist/{path-resolver-DumKAi0n.d.ts → path-resolver-nkmdiFgi.d.ts} +1 -1
  34. package/dist/{plan-templates-BZMi-VpU.d.ts → plan-templates-BmDdJ7UL.d.ts} +2 -2
  35. package/dist/{provider-runner-Dlv8Fvw9.d.ts → provider-runner-BGro2qQB.d.ts} +1 -1
  36. package/dist/sdd/index.d.ts +5 -5
  37. package/dist/storage/index.d.ts +3 -3
  38. package/dist/{tool-executor-BAi4WI2d.d.ts → tool-executor-p4tP9tGF.d.ts} +1 -1
  39. package/dist/types/index.d.ts +8 -8
  40. package/dist/types/index.js +22 -5
  41. package/dist/types/index.js.map +1 -1
  42. package/dist/utils/index.d.ts +107 -1
  43. package/dist/utils/index.js +53 -2
  44. package/dist/utils/index.js.map +1 -1
  45. package/package.json +1 -1
@@ -101,11 +101,25 @@ var init_atomic_write = __esm({
101
101
  }
102
102
  });
103
103
 
104
+ // src/utils/term.ts
105
+ var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
106
+ function isStdoutTTY() {
107
+ return hasStdout() && Boolean(process.stdout.isTTY);
108
+ }
109
+ function writeTo(s, stream) {
110
+ if (!stream || typeof stream.write !== "function") return false;
111
+ stream.write(s);
112
+ return true;
113
+ }
114
+ function writeErr(s, stream = process.stderr) {
115
+ return writeTo(s, stream);
116
+ }
117
+
104
118
  // src/utils/color.ts
105
119
  var isColorTty = () => {
106
120
  if (process.env.NO_COLOR) return false;
107
121
  if (process.env.FORCE_COLOR) return true;
108
- return Boolean(process.stdout?.isTTY);
122
+ return isStdoutTTY();
109
123
  };
110
124
  var COLOR = isColorTty();
111
125
  var wrap = (open3, close) => (s) => COLOR ? `\x1B[${open3}m${s}\x1B[${close}m` : s;
@@ -202,10 +216,10 @@ var DefaultLogger = class _DefaultLogger {
202
216
  if (r <= LEVEL_RANK.warn || this.level === "debug" || this.level === "trace") {
203
217
  const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
204
218
  if (ctx !== void 0) {
205
- process.stderr.write(`${head} ${formatCtx(ctx)}
219
+ writeErr(`${head} ${formatCtx(ctx)}
206
220
  `);
207
221
  } else {
208
- process.stderr.write(`${head}
222
+ writeErr(`${head}
209
223
  `);
210
224
  }
211
225
  }
@@ -1106,9 +1120,9 @@ var DefaultMemoryStore = class {
1106
1120
  async readAll() {
1107
1121
  const parts = [];
1108
1122
  for (const scope of ["project-agents", "project-memory", "user-memory"]) {
1109
- const writeErr = this.writeErrors.get(scope);
1110
- if (writeErr) {
1111
- parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr.message}`);
1123
+ const writeErr2 = this.writeErrors.get(scope);
1124
+ if (writeErr2) {
1125
+ parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr2.message}`);
1112
1126
  }
1113
1127
  const body = await this.read(scope);
1114
1128
  if (body.trim()) parts.push(`## ${labelOf(scope)}
@@ -10772,8 +10786,14 @@ var ParallelEternalEngine = class {
10772
10786
  await this.runOneIteration();
10773
10787
  } catch (err) {
10774
10788
  this.consecutiveFailures++;
10775
- this.opts.onError?.(err instanceof Error ? err : new Error(String(err)), this.consecutiveFailures);
10776
- await this.appendFailure("engine error", err instanceof Error ? err.message : String(err));
10789
+ this.opts.onError?.(
10790
+ err instanceof Error ? err : new Error(String(err)),
10791
+ this.consecutiveFailures
10792
+ );
10793
+ await this.appendFailure(
10794
+ "engine error",
10795
+ err instanceof Error ? err.message : String(err)
10796
+ );
10777
10797
  }
10778
10798
  if (this.stopRequested) break;
10779
10799
  await sleep2(2e3);
@@ -10789,14 +10809,19 @@ var ParallelEternalEngine = class {
10789
10809
  * Called by the REPL in its main loop (REPL drives, engine is stateless per tick).
10790
10810
  */
10791
10811
  async runOneIteration() {
10812
+ const emit = (stage) => {
10813
+ this.opts.onStage?.(stage);
10814
+ };
10792
10815
  this.iterations++;
10793
10816
  const goal = await loadGoal(this.goalPath);
10794
10817
  if (!goal) {
10795
10818
  this.stopRequested = true;
10819
+ emit({ phase: "stopped" });
10796
10820
  return false;
10797
10821
  }
10798
10822
  if (goal.goalState !== "active") {
10799
10823
  this.stopRequested = true;
10824
+ emit({ phase: "stopped" });
10800
10825
  return false;
10801
10826
  }
10802
10827
  if (!this.coordinator) {
@@ -10809,10 +10834,13 @@ var ParallelEternalEngine = class {
10809
10834
  const runner = makeAgentSubagentRunner({ factory: this.agentFactory });
10810
10835
  this.coordinator.setRunner?.(runner);
10811
10836
  }
10837
+ emit({ phase: "decompose" });
10812
10838
  const tasks = await this.decomposeGoal(goal);
10813
10839
  if (!tasks || tasks.length === 0) {
10840
+ emit({ phase: "sleep", ms: 2e3 });
10814
10841
  return false;
10815
10842
  }
10843
+ emit({ phase: "fanout", slots: Math.min(this.slots, tasks.length) });
10816
10844
  const fanOut = await this.fanOut(goal, tasks);
10817
10845
  this.iterationsSinceCompact++;
10818
10846
  const successCount = fanOut.results.filter((r) => r.status === "success").length;
@@ -10829,12 +10857,20 @@ var ParallelEternalEngine = class {
10829
10857
  status,
10830
10858
  note
10831
10859
  });
10860
+ emit({
10861
+ phase: "aggregate",
10862
+ successCount,
10863
+ total: fanOut.results.length,
10864
+ goalComplete: fanOut.goalComplete
10865
+ });
10832
10866
  if (fanOut.goalComplete) {
10833
10867
  this.stopRequested = true;
10834
10868
  this.state = "stopped";
10869
+ emit({ phase: "stopped" });
10835
10870
  return true;
10836
10871
  }
10837
10872
  await this.maybeCompact();
10873
+ emit({ phase: "sleep", ms: 2e3 });
10838
10874
  return fanOut.allSuccessful;
10839
10875
  }
10840
10876
  // -------------------------------------------------------------------------
@@ -10848,7 +10884,9 @@ var ParallelEternalEngine = class {
10848
10884
  (t) => dispatchAgent(t, { classifier: this.dispatchClassifier }).catch(() => null)
10849
10885
  )
10850
10886
  ) : [];
10851
- const recentJournal = goal.journal.slice(-5).map((e) => ` #${e.iteration} [${e.status}] ${e.task}${e.note ? ` \u2014 ${e.note.slice(0, 80)}` : ""}`).join("\n");
10887
+ const recentJournal = goal.journal.slice(-5).map(
10888
+ (e) => ` #${e.iteration} [${e.status}] ${e.task}${e.note ? ` \u2014 ${e.note.slice(0, 80)}` : ""}`
10889
+ ).join("\n");
10852
10890
  const directivePreamble = [
10853
10891
  "\u2550\u2550\u2550 ETERNAL AUTONOMY \u2014 parallel task slot \u2550\u2550\u2550",
10854
10892
  "",
@@ -10891,35 +10929,44 @@ ${personaLine}Task: ${task}
10891
10929
  role: route?.role ?? "generic",
10892
10930
  method: route?.method ?? "none"
10893
10931
  });
10894
- spawnPromises.push((async () => {
10895
- try {
10896
- await coordinator.spawn(
10897
- route ? {
10898
- id: subagentId,
10899
- name: route.definition.config.name,
10900
- role: route.role,
10901
- tools: route.definition.config.tools,
10902
- systemPromptOverride: route.definition.config.prompt,
10903
- timeoutMs: this.timeoutMs
10904
- } : {
10905
- id: subagentId,
10906
- name: `slot-${subagentId.slice(-6)}`,
10907
- // Let the coordinator apply its default budget (roster or generic).
10908
- // Hardcoding low limits here defeats the x10 budget improvement.
10909
- timeoutMs: this.timeoutMs
10910
- }
10911
- );
10912
- subagentIds.push(subagentId);
10913
- taskIds.push(taskId);
10914
- await coordinator.assign(spec);
10915
- } catch {
10916
- }
10917
- })());
10932
+ spawnPromises.push(
10933
+ (async () => {
10934
+ try {
10935
+ await coordinator.spawn(
10936
+ route ? {
10937
+ id: subagentId,
10938
+ name: route.definition.config.name,
10939
+ role: route.role,
10940
+ tools: route.definition.config.tools,
10941
+ systemPromptOverride: route.definition.config.prompt,
10942
+ timeoutMs: this.timeoutMs
10943
+ } : {
10944
+ id: subagentId,
10945
+ name: `slot-${subagentId.slice(-6)}`,
10946
+ // Let the coordinator apply its default budget (roster or generic).
10947
+ // Hardcoding low limits here defeats the x10 budget improvement.
10948
+ timeoutMs: this.timeoutMs
10949
+ }
10950
+ );
10951
+ subagentIds.push(subagentId);
10952
+ taskIds.push(taskId);
10953
+ await coordinator.assign(spec);
10954
+ } catch {
10955
+ }
10956
+ })()
10957
+ );
10918
10958
  }
10919
10959
  await Promise.all(spawnPromises);
10920
10960
  if (taskIds.length === 0) {
10921
- return { results: [], allSuccessful: false, goalComplete: false, partialOutput: "", routes: routeInfo };
10961
+ return {
10962
+ results: [],
10963
+ allSuccessful: false,
10964
+ goalComplete: false,
10965
+ partialOutput: "",
10966
+ routes: routeInfo
10967
+ };
10922
10968
  }
10969
+ this.opts.onStage?.({ phase: "await", taskIds: [...taskIds] });
10923
10970
  let results = [];
10924
10971
  try {
10925
10972
  const ctrl = new AbortController();
@@ -11200,84 +11247,8 @@ function buildGoalPreamble(goal) {
11200
11247
  "BEGIN.]"
11201
11248
  ].join("\n");
11202
11249
  }
11203
-
11204
- // src/coordination/director.ts
11205
11250
  init_atomic_write();
11206
11251
 
11207
- // src/coordination/large-answer-store.ts
11208
- var LargeAnswerStore = class {
11209
- /**
11210
- * Responses above this size (in characters) are stored out-of-context.
11211
- * Below this, the full answer is returned inline (no overhead).
11212
- * Default: 2000 chars ≈ 400-600 tokens.
11213
- */
11214
- sizeThreshold;
11215
- store = /* @__PURE__ */ new Map();
11216
- constructor(sizeThreshold = 2e3) {
11217
- this.sizeThreshold = sizeThreshold;
11218
- }
11219
- /**
11220
- * Store a value, returning a summary + key for inline use.
11221
- * If the value is below sizeThreshold, returns it as-is (no store entry).
11222
- */
11223
- storeAnswer(value) {
11224
- if (value === void 0 || value === null) {
11225
- return { summary: String(value), inline: true };
11226
- }
11227
- const serialized = typeof value === "string" ? value : JSON.stringify(value);
11228
- const size = serialized.length;
11229
- if (size <= this.sizeThreshold) {
11230
- return { summary: serialized.slice(0, 500), inline: true };
11231
- }
11232
- const key = `a-${hashStr(serialized)}`;
11233
- this.store.set(key, {
11234
- key,
11235
- value,
11236
- size,
11237
- storedAt: Date.now()
11238
- });
11239
- return {
11240
- key,
11241
- summary: `[stored: ${size} chars \u2014 use roll_up or ask_result tool to retrieve, key=${key}]`,
11242
- inline: false
11243
- };
11244
- }
11245
- /**
11246
- * Retrieve a previously stored answer by its key.
11247
- * Returns undefined if the key is unknown or the store was cleared.
11248
- */
11249
- retrieveAnswer(key) {
11250
- return this.store.get(key)?.value;
11251
- }
11252
- /**
11253
- * Check if a key exists in the store.
11254
- */
11255
- hasAnswer(key) {
11256
- return this.store.has(key);
11257
- }
11258
- /** Number of stored entries. */
11259
- get size() {
11260
- return this.store.size;
11261
- }
11262
- /** Total characters stored. */
11263
- get totalChars() {
11264
- let total = 0;
11265
- for (const e of this.store.values()) total += e.size;
11266
- return total;
11267
- }
11268
- /** Clear all stored entries. Call at the end of a director run. */
11269
- clear() {
11270
- this.store.clear();
11271
- }
11272
- };
11273
- function hashStr(s) {
11274
- let h = 5381;
11275
- for (let i = 0; i < s.length; i++) {
11276
- h = h * 33 ^ s.charCodeAt(i);
11277
- }
11278
- return (h >>> 0).toString(36);
11279
- }
11280
-
11281
11252
  // src/coordination/in-memory-transport.ts
11282
11253
  var InMemoryBridgeTransport = class {
11283
11254
  subs = /* @__PURE__ */ new Map();
@@ -11425,1051 +11396,344 @@ function createMessage(type, from, payload, to) {
11425
11396
  priority: "normal"
11426
11397
  };
11427
11398
  }
11428
-
11429
- // src/coordination/director-prompts.ts
11430
- var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
11431
- subagents by spawning them, assigning tasks, awaiting completions, and
11432
- rolling up their outputs into your next decision.
11433
-
11434
- Core fleet tools available to you:
11435
- - spawn_subagent \u2014 create a worker with a chosen provider / model / role
11436
- - assign_task \u2014 hand a piece of work to a specific subagent
11437
- - await_tasks \u2014 block until named task ids complete (parallel-safe)
11438
- - ask_subagent \u2014 synchronously query a running subagent via the bridge
11439
- - roll_up \u2014 aggregate finished tasks into a markdown/json summary
11440
- - terminate_subagent \u2014 abort a stuck worker (use sparingly)
11441
- - fleet_status \u2014 snapshot of all subagents and pending tasks
11442
- - fleet_usage \u2014 token + cost breakdown per subagent and total
11443
-
11444
- Working rules:
11445
- 1. Decompose first. Before spawning, decide which sub-tasks are
11446
- independent and can run in parallel. Sequential work doesn't need a
11447
- subagent \u2014 do it yourself.
11448
- 2. Match worker to job. Cheap/fast model for triage, capable model for
11449
- synthesis. Different providers per sibling is allowed and encouraged.
11450
- 3. Always pair an assign with an await. Don't fire-and-forget; you owe
11451
- the user a single coherent answer at the end.
11452
- 4. Roll up before deciding. After await_tasks resolves, call roll_up so
11453
- the results are folded back into your context in a compact form.
11454
- 5. Budget is real. Check fleet_usage periodically. If a subagent is
11455
- thrashing, terminate it rather than letting cost climb silently.
11456
- 6. Never claim a subagent's work as your own without verifying it. If a
11457
- result looks wrong, ask_subagent for clarification before passing it
11458
- to the user.
11459
- 7. Wind down when satisfied. When the results are good enough, call
11460
- work_complete \u2014 no new subagents will spawn and queued tasks complete
11461
- as aborted. Running subagents finish naturally. Call terminate_subagent
11462
- only for ones you need to stop immediately.`;
11463
- var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
11464
- a specific slice of a larger plan \u2014 do that slice well and report back.
11465
-
11466
- Bridge contract:
11467
- - You have a parent (the Director). You may call \`request\` on the
11468
- parent bridge to ask a clarifying question. Use this sparingly; the
11469
- parent is also working.
11470
- - You MAY NOT request the parent's system prompt, tool list, or other
11471
- subagents' context. Those are not yours to read.
11472
- - Your final task output is what the Director sees. Be concise,
11473
- structured, and self-contained \u2014 assume the Director will paste your
11474
- output into its own context.`;
11475
- function composeDirectorPrompt(parts = {}) {
11476
- const sections = [];
11477
- const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
11478
- if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
11479
- if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
11480
- sections.push(`Available roles you can spawn:
11481
- ${parts.rosterSummary.trim()}`);
11482
- }
11483
- if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
11484
- sections.push(parts.basePrompt.trim());
11399
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
11400
+ var IS_WINDOWS = process.platform === "win32";
11401
+ var SEP = IS_WINDOWS ? "\\" : "/";
11402
+ function isGlob(p) {
11403
+ for (const c of p) {
11404
+ if (GLOB_CHARS.has(c)) return true;
11485
11405
  }
11486
- return sections.join("\n\n");
11406
+ return false;
11487
11407
  }
11488
- function composeSubagentPrompt(parts = {}) {
11489
- const sections = [];
11490
- const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
11491
- if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
11492
- if (parts.role && parts.role.trim().length > 0) {
11493
- sections.push(`Role:
11494
- ${parts.role.trim()}`);
11495
- }
11496
- if (parts.task && parts.task.trim().length > 0) {
11497
- sections.push(`Task:
11498
- ${parts.task.trim()}`);
11499
- }
11500
- if (parts.sharedScratchpad && parts.sharedScratchpad.trim().length > 0) {
11501
- sections.push(
11502
- `Shared notes:
11503
- A scratchpad shared with the rest of the fleet is mounted at \`${parts.sharedScratchpad.trim()}\`.
11504
- - Write your final findings as markdown files there (e.g. \`findings.md\`, \`security.md\`).
11505
- - Before starting, list the directory and read any sibling files relevant to your task \u2014 they may already contain context you can build on.
11506
- - Use stable filenames (one file per concern); overwrite instead of appending so the Director sees the latest state.`
11507
- );
11508
- }
11509
- if (parts.override && parts.override.trim().length > 0) {
11510
- sections.push(parts.override.trim());
11408
+ function globToRegex(pat) {
11409
+ let i = 0, re = "^";
11410
+ while (i < pat.length) {
11411
+ const c = pat[i];
11412
+ if (c === "*") {
11413
+ if (pat[i + 1] === "*") {
11414
+ re += ".*";
11415
+ i += 2;
11416
+ if (pat[i] === "/") i++;
11417
+ } else {
11418
+ re += "[^/\\\\]*";
11419
+ i++;
11420
+ }
11421
+ } else if (c === "?") {
11422
+ re += "[^/\\\\]";
11423
+ i++;
11424
+ } else if (c === "[") {
11425
+ let cls = "[";
11426
+ i++;
11427
+ if (pat[i] === "!" || pat[i] === "^") {
11428
+ cls += "^";
11429
+ i++;
11430
+ }
11431
+ while (i < pat.length && pat[i] !== "]") {
11432
+ const ch = pat[i] ?? "";
11433
+ if (ch === "\\") cls += "\\\\";
11434
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
11435
+ else cls += ch;
11436
+ i++;
11437
+ }
11438
+ cls += "]";
11439
+ re += cls;
11440
+ i++;
11441
+ } else {
11442
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
11443
+ i++;
11444
+ }
11511
11445
  }
11512
- return sections.join("\n\n");
11446
+ return new RegExp(re + "$");
11513
11447
  }
11514
- function rosterSummaryFromConfigs(roster) {
11515
- const lines = [];
11516
- for (const [roleId, cfg] of Object.entries(roster)) {
11517
- const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
11518
- const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
11519
- const tail = headline ? ` \u2014 ${headline}` : "";
11520
- lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
11521
- }
11522
- return lines.join("\n");
11448
+ function baseDir(pat) {
11449
+ let i = pat.length - 1;
11450
+ while (i >= 0 && !GLOB_CHARS.has(pat[i]) && pat[i] !== SEP && pat[i] !== "/") i--;
11451
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
11452
+ return cut < 0 ? "." : pat.slice(0, cut);
11523
11453
  }
11524
-
11525
- // src/coordination/fleet-bus.ts
11526
- var FleetBus = class {
11527
- byId = /* @__PURE__ */ new Map();
11528
- byType = /* @__PURE__ */ new Map();
11529
- any = /* @__PURE__ */ new Set();
11530
- /**
11531
- * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
11532
- * `onPattern('*')`) to forward all events with subagent attribution, so
11533
- * new kernel event types are automatically forwarded without any manual
11534
- * registration. `subagent.*` events are excluded because they originate
11535
- * from MultiAgentHost on the parent bus, not the subagent's own bus.
11536
- *
11537
- * Returns a disposer that detaches every subscription; call on
11538
- * subagent teardown so the listeners don't outlive the run.
11539
- */
11540
- attach(subagentId, bus, taskId) {
11541
- const off = bus.onAny((type, payload) => {
11542
- if (type.startsWith("subagent.")) return;
11543
- this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
11544
- });
11545
- return () => {
11546
- off();
11547
- };
11548
- }
11549
- /** Subscribe to every event from one subagent. */
11550
- subscribe(subagentId, handler) {
11551
- let set = this.byId.get(subagentId);
11552
- if (!set) {
11553
- set = /* @__PURE__ */ new Set();
11554
- this.byId.set(subagentId, set);
11454
+ async function expandGlob(pattern) {
11455
+ if (!isGlob(pattern)) return [pattern];
11456
+ const results = /* @__PURE__ */ new Set();
11457
+ const abs = isAbsolute(pattern);
11458
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
11459
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
11460
+ async function walk4(dir, pat) {
11461
+ let entries;
11462
+ try {
11463
+ entries = await fsp.readdir(dir);
11464
+ } catch {
11465
+ return;
11555
11466
  }
11556
- set.add(handler);
11557
- return () => {
11558
- set.delete(handler);
11559
- };
11560
- }
11561
- /** Subscribe to one event type across all subagents. */
11562
- filter(type, handler) {
11563
- let set = this.byType.get(type);
11564
- if (!set) {
11565
- set = /* @__PURE__ */ new Set();
11566
- this.byType.set(type, set);
11467
+ const firstGlob = pat.search(/[*?[\[]/);
11468
+ if (firstGlob < 0) {
11469
+ const re = globToRegex(pat);
11470
+ for (const e of entries) {
11471
+ if (re.test(e)) {
11472
+ const full = `${dir}${SEP}${e}`;
11473
+ results.add(abs ? resolve(full) : full);
11474
+ }
11475
+ }
11476
+ return;
11567
11477
  }
11568
- set.add(handler);
11569
- return () => {
11570
- set.delete(handler);
11571
- };
11572
- }
11573
- /** Subscribe to literally everything. The fleet roll-up uses this. */
11574
- onAny(handler) {
11575
- this.any.add(handler);
11576
- return () => {
11577
- this.any.delete(handler);
11578
- };
11579
- }
11580
- emit(event) {
11581
- const byId = this.byId.get(event.subagentId);
11582
- if (byId)
11583
- for (const h of byId) {
11478
+ const before = pat.slice(0, firstGlob);
11479
+ const rest = pat.slice(firstGlob);
11480
+ if (before.endsWith("**")) {
11481
+ await walk4(dir, rest);
11482
+ for (const e of entries) {
11483
+ const full = `${dir}${SEP}${e}`;
11584
11484
  try {
11585
- h(event);
11485
+ const stat5 = await fsp.stat(full);
11486
+ if (stat5.isDirectory()) await walk4(full, rest);
11586
11487
  } catch {
11587
11488
  }
11588
11489
  }
11589
- const byType = this.byType.get(event.type);
11590
- if (byType)
11591
- for (const h of byType) {
11490
+ } else if (before === "") {
11491
+ const re = globToRegex(rest);
11492
+ for (const e of entries) {
11493
+ if (re.test(e)) {
11494
+ const full = `${dir}${SEP}${e}`;
11495
+ results.add(abs ? resolve(full) : full);
11496
+ }
11497
+ }
11498
+ } else {
11499
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
11500
+ if (entries.includes(seg)) {
11501
+ const full = `${dir}${SEP}${seg}`;
11592
11502
  try {
11593
- h(event);
11503
+ const stat5 = await fsp.stat(full);
11504
+ if (stat5.isDirectory()) await walk4(full, rest);
11594
11505
  } catch {
11595
11506
  }
11596
11507
  }
11597
- for (const h of this.any) {
11598
- try {
11599
- h(event);
11600
- } catch {
11601
- }
11602
11508
  }
11603
11509
  }
11604
- };
11605
- var FleetUsageAggregator = class {
11606
- constructor(bus, priceLookup, metaLookup) {
11607
- this.priceLookup = priceLookup;
11608
- this.metaLookup = metaLookup;
11609
- this.unsub.push(bus.filter("provider.response", (e) => this.onProviderResponse(e)));
11610
- this.unsub.push(bus.filter("tool.executed", (e) => this.onToolExecuted(e)));
11611
- this.unsub.push(bus.filter("iteration.started", (e) => this.onIterationStarted(e)));
11510
+ await walk4(base === "." ? "." : base, relPat);
11511
+ return [...results];
11512
+ }
11513
+
11514
+ // src/coordination/collab-debug.ts
11515
+ var DEFAULT_MAX_TARGET_FILES = 30;
11516
+ var CollabSession = class extends EventEmitter {
11517
+ sessionId;
11518
+ options;
11519
+ snapshot;
11520
+ director;
11521
+ fleetBus;
11522
+ subagentIds = /* @__PURE__ */ new Map();
11523
+ // role → subagentId
11524
+ bugs = /* @__PURE__ */ new Map();
11525
+ plans = /* @__PURE__ */ new Map();
11526
+ evaluations = /* @__PURE__ */ new Map();
11527
+ disposers = new Array();
11528
+ settled = false;
11529
+ timeoutMs;
11530
+ cancelled = false;
11531
+ alerts = [];
11532
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
11533
+ progressBySubagent = /* @__PURE__ */ new Map();
11534
+ /** Last tool call count when a timeout warning was handled. */
11535
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
11536
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
11537
+ _timeoutTimer;
11538
+ constructor(director, fleetBus, options) {
11539
+ super();
11540
+ this.sessionId = randomUUID();
11541
+ this.options = options;
11542
+ this.director = director;
11543
+ this.fleetBus = fleetBus;
11544
+ this.timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
11545
+ if (options.prebuiltSnapshot) {
11546
+ this.snapshot = options.prebuiltSnapshot;
11547
+ } else {
11548
+ this.snapshot = {
11549
+ id: this.sessionId,
11550
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
11551
+ files: []
11552
+ };
11553
+ }
11612
11554
  }
11613
- priceLookup;
11614
- metaLookup;
11615
- perSubagent = /* @__PURE__ */ new Map();
11616
- total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
11617
- unsub = new Array();
11618
- /**
11619
- * Remove a terminated subagent's data from the aggregator and subtract its
11620
- * contribution from the running totals. Call this when a subagent is removed
11621
- * from the fleet so the aggregator doesn't accumulate unbounded data for
11622
- * entities that will never emit events again.
11623
- */
11624
- removeSubagent(subagentId) {
11625
- const snap = this.perSubagent.get(subagentId);
11626
- if (!snap) return;
11627
- this.perSubagent.delete(subagentId);
11628
- this.total.input -= snap.input;
11629
- this.total.output -= snap.output;
11630
- this.total.cacheRead -= snap.cacheRead;
11631
- this.total.cacheWrite -= snap.cacheWrite;
11632
- this.total.cost -= snap.cost;
11555
+ get id() {
11556
+ return this.sessionId;
11633
11557
  }
11634
- /** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
11635
- dispose() {
11636
- for (const off of this.unsub) off();
11637
- this.unsub.length = 0;
11558
+ getSessionAlerts() {
11559
+ return [...this.alerts];
11638
11560
  }
11639
- /** Live snapshot — safe to call from a tool's execute() body. */
11640
- snapshot() {
11641
- return {
11642
- total: { ...this.total },
11643
- perSubagent: Object.fromEntries(
11644
- Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
11645
- )
11646
- };
11561
+ isCancelled() {
11562
+ return this.cancelled;
11647
11563
  }
11648
- ensure(subagentId) {
11649
- let snap = this.perSubagent.get(subagentId);
11650
- if (!snap) {
11651
- const meta = this.metaLookup?.(subagentId);
11652
- snap = {
11653
- subagentId,
11654
- provider: meta?.provider,
11655
- model: meta?.model,
11656
- input: 0,
11657
- output: 0,
11658
- cacheRead: 0,
11659
- cacheWrite: 0,
11660
- cost: 0,
11661
- toolCalls: 0,
11662
- iterations: 0,
11663
- startedAt: Date.now(),
11664
- lastEventAt: Date.now()
11665
- };
11666
- this.perSubagent.set(subagentId, snap);
11667
- }
11668
- return snap;
11564
+ /**
11565
+ * Snapshot of role → subagentId map. The Director calls coordinator.stop()
11566
+ * for each agent when cancelling the session, using this map to enumerate
11567
+ * all three collab agents.
11568
+ */
11569
+ getSubagentIds() {
11570
+ return new Map(this.subagentIds);
11669
11571
  }
11670
- onProviderResponse(e) {
11671
- const snap = this.ensure(e.subagentId);
11672
- const p = e.payload;
11673
- const usage = p?.usage;
11674
- if (!usage) return;
11675
- snap.input += usage.input ?? 0;
11676
- snap.output += usage.output ?? 0;
11677
- snap.cacheRead += usage.cacheRead ?? 0;
11678
- snap.cacheWrite += usage.cacheWrite ?? 0;
11679
- this.total.input += usage.input ?? 0;
11680
- this.total.output += usage.output ?? 0;
11681
- this.total.cacheRead += usage.cacheRead ?? 0;
11682
- this.total.cacheWrite += usage.cacheWrite ?? 0;
11683
- const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
11684
- if (price) {
11685
- const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
11686
- snap.cost += delta;
11687
- this.total.cost += delta;
11572
+ /**
11573
+ * Returns the effective file limit for this session.
11574
+ * Priority: explicit `maxTargetFiles` > dynamic from `contextWindow` > `DEFAULT_MAX_TARGET_FILES`.
11575
+ */
11576
+ effectiveFileLimit() {
11577
+ if (this.options.maxTargetFiles !== void 0) {
11578
+ return this.options.maxTargetFiles;
11688
11579
  }
11689
- snap.lastEventAt = e.ts;
11580
+ if (this.options.contextWindow !== void 0) {
11581
+ return Math.max(5, Math.floor(this.options.contextWindow * 0.4 / 2e3));
11582
+ }
11583
+ return DEFAULT_MAX_TARGET_FILES;
11690
11584
  }
11691
- onToolExecuted(e) {
11692
- const snap = this.ensure(e.subagentId);
11693
- snap.toolCalls += 1;
11694
- snap.lastEventAt = e.ts;
11585
+ async buildSnapshot() {
11586
+ if (this.snapshot.files.length > 0) return this.snapshot;
11587
+ const allFiles = [];
11588
+ for (const pattern of this.options.targetPaths) {
11589
+ const expanded = await expandGlob(pattern);
11590
+ allFiles.push(...expanded);
11591
+ }
11592
+ const limit = this.effectiveFileLimit();
11593
+ if (allFiles.length > limit) {
11594
+ const hint = this.options.contextWindow ? `contextWindow=${this.options.contextWindow} \u2192 calculated limit=${limit}` : `default limit=${DEFAULT_MAX_TARGET_FILES}`;
11595
+ throw new Error(
11596
+ `[collab_debug] Target has ${allFiles.length} files, which exceeds the limit (${hint}). Narrow the target or pass maxTargetFiles / contextWindow to override. For large codebases, run package-by-package or module-by-module sessions instead of targeting the entire repo.`
11597
+ );
11598
+ }
11599
+ for (const filePath of allFiles) {
11600
+ try {
11601
+ const content = await fsp.readFile(filePath, "utf8");
11602
+ const ext = filePath.split(".").pop() ?? "";
11603
+ const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
11604
+ this.snapshot.files.push({ path: filePath, content, language });
11605
+ } catch {
11606
+ this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
11607
+ }
11608
+ }
11609
+ return this.snapshot;
11695
11610
  }
11696
- onIterationStarted(e) {
11697
- const snap = this.ensure(e.subagentId);
11698
- snap.iterations += 1;
11699
- snap.lastEventAt = e.ts;
11611
+ /**
11612
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
11613
+ * collab agents finish early. The session-level timeout timer is also cleared.
11614
+ * Safe to call multiple times (idempotent after first call).
11615
+ */
11616
+ cancel(reason = "Director cancelled collab session") {
11617
+ if (this.settled) return;
11618
+ this.cancelled = true;
11619
+ if (this._timeoutTimer) {
11620
+ clearTimeout(this._timeoutTimer);
11621
+ this._timeoutTimer = void 0;
11622
+ }
11623
+ this.fleetBus.emit({
11624
+ subagentId: this.director.id,
11625
+ ts: Date.now(),
11626
+ type: "director.cancel_collab",
11627
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
11628
+ });
11629
+ this.fleetBus.emit({
11630
+ subagentId: this.director.id,
11631
+ ts: Date.now(),
11632
+ type: "collab.cancelled",
11633
+ payload: { sessionId: this.sessionId, reason }
11634
+ });
11700
11635
  }
11701
- };
11702
- function makeSpawnTool(director, roster) {
11703
- const inputSchema = {
11704
- type: "object",
11705
- properties: {
11706
- role: { type: "string", description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields." },
11707
- description: { type: "string", description: "Free-form task description. When `role` is not set, the director uses the smart dispatcher to route this to the best-matching catalog agent. Use this when you don't know the exact role name." },
11708
- name: { type: "string", description: "Display name for the subagent. Used as a fallback when description-based dispatch does not resolve a role." },
11709
- provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
11710
- model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
11711
- systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
11712
- maxIterations: { type: "number", minimum: 1 },
11713
- maxToolCalls: { type: "number", minimum: 1 },
11714
- maxCostUsd: { type: "number", minimum: 0 },
11715
- timeoutMs: { type: "number", minimum: 1, description: "Hard wall-clock cap in milliseconds. Defaults to none (idle timeout is the default reaper)." },
11716
- idleTimeoutMs: { type: "number", minimum: 1, description: "Idle timeout in ms: reap the subagent after this long with no activity. Resets on every iteration/tool call. Default is role/coordinator-specific." },
11717
- maxTokens: { type: "number", minimum: 1, description: "Maximum total tokens (input + output) the subagent may use." }
11718
- },
11719
- required: []
11720
- };
11721
- return {
11722
- name: "spawn_subagent",
11723
- description: "Create a new subagent under this director. Returns the subagent id.",
11724
- usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
11725
- permission: "auto",
11726
- mutating: false,
11727
- inputSchema,
11728
- async execute(input) {
11729
- const i = input ?? {};
11730
- const role = typeof i.role === "string" ? i.role : void 0;
11731
- const description = typeof i.description === "string" ? i.description : void 0;
11732
- let cfg;
11733
- if (role && roster) {
11734
- const base = roster[role];
11735
- if (!base) return { error: `unknown role "${role}". roster has: ${Object.keys(roster).join(", ")}` };
11736
- cfg = instantiateRosterConfig(role, base);
11737
- } else if (description && !role) {
11738
- const dispatchResult = await dispatchAgent(description, {
11739
- classifier: director.dispatchClassifier,
11740
- catalog: roster
11741
- });
11742
- const dispatchRole = dispatchResult.role;
11743
- if (roster?.[dispatchRole]) {
11744
- cfg = instantiateRosterConfig(dispatchRole, roster[dispatchRole]);
11745
- } else {
11746
- const def = dispatchResult.definition;
11747
- cfg = {
11748
- name: def.config.name ?? dispatchRole,
11749
- role: dispatchRole,
11750
- provider: def.config.provider,
11751
- model: def.config.model
11752
- };
11753
- }
11636
+ async start() {
11637
+ if (this.settled) throw new Error("session already settled");
11638
+ this.settled = true;
11639
+ await this.buildSnapshot();
11640
+ this.wireFleetBus();
11641
+ const [bugHunterId, refactorPlannerId, criticId] = await Promise.all([
11642
+ this.spawnAgent("bug-hunter", this.buildBugHunterTask()),
11643
+ this.spawnAgent("refactor-planner", this.buildRefactorPlannerTask()),
11644
+ this.spawnAgent("critic", this.buildCriticTask())
11645
+ ]);
11646
+ this.subagentIds.set("bug-hunter", bugHunterId);
11647
+ this.subagentIds.set("refactor-planner", refactorPlannerId);
11648
+ this.subagentIds.set("critic", criticId);
11649
+ const timeout = new Promise((_, reject) => {
11650
+ this._timeoutTimer = setTimeout(() => {
11651
+ this.cancel("Session-level timeout reached");
11652
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
11653
+ }, this.timeoutMs);
11654
+ });
11655
+ let results = null;
11656
+ try {
11657
+ results = await Promise.race([
11658
+ Promise.all([
11659
+ this.director.awaitTasks([bugHunterId]),
11660
+ this.director.awaitTasks([refactorPlannerId]),
11661
+ this.director.awaitTasks([criticId])
11662
+ ]),
11663
+ timeout
11664
+ ]);
11665
+ } catch (err) {
11666
+ if (this._timeoutTimer) {
11667
+ clearTimeout(this._timeoutTimer);
11668
+ this._timeoutTimer = void 0;
11754
11669
  }
11755
- cfg ??= { name: i.name ?? "subagent" };
11756
- if (typeof i.name === "string") cfg.name = i.name;
11757
- if (typeof i.provider === "string") cfg.provider = i.provider;
11758
- if (typeof i.model === "string") cfg.model = i.model;
11759
- if (typeof i.systemPromptOverride === "string") cfg.systemPromptOverride = i.systemPromptOverride;
11760
- if (typeof i.maxIterations === "number") cfg.maxIterations = i.maxIterations;
11761
- if (typeof i.maxToolCalls === "number") cfg.maxToolCalls = i.maxToolCalls;
11762
- if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
11763
- if (typeof i.timeoutMs === "number") cfg.timeoutMs = i.timeoutMs;
11764
- if (typeof i.idleTimeoutMs === "number") cfg.idleTimeoutMs = i.idleTimeoutMs;
11765
- if (typeof i.maxTokens === "number") cfg.maxTokens = i.maxTokens;
11766
- try {
11767
- const subagentId = await director.spawn(cfg);
11768
- return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name, role: cfg.role };
11769
- } catch (err) {
11770
- if (err instanceof FleetSpawnBudgetError) {
11771
- return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
11772
- }
11773
- if (err instanceof FleetCostCapError) {
11774
- return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
11670
+ this.cleanup();
11671
+ const error = err instanceof Error ? err : new Error(String(err));
11672
+ this.emit("session.error", error);
11673
+ throw error;
11674
+ }
11675
+ for (const result of results.flat()) {
11676
+ await this.parseAndEmit(result);
11677
+ }
11678
+ const report = this.assembleReport();
11679
+ this.cleanup();
11680
+ this.emit("session.done", report);
11681
+ return report;
11682
+ }
11683
+ async parseAndEmit(result) {
11684
+ if (result.status !== "success" || result.result == null) return;
11685
+ const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
11686
+ for (const obj of this.extractJsonObjects(text)) {
11687
+ const type = "finding" in obj ? "bug.found" : "plan" in obj ? "refactor.plan" : "evaluation" in obj ? "critic.evaluation" : null;
11688
+ if (!type) continue;
11689
+ this.fleetBus.emit({
11690
+ subagentId: result.subagentId,
11691
+ taskId: result.taskId,
11692
+ ts: Date.now(),
11693
+ type,
11694
+ payload: obj
11695
+ });
11696
+ }
11697
+ }
11698
+ extractJsonObjects(text) {
11699
+ const objects = [];
11700
+ let depth = 0;
11701
+ let start = -1;
11702
+ let inString = false;
11703
+ let escaped = false;
11704
+ for (let i = 0; i < text.length; i++) {
11705
+ const ch = text[i];
11706
+ if (inString) {
11707
+ if (escaped) escaped = false;
11708
+ else if (ch === "\\") escaped = true;
11709
+ else if (ch === '"') inString = false;
11710
+ continue;
11711
+ }
11712
+ if (ch === '"') {
11713
+ inString = true;
11714
+ } else if (ch === "{") {
11715
+ if (depth === 0) start = i;
11716
+ depth++;
11717
+ } else if (ch === "}" && depth > 0) {
11718
+ depth--;
11719
+ if (depth === 0 && start >= 0) {
11720
+ const candidate = text.slice(start, i + 1);
11721
+ try {
11722
+ const parsed = JSON.parse(candidate);
11723
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11724
+ objects.push(parsed);
11725
+ }
11726
+ } catch {
11727
+ }
11728
+ start = -1;
11775
11729
  }
11776
- return { error: err instanceof Error ? err.message : String(err) };
11777
11730
  }
11778
11731
  }
11779
- };
11780
- }
11781
- function instantiateRosterConfig(role, base) {
11782
- return {
11783
- ...base,
11784
- // Roster entries are templates. A director may spawn several
11785
- // workers with the same role, so never reuse the template id.
11786
- id: `${role}-${randomUUID().slice(0, 8)}`
11787
- };
11788
- }
11789
- function makeAssignTool(director) {
11790
- const inputSchema = {
11791
- type: "object",
11792
- properties: {
11793
- subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
11794
- description: { type: "string", minLength: 1, description: "The task in natural language \u2014 what you want this subagent to do." },
11795
- maxToolCalls: { type: "number", minimum: 1, description: "Optional per-task tool-call budget override." },
11796
- timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
11797
- },
11798
- required: ["subagentId", "description"]
11799
- };
11800
- return {
11801
- name: "assign_task",
11802
- description: "Hand a task to a previously spawned subagent. Returns the task id.",
11803
- permission: "auto",
11804
- mutating: false,
11805
- inputSchema,
11806
- async execute(input) {
11807
- const i = input;
11808
- const task = { id: randomUUID(), description: i.description, subagentId: i.subagentId, maxToolCalls: i.maxToolCalls, timeoutMs: i.timeoutMs };
11809
- const taskId = await director.assign(task);
11810
- return { taskId, subagentId: i.subagentId };
11811
- }
11812
- };
11813
- }
11814
- function makeAwaitTasksTool(director) {
11815
- return {
11816
- name: "await_tasks",
11817
- description: "Block until every named task completes. Returns the array of TaskResult.",
11818
- permission: "auto",
11819
- mutating: false,
11820
- inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." } }, required: ["taskIds"] },
11821
- async execute(input) {
11822
- const i = input;
11823
- const results = await director.awaitTasks(i.taskIds);
11824
- return { results };
11825
- }
11826
- };
11827
- }
11828
- function makeAskTool(director) {
11829
- return {
11830
- name: "ask_subagent",
11831
- description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
11832
- permission: "auto",
11833
- mutating: false,
11834
- inputSchema: {
11835
- type: "object",
11836
- properties: {
11837
- subagentId: { type: "string", minLength: 1, description: "Subagent to ask. Must be a previously spawned id." },
11838
- question: { type: "string", minLength: 1, description: "The question or instruction." },
11839
- timeoutMs: { type: "number", minimum: 1, description: "Optional timeout in ms (default 30s)." }
11840
- },
11841
- required: ["subagentId", "question"]
11842
- },
11843
- async execute(input) {
11844
- const i = input;
11845
- try {
11846
- const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
11847
- const stored = director.largeAnswerStore.storeAnswer(answer);
11848
- if (stored.inline) {
11849
- return { ok: true, answer: stored.summary };
11850
- }
11851
- return {
11852
- ok: true,
11853
- answer: stored.summary,
11854
- _answerKey: stored.key,
11855
- _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
11856
- };
11857
- } catch (err) {
11858
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
11859
- }
11860
- }
11861
- };
11862
- }
11863
- function makeAskResultTool(director) {
11864
- return {
11865
- name: "ask_result",
11866
- description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
11867
- permission: "auto",
11868
- mutating: false,
11869
- inputSchema: {
11870
- type: "object",
11871
- properties: {
11872
- key: {
11873
- type: "string",
11874
- minLength: 1,
11875
- description: "The `_answerKey` returned by `ask_subagent` for a large response."
11876
- }
11877
- },
11878
- required: ["key"]
11879
- },
11880
- async execute(input) {
11881
- const i = input;
11882
- const value = director.largeAnswerStore.retrieveAnswer(i.key);
11883
- if (value === void 0) {
11884
- return { ok: false, error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.` };
11885
- }
11886
- return { ok: true, value };
11887
- }
11888
- };
11889
- }
11890
- function makeRollUpTool(director) {
11891
- return {
11892
- name: "roll_up",
11893
- description: "Aggregate completed task results into a single formatted summary.",
11894
- permission: "auto",
11895
- mutating: false,
11896
- inputSchema: {
11897
- type: "object",
11898
- properties: {
11899
- taskIds: { type: "array", items: { type: "string" }, description: "Completed task ids to aggregate." },
11900
- style: { type: "string", enum: ["markdown", "json"], description: "Output flavor \u2014 markdown (default) or json." }
11901
- },
11902
- required: ["taskIds"]
11903
- },
11904
- async execute(input) {
11905
- const i = input;
11906
- const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
11907
- return { summary, count: i.taskIds.length };
11908
- }
11909
- };
11910
- }
11911
- function makeTerminateTool(director) {
11912
- return {
11913
- name: "terminate_subagent",
11914
- description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
11915
- permission: "auto",
11916
- mutating: true,
11917
- inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
11918
- async execute(input) {
11919
- const i = input;
11920
- await director.terminate(i.subagentId);
11921
- return { ok: true };
11922
- }
11923
- };
11924
- }
11925
- function makeTerminateAllTool(director) {
11926
- return {
11927
- name: "terminate_all",
11928
- description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
11929
- permission: "auto",
11930
- mutating: true,
11931
- inputSchema: { type: "object", properties: {}, required: [] },
11932
- async execute() {
11933
- await director.terminateAll();
11934
- return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
11935
- }
11936
- };
11937
- }
11938
- function makeFleetStatusTool(director) {
11939
- return {
11940
- name: "fleet_status",
11941
- description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
11942
- permission: "auto",
11943
- mutating: false,
11944
- inputSchema: { type: "object", properties: {}, required: [] },
11945
- async execute() {
11946
- const base = director.status();
11947
- const fm = director.fleetManager;
11948
- const stats = fm?.getFleetStats();
11949
- const fleetStatus = fm?.getFleetStatus();
11950
- return {
11951
- subagents: base.subagents,
11952
- coordinatorStats: stats ? { total: stats.total, running: stats.running, idle: stats.idle, stopped: stats.stopped } : void 0,
11953
- pending: fleetStatus?.pending ?? [],
11954
- usage: fm?.snapshot()
11955
- };
11956
- }
11957
- };
11958
- }
11959
- function makeFleetUsageTool(director) {
11960
- return {
11961
- name: "fleet_usage",
11962
- description: "Token + cost breakdown across the fleet, per-subagent and totals.",
11963
- permission: "auto",
11964
- mutating: false,
11965
- inputSchema: { type: "object", properties: {}, required: [] },
11966
- async execute() {
11967
- return director.snapshot();
11968
- }
11969
- };
11970
- }
11971
- function makeFleetSessionTool(director) {
11972
- return {
11973
- name: "fleet_session",
11974
- description: "Read a subagent's JSONL transcript and extract its last assistant text, stop reason, and tool-use count. Use this to see what a running or timed-out subagent actually produced.",
11975
- permission: "auto",
11976
- mutating: false,
11977
- inputSchema: {
11978
- type: "object",
11979
- properties: {
11980
- subagentId: { type: "string", description: "Subagent id to read the transcript of." },
11981
- tail: { type: "number", description: "Number of trailing JSONL lines to return. Omit for the full transcript." }
11982
- },
11983
- required: ["subagentId"]
11984
- },
11985
- async execute(input) {
11986
- const i = input;
11987
- const result = await director.readSession(i.subagentId, i.tail);
11988
- if (!result) {
11989
- return {
11990
- error: `fleet_session: transcript unavailable for "${i.subagentId}". Is sessionsRoot configured?`
11991
- };
11992
- }
11993
- return result;
11994
- }
11995
- };
11996
- }
11997
- function makeFleetHealthTool(director) {
11998
- return {
11999
- name: "fleet_health",
12000
- description: "Per-subagent health report: budget pressure (pct of limits consumed), last activity timestamp, and current status. Use to decide whether to assign more work to a subagent or spawn a fresh one.",
12001
- permission: "auto",
12002
- mutating: false,
12003
- inputSchema: { type: "object", properties: {}, required: [] },
12004
- async execute() {
12005
- const status = director.status();
12006
- const snapshot = director.snapshot();
12007
- const subagents = status.subagents ?? [];
12008
- const perSubagent = snapshot.perSubagent ?? {};
12009
- return {
12010
- subagents: subagents.map((s) => {
12011
- const usage = perSubagent[s.id];
12012
- return {
12013
- id: s.id,
12014
- status: s.status,
12015
- lastEventAt: usage?.lastEventAt,
12016
- budgetPressure: {
12017
- iterations: usage?.iterations,
12018
- toolCalls: usage?.toolCalls,
12019
- costUsd: usage?.cost
12020
- }
12021
- };
12022
- })
12023
- };
12024
- }
12025
- };
12026
- }
12027
- function makeCollabDebugTool(director) {
12028
- return {
12029
- name: "collab_debug",
12030
- description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
12031
- permission: "auto",
12032
- mutating: false,
12033
- inputSchema: {
12034
- type: "object",
12035
- properties: {
12036
- targetPaths: {
12037
- type: "array",
12038
- items: { type: "string" },
12039
- description: "File paths / glob patterns to scan for bugs."
12040
- },
12041
- timeoutMs: {
12042
- type: "number",
12043
- minimum: 1,
12044
- description: "Timeout in ms. Default: 600000 (10 minutes)."
12045
- },
12046
- maxTargetFiles: {
12047
- type: "number",
12048
- minimum: 1,
12049
- description: "Maximum number of files to include in the snapshot. If not set, the limit is computed dynamically from contextWindow or falls back to the default (30)."
12050
- },
12051
- contextWindow: {
12052
- type: "number",
12053
- minimum: 1,
12054
- description: "Context window size (tokens) of the model. When provided and maxTargetFiles is not set, the file limit is computed dynamically as floor((contextWindow * 0.4) / 2000)."
12055
- }
12056
- },
12057
- required: ["targetPaths"]
12058
- },
12059
- async execute(input) {
12060
- const i = input;
12061
- if (!i.targetPaths?.length) {
12062
- return { error: "collab_debug: targetPaths is required and must be non-empty." };
12063
- }
12064
- const options = {
12065
- targetPaths: i.targetPaths,
12066
- timeoutMs: i.timeoutMs,
12067
- maxTargetFiles: i.maxTargetFiles,
12068
- contextWindow: i.contextWindow
12069
- };
12070
- try {
12071
- const report = await director.spawnCollab(options);
12072
- return {
12073
- sessionId: report.sessionId,
12074
- overallVerdict: report.overallVerdict,
12075
- bugCount: report.bugs.length,
12076
- planCount: report.refactorPlans.length,
12077
- evaluationCount: report.evaluations.length,
12078
- summary: report.summary,
12079
- bugs: report.bugs,
12080
- refactorPlans: report.refactorPlans,
12081
- evaluations: report.evaluations
12082
- };
12083
- } catch (err) {
12084
- const msg = err instanceof Error ? err.message : String(err);
12085
- return { error: "collab_debug failed: " + msg };
12086
- }
12087
- }
12088
- };
12089
- }
12090
- function makeFleetEmitTool(director) {
12091
- return {
12092
- name: "fleet_emit",
12093
- description: "Emit a structured event on the FleetBus. Any subagent can emit any event type; the Director routes it to all listeners. Use it to stream findings, progress updates, or final results to other agents in real time.",
12094
- permission: "auto",
12095
- mutating: false,
12096
- inputSchema: {
12097
- type: "object",
12098
- properties: {
12099
- type: {
12100
- type: "string",
12101
- description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
12102
- },
12103
- payload: {
12104
- type: "object",
12105
- description: "Event payload. Structure depends on event type. Use null if no payload."
12106
- }
12107
- },
12108
- required: ["type"]
12109
- },
12110
- async execute(input) {
12111
- const i = input;
12112
- director.fleet.emit({
12113
- subagentId: director.id,
12114
- ts: Date.now(),
12115
- type: i.type,
12116
- payload: i.payload ?? {}
12117
- });
12118
- return { ok: true, event: i.type };
12119
- }
12120
- };
12121
- }
12122
- function makeWorkCompleteTool(director) {
12123
- return {
12124
- name: "work_complete",
12125
- description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
12126
- permission: "auto",
12127
- mutating: false,
12128
- inputSchema: { type: "object", properties: {}, required: [] },
12129
- async execute() {
12130
- director.workComplete();
12131
- return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
12132
- }
12133
- };
12134
- }
12135
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
12136
- var IS_WINDOWS = process.platform === "win32";
12137
- var SEP = IS_WINDOWS ? "\\" : "/";
12138
- function isGlob(p) {
12139
- for (const c of p) {
12140
- if (GLOB_CHARS.has(c)) return true;
12141
- }
12142
- return false;
12143
- }
12144
- function globToRegex(pat) {
12145
- let i = 0, re = "^";
12146
- while (i < pat.length) {
12147
- const c = pat[i];
12148
- if (c === "*") {
12149
- if (pat[i + 1] === "*") {
12150
- re += ".*";
12151
- i += 2;
12152
- if (pat[i] === "/") i++;
12153
- } else {
12154
- re += "[^/\\\\]*";
12155
- i++;
12156
- }
12157
- } else if (c === "?") {
12158
- re += "[^/\\\\]";
12159
- i++;
12160
- } else if (c === "[") {
12161
- let cls = "[";
12162
- i++;
12163
- if (pat[i] === "!" || pat[i] === "^") {
12164
- cls += "^";
12165
- i++;
12166
- }
12167
- while (i < pat.length && pat[i] !== "]") {
12168
- const ch = pat[i] ?? "";
12169
- if (ch === "\\") cls += "\\\\";
12170
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
12171
- else cls += ch;
12172
- i++;
12173
- }
12174
- cls += "]";
12175
- re += cls;
12176
- i++;
12177
- } else {
12178
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
12179
- i++;
12180
- }
12181
- }
12182
- return new RegExp(re + "$");
12183
- }
12184
- function baseDir(pat) {
12185
- let i = pat.length - 1;
12186
- while (i >= 0 && !GLOB_CHARS.has(pat[i]) && pat[i] !== SEP && pat[i] !== "/") i--;
12187
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
12188
- return cut < 0 ? "." : pat.slice(0, cut);
12189
- }
12190
- async function expandGlob(pattern) {
12191
- if (!isGlob(pattern)) return [pattern];
12192
- const results = /* @__PURE__ */ new Set();
12193
- const abs = isAbsolute(pattern);
12194
- const base = abs ? baseDir(pattern) : baseDir(pattern);
12195
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
12196
- async function walk4(dir, pat) {
12197
- let entries;
12198
- try {
12199
- entries = await fsp.readdir(dir);
12200
- } catch {
12201
- return;
12202
- }
12203
- const firstGlob = pat.search(/[*?[\[]/);
12204
- if (firstGlob < 0) {
12205
- const re = globToRegex(pat);
12206
- for (const e of entries) {
12207
- if (re.test(e)) {
12208
- const full = `${dir}${SEP}${e}`;
12209
- results.add(abs ? resolve(full) : full);
12210
- }
12211
- }
12212
- return;
12213
- }
12214
- const before = pat.slice(0, firstGlob);
12215
- const rest = pat.slice(firstGlob);
12216
- if (before.endsWith("**")) {
12217
- await walk4(dir, rest);
12218
- for (const e of entries) {
12219
- const full = `${dir}${SEP}${e}`;
12220
- try {
12221
- const stat5 = await fsp.stat(full);
12222
- if (stat5.isDirectory()) await walk4(full, rest);
12223
- } catch {
12224
- }
12225
- }
12226
- } else if (before === "") {
12227
- const re = globToRegex(rest);
12228
- for (const e of entries) {
12229
- if (re.test(e)) {
12230
- const full = `${dir}${SEP}${e}`;
12231
- results.add(abs ? resolve(full) : full);
12232
- }
12233
- }
12234
- } else {
12235
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
12236
- if (entries.includes(seg)) {
12237
- const full = `${dir}${SEP}${seg}`;
12238
- try {
12239
- const stat5 = await fsp.stat(full);
12240
- if (stat5.isDirectory()) await walk4(full, rest);
12241
- } catch {
12242
- }
12243
- }
12244
- }
12245
- }
12246
- await walk4(base === "." ? "." : base, relPat);
12247
- return [...results];
12248
- }
12249
-
12250
- // src/coordination/collab-debug.ts
12251
- var DEFAULT_MAX_TARGET_FILES = 30;
12252
- var CollabSession = class extends EventEmitter {
12253
- sessionId;
12254
- options;
12255
- snapshot;
12256
- director;
12257
- fleetBus;
12258
- subagentIds = /* @__PURE__ */ new Map();
12259
- // role → subagentId
12260
- bugs = /* @__PURE__ */ new Map();
12261
- plans = /* @__PURE__ */ new Map();
12262
- evaluations = /* @__PURE__ */ new Map();
12263
- disposers = new Array();
12264
- settled = false;
12265
- timeoutMs;
12266
- cancelled = false;
12267
- alerts = [];
12268
- /** Tracks tool call counts per subagent for progress-based timeout decisions. */
12269
- progressBySubagent = /* @__PURE__ */ new Map();
12270
- /** Last tool call count when a timeout warning was handled. */
12271
- lastTimeoutProgress = /* @__PURE__ */ new Map();
12272
- /** Session-level timeout timer handle (cleared on cancel or natural completion). */
12273
- _timeoutTimer;
12274
- constructor(director, fleetBus, options) {
12275
- super();
12276
- this.sessionId = randomUUID();
12277
- this.options = options;
12278
- this.director = director;
12279
- this.fleetBus = fleetBus;
12280
- this.timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
12281
- if (options.prebuiltSnapshot) {
12282
- this.snapshot = options.prebuiltSnapshot;
12283
- } else {
12284
- this.snapshot = {
12285
- id: this.sessionId,
12286
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
12287
- files: []
12288
- };
12289
- }
12290
- }
12291
- get id() {
12292
- return this.sessionId;
12293
- }
12294
- getSessionAlerts() {
12295
- return [...this.alerts];
12296
- }
12297
- isCancelled() {
12298
- return this.cancelled;
12299
- }
12300
- /**
12301
- * Snapshot of role → subagentId map. The Director calls coordinator.stop()
12302
- * for each agent when cancelling the session, using this map to enumerate
12303
- * all three collab agents.
12304
- */
12305
- getSubagentIds() {
12306
- return new Map(this.subagentIds);
12307
- }
12308
- /**
12309
- * Returns the effective file limit for this session.
12310
- * Priority: explicit `maxTargetFiles` > dynamic from `contextWindow` > `DEFAULT_MAX_TARGET_FILES`.
12311
- */
12312
- effectiveFileLimit() {
12313
- if (this.options.maxTargetFiles !== void 0) {
12314
- return this.options.maxTargetFiles;
12315
- }
12316
- if (this.options.contextWindow !== void 0) {
12317
- return Math.max(5, Math.floor(this.options.contextWindow * 0.4 / 2e3));
12318
- }
12319
- return DEFAULT_MAX_TARGET_FILES;
12320
- }
12321
- async buildSnapshot() {
12322
- if (this.snapshot.files.length > 0) return this.snapshot;
12323
- const allFiles = [];
12324
- for (const pattern of this.options.targetPaths) {
12325
- const expanded = await expandGlob(pattern);
12326
- allFiles.push(...expanded);
12327
- }
12328
- const limit = this.effectiveFileLimit();
12329
- if (allFiles.length > limit) {
12330
- const hint = this.options.contextWindow ? `contextWindow=${this.options.contextWindow} \u2192 calculated limit=${limit}` : `default limit=${DEFAULT_MAX_TARGET_FILES}`;
12331
- throw new Error(
12332
- `[collab_debug] Target has ${allFiles.length} files, which exceeds the limit (${hint}). Narrow the target or pass maxTargetFiles / contextWindow to override. For large codebases, run package-by-package or module-by-module sessions instead of targeting the entire repo.`
12333
- );
12334
- }
12335
- for (const filePath of allFiles) {
12336
- try {
12337
- const content = await fsp.readFile(filePath, "utf8");
12338
- const ext = filePath.split(".").pop() ?? "";
12339
- const language = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" ? "javascript" : ext === "md" ? "markdown" : ext === "json" ? "json" : void 0;
12340
- this.snapshot.files.push({ path: filePath, content, language });
12341
- } catch {
12342
- this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
12343
- }
12344
- }
12345
- return this.snapshot;
12346
- }
12347
- /**
12348
- * Cancel the session. Emits director.cancel_collab on the FleetBus so all
12349
- * collab agents finish early. The session-level timeout timer is also cleared.
12350
- * Safe to call multiple times (idempotent after first call).
12351
- */
12352
- cancel(reason = "Director cancelled collab session") {
12353
- if (this.settled) return;
12354
- this.cancelled = true;
12355
- if (this._timeoutTimer) {
12356
- clearTimeout(this._timeoutTimer);
12357
- this._timeoutTimer = void 0;
12358
- }
12359
- this.fleetBus.emit({
12360
- subagentId: this.director.id,
12361
- ts: Date.now(),
12362
- type: "director.cancel_collab",
12363
- payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
12364
- });
12365
- this.fleetBus.emit({
12366
- subagentId: this.director.id,
12367
- ts: Date.now(),
12368
- type: "collab.cancelled",
12369
- payload: { sessionId: this.sessionId, reason }
12370
- });
12371
- }
12372
- async start() {
12373
- if (this.settled) throw new Error("session already settled");
12374
- this.settled = true;
12375
- await this.buildSnapshot();
12376
- this.wireFleetBus();
12377
- const [bugHunterId, refactorPlannerId, criticId] = await Promise.all([
12378
- this.spawnAgent("bug-hunter", this.buildBugHunterTask()),
12379
- this.spawnAgent("refactor-planner", this.buildRefactorPlannerTask()),
12380
- this.spawnAgent("critic", this.buildCriticTask())
12381
- ]);
12382
- this.subagentIds.set("bug-hunter", bugHunterId);
12383
- this.subagentIds.set("refactor-planner", refactorPlannerId);
12384
- this.subagentIds.set("critic", criticId);
12385
- const timeout = new Promise((_, reject) => {
12386
- this._timeoutTimer = setTimeout(() => {
12387
- this.cancel("Session-level timeout reached");
12388
- reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
12389
- }, this.timeoutMs);
12390
- });
12391
- let results = null;
12392
- try {
12393
- results = await Promise.race([
12394
- Promise.all([
12395
- this.director.awaitTasks([bugHunterId]),
12396
- this.director.awaitTasks([refactorPlannerId]),
12397
- this.director.awaitTasks([criticId])
12398
- ]),
12399
- timeout
12400
- ]);
12401
- } catch (err) {
12402
- if (this._timeoutTimer) {
12403
- clearTimeout(this._timeoutTimer);
12404
- this._timeoutTimer = void 0;
12405
- }
12406
- this.cleanup();
12407
- const error = err instanceof Error ? err : new Error(String(err));
12408
- this.emit("session.error", error);
12409
- throw error;
12410
- }
12411
- for (const result of results.flat()) {
12412
- await this.parseAndEmit(result);
12413
- }
12414
- const report = this.assembleReport();
12415
- this.cleanup();
12416
- this.emit("session.done", report);
12417
- return report;
12418
- }
12419
- async parseAndEmit(result) {
12420
- if (result.status !== "success" || result.result == null) return;
12421
- const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
12422
- for (const obj of this.extractJsonObjects(text)) {
12423
- const type = "finding" in obj ? "bug.found" : "plan" in obj ? "refactor.plan" : "evaluation" in obj ? "critic.evaluation" : null;
12424
- if (!type) continue;
12425
- this.fleetBus.emit({
12426
- subagentId: result.subagentId,
12427
- taskId: result.taskId,
12428
- ts: Date.now(),
12429
- type,
12430
- payload: obj
12431
- });
12432
- }
12433
- }
12434
- extractJsonObjects(text) {
12435
- const objects = [];
12436
- let depth = 0;
12437
- let start = -1;
12438
- let inString = false;
12439
- let escaped = false;
12440
- for (let i = 0; i < text.length; i++) {
12441
- const ch = text[i];
12442
- if (inString) {
12443
- if (escaped) escaped = false;
12444
- else if (ch === "\\") escaped = true;
12445
- else if (ch === '"') inString = false;
12446
- continue;
12447
- }
12448
- if (ch === '"') {
12449
- inString = true;
12450
- } else if (ch === "{") {
12451
- if (depth === 0) start = i;
12452
- depth++;
12453
- } else if (ch === "}" && depth > 0) {
12454
- depth--;
12455
- if (depth === 0 && start >= 0) {
12456
- const candidate = text.slice(start, i + 1);
12457
- try {
12458
- const parsed = JSON.parse(candidate);
12459
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
12460
- objects.push(parsed);
12461
- }
12462
- } catch {
12463
- }
12464
- start = -1;
12465
- }
12466
- }
12467
- }
12468
- return objects;
12469
- }
12470
- budgetForRole(role) {
12471
- if (this.options.budgetOverrides?.[role]) {
12472
- return this.options.budgetOverrides[role];
11732
+ return objects;
11733
+ }
11734
+ budgetForRole(role) {
11735
+ if (this.options.budgetOverrides?.[role]) {
11736
+ return this.options.budgetOverrides[role];
12473
11737
  }
12474
11738
  const defaults = {
12475
11739
  "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
@@ -12530,252 +11794,1054 @@ ${scratchpad}/refactor-plan-${this.sessionId}.md
12530
11794
 
12531
11795
  Emit each plan immediately. Do not wait until planning is complete.`;
12532
11796
  }
12533
- buildCriticTask() {
12534
- const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
12535
- const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
12536
- const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
12537
- const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
12538
- ${f.content}`).join("\n\n");
12539
- return `You are Critic. Evaluate bug findings and refactor plans.
12540
-
12541
- Target files:
12542
- ${fileContents}
11797
+ buildCriticTask() {
11798
+ const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11799
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
11800
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
11801
+ const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11802
+ ${f.content}`).join("\n\n");
11803
+ return `You are Critic. Evaluate bug findings and refactor plans.
11804
+
11805
+ Target files:
11806
+ ${fileContents}
11807
+
11808
+ Read the BugHunter report at: ${bugHunterReportPath}
11809
+ Read the RefactorPlanner report at: ${refactorPlanPath}
11810
+
11811
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
11812
+ { "type": "critic.evaluation", "payload": { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": ["<strength>"], "weaknesses": ["<weakness>"], "concerns": [{ "description": "<concern>", "severity": "<blocking|advisory>" }] } } }
11813
+
11814
+ After all evaluations, write your markdown report to:
11815
+ ${scratchpad}/critic-report-${this.sessionId}.md
11816
+
11817
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
11818
+ }
11819
+ wireFleetBus() {
11820
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
11821
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
11822
+ });
11823
+ this.disposers.push(dTool);
11824
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
11825
+ const payload = e.payload;
11826
+ const role = this.roleFromSubagentId(e.subagentId);
11827
+ if (!role) return;
11828
+ const btwNotes = this.director.getLeaderBtwNotes();
11829
+ const alert = {
11830
+ sessionId: this.sessionId,
11831
+ subagentId: e.subagentId,
11832
+ role,
11833
+ level: "warning" /* WARNING */,
11834
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
11835
+ budgetKind: payload.kind,
11836
+ elapsedMs: payload.timeoutMs,
11837
+ limit: payload.limit,
11838
+ btwNotes
11839
+ };
11840
+ this.alerts.push(alert);
11841
+ this.fleetBus.emit({
11842
+ subagentId: e.subagentId,
11843
+ ts: Date.now(),
11844
+ type: "collab.warning",
11845
+ payload: alert
11846
+ });
11847
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
11848
+ if (decision === "cancel") {
11849
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
11850
+ return;
11851
+ }
11852
+ if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
11853
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
11854
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
11855
+ if (progress <= lastProgress) {
11856
+ payload.deny();
11857
+ return;
11858
+ }
11859
+ this.lastTimeoutProgress.set(e.subagentId, progress);
11860
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
11861
+ setImmediate(() => {
11862
+ payload.extend({ timeoutMs: newLimit });
11863
+ });
11864
+ return;
11865
+ }
11866
+ if (decision === "extend") {
11867
+ setImmediate(() => {
11868
+ const base = Math.max(payload.limit, payload.used);
11869
+ const extra = {};
11870
+ switch (payload.kind) {
11871
+ case "iterations":
11872
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
11873
+ break;
11874
+ case "tool_calls":
11875
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
11876
+ break;
11877
+ case "tokens":
11878
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
11879
+ break;
11880
+ case "cost":
11881
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
11882
+ break;
11883
+ }
11884
+ payload.extend(extra);
11885
+ });
11886
+ return;
11887
+ }
11888
+ if (payload.kind !== "timeout") {
11889
+ setImmediate(() => {
11890
+ const base = Math.max(payload.limit, payload.used);
11891
+ const extra = {};
11892
+ switch (payload.kind) {
11893
+ case "iterations":
11894
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
11895
+ break;
11896
+ case "tool_calls":
11897
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
11898
+ break;
11899
+ case "tokens":
11900
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
11901
+ break;
11902
+ case "cost":
11903
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
11904
+ break;
11905
+ }
11906
+ payload.extend(extra);
11907
+ });
11908
+ }
11909
+ });
11910
+ this.disposers.push(dBudget);
11911
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
11912
+ const payload = e.payload;
11913
+ if (payload.sessionId !== this.sessionId) return;
11914
+ this.cancelled = true;
11915
+ if (this._timeoutTimer) {
11916
+ clearTimeout(this._timeoutTimer);
11917
+ this._timeoutTimer = void 0;
11918
+ }
11919
+ this.fleetBus.emit({
11920
+ subagentId: this.director.id,
11921
+ ts: Date.now(),
11922
+ type: "collab.cancelled",
11923
+ payload: { sessionId: this.sessionId, reason: payload.reason }
11924
+ });
11925
+ });
11926
+ this.disposers.push(dCancel);
11927
+ const d1 = this.fleetBus.filter("bug.found", (e) => {
11928
+ const payload = e.payload;
11929
+ if (payload?.finding) {
11930
+ this.bugs.set(payload.finding.id, payload.finding);
11931
+ this.emit("bug.found", payload);
11932
+ }
11933
+ });
11934
+ this.disposers.push(d1);
11935
+ const d2 = this.fleetBus.filter("refactor.plan", (e) => {
11936
+ const payload = e.payload;
11937
+ if (payload?.plan) {
11938
+ this.plans.set(payload.plan.id, payload.plan);
11939
+ this.emit("refactor.plan", payload);
11940
+ }
11941
+ });
11942
+ this.disposers.push(d2);
11943
+ const d3 = this.fleetBus.filter("critic.evaluation", (e) => {
11944
+ const payload = e.payload;
11945
+ if (payload?.evaluation) {
11946
+ this.evaluations.set(payload.evaluation.id, payload.evaluation);
11947
+ this.emit("critic.evaluation", payload);
11948
+ }
11949
+ });
11950
+ this.disposers.push(d3);
11951
+ }
11952
+ roleFromSubagentId(subagentId) {
11953
+ for (const [role, id] of this.subagentIds) {
11954
+ if (id === subagentId) return role;
11955
+ }
11956
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
11957
+ return match?.[1] ?? null;
11958
+ }
11959
+ assembleReport() {
11960
+ const bugList = Array.from(this.bugs.values());
11961
+ const planList = Array.from(this.plans.values());
11962
+ const evalList = Array.from(this.evaluations.values());
11963
+ let disposition = "completed";
11964
+ if (this.cancelled) disposition = "cancelled";
11965
+ const verdictOrder = {
11966
+ approve: 0,
11967
+ needs_revision: 1,
11968
+ reject: 2
11969
+ };
11970
+ const overallVerdict = evalList.reduce(
11971
+ (worst, eval_) => {
11972
+ const w = verdictOrder[worst];
11973
+ const c = verdictOrder[eval_.verdict];
11974
+ return c > w ? eval_.verdict : worst;
11975
+ },
11976
+ "approve"
11977
+ );
11978
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
11979
+ return {
11980
+ sessionId: this.sessionId,
11981
+ startedAt: this.snapshot.createdAt,
11982
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11983
+ targetPaths: this.options.targetPaths,
11984
+ disposition,
11985
+ bugs: bugList,
11986
+ refactorPlans: planList,
11987
+ evaluations: evalList,
11988
+ alerts: [...this.alerts],
11989
+ overallVerdict,
11990
+ summary
11991
+ };
11992
+ }
11993
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
11994
+ const lines = [
11995
+ `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
11996
+ "",
11997
+ `**Target:** ${this.options.targetPaths.join(", ")}`,
11998
+ `**Disposition:** ${disposition.toUpperCase()}`,
11999
+ `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
12000
+ ""
12001
+ ];
12002
+ if (this.alerts.length > 0) {
12003
+ lines.push("### Alerts", "");
12004
+ for (const alert of this.alerts) {
12005
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
12006
+ }
12007
+ lines.push("");
12008
+ }
12009
+ if (bugs.length > 0) {
12010
+ lines.push("### Bugs Found", "");
12011
+ for (const b of bugs) {
12012
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
12013
+ }
12014
+ lines.push("");
12015
+ }
12016
+ if (plans.length > 0) {
12017
+ lines.push("### Refactor Plans", "");
12018
+ for (const p of plans) {
12019
+ lines.push(`- **Phase plan** (risk: ${p.riskScore}, ~${p.estimatedChangeCount} changes)`);
12020
+ for (const phase of p.phases) {
12021
+ lines.push(` - Phase ${phase.number}: ${phase.title} [${phase.risk}]`);
12022
+ }
12023
+ }
12024
+ lines.push("");
12025
+ }
12026
+ if (evals.length > 0) {
12027
+ lines.push("### Critic Evaluations", "");
12028
+ for (const e of evals) {
12029
+ lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
12030
+ for (const c of e.concerns) {
12031
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
12032
+ }
12033
+ }
12034
+ lines.push("");
12035
+ }
12036
+ return lines.join("\n");
12037
+ }
12038
+ cleanup() {
12039
+ for (const dispose of this.disposers) dispose();
12040
+ this.disposers.length = 0;
12041
+ }
12042
+ };
12543
12043
 
12544
- Read the BugHunter report at: ${bugHunterReportPath}
12545
- Read the RefactorPlanner report at: ${refactorPlanPath}
12044
+ // src/coordination/director-prompts.ts
12045
+ var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
12046
+ subagents by spawning them, assigning tasks, awaiting completions, and
12047
+ rolling up their outputs into your next decision.
12546
12048
 
12547
- For each bug and refactor plan, emit your evaluation using fleet_emit:
12548
- { "type": "critic.evaluation", "payload": { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": ["<strength>"], "weaknesses": ["<weakness>"], "concerns": [{ "description": "<concern>", "severity": "<blocking|advisory>" }] } } }
12049
+ Core fleet tools available to you:
12050
+ - spawn_subagent \u2014 create a worker with a chosen provider / model / role
12051
+ - assign_task \u2014 hand a piece of work to a specific subagent
12052
+ - await_tasks \u2014 block until named task ids complete (parallel-safe)
12053
+ - ask_subagent \u2014 synchronously query a running subagent via the bridge
12054
+ - roll_up \u2014 aggregate finished tasks into a markdown/json summary
12055
+ - terminate_subagent \u2014 abort a stuck worker (use sparingly)
12056
+ - fleet_status \u2014 snapshot of all subagents and pending tasks
12057
+ - fleet_usage \u2014 token + cost breakdown per subagent and total
12549
12058
 
12550
- After all evaluations, write your markdown report to:
12551
- ${scratchpad}/critic-report-${this.sessionId}.md
12059
+ Working rules:
12060
+ 1. Decompose first. Before spawning, decide which sub-tasks are
12061
+ independent and can run in parallel. Sequential work doesn't need a
12062
+ subagent \u2014 do it yourself.
12063
+ 2. Match worker to job. Cheap/fast model for triage, capable model for
12064
+ synthesis. Different providers per sibling is allowed and encouraged.
12065
+ 3. Always pair an assign with an await. Don't fire-and-forget; you owe
12066
+ the user a single coherent answer at the end.
12067
+ 4. Roll up before deciding. After await_tasks resolves, call roll_up so
12068
+ the results are folded back into your context in a compact form.
12069
+ 5. Budget is real. Check fleet_usage periodically. If a subagent is
12070
+ thrashing, terminate it rather than letting cost climb silently.
12071
+ 6. Never claim a subagent's work as your own without verifying it. If a
12072
+ result looks wrong, ask_subagent for clarification before passing it
12073
+ to the user.
12074
+ 7. Wind down when satisfied. When the results are good enough, call
12075
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
12076
+ as aborted. Running subagents finish naturally. Call terminate_subagent
12077
+ only for ones you need to stop immediately.`;
12078
+ var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
12079
+ a specific slice of a larger plan \u2014 do that slice well and report back.
12552
12080
 
12553
- Emit each evaluation immediately. Do not wait until you have read all reports.`;
12081
+ Bridge contract:
12082
+ - You have a parent (the Director). You may call \`request\` on the
12083
+ parent bridge to ask a clarifying question. Use this sparingly; the
12084
+ parent is also working.
12085
+ - You MAY NOT request the parent's system prompt, tool list, or other
12086
+ subagents' context. Those are not yours to read.
12087
+ - Your final task output is what the Director sees. Be concise,
12088
+ structured, and self-contained \u2014 assume the Director will paste your
12089
+ output into its own context.`;
12090
+ function composeDirectorPrompt(parts = {}) {
12091
+ const sections = [];
12092
+ const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
12093
+ if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
12094
+ if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
12095
+ sections.push(`Available roles you can spawn:
12096
+ ${parts.rosterSummary.trim()}`);
12554
12097
  }
12555
- wireFleetBus() {
12556
- const dTool = this.fleetBus.filter("tool.executed", (e) => {
12557
- this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
12558
- });
12559
- this.disposers.push(dTool);
12560
- const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
12561
- const payload = e.payload;
12562
- const role = this.roleFromSubagentId(e.subagentId);
12563
- if (!role) return;
12564
- const btwNotes = this.director.getLeaderBtwNotes();
12565
- const alert = {
12566
- sessionId: this.sessionId,
12567
- subagentId: e.subagentId,
12568
- role,
12569
- level: "warning" /* WARNING */,
12570
- message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
12571
- budgetKind: payload.kind,
12572
- elapsedMs: payload.timeoutMs,
12573
- limit: payload.limit,
12574
- btwNotes
12575
- };
12576
- this.alerts.push(alert);
12577
- this.fleetBus.emit({
12578
- subagentId: e.subagentId,
12579
- ts: Date.now(),
12580
- type: "collab.warning",
12581
- payload: alert
12582
- });
12583
- const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
12584
- if (decision === "cancel") {
12585
- this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
12586
- return;
12098
+ if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
12099
+ sections.push(parts.basePrompt.trim());
12100
+ }
12101
+ return sections.join("\n\n");
12102
+ }
12103
+ function composeSubagentPrompt(parts = {}) {
12104
+ const sections = [];
12105
+ const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
12106
+ if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
12107
+ if (parts.role && parts.role.trim().length > 0) {
12108
+ sections.push(`Role:
12109
+ ${parts.role.trim()}`);
12110
+ }
12111
+ if (parts.task && parts.task.trim().length > 0) {
12112
+ sections.push(`Task:
12113
+ ${parts.task.trim()}`);
12114
+ }
12115
+ if (parts.sharedScratchpad && parts.sharedScratchpad.trim().length > 0) {
12116
+ sections.push(
12117
+ `Shared notes:
12118
+ A scratchpad shared with the rest of the fleet is mounted at \`${parts.sharedScratchpad.trim()}\`.
12119
+ - Write your final findings as markdown files there (e.g. \`findings.md\`, \`security.md\`).
12120
+ - Before starting, list the directory and read any sibling files relevant to your task \u2014 they may already contain context you can build on.
12121
+ - Use stable filenames (one file per concern); overwrite instead of appending so the Director sees the latest state.`
12122
+ );
12123
+ }
12124
+ if (parts.override && parts.override.trim().length > 0) {
12125
+ sections.push(parts.override.trim());
12126
+ }
12127
+ return sections.join("\n\n");
12128
+ }
12129
+ function rosterSummaryFromConfigs(roster) {
12130
+ const lines = [];
12131
+ for (const [roleId, cfg] of Object.entries(roster)) {
12132
+ const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
12133
+ const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
12134
+ const tail = headline ? ` \u2014 ${headline}` : "";
12135
+ lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
12136
+ }
12137
+ return lines.join("\n");
12138
+ }
12139
+ function makeSpawnTool(director, roster) {
12140
+ const inputSchema = {
12141
+ type: "object",
12142
+ properties: {
12143
+ role: { type: "string", description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields." },
12144
+ description: { type: "string", description: "Free-form task description. When `role` is not set, the director uses the smart dispatcher to route this to the best-matching catalog agent. Use this when you don't know the exact role name." },
12145
+ name: { type: "string", description: "Display name for the subagent. Used as a fallback when description-based dispatch does not resolve a role." },
12146
+ provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
12147
+ model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
12148
+ systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
12149
+ maxIterations: { type: "number", minimum: 1 },
12150
+ maxToolCalls: { type: "number", minimum: 1 },
12151
+ maxCostUsd: { type: "number", minimum: 0 },
12152
+ timeoutMs: { type: "number", minimum: 1, description: "Hard wall-clock cap in milliseconds. Defaults to none (idle timeout is the default reaper)." },
12153
+ idleTimeoutMs: { type: "number", minimum: 1, description: "Idle timeout in ms: reap the subagent after this long with no activity. Resets on every iteration/tool call. Default is role/coordinator-specific." },
12154
+ maxTokens: { type: "number", minimum: 1, description: "Maximum total tokens (input + output) the subagent may use." }
12155
+ },
12156
+ required: []
12157
+ };
12158
+ return {
12159
+ name: "spawn_subagent",
12160
+ description: "Create a new subagent under this director. Returns the subagent id.",
12161
+ usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
12162
+ permission: "auto",
12163
+ mutating: false,
12164
+ inputSchema,
12165
+ async execute(input) {
12166
+ const i = input ?? {};
12167
+ const role = typeof i.role === "string" ? i.role : void 0;
12168
+ const description = typeof i.description === "string" ? i.description : void 0;
12169
+ let cfg;
12170
+ if (role && roster) {
12171
+ const base = roster[role];
12172
+ if (!base) return { error: `unknown role "${role}". roster has: ${Object.keys(roster).join(", ")}` };
12173
+ cfg = instantiateRosterConfig(role, base);
12174
+ } else if (description && !role) {
12175
+ const dispatchResult = await dispatchAgent(description, {
12176
+ classifier: director.dispatchClassifier,
12177
+ catalog: roster
12178
+ });
12179
+ const dispatchRole = dispatchResult.role;
12180
+ if (roster?.[dispatchRole]) {
12181
+ cfg = instantiateRosterConfig(dispatchRole, roster[dispatchRole]);
12182
+ } else {
12183
+ const def = dispatchResult.definition;
12184
+ cfg = {
12185
+ name: def.config.name ?? dispatchRole,
12186
+ role: dispatchRole,
12187
+ provider: def.config.provider,
12188
+ model: def.config.model
12189
+ };
12190
+ }
12191
+ }
12192
+ cfg ??= { name: i.name ?? "subagent" };
12193
+ if (typeof i.name === "string") cfg.name = i.name;
12194
+ if (typeof i.provider === "string") cfg.provider = i.provider;
12195
+ if (typeof i.model === "string") cfg.model = i.model;
12196
+ if (typeof i.systemPromptOverride === "string") cfg.systemPromptOverride = i.systemPromptOverride;
12197
+ if (typeof i.maxIterations === "number") cfg.maxIterations = i.maxIterations;
12198
+ if (typeof i.maxToolCalls === "number") cfg.maxToolCalls = i.maxToolCalls;
12199
+ if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
12200
+ if (typeof i.timeoutMs === "number") cfg.timeoutMs = i.timeoutMs;
12201
+ if (typeof i.idleTimeoutMs === "number") cfg.idleTimeoutMs = i.idleTimeoutMs;
12202
+ if (typeof i.maxTokens === "number") cfg.maxTokens = i.maxTokens;
12203
+ try {
12204
+ const subagentId = await director.spawn(cfg);
12205
+ return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name, role: cfg.role };
12206
+ } catch (err) {
12207
+ if (err instanceof FleetSpawnBudgetError) {
12208
+ return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
12209
+ }
12210
+ if (err instanceof FleetCostCapError) {
12211
+ return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
12212
+ }
12213
+ return { error: err instanceof Error ? err.message : String(err) };
12214
+ }
12215
+ }
12216
+ };
12217
+ }
12218
+ function instantiateRosterConfig(role, base) {
12219
+ return {
12220
+ ...base,
12221
+ // Roster entries are templates. A director may spawn several
12222
+ // workers with the same role, so never reuse the template id.
12223
+ id: `${role}-${randomUUID().slice(0, 8)}`
12224
+ };
12225
+ }
12226
+ function makeAssignTool(director) {
12227
+ const inputSchema = {
12228
+ type: "object",
12229
+ properties: {
12230
+ subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
12231
+ description: { type: "string", minLength: 1, description: "The task in natural language \u2014 what you want this subagent to do." },
12232
+ maxToolCalls: { type: "number", minimum: 1, description: "Optional per-task tool-call budget override." },
12233
+ timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
12234
+ },
12235
+ required: ["subagentId", "description"]
12236
+ };
12237
+ return {
12238
+ name: "assign_task",
12239
+ description: "Hand a task to a previously spawned subagent. Returns the task id.",
12240
+ permission: "auto",
12241
+ mutating: false,
12242
+ inputSchema,
12243
+ async execute(input) {
12244
+ const i = input;
12245
+ const task = { id: randomUUID(), description: i.description, subagentId: i.subagentId, maxToolCalls: i.maxToolCalls, timeoutMs: i.timeoutMs };
12246
+ const taskId = await director.assign(task);
12247
+ return { taskId, subagentId: i.subagentId };
12248
+ }
12249
+ };
12250
+ }
12251
+ function makeAwaitTasksTool(director) {
12252
+ return {
12253
+ name: "await_tasks",
12254
+ description: "Block until every named task completes. Returns the array of TaskResult.",
12255
+ permission: "auto",
12256
+ mutating: false,
12257
+ inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." } }, required: ["taskIds"] },
12258
+ async execute(input) {
12259
+ const i = input;
12260
+ const results = await director.awaitTasks(i.taskIds);
12261
+ return { results };
12262
+ }
12263
+ };
12264
+ }
12265
+ function makeAskTool(director) {
12266
+ return {
12267
+ name: "ask_subagent",
12268
+ description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
12269
+ permission: "auto",
12270
+ mutating: false,
12271
+ inputSchema: {
12272
+ type: "object",
12273
+ properties: {
12274
+ subagentId: { type: "string", minLength: 1, description: "Subagent to ask. Must be a previously spawned id." },
12275
+ question: { type: "string", minLength: 1, description: "The question or instruction." },
12276
+ timeoutMs: { type: "number", minimum: 1, description: "Optional timeout in ms (default 30s)." }
12277
+ },
12278
+ required: ["subagentId", "question"]
12279
+ },
12280
+ async execute(input) {
12281
+ const i = input;
12282
+ try {
12283
+ const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
12284
+ const stored = director.largeAnswerStore.storeAnswer(answer);
12285
+ if (stored.inline) {
12286
+ return { ok: true, answer: stored.summary };
12287
+ }
12288
+ return {
12289
+ ok: true,
12290
+ answer: stored.summary,
12291
+ _answerKey: stored.key,
12292
+ _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
12293
+ };
12294
+ } catch (err) {
12295
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
12587
12296
  }
12588
- if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
12589
- const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
12590
- const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
12591
- if (progress <= lastProgress) {
12592
- payload.deny();
12593
- return;
12297
+ }
12298
+ };
12299
+ }
12300
+ function makeAskResultTool(director) {
12301
+ return {
12302
+ name: "ask_result",
12303
+ description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
12304
+ permission: "auto",
12305
+ mutating: false,
12306
+ inputSchema: {
12307
+ type: "object",
12308
+ properties: {
12309
+ key: {
12310
+ type: "string",
12311
+ minLength: 1,
12312
+ description: "The `_answerKey` returned by `ask_subagent` for a large response."
12594
12313
  }
12595
- this.lastTimeoutProgress.set(e.subagentId, progress);
12596
- const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
12597
- setImmediate(() => {
12598
- payload.extend({ timeoutMs: newLimit });
12599
- });
12600
- return;
12314
+ },
12315
+ required: ["key"]
12316
+ },
12317
+ async execute(input) {
12318
+ const i = input;
12319
+ const value = director.largeAnswerStore.retrieveAnswer(i.key);
12320
+ if (value === void 0) {
12321
+ return { ok: false, error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.` };
12601
12322
  }
12602
- if (decision === "extend") {
12603
- setImmediate(() => {
12604
- const base = Math.max(payload.limit, payload.used);
12605
- const extra = {};
12606
- switch (payload.kind) {
12607
- case "iterations":
12608
- extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
12609
- break;
12610
- case "tool_calls":
12611
- extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
12612
- break;
12613
- case "tokens":
12614
- extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
12615
- break;
12616
- case "cost":
12617
- extra.maxCostUsd = Math.min(base * 1.5, 100);
12618
- break;
12619
- }
12620
- payload.extend(extra);
12621
- });
12622
- return;
12323
+ return { ok: true, value };
12324
+ }
12325
+ };
12326
+ }
12327
+ function makeRollUpTool(director) {
12328
+ return {
12329
+ name: "roll_up",
12330
+ description: "Aggregate completed task results into a single formatted summary.",
12331
+ permission: "auto",
12332
+ mutating: false,
12333
+ inputSchema: {
12334
+ type: "object",
12335
+ properties: {
12336
+ taskIds: { type: "array", items: { type: "string" }, description: "Completed task ids to aggregate." },
12337
+ style: { type: "string", enum: ["markdown", "json"], description: "Output flavor \u2014 markdown (default) or json." }
12338
+ },
12339
+ required: ["taskIds"]
12340
+ },
12341
+ async execute(input) {
12342
+ const i = input;
12343
+ const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
12344
+ return { summary, count: i.taskIds.length };
12345
+ }
12346
+ };
12347
+ }
12348
+ function makeTerminateTool(director) {
12349
+ return {
12350
+ name: "terminate_subagent",
12351
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
12352
+ permission: "auto",
12353
+ mutating: true,
12354
+ inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
12355
+ async execute(input) {
12356
+ const i = input;
12357
+ await director.terminate(i.subagentId);
12358
+ return { ok: true };
12359
+ }
12360
+ };
12361
+ }
12362
+ function makeTerminateAllTool(director) {
12363
+ return {
12364
+ name: "terminate_all",
12365
+ description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
12366
+ permission: "auto",
12367
+ mutating: true,
12368
+ inputSchema: { type: "object", properties: {}, required: [] },
12369
+ async execute() {
12370
+ await director.terminateAll();
12371
+ return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
12372
+ }
12373
+ };
12374
+ }
12375
+ function makeFleetStatusTool(director) {
12376
+ return {
12377
+ name: "fleet_status",
12378
+ description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
12379
+ permission: "auto",
12380
+ mutating: false,
12381
+ inputSchema: { type: "object", properties: {}, required: [] },
12382
+ async execute() {
12383
+ const base = director.status();
12384
+ const fm = director.fleetManager;
12385
+ const stats = fm?.getFleetStats();
12386
+ const fleetStatus = fm?.getFleetStatus();
12387
+ return {
12388
+ subagents: base.subagents,
12389
+ coordinatorStats: stats ? { total: stats.total, running: stats.running, idle: stats.idle, stopped: stats.stopped } : void 0,
12390
+ pending: fleetStatus?.pending ?? [],
12391
+ usage: fm?.snapshot()
12392
+ };
12393
+ }
12394
+ };
12395
+ }
12396
+ function makeFleetUsageTool(director) {
12397
+ return {
12398
+ name: "fleet_usage",
12399
+ description: "Token + cost breakdown across the fleet, per-subagent and totals.",
12400
+ permission: "auto",
12401
+ mutating: false,
12402
+ inputSchema: { type: "object", properties: {}, required: [] },
12403
+ async execute() {
12404
+ return director.snapshot();
12405
+ }
12406
+ };
12407
+ }
12408
+ function makeFleetSessionTool(director) {
12409
+ return {
12410
+ name: "fleet_session",
12411
+ description: "Read a subagent's JSONL transcript and extract its last assistant text, stop reason, and tool-use count. Use this to see what a running or timed-out subagent actually produced.",
12412
+ permission: "auto",
12413
+ mutating: false,
12414
+ inputSchema: {
12415
+ type: "object",
12416
+ properties: {
12417
+ subagentId: { type: "string", description: "Subagent id to read the transcript of." },
12418
+ tail: { type: "number", description: "Number of trailing JSONL lines to return. Omit for the full transcript." }
12419
+ },
12420
+ required: ["subagentId"]
12421
+ },
12422
+ async execute(input) {
12423
+ const i = input;
12424
+ const result = await director.readSession(i.subagentId, i.tail);
12425
+ if (!result) {
12426
+ return {
12427
+ error: `fleet_session: transcript unavailable for "${i.subagentId}". Is sessionsRoot configured?`
12428
+ };
12623
12429
  }
12624
- if (payload.kind !== "timeout") {
12625
- setImmediate(() => {
12626
- const base = Math.max(payload.limit, payload.used);
12627
- const extra = {};
12628
- switch (payload.kind) {
12629
- case "iterations":
12630
- extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
12631
- break;
12632
- case "tool_calls":
12633
- extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
12634
- break;
12635
- case "tokens":
12636
- extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
12637
- break;
12638
- case "cost":
12639
- extra.maxCostUsd = Math.min(base * 1.25, 100);
12640
- break;
12641
- }
12642
- payload.extend(extra);
12643
- });
12430
+ return result;
12431
+ }
12432
+ };
12433
+ }
12434
+ function makeFleetHealthTool(director) {
12435
+ return {
12436
+ name: "fleet_health",
12437
+ description: "Per-subagent health report: budget pressure (pct of limits consumed), last activity timestamp, and current status. Use to decide whether to assign more work to a subagent or spawn a fresh one.",
12438
+ permission: "auto",
12439
+ mutating: false,
12440
+ inputSchema: { type: "object", properties: {}, required: [] },
12441
+ async execute() {
12442
+ const status = director.status();
12443
+ const snapshot = director.snapshot();
12444
+ const subagents = status.subagents ?? [];
12445
+ const perSubagent = snapshot.perSubagent ?? {};
12446
+ return {
12447
+ subagents: subagents.map((s) => {
12448
+ const usage = perSubagent[s.id];
12449
+ return {
12450
+ id: s.id,
12451
+ status: s.status,
12452
+ lastEventAt: usage?.lastEventAt,
12453
+ budgetPressure: {
12454
+ iterations: usage?.iterations,
12455
+ toolCalls: usage?.toolCalls,
12456
+ costUsd: usage?.cost
12457
+ }
12458
+ };
12459
+ })
12460
+ };
12461
+ }
12462
+ };
12463
+ }
12464
+ function makeCollabDebugTool(director) {
12465
+ return {
12466
+ name: "collab_debug",
12467
+ description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
12468
+ permission: "auto",
12469
+ mutating: false,
12470
+ inputSchema: {
12471
+ type: "object",
12472
+ properties: {
12473
+ targetPaths: {
12474
+ type: "array",
12475
+ items: { type: "string" },
12476
+ description: "File paths / glob patterns to scan for bugs."
12477
+ },
12478
+ timeoutMs: {
12479
+ type: "number",
12480
+ minimum: 1,
12481
+ description: "Timeout in ms. Default: 600000 (10 minutes)."
12482
+ },
12483
+ maxTargetFiles: {
12484
+ type: "number",
12485
+ minimum: 1,
12486
+ description: "Maximum number of files to include in the snapshot. If not set, the limit is computed dynamically from contextWindow or falls back to the default (30)."
12487
+ },
12488
+ contextWindow: {
12489
+ type: "number",
12490
+ minimum: 1,
12491
+ description: "Context window size (tokens) of the model. When provided and maxTargetFiles is not set, the file limit is computed dynamically as floor((contextWindow * 0.4) / 2000)."
12492
+ }
12493
+ },
12494
+ required: ["targetPaths"]
12495
+ },
12496
+ async execute(input) {
12497
+ const i = input;
12498
+ if (!i.targetPaths?.length) {
12499
+ return { error: "collab_debug: targetPaths is required and must be non-empty." };
12644
12500
  }
12645
- });
12646
- this.disposers.push(dBudget);
12647
- const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
12648
- const payload = e.payload;
12649
- if (payload.sessionId !== this.sessionId) return;
12650
- this.cancelled = true;
12651
- if (this._timeoutTimer) {
12652
- clearTimeout(this._timeoutTimer);
12653
- this._timeoutTimer = void 0;
12501
+ const options = {
12502
+ targetPaths: i.targetPaths,
12503
+ timeoutMs: i.timeoutMs,
12504
+ maxTargetFiles: i.maxTargetFiles,
12505
+ contextWindow: i.contextWindow
12506
+ };
12507
+ try {
12508
+ const report = await director.spawnCollab(options);
12509
+ return {
12510
+ sessionId: report.sessionId,
12511
+ overallVerdict: report.overallVerdict,
12512
+ bugCount: report.bugs.length,
12513
+ planCount: report.refactorPlans.length,
12514
+ evaluationCount: report.evaluations.length,
12515
+ summary: report.summary,
12516
+ bugs: report.bugs,
12517
+ refactorPlans: report.refactorPlans,
12518
+ evaluations: report.evaluations
12519
+ };
12520
+ } catch (err) {
12521
+ const msg = err instanceof Error ? err.message : String(err);
12522
+ return { error: "collab_debug failed: " + msg };
12654
12523
  }
12655
- this.fleetBus.emit({
12656
- subagentId: this.director.id,
12524
+ }
12525
+ };
12526
+ }
12527
+ function makeFleetEmitTool(director) {
12528
+ return {
12529
+ name: "fleet_emit",
12530
+ description: "Emit a structured event on the FleetBus. Any subagent can emit any event type; the Director routes it to all listeners. Use it to stream findings, progress updates, or final results to other agents in real time.",
12531
+ permission: "auto",
12532
+ mutating: false,
12533
+ inputSchema: {
12534
+ type: "object",
12535
+ properties: {
12536
+ type: {
12537
+ type: "string",
12538
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
12539
+ },
12540
+ payload: {
12541
+ type: "object",
12542
+ description: "Event payload. Structure depends on event type. Use null if no payload."
12543
+ }
12544
+ },
12545
+ required: ["type"]
12546
+ },
12547
+ async execute(input) {
12548
+ const i = input;
12549
+ director.fleet.emit({
12550
+ subagentId: director.id,
12657
12551
  ts: Date.now(),
12658
- type: "collab.cancelled",
12659
- payload: { sessionId: this.sessionId, reason: payload.reason }
12552
+ type: i.type,
12553
+ payload: i.payload ?? {}
12660
12554
  });
12555
+ return { ok: true, event: i.type };
12556
+ }
12557
+ };
12558
+ }
12559
+ function makeWorkCompleteTool(director) {
12560
+ return {
12561
+ name: "work_complete",
12562
+ description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
12563
+ permission: "auto",
12564
+ mutating: false,
12565
+ inputSchema: { type: "object", properties: {}, required: [] },
12566
+ async execute() {
12567
+ director.workComplete();
12568
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
12569
+ }
12570
+ };
12571
+ }
12572
+
12573
+ // src/coordination/fleet-bus.ts
12574
+ var FleetBus = class {
12575
+ byId = /* @__PURE__ */ new Map();
12576
+ byType = /* @__PURE__ */ new Map();
12577
+ any = /* @__PURE__ */ new Set();
12578
+ /**
12579
+ * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
12580
+ * `onPattern('*')`) to forward all events with subagent attribution, so
12581
+ * new kernel event types are automatically forwarded without any manual
12582
+ * registration. `subagent.*` events are excluded because they originate
12583
+ * from MultiAgentHost on the parent bus, not the subagent's own bus.
12584
+ *
12585
+ * Returns a disposer that detaches every subscription; call on
12586
+ * subagent teardown so the listeners don't outlive the run.
12587
+ */
12588
+ attach(subagentId, bus, taskId) {
12589
+ const off = bus.onAny((type, payload) => {
12590
+ if (type.startsWith("subagent.")) return;
12591
+ this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
12661
12592
  });
12662
- this.disposers.push(dCancel);
12663
- const d1 = this.fleetBus.filter("bug.found", (e) => {
12664
- const payload = e.payload;
12665
- if (payload?.finding) {
12666
- this.bugs.set(payload.finding.id, payload.finding);
12667
- this.emit("bug.found", payload);
12593
+ return () => {
12594
+ off();
12595
+ };
12596
+ }
12597
+ /** Subscribe to every event from one subagent. */
12598
+ subscribe(subagentId, handler) {
12599
+ let set = this.byId.get(subagentId);
12600
+ if (!set) {
12601
+ set = /* @__PURE__ */ new Set();
12602
+ this.byId.set(subagentId, set);
12603
+ }
12604
+ set.add(handler);
12605
+ return () => {
12606
+ set.delete(handler);
12607
+ };
12608
+ }
12609
+ /** Subscribe to one event type across all subagents. */
12610
+ filter(type, handler) {
12611
+ let set = this.byType.get(type);
12612
+ if (!set) {
12613
+ set = /* @__PURE__ */ new Set();
12614
+ this.byType.set(type, set);
12615
+ }
12616
+ set.add(handler);
12617
+ return () => {
12618
+ set.delete(handler);
12619
+ };
12620
+ }
12621
+ /** Subscribe to literally everything. The fleet roll-up uses this. */
12622
+ onAny(handler) {
12623
+ this.any.add(handler);
12624
+ return () => {
12625
+ this.any.delete(handler);
12626
+ };
12627
+ }
12628
+ emit(event) {
12629
+ const byId = this.byId.get(event.subagentId);
12630
+ if (byId)
12631
+ for (const h of byId) {
12632
+ try {
12633
+ h(event);
12634
+ } catch {
12635
+ }
12668
12636
  }
12669
- });
12670
- this.disposers.push(d1);
12671
- const d2 = this.fleetBus.filter("refactor.plan", (e) => {
12672
- const payload = e.payload;
12673
- if (payload?.plan) {
12674
- this.plans.set(payload.plan.id, payload.plan);
12675
- this.emit("refactor.plan", payload);
12637
+ const byType = this.byType.get(event.type);
12638
+ if (byType)
12639
+ for (const h of byType) {
12640
+ try {
12641
+ h(event);
12642
+ } catch {
12643
+ }
12676
12644
  }
12677
- });
12678
- this.disposers.push(d2);
12679
- const d3 = this.fleetBus.filter("critic.evaluation", (e) => {
12680
- const payload = e.payload;
12681
- if (payload?.evaluation) {
12682
- this.evaluations.set(payload.evaluation.id, payload.evaluation);
12683
- this.emit("critic.evaluation", payload);
12645
+ for (const h of this.any) {
12646
+ try {
12647
+ h(event);
12648
+ } catch {
12684
12649
  }
12685
- });
12686
- this.disposers.push(d3);
12687
- }
12688
- roleFromSubagentId(subagentId) {
12689
- for (const [role, id] of this.subagentIds) {
12690
- if (id === subagentId) return role;
12691
12650
  }
12692
- const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
12693
- return match?.[1] ?? null;
12694
12651
  }
12695
- assembleReport() {
12696
- const bugList = Array.from(this.bugs.values());
12697
- const planList = Array.from(this.plans.values());
12698
- const evalList = Array.from(this.evaluations.values());
12699
- let disposition = "completed";
12700
- if (this.cancelled) disposition = "cancelled";
12701
- const verdictOrder = {
12702
- approve: 0,
12703
- needs_revision: 1,
12704
- reject: 2
12705
- };
12706
- const overallVerdict = evalList.reduce(
12707
- (worst, eval_) => {
12708
- const w = verdictOrder[worst];
12709
- const c = verdictOrder[eval_.verdict];
12710
- return c > w ? eval_.verdict : worst;
12711
- },
12712
- "approve"
12713
- );
12714
- const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
12652
+ };
12653
+ var FleetUsageAggregator = class {
12654
+ constructor(bus, priceLookup, metaLookup) {
12655
+ this.priceLookup = priceLookup;
12656
+ this.metaLookup = metaLookup;
12657
+ this.unsub.push(bus.filter("provider.response", (e) => this.onProviderResponse(e)));
12658
+ this.unsub.push(bus.filter("tool.executed", (e) => this.onToolExecuted(e)));
12659
+ this.unsub.push(bus.filter("iteration.started", (e) => this.onIterationStarted(e)));
12660
+ }
12661
+ priceLookup;
12662
+ metaLookup;
12663
+ perSubagent = /* @__PURE__ */ new Map();
12664
+ total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
12665
+ unsub = new Array();
12666
+ /**
12667
+ * Remove a terminated subagent's data from the aggregator and subtract its
12668
+ * contribution from the running totals. Call this when a subagent is removed
12669
+ * from the fleet so the aggregator doesn't accumulate unbounded data for
12670
+ * entities that will never emit events again.
12671
+ */
12672
+ removeSubagent(subagentId) {
12673
+ const snap = this.perSubagent.get(subagentId);
12674
+ if (!snap) return;
12675
+ this.perSubagent.delete(subagentId);
12676
+ this.total.input -= snap.input;
12677
+ this.total.output -= snap.output;
12678
+ this.total.cacheRead -= snap.cacheRead;
12679
+ this.total.cacheWrite -= snap.cacheWrite;
12680
+ this.total.cost -= snap.cost;
12681
+ }
12682
+ /** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
12683
+ dispose() {
12684
+ for (const off of this.unsub) off();
12685
+ this.unsub.length = 0;
12686
+ }
12687
+ /** Live snapshot — safe to call from a tool's execute() body. */
12688
+ snapshot() {
12715
12689
  return {
12716
- sessionId: this.sessionId,
12717
- startedAt: this.snapshot.createdAt,
12718
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12719
- targetPaths: this.options.targetPaths,
12720
- disposition,
12721
- bugs: bugList,
12722
- refactorPlans: planList,
12723
- evaluations: evalList,
12724
- alerts: [...this.alerts],
12725
- overallVerdict,
12726
- summary
12690
+ total: { ...this.total },
12691
+ perSubagent: Object.fromEntries(
12692
+ Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
12693
+ )
12727
12694
  };
12728
12695
  }
12729
- buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
12730
- const lines = [
12731
- `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
12732
- "",
12733
- `**Target:** ${this.options.targetPaths.join(", ")}`,
12734
- `**Disposition:** ${disposition.toUpperCase()}`,
12735
- `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
12736
- ""
12737
- ];
12738
- if (this.alerts.length > 0) {
12739
- lines.push("### Alerts", "");
12740
- for (const alert of this.alerts) {
12741
- lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
12742
- }
12743
- lines.push("");
12696
+ ensure(subagentId) {
12697
+ let snap = this.perSubagent.get(subagentId);
12698
+ if (!snap) {
12699
+ const meta = this.metaLookup?.(subagentId);
12700
+ snap = {
12701
+ subagentId,
12702
+ provider: meta?.provider,
12703
+ model: meta?.model,
12704
+ input: 0,
12705
+ output: 0,
12706
+ cacheRead: 0,
12707
+ cacheWrite: 0,
12708
+ cost: 0,
12709
+ toolCalls: 0,
12710
+ iterations: 0,
12711
+ startedAt: Date.now(),
12712
+ lastEventAt: Date.now()
12713
+ };
12714
+ this.perSubagent.set(subagentId, snap);
12744
12715
  }
12745
- if (bugs.length > 0) {
12746
- lines.push("### Bugs Found", "");
12747
- for (const b of bugs) {
12748
- lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
12749
- }
12750
- lines.push("");
12716
+ return snap;
12717
+ }
12718
+ onProviderResponse(e) {
12719
+ const snap = this.ensure(e.subagentId);
12720
+ const p = e.payload;
12721
+ const usage = p?.usage;
12722
+ if (!usage) return;
12723
+ snap.input += usage.input ?? 0;
12724
+ snap.output += usage.output ?? 0;
12725
+ snap.cacheRead += usage.cacheRead ?? 0;
12726
+ snap.cacheWrite += usage.cacheWrite ?? 0;
12727
+ this.total.input += usage.input ?? 0;
12728
+ this.total.output += usage.output ?? 0;
12729
+ this.total.cacheRead += usage.cacheRead ?? 0;
12730
+ this.total.cacheWrite += usage.cacheWrite ?? 0;
12731
+ const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
12732
+ if (price) {
12733
+ const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
12734
+ snap.cost += delta;
12735
+ this.total.cost += delta;
12751
12736
  }
12752
- if (plans.length > 0) {
12753
- lines.push("### Refactor Plans", "");
12754
- for (const p of plans) {
12755
- lines.push(`- **Phase plan** (risk: ${p.riskScore}, ~${p.estimatedChangeCount} changes)`);
12756
- for (const phase of p.phases) {
12757
- lines.push(` - Phase ${phase.number}: ${phase.title} [${phase.risk}]`);
12758
- }
12759
- }
12760
- lines.push("");
12737
+ snap.lastEventAt = e.ts;
12738
+ }
12739
+ onToolExecuted(e) {
12740
+ const snap = this.ensure(e.subagentId);
12741
+ snap.toolCalls += 1;
12742
+ snap.lastEventAt = e.ts;
12743
+ }
12744
+ onIterationStarted(e) {
12745
+ const snap = this.ensure(e.subagentId);
12746
+ snap.iterations += 1;
12747
+ snap.lastEventAt = e.ts;
12748
+ }
12749
+ };
12750
+
12751
+ // src/coordination/large-answer-store.ts
12752
+ var LargeAnswerStore = class {
12753
+ /**
12754
+ * Responses above this size (in characters) are stored out-of-context.
12755
+ * Below this, the full answer is returned inline (no overhead).
12756
+ * Default: 2000 chars ≈ 400-600 tokens.
12757
+ */
12758
+ sizeThreshold;
12759
+ store = /* @__PURE__ */ new Map();
12760
+ constructor(sizeThreshold = 2e3) {
12761
+ this.sizeThreshold = sizeThreshold;
12762
+ }
12763
+ /**
12764
+ * Store a value, returning a summary + key for inline use.
12765
+ * If the value is below sizeThreshold, returns it as-is (no store entry).
12766
+ */
12767
+ storeAnswer(value) {
12768
+ if (value === void 0 || value === null) {
12769
+ return { summary: String(value), inline: true };
12761
12770
  }
12762
- if (evals.length > 0) {
12763
- lines.push("### Critic Evaluations", "");
12764
- for (const e of evals) {
12765
- lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
12766
- for (const c of e.concerns) {
12767
- if (c.severity === "blocking") lines.push(` - ${c.description}`);
12768
- }
12769
- }
12770
- lines.push("");
12771
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
12772
+ const size = serialized.length;
12773
+ if (size <= this.sizeThreshold) {
12774
+ return { summary: serialized.slice(0, 500), inline: true };
12771
12775
  }
12772
- return lines.join("\n");
12776
+ const key = `a-${hashStr(serialized)}`;
12777
+ this.store.set(key, {
12778
+ key,
12779
+ value,
12780
+ size,
12781
+ storedAt: Date.now()
12782
+ });
12783
+ return {
12784
+ key,
12785
+ summary: `[stored: ${size} chars \u2014 use roll_up or ask_result tool to retrieve, key=${key}]`,
12786
+ inline: false
12787
+ };
12773
12788
  }
12774
- cleanup() {
12775
- for (const dispose of this.disposers) dispose();
12776
- this.disposers.length = 0;
12789
+ /**
12790
+ * Retrieve a previously stored answer by its key.
12791
+ * Returns undefined if the key is unknown or the store was cleared.
12792
+ */
12793
+ retrieveAnswer(key) {
12794
+ return this.store.get(key)?.value;
12795
+ }
12796
+ /**
12797
+ * Check if a key exists in the store.
12798
+ */
12799
+ hasAnswer(key) {
12800
+ return this.store.has(key);
12801
+ }
12802
+ /** Number of stored entries. */
12803
+ get size() {
12804
+ return this.store.size;
12805
+ }
12806
+ /** Total characters stored. */
12807
+ get totalChars() {
12808
+ let total = 0;
12809
+ for (const e of this.store.values()) total += e.size;
12810
+ return total;
12811
+ }
12812
+ /** Clear all stored entries. Call at the end of a director run. */
12813
+ clear() {
12814
+ this.store.clear();
12777
12815
  }
12778
12816
  };
12817
+ function hashStr(s) {
12818
+ let h = 5381;
12819
+ for (let i = 0; i < s.length; i++) {
12820
+ h = h * 33 ^ s.charCodeAt(i);
12821
+ }
12822
+ return (h >>> 0).toString(36);
12823
+ }
12824
+ var ROLE_TO_PHASE = (() => {
12825
+ const map = {};
12826
+ for (const [phase, defs] of Object.entries(AGENTS_BY_PHASE)) {
12827
+ for (const def of defs) {
12828
+ const role = def.config.role;
12829
+ if (role) map[role] = phase;
12830
+ }
12831
+ }
12832
+ return map;
12833
+ })();
12834
+ function phaseForRole(role) {
12835
+ return role ? ROLE_TO_PHASE[role] : void 0;
12836
+ }
12837
+ function resolveModelMatrix(matrix, role) {
12838
+ if (!matrix) return void 0;
12839
+ if (role && matrix[role]) return matrix[role];
12840
+ const phase = phaseForRole(role);
12841
+ if (phase && matrix[phase]) return matrix[phase];
12842
+ if (matrix["*"]) return matrix["*"];
12843
+ return void 0;
12844
+ }
12779
12845
 
12780
12846
  // src/coordination/director.ts
12781
12847
  var FleetSpawnBudgetError = class extends Error {
@@ -12854,6 +12920,12 @@ var Director = class _Director {
12854
12920
  getLeaderContextPressure() {
12855
12921
  return this.leaderContextPressure;
12856
12922
  }
12923
+ resolveMaxContext() {
12924
+ const resolved = typeof this.maxContext === "function" ? this.maxContext() : this.maxContext;
12925
+ return resolved && resolved > 0 ? resolved : 128e3;
12926
+ }
12927
+ /** Optional Brain arbiter for director-level policy decisions. */
12928
+ brain;
12857
12929
  /**
12858
12930
  * Optional fleet-level policy container. When provided the Director
12859
12931
  * delegates spawn budgeting, manifest entries, and checkpointing to it
@@ -12945,8 +13017,11 @@ var Director = class _Director {
12945
13017
  leaderContextPressure = 0;
12946
13018
  /** Maximum context load fraction before spawn is refused. */
12947
13019
  maxLeaderContextLoad;
12948
- /** Provider's max context window in tokens. */
13020
+ /** Provider's max context window in tokens, or a live resolver for runtime model switches. */
12949
13021
  maxContext;
13022
+ /** Per-task model matrix (static record or live getter); resolved
13023
+ * per-spawn when no explicit model is set. */
13024
+ modelMatrix;
12950
13025
  /**
12951
13026
  * When set by `workComplete()`, the director stops dispatching new tasks
12952
13027
  * and terminates all running subagents. Used when the director's LLM decides
@@ -12962,6 +13037,7 @@ var Director = class _Director {
12962
13037
  largeAnswerStore;
12963
13038
  constructor(opts) {
12964
13039
  this.id = opts.config.coordinatorId || randomUUID();
13040
+ this.brain = opts.brain;
12965
13041
  this.manifestPath = opts.manifestPath;
12966
13042
  this.roster = opts.roster;
12967
13043
  this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -12977,20 +13053,23 @@ var Director = class _Director {
12977
13053
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
12978
13054
  this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
12979
13055
  this.maxContext = opts.maxContext ?? 128e3;
13056
+ this.modelMatrix = opts.modelMatrix;
12980
13057
  this.sessionsRoot = opts.sessionsRoot;
12981
13058
  this.directorRunId = opts.directorRunId ?? this.id;
12982
- this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
12983
- directorRunId: this.id,
12984
- maxSpawns: opts.maxSpawns,
12985
- spawnDepth: this.spawnDepth,
12986
- maxSpawnDepth: this.maxSpawnDepth,
12987
- directorBudget: opts.directorBudget
12988
- }, opts.checkpointDebounceMs ?? 250) : null;
13059
+ this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
13060
+ opts.stateCheckpointPath,
13061
+ {
13062
+ directorRunId: this.id,
13063
+ maxSpawns: opts.maxSpawns,
13064
+ spawnDepth: this.spawnDepth,
13065
+ maxSpawnDepth: this.maxSpawnDepth,
13066
+ directorBudget: opts.directorBudget
13067
+ },
13068
+ opts.checkpointDebounceMs ?? 250
13069
+ ) : null;
12989
13070
  this.fleetManager = opts.fleetManager;
12990
13071
  if (this.sharedScratchpadPath) {
12991
- void fsp.mkdir(this.sharedScratchpadPath, { recursive: true }).catch(
12992
- (err) => this.logShutdownError("shared_scratchpad_mkdir", err)
12993
- );
13072
+ void fsp.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
12994
13073
  }
12995
13074
  this.transport = new InMemoryBridgeTransport();
12996
13075
  this.bridge = new InMemoryAgentBridge(
@@ -13103,33 +13182,81 @@ var Director = class _Director {
13103
13182
  return;
13104
13183
  }
13105
13184
  }
13106
- extendCounts.set(guardKey, prior + 1);
13107
- setImmediate(() => {
13108
- const extra = {};
13109
- const base = Math.max(payload.limit, payload.used);
13110
- const grow = (ceiling) => Math.min(Math.ceil(base * 1.5), ceiling);
13111
- let newLimit = base;
13112
- switch (payload.kind) {
13113
- case "iterations":
13114
- newLimit = grow(5e4);
13115
- extra.maxIterations = newLimit;
13116
- break;
13117
- case "tool_calls":
13118
- newLimit = grow(1e5);
13119
- extra.maxToolCalls = newLimit;
13120
- break;
13121
- case "tokens":
13122
- newLimit = grow(5e6);
13123
- extra.maxTokens = newLimit;
13124
- break;
13125
- case "cost":
13126
- newLimit = Math.min(base * 1.5, 100);
13127
- extra.maxCostUsd = newLimit;
13128
- break;
13129
- }
13130
- this.recordExtension(e.subagentId, e.taskId, payload.kind, newLimit);
13131
- payload.extend(extra);
13132
- });
13185
+ const grantExtension = () => {
13186
+ setImmediate(() => {
13187
+ const extra = {};
13188
+ const base = Math.max(payload.limit, payload.used);
13189
+ const grow = (ceiling) => Math.min(Math.ceil(base * 1.5), ceiling);
13190
+ let newLimit = base;
13191
+ switch (payload.kind) {
13192
+ case "iterations":
13193
+ newLimit = grow(5e4);
13194
+ extra.maxIterations = newLimit;
13195
+ break;
13196
+ case "tool_calls":
13197
+ newLimit = grow(1e5);
13198
+ extra.maxToolCalls = newLimit;
13199
+ break;
13200
+ case "tokens":
13201
+ newLimit = grow(5e6);
13202
+ extra.maxTokens = newLimit;
13203
+ break;
13204
+ case "cost":
13205
+ newLimit = Math.min(base * 1.5, 100);
13206
+ extra.maxCostUsd = newLimit;
13207
+ break;
13208
+ }
13209
+ extendCounts.set(guardKey, prior + 1);
13210
+ this.recordExtension(e.subagentId, e.taskId, payload.kind, newLimit);
13211
+ payload.extend(extra);
13212
+ });
13213
+ };
13214
+ if (this.brain) {
13215
+ void this.brain.decide({
13216
+ id: `director-budget-${e.subagentId}-${payload.kind}`,
13217
+ source: "director",
13218
+ question: `Should the director extend the ${payload.kind} budget for subagent ${e.subagentId}?`,
13219
+ context: [
13220
+ e.taskId ? `Task id: ${e.taskId}` : void 0,
13221
+ `Used: ${payload.used}`,
13222
+ `Limit: ${payload.limit}`,
13223
+ `Prior extensions for this kind: ${prior}`
13224
+ ].filter(Boolean).join("\n"),
13225
+ risk: payload.kind === "cost" ? "high" : "medium",
13226
+ fallback: "continue",
13227
+ options: [
13228
+ {
13229
+ id: "extend",
13230
+ label: "Grant the director default budget extension",
13231
+ consequence: "The subagent continues with a larger per-kind budget.",
13232
+ risk: payload.kind === "cost" ? "high" : "medium",
13233
+ recommended: true
13234
+ },
13235
+ {
13236
+ id: "stop",
13237
+ label: "Stop this subagent at the current budget limit",
13238
+ consequence: "The current task will fail or stop due to budget pressure.",
13239
+ risk: "low"
13240
+ }
13241
+ ]
13242
+ }).then((decision) => {
13243
+ if (decision.type === "deny") {
13244
+ payload.deny();
13245
+ return;
13246
+ }
13247
+ if (decision.type === "ask_human") {
13248
+ payload.deny();
13249
+ return;
13250
+ }
13251
+ if (decision.optionId === "stop" || /\bstop\b/i.test(decision.text)) {
13252
+ payload.deny();
13253
+ return;
13254
+ }
13255
+ grantExtension();
13256
+ }).catch(() => payload.deny());
13257
+ return;
13258
+ }
13259
+ grantExtension();
13133
13260
  });
13134
13261
  this.largeAnswerStore = new LargeAnswerStore(2e3);
13135
13262
  }
@@ -13168,7 +13295,12 @@ var Director = class _Director {
13168
13295
  */
13169
13296
  workComplete() {
13170
13297
  this.workCompleteFlag = true;
13171
- this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
13298
+ this.fleet.emit({
13299
+ subagentId: this.id,
13300
+ ts: Date.now(),
13301
+ type: "director.work_complete",
13302
+ payload: {}
13303
+ });
13172
13304
  }
13173
13305
  /** Returns true if `workComplete()` has been called on this director. */
13174
13306
  isWorkComplete() {
@@ -13300,13 +13432,25 @@ var Director = class _Director {
13300
13432
  "workComplete() has been called \u2014 director closed further spawning"
13301
13433
  );
13302
13434
  }
13435
+ if (!config.model && this.modelMatrix) {
13436
+ const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
13437
+ const entry = resolveModelMatrix(matrix, config.role);
13438
+ if (entry) {
13439
+ config.model = entry.model;
13440
+ if (entry.provider) config.provider = entry.provider;
13441
+ }
13442
+ }
13303
13443
  if (this.fleetManager) {
13304
13444
  const rejection = this.fleetManager.canSpawn(config);
13305
13445
  if (rejection) {
13306
- if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
13307
- if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
13308
- if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
13309
- if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
13446
+ if (rejection.kind === "max_spawn_depth")
13447
+ throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
13448
+ if (rejection.kind === "max_spawns")
13449
+ throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
13450
+ if (rejection.kind === "max_cost_usd")
13451
+ throw new FleetCostCapError(rejection.limit, rejection.observed);
13452
+ if (rejection.kind === "max_context_load")
13453
+ throw new FleetContextOverflowError(rejection.limit, rejection.observed);
13310
13454
  }
13311
13455
  } else {
13312
13456
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -13322,7 +13466,8 @@ var Director = class _Director {
13322
13466
  }
13323
13467
  }
13324
13468
  if (this.maxLeaderContextLoad < 1) {
13325
- const threshold = this.maxContext * this.maxLeaderContextLoad;
13469
+ const maxContext = this.resolveMaxContext();
13470
+ const threshold = maxContext * this.maxLeaderContextLoad;
13326
13471
  if (this.leaderContextPressure >= threshold) {
13327
13472
  throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
13328
13473
  }
@@ -13336,7 +13481,9 @@ var Director = class _Director {
13336
13481
  this.fleetManager.assignNicknameAndRecord(config);
13337
13482
  } else {
13338
13483
  config.name = assignNickname(role, this._usedNicknames);
13339
- this._usedNicknames.add(config.name.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-"));
13484
+ this._usedNicknames.add(
13485
+ config.name.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-")
13486
+ );
13340
13487
  }
13341
13488
  }
13342
13489
  result = await this.coordinator.spawn(config);
@@ -13571,7 +13718,11 @@ var Director = class _Director {
13571
13718
  subagentId: taskWithId.subagentId ?? "unassigned",
13572
13719
  taskId: taskWithId.id,
13573
13720
  status: "stopped",
13574
- error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
13721
+ error: {
13722
+ kind: "aborted_by_parent",
13723
+ message: "Director called workComplete() \u2014 no further tasks will run",
13724
+ retryable: false
13725
+ },
13575
13726
  iterations: 0,
13576
13727
  toolCalls: 0,
13577
13728
  durationMs: 0
@@ -14497,12 +14648,12 @@ var DefaultModelsRegistry = class {
14497
14648
  */
14498
14649
  async loadOverlay(opts = {}) {
14499
14650
  if (this.overlayPayload && !opts.force) return this.overlayPayload;
14500
- if (this.overlay) {
14651
+ if (hasEntries(this.overlay)) {
14501
14652
  this.overlayPayload = this.overlay;
14502
14653
  return this.overlayPayload;
14503
14654
  }
14504
14655
  const fetched = await this.loadOverlayFromUrl(opts);
14505
- if (fetched) {
14656
+ if (hasEntries(fetched)) {
14506
14657
  this.overlayPayload = fetched;
14507
14658
  return fetched;
14508
14659
  }
@@ -14629,6 +14780,9 @@ var DefaultModelsRegistry = class {
14629
14780
  return path15.resolve(this.cacheFile);
14630
14781
  }
14631
14782
  };
14783
+ function hasEntries(payload) {
14784
+ return payload !== void 0 && Object.keys(payload).length > 0;
14785
+ }
14632
14786
 
14633
14787
  // src/models/mode-store.ts
14634
14788
  init_atomic_write();