@wrongstack/core 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) 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-Bzeueq2J.d.ts} +9 -10
  3. package/dist/coordination/index.d.ts +8 -8
  4. package/dist/coordination/index.js +393 -179
  5. package/dist/coordination/index.js.map +1 -1
  6. package/dist/defaults/index.d.ts +10 -10
  7. package/dist/defaults/index.js +190 -55
  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 +6 -6
  11. package/dist/execution/index.js +116 -31
  12. package/dist/execution/index.js.map +1 -1
  13. package/dist/extension/index.d.ts +2 -2
  14. package/dist/{index-BtNRyJft.d.ts → index-BciJeH8g.d.ts} +1 -1
  15. package/dist/index.d.ts +13 -13
  16. package/dist/index.js +294 -59
  17. package/dist/index.js.map +1 -1
  18. package/dist/infrastructure/index.d.ts +2 -2
  19. package/dist/kernel/index.d.ts +2 -2
  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-tlSWD4cE.d.ts} +11 -1
  24. package/dist/{null-fleet-bus-BZUrXVcd.d.ts → null-fleet-bus-DXEvZKHW.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/{plan-templates-C-IOLJ8Q.d.ts → plan-templates-BnlpEkX8.d.ts} +1 -1
  28. package/dist/{provider-runner-iST8U3ni.d.ts → provider-runner-BrA0XR-l.d.ts} +1 -1
  29. package/dist/sdd/index.d.ts +4 -4
  30. package/dist/sdd/index.js +114 -28
  31. package/dist/sdd/index.js.map +1 -1
  32. package/dist/storage/index.d.ts +2 -2
  33. package/dist/{tool-executor-CSwXjifK.d.ts → tool-executor-Cx2ndr0L.d.ts} +1 -1
  34. package/dist/types/index.d.ts +7 -7
  35. 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.
@@ -8239,29 +8262,35 @@ var SubagentBudget = class _SubagentBudget {
8239
8262
  * same kind are no-ops — without this dedup, every `recordIteration`
8240
8263
  * after the limit is reached spawns a fresh decision Promise (until
8241
8264
  * the first one lands and patches limits), flooding the FleetBus
8242
- * with redundant threshold events. Cleared in `checkLimitAsync`'s
8265
+ * with redundant threshold events. Cleared in `negotiateExtension`'s
8243
8266
  * `finally`.
8244
8267
  */
8245
8268
  pendingExtensions = /* @__PURE__ */ new Set();
8246
8269
  /**
8247
- * Hard cap on how long `checkLimitAsync` waits for the coordinator to
8270
+ * Hard cap on how long `negotiateExtension` waits for the coordinator to
8248
8271
  * respond before defaulting to 'stop'. Without this fallback an absent
8249
8272
  * 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.
8273
+ * leaves the budget over-limit and never enforces anything.
8254
8274
  */
8255
8275
  static DECISION_TIMEOUT_MS = 6e4;
8256
8276
  /**
8257
8277
  * Injected by the runner when wiring the budget to its EventBus.
8258
- * Used by `checkLimitAsync` to emit `budget.threshold_reached` events.
8278
+ * Used to emit `budget.threshold_reached` events in `'auto'` mode.
8259
8279
  */
8260
8280
  _events;
8281
+ /**
8282
+ * Negotiation mode — controls whether a threshold hit tries to emit
8283
+ * `budget.threshold_reached` and wait for a coordinator decision, or
8284
+ * falls straight through to a synchronous hard stop.
8285
+ *
8286
+ * `'auto'` (default) — emit on the EventBus and wait; times out to 'stop'.
8287
+ * `'sync'` — throw `BudgetExceededError` immediately regardless of listeners.
8288
+ */
8289
+ _mode;
8261
8290
  /**
8262
8291
  * 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.
8292
+ * invoke it rather than throw immediately. The handler decides whether to
8293
+ * throw synchronously, continue, or ask the coordinator for an extension.
8265
8294
  */
8266
8295
  get onThreshold() {
8267
8296
  return this._onThreshold;
@@ -8269,7 +8298,12 @@ var SubagentBudget = class _SubagentBudget {
8269
8298
  set onThreshold(fn) {
8270
8299
  this._onThreshold = fn;
8271
8300
  }
8272
- constructor(limits = {}) {
8301
+ /** Returns the current negotiation mode. */
8302
+ get mode() {
8303
+ return this._mode;
8304
+ }
8305
+ constructor(limits = {}, mode = "auto") {
8306
+ this._mode = mode;
8273
8307
  this.limits = { ...limits };
8274
8308
  }
8275
8309
  start() {
@@ -8285,16 +8319,23 @@ var SubagentBudget = class _SubagentBudget {
8285
8319
  return false;
8286
8320
  }
8287
8321
  /**
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.
8322
+ * Synchronous budget check. Always throws synchronously so callers (especially
8323
+ * test event handlers using `expect().toThrow()`) get an unhandled rejection
8324
+ * when the budget is exceeded without a handler.
8325
+ *
8326
+ * Decision table:
8327
+ * - no `onThreshold` handler → throw `BudgetExceededError` (hard stop, always)
8328
+ * - `mode === 'sync'` → throw `BudgetExceededError` (hard stop, always)
8329
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
8330
+ * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
8293
8331
  */
8294
8332
  checkLimit(kind, used, limit) {
8295
8333
  if (!this._onThreshold) {
8296
8334
  throw new BudgetExceededError(kind, limit, used);
8297
8335
  }
8336
+ if (this._mode === "sync") {
8337
+ throw new BudgetExceededError(kind, limit, used);
8338
+ }
8298
8339
  const bus = this._events;
8299
8340
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
8300
8341
  throw new BudgetExceededError(kind, limit, used);
@@ -8311,8 +8352,8 @@ var SubagentBudget = class _SubagentBudget {
8311
8352
  * `pendingExtensions` slot in `finally`.
8312
8353
  *
8313
8354
  * The 'continue' return from a sync handler is treated as
8314
- * `{ extend: {} }` — keep going without patching, next overrun will
8315
- * fire a fresh signal.
8355
+ * `{ extend: {} }` — keep going without patching; next overrun fires
8356
+ * a fresh signal.
8316
8357
  */
8317
8358
  async negotiateExtension(kind, used, limit) {
8318
8359
  try {
@@ -8320,11 +8361,6 @@ var SubagentBudget = class _SubagentBudget {
8320
8361
  kind,
8321
8362
  used,
8322
8363
  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
8364
  requestDecision: () => {
8329
8365
  const bus = this._events;
8330
8366
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -8408,12 +8444,12 @@ var SubagentBudget = class _SubagentBudget {
8408
8444
  }
8409
8445
  }
8410
8446
  /**
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.
8447
+ * Wall-clock budget check. Unlike other limits, timeout check passes through
8448
+ * `checkLimit` and is subject to the same negotiation-mode decision table.
8449
+ * In practice, `'sync'` mode (the usual test configuration) means a timeout
8450
+ * immediately throws `BudgetExceededError`. In production with a coordinator,
8451
+ * a timeout emits `budget.threshold_reached` so the Director can decide whether
8452
+ * to extend or abort.
8417
8453
  */
8418
8454
  checkTimeout() {
8419
8455
  if (this.startTime === null || this.limits.timeoutMs === void 0) return;
@@ -10794,8 +10830,7 @@ function scoreAgents(task, catalog = AGENT_CATALOG) {
10794
10830
  const hay = normalize2(task);
10795
10831
  const out = [];
10796
10832
  for (const def of Object.values(catalog)) {
10797
- const role = def.config.role;
10798
- if (!role) continue;
10833
+ if (!def?.config?.role) continue;
10799
10834
  let score = 0;
10800
10835
  const matched = [];
10801
10836
  for (const kw of def.capability.keywords) {
@@ -10807,7 +10842,7 @@ function scoreAgents(task, catalog = AGENT_CATALOG) {
10807
10842
  }
10808
10843
  }
10809
10844
  if (score > 0) {
10810
- out.push({ role, name: def.config.name, score, matched });
10845
+ out.push({ role: def.config.role, name: def.config.name, score, matched });
10811
10846
  }
10812
10847
  }
10813
10848
  out.sort((a, b) => b.score - a.score);
@@ -11178,6 +11213,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11178
11213
  coordinatorId;
11179
11214
  config;
11180
11215
  runner;
11216
+ fleetBus;
11181
11217
  subagents = /* @__PURE__ */ new Map();
11182
11218
  pendingTasks = [];
11183
11219
  completedResults = [];
@@ -11206,6 +11242,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11206
11242
  setRunner(runner) {
11207
11243
  this.runner = runner;
11208
11244
  }
11245
+ /**
11246
+ * Wire a FleetBus for director-mode event emission. Call after the
11247
+ * FleetManager is constructed so the coordinator can emit lifecycle
11248
+ * events the TUI and monitoring tools subscribe to.
11249
+ */
11250
+ setFleetBus(fleet) {
11251
+ this.fleetBus = fleet;
11252
+ }
11209
11253
  /**
11210
11254
  * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
11211
11255
  * preempt running tasks — already-dispatched subagents finish their
@@ -11241,6 +11285,18 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11241
11285
  abortController: new AbortController()
11242
11286
  });
11243
11287
  this.emit("subagent.started", { subagent: { ...subagent, id } });
11288
+ this.fleetBus?.emit({
11289
+ subagentId: id,
11290
+ ts: Date.now(),
11291
+ type: "subagent.assigned",
11292
+ payload: {
11293
+ subagentId: id,
11294
+ name: subagent.name,
11295
+ provider: subagent.provider,
11296
+ model: subagent.model
11297
+ }
11298
+ });
11299
+ this.emitCoordinatorStats();
11244
11300
  return { subagentId: id, agentId: id };
11245
11301
  }
11246
11302
  async assign(task) {
@@ -11273,6 +11329,13 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11273
11329
  subagent.currentTask = void 0;
11274
11330
  subagent.context.parentBridge = null;
11275
11331
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
11332
+ this.fleetBus?.emit({
11333
+ subagentId,
11334
+ ts: Date.now(),
11335
+ type: "subagent.stopped",
11336
+ payload: { subagentId, reason: "stopped by coordinator" }
11337
+ });
11338
+ this.emitCoordinatorStats();
11276
11339
  }
11277
11340
  async stopAll() {
11278
11341
  this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
@@ -11304,6 +11367,22 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11304
11367
  completed: this.completedResults.length
11305
11368
  };
11306
11369
  }
11370
+ /** Emit a reactive coordinator.stats event on FleetBus so the TUI can subscribe. */
11371
+ emitCoordinatorStats() {
11372
+ const stats = this.getStats();
11373
+ const subagentStatuses = Array.from(this.subagents.entries()).map(([id, s]) => ({
11374
+ subagentId: id,
11375
+ taskId: s.currentTask ?? "",
11376
+ status: s.status,
11377
+ assigned: s.context.parentBridge !== null
11378
+ }));
11379
+ this.fleetBus?.emit({
11380
+ subagentId: this.coordinatorId,
11381
+ ts: Date.now(),
11382
+ type: "coordinator.stats",
11383
+ payload: { ...stats, subagentStatuses }
11384
+ });
11385
+ }
11307
11386
  getStatus() {
11308
11387
  return {
11309
11388
  coordinatorId: this.coordinatorId,
@@ -11478,7 +11557,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11478
11557
  subagent.currentTask = task.id;
11479
11558
  task.subagentId = subagentId;
11480
11559
  subagent.context.tasks.push(task);
11560
+ this.fleetBus?.emit({
11561
+ subagentId,
11562
+ taskId: task.id,
11563
+ ts: Date.now(),
11564
+ type: "subagent.running",
11565
+ payload: { subagentId, taskId: task.id }
11566
+ });
11481
11567
  this.emit("task.assigned", { task, subagentId });
11568
+ this.emitCoordinatorStats();
11482
11569
  const rawMaxIterations = subagent.config.maxIterations;
11483
11570
  const rawMaxToolCalls = subagent.config.maxToolCalls;
11484
11571
  const rawMaxTokens = subagent.config.maxTokens;
@@ -11622,13 +11709,34 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11622
11709
  if (subagent.abortController.signal.aborted) {
11623
11710
  subagent.abortController = new AbortController();
11624
11711
  }
11712
+ this.fleetBus?.emit({
11713
+ subagentId: result.subagentId,
11714
+ ts: Date.now(),
11715
+ type: "subagent.idle",
11716
+ payload: { subagentId: result.subagentId }
11717
+ });
11625
11718
  }
11626
11719
  this.terminating.delete(result.subagentId);
11627
11720
  this.emit("task.completed", {
11628
11721
  task: subagent?.context.tasks.find((t2) => t2.id === result.taskId) ?? { id: result.taskId },
11629
11722
  result
11630
11723
  });
11724
+ this.fleetBus?.emit({
11725
+ subagentId: result.subagentId,
11726
+ taskId: result.taskId,
11727
+ ts: Date.now(),
11728
+ type: "subagent.completed",
11729
+ payload: {
11730
+ subagentId: result.subagentId,
11731
+ taskId: result.taskId,
11732
+ status: result.status,
11733
+ iterations: result.iterations,
11734
+ toolCalls: result.toolCalls,
11735
+ durationMs: result.durationMs
11736
+ }
11737
+ });
11631
11738
  this.tryDispatchNext();
11739
+ this.emitCoordinatorStats();
11632
11740
  if (this.isDone()) {
11633
11741
  this.emit("done", {
11634
11742
  results: this.completedResults,
@@ -12335,18 +12443,17 @@ var FleetBus = class {
12335
12443
  byType = /* @__PURE__ */ new Map();
12336
12444
  any = /* @__PURE__ */ new Set();
12337
12445
  /**
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.
12446
+ * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
12447
+ * `onPattern('*')`) to forward all events with subagent attribution, so
12448
+ * new kernel event types are automatically forwarded without any manual
12449
+ * registration. `subagent.*` events are excluded because they originate
12450
+ * from MultiAgentHost on the parent bus, not the subagent's own bus.
12344
12451
  *
12345
12452
  * Returns a disposer that detaches every subscription; call on
12346
12453
  * subagent teardown so the listeners don't outlive the run.
12347
12454
  */
12348
12455
  attach(subagentId, bus, taskId) {
12349
- const off = bus.onPattern("*", (type, payload) => {
12456
+ const off = bus.onAny((type, payload) => {
12350
12457
  if (type.startsWith("subagent.")) return;
12351
12458
  this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
12352
12459
  });
@@ -12468,7 +12575,7 @@ var FleetUsageAggregator = class {
12468
12575
  this.total.output += usage.output ?? 0;
12469
12576
  this.total.cacheRead += usage.cacheRead ?? 0;
12470
12577
  this.total.cacheWrite += usage.cacheWrite ?? 0;
12471
- const price = this.priceLookup?.(e.subagentId);
12578
+ const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
12472
12579
  if (price) {
12473
12580
  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
12581
  snap.cost += delta;
@@ -12491,8 +12598,9 @@ function makeSpawnTool(director, roster) {
12491
12598
  const inputSchema = {
12492
12599
  type: "object",
12493
12600
  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." },
12601
+ role: { type: "string", description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields." },
12602
+ 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." },
12603
+ name: { type: "string", description: "Display name for the subagent. Used as a fallback when description-based dispatch does not resolve a role." },
12496
12604
  provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
12497
12605
  model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
12498
12606
  systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
@@ -12505,18 +12613,38 @@ function makeSpawnTool(director, roster) {
12505
12613
  return {
12506
12614
  name: "spawn_subagent",
12507
12615
  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 }`.",
12616
+ usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
12509
12617
  permission: "auto",
12510
12618
  mutating: false,
12511
12619
  inputSchema,
12512
12620
  async execute(input) {
12513
12621
  const i = input ?? {};
12514
12622
  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)"}` };
12623
+ const description = typeof i.description === "string" ? i.description : void 0;
12624
+ let cfg;
12625
+ if (role && roster) {
12626
+ const base = roster[role];
12627
+ if (!base) return { error: `unknown role "${role}". roster has: ${Object.keys(roster).join(", ")}` };
12628
+ cfg = instantiateRosterConfig(role, base);
12629
+ } else if (description && !role) {
12630
+ const dispatchResult = await dispatchAgent(description, {
12631
+ classifier: director.dispatchClassifier,
12632
+ catalog: roster
12633
+ });
12634
+ const dispatchRole = dispatchResult.role;
12635
+ if (roster && roster[dispatchRole]) {
12636
+ cfg = instantiateRosterConfig(dispatchRole, roster[dispatchRole]);
12637
+ } else {
12638
+ const def = dispatchResult.definition;
12639
+ cfg = {
12640
+ name: def.config.name ?? dispatchRole,
12641
+ role: dispatchRole,
12642
+ provider: def.config.provider,
12643
+ model: def.config.model
12644
+ };
12645
+ }
12518
12646
  }
12519
- const cfg = base ? instantiateRosterConfig(role, base) : { name: i.name ?? "subagent" };
12647
+ cfg ??= { name: i.name ?? "subagent" };
12520
12648
  if (typeof i.name === "string") cfg.name = i.name;
12521
12649
  if (typeof i.provider === "string") cfg.provider = i.provider;
12522
12650
  if (typeof i.model === "string") cfg.model = i.model;
@@ -12526,7 +12654,7 @@ function makeSpawnTool(director, roster) {
12526
12654
  if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
12527
12655
  try {
12528
12656
  const subagentId = await director.spawn(cfg);
12529
- return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
12657
+ return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name, role: cfg.role };
12530
12658
  } catch (err) {
12531
12659
  if (err instanceof FleetSpawnBudgetError) {
12532
12660
  return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
@@ -12650,12 +12778,21 @@ function makeTerminateTool(director) {
12650
12778
  function makeFleetStatusTool(director) {
12651
12779
  return {
12652
12780
  name: "fleet_status",
12653
- description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts.",
12781
+ description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
12654
12782
  permission: "auto",
12655
12783
  mutating: false,
12656
12784
  inputSchema: { type: "object", properties: {}, required: [] },
12657
12785
  async execute() {
12658
- return director.status();
12786
+ const base = director.status();
12787
+ const fm = director.fleetManager;
12788
+ const stats = fm?.getFleetStats();
12789
+ const fleetStatus = fm?.getFleetStatus();
12790
+ return {
12791
+ subagents: base.subagents,
12792
+ coordinatorStats: stats ? { total: stats.total, running: stats.running, idle: stats.idle, stopped: stats.stopped } : void 0,
12793
+ pending: fleetStatus?.pending ?? [],
12794
+ usage: fm?.snapshot()
12795
+ };
12659
12796
  }
12660
12797
  };
12661
12798
  }
@@ -12857,6 +12994,8 @@ var Director = class {
12857
12994
  * default cap.
12858
12995
  */
12859
12996
  taskCompletedListener = null;
12997
+ /** Optional LLM classifier for smart dispatch. Passed from options. */
12998
+ dispatchClassifier;
12860
12999
  constructor(opts) {
12861
13000
  this.id = opts.config.coordinatorId || randomUUID();
12862
13001
  this.manifestPath = opts.manifestPath;
@@ -12869,6 +13008,7 @@ var Director = class {
12869
13008
  this.spawnDepth = opts.spawnDepth ?? 0;
12870
13009
  this.sessionWriter = opts.sessionWriter ?? null;
12871
13010
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
13011
+ this.dispatchClassifier = opts.dispatchClassifier;
12872
13012
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
12873
13013
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
12874
13014
  this.sessionsRoot = opts.sessionsRoot;
@@ -12898,7 +13038,10 @@ var Director = class {
12898
13038
  this.fleet = new FleetBus();
12899
13039
  this.usage = new FleetUsageAggregator(
12900
13040
  this.fleet,
12901
- (id) => this.priceLookups.get(id),
13041
+ (id, provider, model) => {
13042
+ if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
13043
+ return void 0;
13044
+ },
12902
13045
  (id) => this.subagentMeta.get(id)
12903
13046
  );
12904
13047
  }
@@ -12906,6 +13049,8 @@ var Director = class {
12906
13049
  { ...opts.config, coordinatorId: this.id },
12907
13050
  { runner: opts.runner }
12908
13051
  );
13052
+ this.coordinator.setFleetBus(this.fleet);
13053
+ this.fleetManager?.setCoordinator(this.coordinator);
12909
13054
  this.taskCompletedListener = (payload) => {
12910
13055
  const r = payload.result;
12911
13056
  this.completed.set(r.taskId, r);
@@ -12940,7 +13085,11 @@ var Director = class {
12940
13085
  title
12941
13086
  }
12942
13087
  );
12943
- this.scheduleManifest();
13088
+ if (this.fleetManager) {
13089
+ this.fleetManager.flushManifest();
13090
+ } else {
13091
+ this.scheduleManifest();
13092
+ }
12944
13093
  };
12945
13094
  this.coordinator.on("task.completed", this.taskCompletedListener);
12946
13095
  const extendCounts = /* @__PURE__ */ new Map();
@@ -13032,10 +13181,17 @@ var Director = class {
13032
13181
  }
13033
13182
  }
13034
13183
  /** Debounced manifest writer. A burst of spawn/assign/complete events
13035
- * collapses into one write. Set `manifestDebounceMs` to 0 to disable. */
13184
+ * collapses into one write. Set `manifestDebounceMs` to 0 to write
13185
+ * synchronously (no debounce); set to negative to disable entirely. */
13036
13186
  scheduleManifest() {
13037
- if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
13038
- if (this.manifestTimer) return;
13187
+ if (!this.manifestPath) return;
13188
+ if (this.manifestDebounceMs === 0) {
13189
+ void this.writeManifest().catch(
13190
+ (err) => this.logShutdownError("manifest_write_debounced", err)
13191
+ );
13192
+ return;
13193
+ }
13194
+ if (this.manifestDebounceMs < 0) return;
13039
13195
  this.manifestTimer = setTimeout(() => {
13040
13196
  this.manifestTimer = null;
13041
13197
  void this.writeManifest().catch(
@@ -13084,7 +13240,9 @@ var Director = class {
13084
13240
  provider: config.provider,
13085
13241
  model: config.model
13086
13242
  });
13087
- if (priceLookup) this.priceLookups.set(result.subagentId, priceLookup);
13243
+ if (priceLookup && config.provider && config.model) {
13244
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
13245
+ }
13088
13246
  }
13089
13247
  const subagentBridge = new InMemoryAgentBridge(
13090
13248
  { agentId: result.subagentId, coordinatorId: this.id },
@@ -20198,6 +20356,8 @@ var FleetManager = class {
20198
20356
  pendingTasks = /* @__PURE__ */ new Map();
20199
20357
  subagentMeta = /* @__PURE__ */ new Map();
20200
20358
  priceLookups = /* @__PURE__ */ new Map();
20359
+ /** The coordinator (wired via setCoordinator by Director after construction). */
20360
+ coordinator = null;
20201
20361
  constructor(opts = {}) {
20202
20362
  this.manifestPath = opts.manifestPath;
20203
20363
  this.sessionsRoot = opts.sessionsRoot;
@@ -20222,7 +20382,10 @@ var FleetManager = class {
20222
20382
  this.fleet = new FleetBus();
20223
20383
  this.usage = new FleetUsageAggregator(
20224
20384
  this.fleet,
20225
- (id) => this.priceLookups.get(id),
20385
+ (id, provider, model) => {
20386
+ if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
20387
+ return void 0;
20388
+ },
20226
20389
  (id) => this.subagentMeta.get(id)
20227
20390
  );
20228
20391
  }
@@ -20232,11 +20395,26 @@ var FleetManager = class {
20232
20395
  get fleetBus() {
20233
20396
  return this.fleet;
20234
20397
  }
20398
+ /**
20399
+ * Wire the coordinator after Director construction. The coordinator
20400
+ * is not available when FleetManager is constructed standalone.
20401
+ * Once set, `getFleetStats()` delegates to `coordinator.getStats()`.
20402
+ */
20403
+ setCoordinator(coordinator) {
20404
+ this.coordinator = coordinator;
20405
+ }
20235
20406
  snapshot() {
20236
20407
  return this.usage.snapshot();
20237
20408
  }
20238
20409
  getSubagentMeta(id) {
20239
- return this.subagentMeta.get(id);
20410
+ const meta = this.subagentMeta.get(id);
20411
+ const manifest = this.manifestEntries.get(id);
20412
+ if (!meta && !manifest) return void 0;
20413
+ return {
20414
+ provider: meta?.provider ?? manifest?.provider,
20415
+ model: meta?.model ?? manifest?.model,
20416
+ name: manifest?.name
20417
+ };
20240
20418
  }
20241
20419
  /**
20242
20420
  * Returns null if the spawn is allowed, or an object describing
@@ -20273,7 +20451,9 @@ var FleetManager = class {
20273
20451
  provider: config.provider,
20274
20452
  model: config.model
20275
20453
  });
20276
- if (priceLookup) this.priceLookups.set(subagentId, priceLookup);
20454
+ if (priceLookup && config.provider && config.model) {
20455
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
20456
+ }
20277
20457
  this.manifestEntries.set(subagentId, {
20278
20458
  subagentId,
20279
20459
  name: config.name,
@@ -20329,9 +20509,21 @@ var FleetManager = class {
20329
20509
  /**
20330
20510
  * Debounced manifest write. Call after any state mutation
20331
20511
  * (spawn, assign, complete) so a burst collapses into one write.
20512
+ * When `manifestDebounceMs` is 0, writes are synchronous (no debounce).
20332
20513
  */
20333
20514
  scheduleManifest() {
20334
- if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
20515
+ if (!this.manifestPath) return;
20516
+ if (this.manifestDebounceMs === 0) {
20517
+ void this.writeManifest().catch((err) => {
20518
+ const detail = err instanceof Error ? err.message : String(err);
20519
+ process.emitWarning(
20520
+ `FleetManager manifest write failed: ${detail}`,
20521
+ "FleetManagerWarning"
20522
+ );
20523
+ });
20524
+ return;
20525
+ }
20526
+ if (this.manifestDebounceMs < 0) return;
20335
20527
  if (this.manifestTimer) return;
20336
20528
  this.manifestTimer = setTimeout(() => {
20337
20529
  this.manifestTimer = null;
@@ -20344,6 +20536,24 @@ var FleetManager = class {
20344
20536
  });
20345
20537
  }, this.manifestDebounceMs);
20346
20538
  }
20539
+ /**
20540
+ * Bypass the debounce timer and write the manifest immediately.
20541
+ * Clears any pending debounce timer before writing.
20542
+ */
20543
+ async flushManifest() {
20544
+ if (!this.manifestPath) return;
20545
+ if (this.manifestTimer) {
20546
+ clearTimeout(this.manifestTimer);
20547
+ this.manifestTimer = null;
20548
+ }
20549
+ await this.writeManifest().catch((err) => {
20550
+ const detail = err instanceof Error ? err.message : String(err);
20551
+ process.emitWarning(
20552
+ `FleetManager manifest write failed: ${detail}`,
20553
+ "FleetManagerWarning"
20554
+ );
20555
+ });
20556
+ }
20347
20557
  /** Best-effort session event writer. Swallows failures. */
20348
20558
  async appendSessionEvent(event) {
20349
20559
  if (!this.sessionWriter) return;
@@ -20361,6 +20571,31 @@ var FleetManager = class {
20361
20571
  removePendingTask(taskId) {
20362
20572
  this.pendingTasks.delete(taskId);
20363
20573
  }
20574
+ getFleetStats() {
20575
+ if (!this.coordinator) {
20576
+ return {
20577
+ total: 0,
20578
+ running: 0,
20579
+ idle: 0,
20580
+ stopped: 0,
20581
+ inFlight: 0,
20582
+ pending: 0,
20583
+ completed: 0,
20584
+ subagentStatuses: []
20585
+ };
20586
+ }
20587
+ const stats = this.coordinator.getStats();
20588
+ const subagentStatuses = [];
20589
+ for (const [subagentId, s] of this.coordinator["subagents"]) {
20590
+ subagentStatuses.push({
20591
+ subagentId,
20592
+ taskId: s.currentTask ?? "",
20593
+ status: s.status,
20594
+ assigned: s.context.parentBridge !== null
20595
+ });
20596
+ }
20597
+ return { ...stats, subagentStatuses };
20598
+ }
20364
20599
  getFleetStatus() {
20365
20600
  const pending = Array.from(this.pendingTasks.entries()).map(([taskId, v]) => ({
20366
20601
  taskId,