@wrongstack/core 0.7.8 → 0.8.2

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 (41) hide show
  1. package/dist/{agent-bridge-hXRN-GO_.d.ts → agent-bridge-DPxcUVkn.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-B2zguWzh.d.ts → agent-subagent-runner-Cav3yEJM.d.ts} +9 -10
  3. package/dist/coordination/index.d.ts +9 -9
  4. package/dist/coordination/index.js +393 -179
  5. package/dist/coordination/index.js.map +1 -1
  6. package/dist/defaults/index.d.ts +13 -13
  7. package/dist/defaults/index.js +261 -64
  8. package/dist/defaults/index.js.map +1 -1
  9. package/dist/{events-D4pGukpI.d.ts → events-DyhxkstG.d.ts} +33 -0
  10. package/dist/execution/index.d.ts +24 -7
  11. package/dist/execution/index.js +158 -37
  12. package/dist/execution/index.js.map +1 -1
  13. package/dist/extension/index.d.ts +3 -3
  14. package/dist/{index-BtNRyJft.d.ts → index-oYZeWsuJ.d.ts} +2 -2
  15. package/dist/index.d.ts +16 -16
  16. package/dist/index.js +365 -68
  17. package/dist/index.js.map +1 -1
  18. package/dist/infrastructure/index.d.ts +2 -2
  19. package/dist/kernel/index.d.ts +3 -3
  20. package/dist/kernel/index.js +23 -0
  21. package/dist/kernel/index.js.map +1 -1
  22. package/dist/{multi-agent-7OK4pEKW.d.ts → multi-agent-CRMznZmf.d.ts} +64 -30
  23. package/dist/{multi-agent-coordinator-3Ypfg-hr.d.ts → multi-agent-coordinator-IQKrMfXz.d.ts} +11 -1
  24. package/dist/{null-fleet-bus-BZUrXVcd.d.ts → null-fleet-bus-sKnVwEd8.d.ts} +92 -6
  25. package/dist/observability/index.d.ts +1 -1
  26. package/dist/{path-resolver-DPUjF10O.d.ts → path-resolver-1CIYbH2Q.d.ts} +1 -1
  27. package/dist/{permission-policy-D5Gj1o2K.d.ts → permission-policy-BBa1M1xc.d.ts} +10 -1
  28. package/dist/{plan-templates-C-IOLJ8Q.d.ts → plan-templates-BnlpEkX8.d.ts} +1 -1
  29. package/dist/{provider-runner-iST8U3ni.d.ts → provider-runner-BrA0XR-l.d.ts} +1 -1
  30. package/dist/sdd/index.d.ts +5 -5
  31. package/dist/sdd/index.js +114 -28
  32. package/dist/sdd/index.js.map +1 -1
  33. package/dist/{secret-scrubber-DXkhwuka.d.ts → secret-scrubber-C0n1EqrC.d.ts} +1 -1
  34. package/dist/{secret-scrubber-nI8qjaqW.d.ts → secret-scrubber-CyE1-EMG.d.ts} +1 -1
  35. package/dist/security/index.d.ts +3 -3
  36. package/dist/security/index.js +29 -3
  37. package/dist/security/index.js.map +1 -1
  38. package/dist/storage/index.d.ts +2 -2
  39. package/dist/{tool-executor-CSwXjifK.d.ts → tool-executor-QwfWnQZ8.d.ts} +1 -1
  40. package/dist/types/index.d.ts +9 -9
  41. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -586,6 +586,16 @@ var EventBus = class {
586
586
  this.off(event, wrapper);
587
587
  };
588
588
  }
589
+ /**
590
+ * Subscribe to all events, regardless of name. Short-hand for
591
+ * `onPattern('*')`. Use for logging, debugging, or forwarding every
592
+ * event to another bus (as FleetBus does).
593
+ *
594
+ * Returns an unsubscribe function.
595
+ */
596
+ onAny(fn) {
597
+ return this.onPattern("*", fn);
598
+ }
589
599
  /**
590
600
  * Subscribe to all events whose name matches a glob-style prefix.
591
601
  * `'tool.*'` matches `tool.started`, `tool.executed`, `tool.progress`, etc.
@@ -720,6 +730,19 @@ var ScopedEventBus = class extends EventBus {
720
730
  this.registrations.set(key, unsub);
721
731
  return unsub;
722
732
  }
733
+ /**
734
+ * Subscribe to all events. Alias for `onPattern('*')` — the listener is
735
+ * tracked so that `teardown()` will remove it automatically.
736
+ */
737
+ onAny(fn) {
738
+ const key = this.nextKey++;
739
+ const unsub = EventBus.prototype.onPattern.call(this, "*", fn);
740
+ this.registrations.set(key, unsub);
741
+ return () => {
742
+ this.registrations.delete(key);
743
+ unsub();
744
+ };
745
+ }
723
746
  /**
724
747
  * Identical to `EventBus.onPattern` but the listener is tracked so that
725
748
  * `teardown()` will remove it automatically.
@@ -6159,10 +6182,36 @@ var DefaultPermissionPolicy = class {
6159
6182
  return void 0;
6160
6183
  }
6161
6184
  };
6162
- var AutoApprovePermissionPolicy = class {
6185
+ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
6186
+ /**
6187
+ * Tools that are too dangerous to auto-approve even in a delegated
6188
+ * subagent context. Subagents run non-interactively under a director
6189
+ * and cannot answer prompts, but inherited authorization does not
6190
+ * imply blanket permission for destructive or privilege-escalating
6191
+ * operations. These tools remain at their declared `permission`
6192
+ * level so the leader must explicitly allow them per-spawn.
6193
+ */
6194
+ static DENY = /* @__PURE__ */ new Set([
6195
+ "bash",
6196
+ // arbitrary shell — use exec for constrained shell
6197
+ "write",
6198
+ // arbitrary file write
6199
+ "scaffold",
6200
+ // arbitrary file generation outside project root
6201
+ "patch",
6202
+ // arbitrary diff application
6203
+ "install",
6204
+ // installs from arbitrary package sources
6205
+ "exec"
6206
+ // restricted shell but with arbitrary command args
6207
+ ]);
6163
6208
  async evaluate(tool) {
6164
- if (tool.permission === "deny") {
6165
- return { permission: "deny", source: "default", reason: "tool default deny" };
6209
+ if (tool.permission === "deny" || _AutoApprovePermissionPolicy.DENY.has(tool.name)) {
6210
+ return {
6211
+ permission: "deny",
6212
+ source: "subagent_guard",
6213
+ reason: _AutoApprovePermissionPolicy.DENY.has(tool.name) ? `tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow it explicitly` : "tool default deny"
6214
+ };
6166
6215
  }
6167
6216
  return { permission: "auto", source: "yolo" };
6168
6217
  }
@@ -7239,6 +7288,7 @@ Summarize the following message range:`;
7239
7288
  };
7240
7289
 
7241
7290
  // src/execution/auto-compaction-middleware.ts
7291
+ var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
7242
7292
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7243
7293
  name = "AutoCompaction";
7244
7294
  compactor;
@@ -7259,6 +7309,16 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7259
7309
  * short conversations, causing premature compaction triggers.
7260
7310
  */
7261
7311
  static OVERHEAD_FACTOR = 1.3;
7312
+ /**
7313
+ * Once a compaction attempt reduces nothing (preserveK protects everything,
7314
+ * no oversized tool_results remain to elide), retrying on every iteration
7315
+ * just spams `compaction.fired` events without making progress. We remember
7316
+ * the no-op and skip until either the pressure level escalates or context
7317
+ * has grown by at least this many tokens since the failed attempt.
7318
+ */
7319
+ static NOOP_RETRY_DELTA_TOKENS = 2e3;
7320
+ /** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
7321
+ lastNoopAttempt = null;
7262
7322
  /**
7263
7323
  * @param compactor Compactor to use for compaction.
7264
7324
  * @param maxContext Provider's max context window in tokens.
@@ -7300,19 +7360,44 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7300
7360
  hard: this.hardThreshold
7301
7361
  };
7302
7362
  const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
7303
- if (load >= thresholds.hard) {
7304
- await this.compact(ctx, true, { level: "hard", tokens, load });
7305
- } else if (load >= thresholds.soft) {
7306
- await this.compact(ctx, aggressiveOn !== "hard", { level: "soft", tokens, load });
7307
- } else if (load >= thresholds.warn) {
7308
- await this.compact(ctx, aggressiveOn === "warn", { level: "warn", tokens, load });
7363
+ const level = load >= thresholds.hard ? "hard" : load >= thresholds.soft ? "soft" : load >= thresholds.warn ? "warn" : null;
7364
+ if (!level) {
7365
+ this.lastNoopAttempt = null;
7366
+ return next(ctx);
7367
+ }
7368
+ if (this.shouldSkipNoopRetry(level, tokens)) {
7369
+ return next(ctx);
7309
7370
  }
7371
+ const aggressive = level === "hard" ? true : level === "soft" ? aggressiveOn !== "hard" : aggressiveOn === "warn";
7372
+ await this.compact(ctx, aggressive, { level, tokens, load });
7310
7373
  return next(ctx);
7311
7374
  };
7312
7375
  }
7376
+ /**
7377
+ * Returns true when the previous compaction at the same or higher pressure
7378
+ * level reduced nothing and context has not grown materially since. Prevents
7379
+ * a stuck preserveK window from spamming compaction events every iteration.
7380
+ */
7381
+ shouldSkipNoopRetry(level, tokens) {
7382
+ const stuck = this.lastNoopAttempt;
7383
+ if (!stuck) return false;
7384
+ if (LEVEL_RANK2[level] > LEVEL_RANK2[stuck.level]) return false;
7385
+ const delta = tokens - stuck.tokens;
7386
+ return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
7387
+ }
7388
+ recordAttempt(level, tokens, report) {
7389
+ const reduced = report.before > report.after;
7390
+ const repaired = !!report.repaired;
7391
+ if (reduced || repaired) {
7392
+ this.lastNoopAttempt = null;
7393
+ } else {
7394
+ this.lastNoopAttempt = { level, tokens };
7395
+ }
7396
+ }
7313
7397
  async compact(ctx, aggressive, pressure) {
7314
7398
  try {
7315
7399
  const report = await this.compactor.compact(ctx, { aggressive });
7400
+ this.recordAttempt(pressure.level, pressure.tokens, report);
7316
7401
  this.events?.emit("compaction.fired", {
7317
7402
  level: pressure.level,
7318
7403
  tokens: pressure.tokens,
@@ -8239,29 +8324,35 @@ var SubagentBudget = class _SubagentBudget {
8239
8324
  * same kind are no-ops — without this dedup, every `recordIteration`
8240
8325
  * after the limit is reached spawns a fresh decision Promise (until
8241
8326
  * the first one lands and patches limits), flooding the FleetBus
8242
- * with redundant threshold events. Cleared in `checkLimitAsync`'s
8327
+ * with redundant threshold events. Cleared in `negotiateExtension`'s
8243
8328
  * `finally`.
8244
8329
  */
8245
8330
  pendingExtensions = /* @__PURE__ */ new Set();
8246
8331
  /**
8247
- * Hard cap on how long `checkLimitAsync` waits for the coordinator to
8332
+ * Hard cap on how long `negotiateExtension` waits for the coordinator to
8248
8333
  * respond before defaulting to 'stop'. Without this fallback an absent
8249
8334
  * or hung listener (Director not built / event filter detached mid-run)
8250
- * leaves the budget over-limit and never enforces anything, since
8251
- * `checkLimit` returns synchronously via `void this.checkLimitAsync`.
8252
- * Raised from 30s to 60s to give subagents more headroom before
8253
- * the threshold negotiation times out and triggers a hard stop.
8335
+ * leaves the budget over-limit and never enforces anything.
8254
8336
  */
8255
8337
  static DECISION_TIMEOUT_MS = 6e4;
8256
8338
  /**
8257
8339
  * Injected by the runner when wiring the budget to its EventBus.
8258
- * Used by `checkLimitAsync` to emit `budget.threshold_reached` events.
8340
+ * Used to emit `budget.threshold_reached` events in `'auto'` mode.
8259
8341
  */
8260
8342
  _events;
8343
+ /**
8344
+ * Negotiation mode — controls whether a threshold hit tries to emit
8345
+ * `budget.threshold_reached` and wait for a coordinator decision, or
8346
+ * falls straight through to a synchronous hard stop.
8347
+ *
8348
+ * `'auto'` (default) — emit on the EventBus and wait; times out to 'stop'.
8349
+ * `'sync'` — throw `BudgetExceededError` immediately regardless of listeners.
8350
+ */
8351
+ _mode;
8261
8352
  /**
8262
8353
  * Optional callback for soft-limit handling. When set, the budget will
8263
- * call this instead of throwing when a limit is first reached. The
8264
- * handler decides whether to throw, continue, or ask the coordinator.
8354
+ * invoke it rather than throw immediately. The handler decides whether to
8355
+ * throw synchronously, continue, or ask the coordinator for an extension.
8265
8356
  */
8266
8357
  get onThreshold() {
8267
8358
  return this._onThreshold;
@@ -8269,7 +8360,12 @@ var SubagentBudget = class _SubagentBudget {
8269
8360
  set onThreshold(fn) {
8270
8361
  this._onThreshold = fn;
8271
8362
  }
8272
- constructor(limits = {}) {
8363
+ /** Returns the current negotiation mode. */
8364
+ get mode() {
8365
+ return this._mode;
8366
+ }
8367
+ constructor(limits = {}, mode = "auto") {
8368
+ this._mode = mode;
8273
8369
  this.limits = { ...limits };
8274
8370
  }
8275
8371
  start() {
@@ -8285,16 +8381,23 @@ var SubagentBudget = class _SubagentBudget {
8285
8381
  return false;
8286
8382
  }
8287
8383
  /**
8288
- * Synchronous budget check always throws synchronously so callers
8289
- * (especially test event handlers using `expect().toThrow()`) get an
8290
- * unhandled rejection when the budget is exceeded without a handler.
8291
- * When `onThreshold` IS configured, the actual async threshold-handling
8292
- * is dispatched as a fire-and-forget promise.
8384
+ * Synchronous budget check. Always throws synchronously so callers (especially
8385
+ * test event handlers using `expect().toThrow()`) get an unhandled rejection
8386
+ * when the budget is exceeded without a handler.
8387
+ *
8388
+ * Decision table:
8389
+ * - no `onThreshold` handler → throw `BudgetExceededError` (hard stop, always)
8390
+ * - `mode === 'sync'` → throw `BudgetExceededError` (hard stop, always)
8391
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
8392
+ * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
8293
8393
  */
8294
8394
  checkLimit(kind, used, limit) {
8295
8395
  if (!this._onThreshold) {
8296
8396
  throw new BudgetExceededError(kind, limit, used);
8297
8397
  }
8398
+ if (this._mode === "sync") {
8399
+ throw new BudgetExceededError(kind, limit, used);
8400
+ }
8298
8401
  const bus = this._events;
8299
8402
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
8300
8403
  throw new BudgetExceededError(kind, limit, used);
@@ -8311,8 +8414,8 @@ var SubagentBudget = class _SubagentBudget {
8311
8414
  * `pendingExtensions` slot in `finally`.
8312
8415
  *
8313
8416
  * The 'continue' return from a sync handler is treated as
8314
- * `{ extend: {} }` — keep going without patching, next overrun will
8315
- * fire a fresh signal.
8417
+ * `{ extend: {} }` — keep going without patching; next overrun fires
8418
+ * a fresh signal.
8316
8419
  */
8317
8420
  async negotiateExtension(kind, used, limit) {
8318
8421
  try {
@@ -8320,11 +8423,6 @@ var SubagentBudget = class _SubagentBudget {
8320
8423
  kind,
8321
8424
  used,
8322
8425
  limit,
8323
- // Inject a requestDecision helper the handler can call to emit the
8324
- // budget.threshold_reached event and wait for the coordinator's verdict.
8325
- // A hard fallback timer guarantees the promise eventually resolves
8326
- // even if no listener responds — without it, an absent/detached
8327
- // Director would leave the budget permanently in "asking" state.
8328
8426
  requestDecision: () => {
8329
8427
  const bus = this._events;
8330
8428
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -8408,12 +8506,12 @@ var SubagentBudget = class _SubagentBudget {
8408
8506
  }
8409
8507
  }
8410
8508
  /**
8411
- * Wall-clock budget check. Unlike other limits, timeout is treated as a
8412
- * warning-only event it NEVER hard-stops the subagent. When the
8413
- * elapsed time exceeds timeoutMs, emits `budget.threshold_reached` with
8414
- * kind='timeout' so the Director can decide whether to extend or warn.
8415
- * Call this from the iteration loop so a hung tool gets a chance to
8416
- * negotiate more time before the coordinator's Promise.race kills it.
8509
+ * Wall-clock budget check. Unlike other limits, timeout check passes through
8510
+ * `checkLimit` and is subject to the same negotiation-mode decision table.
8511
+ * In practice, `'sync'` mode (the usual test configuration) means a timeout
8512
+ * immediately throws `BudgetExceededError`. In production with a coordinator,
8513
+ * a timeout emits `budget.threshold_reached` so the Director can decide whether
8514
+ * to extend or abort.
8417
8515
  */
8418
8516
  checkTimeout() {
8419
8517
  if (this.startTime === null || this.limits.timeoutMs === void 0) return;
@@ -10794,8 +10892,7 @@ function scoreAgents(task, catalog = AGENT_CATALOG) {
10794
10892
  const hay = normalize2(task);
10795
10893
  const out = [];
10796
10894
  for (const def of Object.values(catalog)) {
10797
- const role = def.config.role;
10798
- if (!role) continue;
10895
+ if (!def?.config?.role) continue;
10799
10896
  let score = 0;
10800
10897
  const matched = [];
10801
10898
  for (const kw of def.capability.keywords) {
@@ -10807,7 +10904,7 @@ function scoreAgents(task, catalog = AGENT_CATALOG) {
10807
10904
  }
10808
10905
  }
10809
10906
  if (score > 0) {
10810
- out.push({ role, name: def.config.name, score, matched });
10907
+ out.push({ role: def.config.role, name: def.config.name, score, matched });
10811
10908
  }
10812
10909
  }
10813
10910
  out.sort((a, b) => b.score - a.score);
@@ -11178,6 +11275,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11178
11275
  coordinatorId;
11179
11276
  config;
11180
11277
  runner;
11278
+ fleetBus;
11181
11279
  subagents = /* @__PURE__ */ new Map();
11182
11280
  pendingTasks = [];
11183
11281
  completedResults = [];
@@ -11206,6 +11304,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11206
11304
  setRunner(runner) {
11207
11305
  this.runner = runner;
11208
11306
  }
11307
+ /**
11308
+ * Wire a FleetBus for director-mode event emission. Call after the
11309
+ * FleetManager is constructed so the coordinator can emit lifecycle
11310
+ * events the TUI and monitoring tools subscribe to.
11311
+ */
11312
+ setFleetBus(fleet) {
11313
+ this.fleetBus = fleet;
11314
+ }
11209
11315
  /**
11210
11316
  * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
11211
11317
  * preempt running tasks — already-dispatched subagents finish their
@@ -11241,6 +11347,18 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11241
11347
  abortController: new AbortController()
11242
11348
  });
11243
11349
  this.emit("subagent.started", { subagent: { ...subagent, id } });
11350
+ this.fleetBus?.emit({
11351
+ subagentId: id,
11352
+ ts: Date.now(),
11353
+ type: "subagent.assigned",
11354
+ payload: {
11355
+ subagentId: id,
11356
+ name: subagent.name,
11357
+ provider: subagent.provider,
11358
+ model: subagent.model
11359
+ }
11360
+ });
11361
+ this.emitCoordinatorStats();
11244
11362
  return { subagentId: id, agentId: id };
11245
11363
  }
11246
11364
  async assign(task) {
@@ -11273,6 +11391,13 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11273
11391
  subagent.currentTask = void 0;
11274
11392
  subagent.context.parentBridge = null;
11275
11393
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
11394
+ this.fleetBus?.emit({
11395
+ subagentId,
11396
+ ts: Date.now(),
11397
+ type: "subagent.stopped",
11398
+ payload: { subagentId, reason: "stopped by coordinator" }
11399
+ });
11400
+ this.emitCoordinatorStats();
11276
11401
  }
11277
11402
  async stopAll() {
11278
11403
  this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
@@ -11304,6 +11429,22 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11304
11429
  completed: this.completedResults.length
11305
11430
  };
11306
11431
  }
11432
+ /** Emit a reactive coordinator.stats event on FleetBus so the TUI can subscribe. */
11433
+ emitCoordinatorStats() {
11434
+ const stats = this.getStats();
11435
+ const subagentStatuses = Array.from(this.subagents.entries()).map(([id, s]) => ({
11436
+ subagentId: id,
11437
+ taskId: s.currentTask ?? "",
11438
+ status: s.status,
11439
+ assigned: s.context.parentBridge !== null
11440
+ }));
11441
+ this.fleetBus?.emit({
11442
+ subagentId: this.coordinatorId,
11443
+ ts: Date.now(),
11444
+ type: "coordinator.stats",
11445
+ payload: { ...stats, subagentStatuses }
11446
+ });
11447
+ }
11307
11448
  getStatus() {
11308
11449
  return {
11309
11450
  coordinatorId: this.coordinatorId,
@@ -11478,7 +11619,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11478
11619
  subagent.currentTask = task.id;
11479
11620
  task.subagentId = subagentId;
11480
11621
  subagent.context.tasks.push(task);
11622
+ this.fleetBus?.emit({
11623
+ subagentId,
11624
+ taskId: task.id,
11625
+ ts: Date.now(),
11626
+ type: "subagent.running",
11627
+ payload: { subagentId, taskId: task.id }
11628
+ });
11481
11629
  this.emit("task.assigned", { task, subagentId });
11630
+ this.emitCoordinatorStats();
11482
11631
  const rawMaxIterations = subagent.config.maxIterations;
11483
11632
  const rawMaxToolCalls = subagent.config.maxToolCalls;
11484
11633
  const rawMaxTokens = subagent.config.maxTokens;
@@ -11622,13 +11771,34 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11622
11771
  if (subagent.abortController.signal.aborted) {
11623
11772
  subagent.abortController = new AbortController();
11624
11773
  }
11774
+ this.fleetBus?.emit({
11775
+ subagentId: result.subagentId,
11776
+ ts: Date.now(),
11777
+ type: "subagent.idle",
11778
+ payload: { subagentId: result.subagentId }
11779
+ });
11625
11780
  }
11626
11781
  this.terminating.delete(result.subagentId);
11627
11782
  this.emit("task.completed", {
11628
11783
  task: subagent?.context.tasks.find((t2) => t2.id === result.taskId) ?? { id: result.taskId },
11629
11784
  result
11630
11785
  });
11786
+ this.fleetBus?.emit({
11787
+ subagentId: result.subagentId,
11788
+ taskId: result.taskId,
11789
+ ts: Date.now(),
11790
+ type: "subagent.completed",
11791
+ payload: {
11792
+ subagentId: result.subagentId,
11793
+ taskId: result.taskId,
11794
+ status: result.status,
11795
+ iterations: result.iterations,
11796
+ toolCalls: result.toolCalls,
11797
+ durationMs: result.durationMs
11798
+ }
11799
+ });
11631
11800
  this.tryDispatchNext();
11801
+ this.emitCoordinatorStats();
11632
11802
  if (this.isDone()) {
11633
11803
  this.emit("done", {
11634
11804
  results: this.completedResults,
@@ -12335,18 +12505,17 @@ var FleetBus = class {
12335
12505
  byType = /* @__PURE__ */ new Map();
12336
12506
  any = /* @__PURE__ */ new Set();
12337
12507
  /**
12338
- * Hook a subagent's EventBus into the fleet. EventBus is strongly
12339
- * typed and doesn't expose an `onAny` hook, so we subscribe to the
12340
- * canonical set of event types a subagent emits during a run. New
12341
- * event types added to the kernel must be added here too — but the
12342
- * cost is a tiny single line per type, and the explicit list keeps
12343
- * the wire format clear.
12508
+ * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
12509
+ * `onPattern('*')`) to forward all events with subagent attribution, so
12510
+ * new kernel event types are automatically forwarded without any manual
12511
+ * registration. `subagent.*` events are excluded because they originate
12512
+ * from MultiAgentHost on the parent bus, not the subagent's own bus.
12344
12513
  *
12345
12514
  * Returns a disposer that detaches every subscription; call on
12346
12515
  * subagent teardown so the listeners don't outlive the run.
12347
12516
  */
12348
12517
  attach(subagentId, bus, taskId) {
12349
- const off = bus.onPattern("*", (type, payload) => {
12518
+ const off = bus.onAny((type, payload) => {
12350
12519
  if (type.startsWith("subagent.")) return;
12351
12520
  this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
12352
12521
  });
@@ -12468,7 +12637,7 @@ var FleetUsageAggregator = class {
12468
12637
  this.total.output += usage.output ?? 0;
12469
12638
  this.total.cacheRead += usage.cacheRead ?? 0;
12470
12639
  this.total.cacheWrite += usage.cacheWrite ?? 0;
12471
- const price = this.priceLookup?.(e.subagentId);
12640
+ const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
12472
12641
  if (price) {
12473
12642
  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);
12474
12643
  snap.cost += delta;
@@ -12491,8 +12660,9 @@ function makeSpawnTool(director, roster) {
12491
12660
  const inputSchema = {
12492
12661
  type: "object",
12493
12662
  properties: {
12494
- role: { type: "string", description: "Roster role id (preferred). When set, the spawn uses the matching config from the roster and ignores other fields." },
12495
- name: { type: "string", description: "Display name for the subagent. Required when not using roster." },
12663
+ role: { type: "string", description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields." },
12664
+ 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." },
12665
+ name: { type: "string", description: "Display name for the subagent. Used as a fallback when description-based dispatch does not resolve a role." },
12496
12666
  provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
12497
12667
  model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
12498
12668
  systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
@@ -12505,18 +12675,38 @@ function makeSpawnTool(director, roster) {
12505
12675
  return {
12506
12676
  name: "spawn_subagent",
12507
12677
  description: "Create a new subagent under this director. Returns the subagent id.",
12508
- usageHint: "Either pass `role` (matches the roster) OR pass `name` + optional `provider`/`model`. Returns `{ subagentId }`.",
12678
+ usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
12509
12679
  permission: "auto",
12510
12680
  mutating: false,
12511
12681
  inputSchema,
12512
12682
  async execute(input) {
12513
12683
  const i = input ?? {};
12514
12684
  const role = typeof i.role === "string" ? i.role : void 0;
12515
- const base = role && roster ? roster[role] : void 0;
12516
- if (role && !base) {
12517
- return { error: `unknown role "${role}". roster has: ${roster ? Object.keys(roster).join(", ") : "(empty)"}` };
12685
+ const description = typeof i.description === "string" ? i.description : void 0;
12686
+ let cfg;
12687
+ if (role && roster) {
12688
+ const base = roster[role];
12689
+ if (!base) return { error: `unknown role "${role}". roster has: ${Object.keys(roster).join(", ")}` };
12690
+ cfg = instantiateRosterConfig(role, base);
12691
+ } else if (description && !role) {
12692
+ const dispatchResult = await dispatchAgent(description, {
12693
+ classifier: director.dispatchClassifier,
12694
+ catalog: roster
12695
+ });
12696
+ const dispatchRole = dispatchResult.role;
12697
+ if (roster && roster[dispatchRole]) {
12698
+ cfg = instantiateRosterConfig(dispatchRole, roster[dispatchRole]);
12699
+ } else {
12700
+ const def = dispatchResult.definition;
12701
+ cfg = {
12702
+ name: def.config.name ?? dispatchRole,
12703
+ role: dispatchRole,
12704
+ provider: def.config.provider,
12705
+ model: def.config.model
12706
+ };
12707
+ }
12518
12708
  }
12519
- const cfg = base ? instantiateRosterConfig(role, base) : { name: i.name ?? "subagent" };
12709
+ cfg ??= { name: i.name ?? "subagent" };
12520
12710
  if (typeof i.name === "string") cfg.name = i.name;
12521
12711
  if (typeof i.provider === "string") cfg.provider = i.provider;
12522
12712
  if (typeof i.model === "string") cfg.model = i.model;
@@ -12526,7 +12716,7 @@ function makeSpawnTool(director, roster) {
12526
12716
  if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
12527
12717
  try {
12528
12718
  const subagentId = await director.spawn(cfg);
12529
- return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
12719
+ return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name, role: cfg.role };
12530
12720
  } catch (err) {
12531
12721
  if (err instanceof FleetSpawnBudgetError) {
12532
12722
  return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
@@ -12650,12 +12840,21 @@ function makeTerminateTool(director) {
12650
12840
  function makeFleetStatusTool(director) {
12651
12841
  return {
12652
12842
  name: "fleet_status",
12653
- description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts.",
12843
+ description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
12654
12844
  permission: "auto",
12655
12845
  mutating: false,
12656
12846
  inputSchema: { type: "object", properties: {}, required: [] },
12657
12847
  async execute() {
12658
- return director.status();
12848
+ const base = director.status();
12849
+ const fm = director.fleetManager;
12850
+ const stats = fm?.getFleetStats();
12851
+ const fleetStatus = fm?.getFleetStatus();
12852
+ return {
12853
+ subagents: base.subagents,
12854
+ coordinatorStats: stats ? { total: stats.total, running: stats.running, idle: stats.idle, stopped: stats.stopped } : void 0,
12855
+ pending: fleetStatus?.pending ?? [],
12856
+ usage: fm?.snapshot()
12857
+ };
12659
12858
  }
12660
12859
  };
12661
12860
  }
@@ -12857,6 +13056,8 @@ var Director = class {
12857
13056
  * default cap.
12858
13057
  */
12859
13058
  taskCompletedListener = null;
13059
+ /** Optional LLM classifier for smart dispatch. Passed from options. */
13060
+ dispatchClassifier;
12860
13061
  constructor(opts) {
12861
13062
  this.id = opts.config.coordinatorId || randomUUID();
12862
13063
  this.manifestPath = opts.manifestPath;
@@ -12869,6 +13070,7 @@ var Director = class {
12869
13070
  this.spawnDepth = opts.spawnDepth ?? 0;
12870
13071
  this.sessionWriter = opts.sessionWriter ?? null;
12871
13072
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
13073
+ this.dispatchClassifier = opts.dispatchClassifier;
12872
13074
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
12873
13075
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
12874
13076
  this.sessionsRoot = opts.sessionsRoot;
@@ -12898,7 +13100,10 @@ var Director = class {
12898
13100
  this.fleet = new FleetBus();
12899
13101
  this.usage = new FleetUsageAggregator(
12900
13102
  this.fleet,
12901
- (id) => this.priceLookups.get(id),
13103
+ (id, provider, model) => {
13104
+ if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
13105
+ return void 0;
13106
+ },
12902
13107
  (id) => this.subagentMeta.get(id)
12903
13108
  );
12904
13109
  }
@@ -12906,6 +13111,8 @@ var Director = class {
12906
13111
  { ...opts.config, coordinatorId: this.id },
12907
13112
  { runner: opts.runner }
12908
13113
  );
13114
+ this.coordinator.setFleetBus(this.fleet);
13115
+ this.fleetManager?.setCoordinator(this.coordinator);
12909
13116
  this.taskCompletedListener = (payload) => {
12910
13117
  const r = payload.result;
12911
13118
  this.completed.set(r.taskId, r);
@@ -12940,7 +13147,11 @@ var Director = class {
12940
13147
  title
12941
13148
  }
12942
13149
  );
12943
- this.scheduleManifest();
13150
+ if (this.fleetManager) {
13151
+ this.fleetManager.flushManifest();
13152
+ } else {
13153
+ this.scheduleManifest();
13154
+ }
12944
13155
  };
12945
13156
  this.coordinator.on("task.completed", this.taskCompletedListener);
12946
13157
  const extendCounts = /* @__PURE__ */ new Map();
@@ -13032,10 +13243,17 @@ var Director = class {
13032
13243
  }
13033
13244
  }
13034
13245
  /** Debounced manifest writer. A burst of spawn/assign/complete events
13035
- * collapses into one write. Set `manifestDebounceMs` to 0 to disable. */
13246
+ * collapses into one write. Set `manifestDebounceMs` to 0 to write
13247
+ * synchronously (no debounce); set to negative to disable entirely. */
13036
13248
  scheduleManifest() {
13037
- if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
13038
- if (this.manifestTimer) return;
13249
+ if (!this.manifestPath) return;
13250
+ if (this.manifestDebounceMs === 0) {
13251
+ void this.writeManifest().catch(
13252
+ (err) => this.logShutdownError("manifest_write_debounced", err)
13253
+ );
13254
+ return;
13255
+ }
13256
+ if (this.manifestDebounceMs < 0) return;
13039
13257
  this.manifestTimer = setTimeout(() => {
13040
13258
  this.manifestTimer = null;
13041
13259
  void this.writeManifest().catch(
@@ -13084,7 +13302,9 @@ var Director = class {
13084
13302
  provider: config.provider,
13085
13303
  model: config.model
13086
13304
  });
13087
- if (priceLookup) this.priceLookups.set(result.subagentId, priceLookup);
13305
+ if (priceLookup && config.provider && config.model) {
13306
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
13307
+ }
13088
13308
  }
13089
13309
  const subagentBridge = new InMemoryAgentBridge(
13090
13310
  { agentId: result.subagentId, coordinatorId: this.id },
@@ -20198,6 +20418,8 @@ var FleetManager = class {
20198
20418
  pendingTasks = /* @__PURE__ */ new Map();
20199
20419
  subagentMeta = /* @__PURE__ */ new Map();
20200
20420
  priceLookups = /* @__PURE__ */ new Map();
20421
+ /** The coordinator (wired via setCoordinator by Director after construction). */
20422
+ coordinator = null;
20201
20423
  constructor(opts = {}) {
20202
20424
  this.manifestPath = opts.manifestPath;
20203
20425
  this.sessionsRoot = opts.sessionsRoot;
@@ -20222,7 +20444,10 @@ var FleetManager = class {
20222
20444
  this.fleet = new FleetBus();
20223
20445
  this.usage = new FleetUsageAggregator(
20224
20446
  this.fleet,
20225
- (id) => this.priceLookups.get(id),
20447
+ (id, provider, model) => {
20448
+ if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
20449
+ return void 0;
20450
+ },
20226
20451
  (id) => this.subagentMeta.get(id)
20227
20452
  );
20228
20453
  }
@@ -20232,11 +20457,26 @@ var FleetManager = class {
20232
20457
  get fleetBus() {
20233
20458
  return this.fleet;
20234
20459
  }
20460
+ /**
20461
+ * Wire the coordinator after Director construction. The coordinator
20462
+ * is not available when FleetManager is constructed standalone.
20463
+ * Once set, `getFleetStats()` delegates to `coordinator.getStats()`.
20464
+ */
20465
+ setCoordinator(coordinator) {
20466
+ this.coordinator = coordinator;
20467
+ }
20235
20468
  snapshot() {
20236
20469
  return this.usage.snapshot();
20237
20470
  }
20238
20471
  getSubagentMeta(id) {
20239
- return this.subagentMeta.get(id);
20472
+ const meta = this.subagentMeta.get(id);
20473
+ const manifest = this.manifestEntries.get(id);
20474
+ if (!meta && !manifest) return void 0;
20475
+ return {
20476
+ provider: meta?.provider ?? manifest?.provider,
20477
+ model: meta?.model ?? manifest?.model,
20478
+ name: manifest?.name
20479
+ };
20240
20480
  }
20241
20481
  /**
20242
20482
  * Returns null if the spawn is allowed, or an object describing
@@ -20273,7 +20513,9 @@ var FleetManager = class {
20273
20513
  provider: config.provider,
20274
20514
  model: config.model
20275
20515
  });
20276
- if (priceLookup) this.priceLookups.set(subagentId, priceLookup);
20516
+ if (priceLookup && config.provider && config.model) {
20517
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
20518
+ }
20277
20519
  this.manifestEntries.set(subagentId, {
20278
20520
  subagentId,
20279
20521
  name: config.name,
@@ -20329,9 +20571,21 @@ var FleetManager = class {
20329
20571
  /**
20330
20572
  * Debounced manifest write. Call after any state mutation
20331
20573
  * (spawn, assign, complete) so a burst collapses into one write.
20574
+ * When `manifestDebounceMs` is 0, writes are synchronous (no debounce).
20332
20575
  */
20333
20576
  scheduleManifest() {
20334
- if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
20577
+ if (!this.manifestPath) return;
20578
+ if (this.manifestDebounceMs === 0) {
20579
+ void this.writeManifest().catch((err) => {
20580
+ const detail = err instanceof Error ? err.message : String(err);
20581
+ process.emitWarning(
20582
+ `FleetManager manifest write failed: ${detail}`,
20583
+ "FleetManagerWarning"
20584
+ );
20585
+ });
20586
+ return;
20587
+ }
20588
+ if (this.manifestDebounceMs < 0) return;
20335
20589
  if (this.manifestTimer) return;
20336
20590
  this.manifestTimer = setTimeout(() => {
20337
20591
  this.manifestTimer = null;
@@ -20344,6 +20598,24 @@ var FleetManager = class {
20344
20598
  });
20345
20599
  }, this.manifestDebounceMs);
20346
20600
  }
20601
+ /**
20602
+ * Bypass the debounce timer and write the manifest immediately.
20603
+ * Clears any pending debounce timer before writing.
20604
+ */
20605
+ async flushManifest() {
20606
+ if (!this.manifestPath) return;
20607
+ if (this.manifestTimer) {
20608
+ clearTimeout(this.manifestTimer);
20609
+ this.manifestTimer = null;
20610
+ }
20611
+ await this.writeManifest().catch((err) => {
20612
+ const detail = err instanceof Error ? err.message : String(err);
20613
+ process.emitWarning(
20614
+ `FleetManager manifest write failed: ${detail}`,
20615
+ "FleetManagerWarning"
20616
+ );
20617
+ });
20618
+ }
20347
20619
  /** Best-effort session event writer. Swallows failures. */
20348
20620
  async appendSessionEvent(event) {
20349
20621
  if (!this.sessionWriter) return;
@@ -20361,6 +20633,31 @@ var FleetManager = class {
20361
20633
  removePendingTask(taskId) {
20362
20634
  this.pendingTasks.delete(taskId);
20363
20635
  }
20636
+ getFleetStats() {
20637
+ if (!this.coordinator) {
20638
+ return {
20639
+ total: 0,
20640
+ running: 0,
20641
+ idle: 0,
20642
+ stopped: 0,
20643
+ inFlight: 0,
20644
+ pending: 0,
20645
+ completed: 0,
20646
+ subagentStatuses: []
20647
+ };
20648
+ }
20649
+ const stats = this.coordinator.getStats();
20650
+ const subagentStatuses = [];
20651
+ for (const [subagentId, s] of this.coordinator["subagents"]) {
20652
+ subagentStatuses.push({
20653
+ subagentId,
20654
+ taskId: s.currentTask ?? "",
20655
+ status: s.status,
20656
+ assigned: s.context.parentBridge !== null
20657
+ });
20658
+ }
20659
+ return { ...stats, subagentStatuses };
20660
+ }
20364
20661
  getFleetStatus() {
20365
20662
  const pending = Array.from(this.pendingTasks.entries()).map(([taskId, v]) => ({
20366
20663
  taskId,