@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
@@ -479,18 +479,17 @@ var FleetBus = class {
479
479
  byType = /* @__PURE__ */ new Map();
480
480
  any = /* @__PURE__ */ new Set();
481
481
  /**
482
- * Hook a subagent's EventBus into the fleet. EventBus is strongly
483
- * typed and doesn't expose an `onAny` hook, so we subscribe to the
484
- * canonical set of event types a subagent emits during a run. New
485
- * event types added to the kernel must be added here too — but the
486
- * cost is a tiny single line per type, and the explicit list keeps
487
- * the wire format clear.
482
+ * Hook a subagent's EventBus into the fleet. Uses `onAny()` (an alias for
483
+ * `onPattern('*')`) to forward all events with subagent attribution, so
484
+ * new kernel event types are automatically forwarded without any manual
485
+ * registration. `subagent.*` events are excluded because they originate
486
+ * from MultiAgentHost on the parent bus, not the subagent's own bus.
488
487
  *
489
488
  * Returns a disposer that detaches every subscription; call on
490
489
  * subagent teardown so the listeners don't outlive the run.
491
490
  */
492
491
  attach(subagentId, bus, taskId) {
493
- const off = bus.onPattern("*", (type, payload) => {
492
+ const off = bus.onAny((type, payload) => {
494
493
  if (type.startsWith("subagent.")) return;
495
494
  this.emit({ subagentId, taskId, ts: Date.now(), type, payload });
496
495
  });
@@ -612,7 +611,7 @@ var FleetUsageAggregator = class {
612
611
  this.total.output += usage.output ?? 0;
613
612
  this.total.cacheRead += usage.cacheRead ?? 0;
614
613
  this.total.cacheWrite += usage.cacheWrite ?? 0;
615
- const price = this.priceLookup?.(e.subagentId);
614
+ const price = this.priceLookup?.(e.subagentId, snap.provider, snap.model);
616
615
  if (price) {
617
616
  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);
618
617
  snap.cost += delta;
@@ -785,29 +784,35 @@ var SubagentBudget = class _SubagentBudget {
785
784
  * same kind are no-ops — without this dedup, every `recordIteration`
786
785
  * after the limit is reached spawns a fresh decision Promise (until
787
786
  * the first one lands and patches limits), flooding the FleetBus
788
- * with redundant threshold events. Cleared in `checkLimitAsync`'s
787
+ * with redundant threshold events. Cleared in `negotiateExtension`'s
789
788
  * `finally`.
790
789
  */
791
790
  pendingExtensions = /* @__PURE__ */ new Set();
792
791
  /**
793
- * Hard cap on how long `checkLimitAsync` waits for the coordinator to
792
+ * Hard cap on how long `negotiateExtension` waits for the coordinator to
794
793
  * respond before defaulting to 'stop'. Without this fallback an absent
795
794
  * or hung listener (Director not built / event filter detached mid-run)
796
- * leaves the budget over-limit and never enforces anything, since
797
- * `checkLimit` returns synchronously via `void this.checkLimitAsync`.
798
- * Raised from 30s to 60s to give subagents more headroom before
799
- * the threshold negotiation times out and triggers a hard stop.
795
+ * leaves the budget over-limit and never enforces anything.
800
796
  */
801
797
  static DECISION_TIMEOUT_MS = 6e4;
802
798
  /**
803
799
  * Injected by the runner when wiring the budget to its EventBus.
804
- * Used by `checkLimitAsync` to emit `budget.threshold_reached` events.
800
+ * Used to emit `budget.threshold_reached` events in `'auto'` mode.
805
801
  */
806
802
  _events;
803
+ /**
804
+ * Negotiation mode — controls whether a threshold hit tries to emit
805
+ * `budget.threshold_reached` and wait for a coordinator decision, or
806
+ * falls straight through to a synchronous hard stop.
807
+ *
808
+ * `'auto'` (default) — emit on the EventBus and wait; times out to 'stop'.
809
+ * `'sync'` — throw `BudgetExceededError` immediately regardless of listeners.
810
+ */
811
+ _mode;
807
812
  /**
808
813
  * Optional callback for soft-limit handling. When set, the budget will
809
- * call this instead of throwing when a limit is first reached. The
810
- * handler decides whether to throw, continue, or ask the coordinator.
814
+ * invoke it rather than throw immediately. The handler decides whether to
815
+ * throw synchronously, continue, or ask the coordinator for an extension.
811
816
  */
812
817
  get onThreshold() {
813
818
  return this._onThreshold;
@@ -815,7 +820,12 @@ var SubagentBudget = class _SubagentBudget {
815
820
  set onThreshold(fn) {
816
821
  this._onThreshold = fn;
817
822
  }
818
- constructor(limits = {}) {
823
+ /** Returns the current negotiation mode. */
824
+ get mode() {
825
+ return this._mode;
826
+ }
827
+ constructor(limits = {}, mode = "auto") {
828
+ this._mode = mode;
819
829
  this.limits = { ...limits };
820
830
  }
821
831
  start() {
@@ -831,16 +841,23 @@ var SubagentBudget = class _SubagentBudget {
831
841
  return false;
832
842
  }
833
843
  /**
834
- * Synchronous budget check always throws synchronously so callers
835
- * (especially test event handlers using `expect().toThrow()`) get an
836
- * unhandled rejection when the budget is exceeded without a handler.
837
- * When `onThreshold` IS configured, the actual async threshold-handling
838
- * is dispatched as a fire-and-forget promise.
844
+ * Synchronous budget check. Always throws synchronously so callers (especially
845
+ * test event handlers using `expect().toThrow()`) get an unhandled rejection
846
+ * when the budget is exceeded without a handler.
847
+ *
848
+ * Decision table:
849
+ * - no `onThreshold` handler → throw `BudgetExceededError` (hard stop, always)
850
+ * - `mode === 'sync'` → throw `BudgetExceededError` (hard stop, always)
851
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
852
+ * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
839
853
  */
840
854
  checkLimit(kind, used, limit) {
841
855
  if (!this._onThreshold) {
842
856
  throw new BudgetExceededError(kind, limit, used);
843
857
  }
858
+ if (this._mode === "sync") {
859
+ throw new BudgetExceededError(kind, limit, used);
860
+ }
844
861
  const bus = this._events;
845
862
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
846
863
  throw new BudgetExceededError(kind, limit, used);
@@ -857,8 +874,8 @@ var SubagentBudget = class _SubagentBudget {
857
874
  * `pendingExtensions` slot in `finally`.
858
875
  *
859
876
  * The 'continue' return from a sync handler is treated as
860
- * `{ extend: {} }` — keep going without patching, next overrun will
861
- * fire a fresh signal.
877
+ * `{ extend: {} }` — keep going without patching; next overrun fires
878
+ * a fresh signal.
862
879
  */
863
880
  async negotiateExtension(kind, used, limit) {
864
881
  try {
@@ -866,11 +883,6 @@ var SubagentBudget = class _SubagentBudget {
866
883
  kind,
867
884
  used,
868
885
  limit,
869
- // Inject a requestDecision helper the handler can call to emit the
870
- // budget.threshold_reached event and wait for the coordinator's verdict.
871
- // A hard fallback timer guarantees the promise eventually resolves
872
- // even if no listener responds — without it, an absent/detached
873
- // Director would leave the budget permanently in "asking" state.
874
886
  requestDecision: () => {
875
887
  const bus = this._events;
876
888
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -954,12 +966,12 @@ var SubagentBudget = class _SubagentBudget {
954
966
  }
955
967
  }
956
968
  /**
957
- * Wall-clock budget check. Unlike other limits, timeout is treated as a
958
- * warning-only event it NEVER hard-stops the subagent. When the
959
- * elapsed time exceeds timeoutMs, emits `budget.threshold_reached` with
960
- * kind='timeout' so the Director can decide whether to extend or warn.
961
- * Call this from the iteration loop so a hung tool gets a chance to
962
- * negotiate more time before the coordinator's Promise.race kills it.
969
+ * Wall-clock budget check. Unlike other limits, timeout check passes through
970
+ * `checkLimit` and is subject to the same negotiation-mode decision table.
971
+ * In practice, `'sync'` mode (the usual test configuration) means a timeout
972
+ * immediately throws `BudgetExceededError`. In production with a coordinator,
973
+ * a timeout emits `budget.threshold_reached` so the Director can decide whether
974
+ * to extend or abort.
963
975
  */
964
976
  checkTimeout() {
965
977
  if (this.startTime === null || this.limits.timeoutMs === void 0) return;
@@ -3433,6 +3445,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3433
3445
  coordinatorId;
3434
3446
  config;
3435
3447
  runner;
3448
+ fleetBus;
3436
3449
  subagents = /* @__PURE__ */ new Map();
3437
3450
  pendingTasks = [];
3438
3451
  completedResults = [];
@@ -3461,6 +3474,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3461
3474
  setRunner(runner) {
3462
3475
  this.runner = runner;
3463
3476
  }
3477
+ /**
3478
+ * Wire a FleetBus for director-mode event emission. Call after the
3479
+ * FleetManager is constructed so the coordinator can emit lifecycle
3480
+ * events the TUI and monitoring tools subscribe to.
3481
+ */
3482
+ setFleetBus(fleet) {
3483
+ this.fleetBus = fleet;
3484
+ }
3464
3485
  /**
3465
3486
  * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
3466
3487
  * preempt running tasks — already-dispatched subagents finish their
@@ -3496,6 +3517,18 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3496
3517
  abortController: new AbortController()
3497
3518
  });
3498
3519
  this.emit("subagent.started", { subagent: { ...subagent, id } });
3520
+ this.fleetBus?.emit({
3521
+ subagentId: id,
3522
+ ts: Date.now(),
3523
+ type: "subagent.assigned",
3524
+ payload: {
3525
+ subagentId: id,
3526
+ name: subagent.name,
3527
+ provider: subagent.provider,
3528
+ model: subagent.model
3529
+ }
3530
+ });
3531
+ this.emitCoordinatorStats();
3499
3532
  return { subagentId: id, agentId: id };
3500
3533
  }
3501
3534
  async assign(task) {
@@ -3528,6 +3561,13 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3528
3561
  subagent.currentTask = void 0;
3529
3562
  subagent.context.parentBridge = null;
3530
3563
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
3564
+ this.fleetBus?.emit({
3565
+ subagentId,
3566
+ ts: Date.now(),
3567
+ type: "subagent.stopped",
3568
+ payload: { subagentId, reason: "stopped by coordinator" }
3569
+ });
3570
+ this.emitCoordinatorStats();
3531
3571
  }
3532
3572
  async stopAll() {
3533
3573
  this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
@@ -3559,6 +3599,22 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3559
3599
  completed: this.completedResults.length
3560
3600
  };
3561
3601
  }
3602
+ /** Emit a reactive coordinator.stats event on FleetBus so the TUI can subscribe. */
3603
+ emitCoordinatorStats() {
3604
+ const stats = this.getStats();
3605
+ const subagentStatuses = Array.from(this.subagents.entries()).map(([id, s]) => ({
3606
+ subagentId: id,
3607
+ taskId: s.currentTask ?? "",
3608
+ status: s.status,
3609
+ assigned: s.context.parentBridge !== null
3610
+ }));
3611
+ this.fleetBus?.emit({
3612
+ subagentId: this.coordinatorId,
3613
+ ts: Date.now(),
3614
+ type: "coordinator.stats",
3615
+ payload: { ...stats, subagentStatuses }
3616
+ });
3617
+ }
3562
3618
  getStatus() {
3563
3619
  return {
3564
3620
  coordinatorId: this.coordinatorId,
@@ -3733,7 +3789,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3733
3789
  subagent.currentTask = task.id;
3734
3790
  task.subagentId = subagentId;
3735
3791
  subagent.context.tasks.push(task);
3792
+ this.fleetBus?.emit({
3793
+ subagentId,
3794
+ taskId: task.id,
3795
+ ts: Date.now(),
3796
+ type: "subagent.running",
3797
+ payload: { subagentId, taskId: task.id }
3798
+ });
3736
3799
  this.emit("task.assigned", { task, subagentId });
3800
+ this.emitCoordinatorStats();
3737
3801
  const rawMaxIterations = subagent.config.maxIterations;
3738
3802
  const rawMaxToolCalls = subagent.config.maxToolCalls;
3739
3803
  const rawMaxTokens = subagent.config.maxTokens;
@@ -3877,13 +3941,34 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3877
3941
  if (subagent.abortController.signal.aborted) {
3878
3942
  subagent.abortController = new AbortController();
3879
3943
  }
3944
+ this.fleetBus?.emit({
3945
+ subagentId: result.subagentId,
3946
+ ts: Date.now(),
3947
+ type: "subagent.idle",
3948
+ payload: { subagentId: result.subagentId }
3949
+ });
3880
3950
  }
3881
3951
  this.terminating.delete(result.subagentId);
3882
3952
  this.emit("task.completed", {
3883
3953
  task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
3884
3954
  result
3885
3955
  });
3956
+ this.fleetBus?.emit({
3957
+ subagentId: result.subagentId,
3958
+ taskId: result.taskId,
3959
+ ts: Date.now(),
3960
+ type: "subagent.completed",
3961
+ payload: {
3962
+ subagentId: result.subagentId,
3963
+ taskId: result.taskId,
3964
+ status: result.status,
3965
+ iterations: result.iterations,
3966
+ toolCalls: result.toolCalls,
3967
+ durationMs: result.durationMs
3968
+ }
3969
+ });
3886
3970
  this.tryDispatchNext();
3971
+ this.emitCoordinatorStats();
3887
3972
  if (this.isDone()) {
3888
3973
  this.emit("done", {
3889
3974
  results: this.completedResults,
@@ -3995,12 +4080,137 @@ function providerErrorToSubagentError(err, message, cause) {
3995
4080
  }
3996
4081
  return { kind: "unknown", message, retryable: err.retryable, cause };
3997
4082
  }
4083
+
4084
+ // src/coordination/dispatcher.ts
4085
+ var DEFAULT_DISPATCH_ROLE = "executor";
4086
+ function normalize(text) {
4087
+ return ` ${text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
4088
+ }
4089
+ function scoreAgents(task, catalog = AGENT_CATALOG) {
4090
+ const hay = normalize(task);
4091
+ const out = [];
4092
+ for (const def of Object.values(catalog)) {
4093
+ if (!def?.config?.role) continue;
4094
+ let score = 0;
4095
+ const matched = [];
4096
+ for (const kw of def.capability.keywords) {
4097
+ const needle = normalize(kw);
4098
+ if (hay.includes(needle.trimEnd() + " ") || hay.includes(" " + needle.trimStart())) {
4099
+ const words = kw.trim().split(/\s+/).length;
4100
+ score += words;
4101
+ matched.push(kw);
4102
+ }
4103
+ }
4104
+ if (score > 0) {
4105
+ out.push({ role: def.config.role, name: def.config.name, score, matched });
4106
+ }
4107
+ }
4108
+ out.sort((a, b) => b.score - a.score);
4109
+ return out;
4110
+ }
4111
+ function heuristicConfidence(candidates) {
4112
+ if (candidates.length === 0) return 0;
4113
+ const top = candidates[0].score;
4114
+ const second = candidates[1]?.score ?? 0;
4115
+ const strength = Math.min(1, top / 3);
4116
+ const margin = (top - second + 1) / (top + 1);
4117
+ return Math.min(1, strength * margin);
4118
+ }
4119
+ async function dispatchAgent(task, opts = {}) {
4120
+ const catalog = opts.catalog ?? AGENT_CATALOG;
4121
+ const threshold = opts.confidenceThreshold ?? 0.4;
4122
+ const maxCandidates = opts.maxCandidates ?? 6;
4123
+ const candidates = scoreAgents(task, catalog);
4124
+ const confidence = heuristicConfidence(candidates);
4125
+ const top = candidates[0];
4126
+ if (top && confidence >= threshold) {
4127
+ return {
4128
+ role: top.role,
4129
+ definition: catalog[top.role],
4130
+ confidence,
4131
+ method: "heuristic",
4132
+ reason: `Matched keywords: ${top.matched.slice(0, 4).join(", ")}`,
4133
+ alternatives: candidates.slice(1, maxCandidates)
4134
+ };
4135
+ }
4136
+ if (opts.classifier) {
4137
+ const pool = (candidates.length > 0 ? candidates.slice(0, maxCandidates).map((c) => catalog[c.role]) : ALL_AGENT_DEFINITIONS).map((d) => ({
4138
+ role: d.config.role,
4139
+ name: d.config.name,
4140
+ summary: d.capability.summary
4141
+ }));
4142
+ try {
4143
+ const choice = await opts.classifier(task, pool);
4144
+ if (choice && catalog[choice.role]) {
4145
+ return {
4146
+ role: choice.role,
4147
+ definition: catalog[choice.role],
4148
+ confidence: 1,
4149
+ method: "llm",
4150
+ reason: choice.reason ?? "Selected by LLM classifier",
4151
+ alternatives: candidates.slice(0, maxCandidates).filter((c) => c.role !== choice.role)
4152
+ };
4153
+ }
4154
+ } catch {
4155
+ }
4156
+ }
4157
+ if (top) {
4158
+ return {
4159
+ role: top.role,
4160
+ definition: catalog[top.role],
4161
+ confidence,
4162
+ method: "heuristic",
4163
+ reason: `Weak match (${top.matched.slice(0, 3).join(", ") || "low signal"})`,
4164
+ alternatives: candidates.slice(1, maxCandidates)
4165
+ };
4166
+ }
4167
+ const fallbackRole = catalog[DEFAULT_DISPATCH_ROLE] ? DEFAULT_DISPATCH_ROLE : Object.keys(catalog)[0];
4168
+ return {
4169
+ role: fallbackRole,
4170
+ definition: catalog[fallbackRole],
4171
+ confidence: 0,
4172
+ method: "fallback",
4173
+ reason: "No keyword signal; defaulting to the generalist Executor",
4174
+ alternatives: []
4175
+ };
4176
+ }
4177
+ function makeLLMClassifier(complete) {
4178
+ return async (task, candidates) => {
4179
+ const list = candidates.map((c, i) => `${i + 1}. ${c.role} \u2014 ${c.summary}`).join("\n");
4180
+ const prompt = `You are an agent router. Pick the single best agent for the task.
4181
+
4182
+ Task:
4183
+ ${task}
4184
+
4185
+ Agents:
4186
+ ${list}
4187
+
4188
+ Reply with ONLY a compact JSON object: {"role":"<one role id from the list>","reason":"<short why>"}.
4189
+ Do not add prose, markdown, or code fences.`;
4190
+ const raw = (await complete(prompt)).trim();
4191
+ const match = raw.match(/\{[\s\S]*\}/);
4192
+ if (!match) return null;
4193
+ try {
4194
+ const parsed = JSON.parse(match[0]);
4195
+ if (typeof parsed.role !== "string") return null;
4196
+ const role = parsed.role.trim();
4197
+ const valid = candidates.some((c) => c.role === role);
4198
+ if (!valid) return null;
4199
+ return { role, reason: typeof parsed.reason === "string" ? parsed.reason : void 0 };
4200
+ } catch {
4201
+ return null;
4202
+ }
4203
+ };
4204
+ }
4205
+
4206
+ // src/coordination/director-tools.ts
3998
4207
  function makeSpawnTool(director, roster) {
3999
4208
  const inputSchema = {
4000
4209
  type: "object",
4001
4210
  properties: {
4002
- role: { type: "string", description: "Roster role id (preferred). When set, the spawn uses the matching config from the roster and ignores other fields." },
4003
- name: { type: "string", description: "Display name for the subagent. Required when not using roster." },
4211
+ role: { type: "string", description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields." },
4212
+ 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." },
4213
+ name: { type: "string", description: "Display name for the subagent. Used as a fallback when description-based dispatch does not resolve a role." },
4004
4214
  provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
4005
4215
  model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
4006
4216
  systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
@@ -4013,18 +4223,38 @@ function makeSpawnTool(director, roster) {
4013
4223
  return {
4014
4224
  name: "spawn_subagent",
4015
4225
  description: "Create a new subagent under this director. Returns the subagent id.",
4016
- usageHint: "Either pass `role` (matches the roster) OR pass `name` + optional `provider`/`model`. Returns `{ subagentId }`.",
4226
+ usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`.",
4017
4227
  permission: "auto",
4018
4228
  mutating: false,
4019
4229
  inputSchema,
4020
4230
  async execute(input) {
4021
4231
  const i = input ?? {};
4022
4232
  const role = typeof i.role === "string" ? i.role : void 0;
4023
- const base = role && roster ? roster[role] : void 0;
4024
- if (role && !base) {
4025
- return { error: `unknown role "${role}". roster has: ${roster ? Object.keys(roster).join(", ") : "(empty)"}` };
4233
+ const description = typeof i.description === "string" ? i.description : void 0;
4234
+ let cfg;
4235
+ if (role && roster) {
4236
+ const base = roster[role];
4237
+ if (!base) return { error: `unknown role "${role}". roster has: ${Object.keys(roster).join(", ")}` };
4238
+ cfg = instantiateRosterConfig(role, base);
4239
+ } else if (description && !role) {
4240
+ const dispatchResult = await dispatchAgent(description, {
4241
+ classifier: director.dispatchClassifier,
4242
+ catalog: roster
4243
+ });
4244
+ const dispatchRole = dispatchResult.role;
4245
+ if (roster && roster[dispatchRole]) {
4246
+ cfg = instantiateRosterConfig(dispatchRole, roster[dispatchRole]);
4247
+ } else {
4248
+ const def = dispatchResult.definition;
4249
+ cfg = {
4250
+ name: def.config.name ?? dispatchRole,
4251
+ role: dispatchRole,
4252
+ provider: def.config.provider,
4253
+ model: def.config.model
4254
+ };
4255
+ }
4026
4256
  }
4027
- const cfg = base ? instantiateRosterConfig(role, base) : { name: i.name ?? "subagent" };
4257
+ cfg ??= { name: i.name ?? "subagent" };
4028
4258
  if (typeof i.name === "string") cfg.name = i.name;
4029
4259
  if (typeof i.provider === "string") cfg.provider = i.provider;
4030
4260
  if (typeof i.model === "string") cfg.model = i.model;
@@ -4034,7 +4264,7 @@ function makeSpawnTool(director, roster) {
4034
4264
  if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
4035
4265
  try {
4036
4266
  const subagentId = await director.spawn(cfg);
4037
- return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
4267
+ return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name, role: cfg.role };
4038
4268
  } catch (err) {
4039
4269
  if (err instanceof FleetSpawnBudgetError) {
4040
4270
  return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
@@ -4158,12 +4388,21 @@ function makeTerminateTool(director) {
4158
4388
  function makeFleetStatusTool(director) {
4159
4389
  return {
4160
4390
  name: "fleet_status",
4161
- description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts.",
4391
+ description: "Snapshot of the fleet \u2014 every subagent's current status, coordinator counts (total/running/idle/stopped), pending task descriptions, and usage rollup.",
4162
4392
  permission: "auto",
4163
4393
  mutating: false,
4164
4394
  inputSchema: { type: "object", properties: {}, required: [] },
4165
4395
  async execute() {
4166
- return director.status();
4396
+ const base = director.status();
4397
+ const fm = director.fleetManager;
4398
+ const stats = fm?.getFleetStats();
4399
+ const fleetStatus = fm?.getFleetStatus();
4400
+ return {
4401
+ subagents: base.subagents,
4402
+ coordinatorStats: stats ? { total: stats.total, running: stats.running, idle: stats.idle, stopped: stats.stopped } : void 0,
4403
+ pending: fleetStatus?.pending ?? [],
4404
+ usage: fm?.snapshot()
4405
+ };
4167
4406
  }
4168
4407
  };
4169
4408
  }
@@ -4365,6 +4604,8 @@ var Director = class {
4365
4604
  * default cap.
4366
4605
  */
4367
4606
  taskCompletedListener = null;
4607
+ /** Optional LLM classifier for smart dispatch. Passed from options. */
4608
+ dispatchClassifier;
4368
4609
  constructor(opts) {
4369
4610
  this.id = opts.config.coordinatorId || randomUUID();
4370
4611
  this.manifestPath = opts.manifestPath;
@@ -4377,6 +4618,7 @@ var Director = class {
4377
4618
  this.spawnDepth = opts.spawnDepth ?? 0;
4378
4619
  this.sessionWriter = opts.sessionWriter ?? null;
4379
4620
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
4621
+ this.dispatchClassifier = opts.dispatchClassifier;
4380
4622
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
4381
4623
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
4382
4624
  this.sessionsRoot = opts.sessionsRoot;
@@ -4406,7 +4648,10 @@ var Director = class {
4406
4648
  this.fleet = new FleetBus();
4407
4649
  this.usage = new FleetUsageAggregator(
4408
4650
  this.fleet,
4409
- (id) => this.priceLookups.get(id),
4651
+ (id, provider, model) => {
4652
+ if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
4653
+ return void 0;
4654
+ },
4410
4655
  (id) => this.subagentMeta.get(id)
4411
4656
  );
4412
4657
  }
@@ -4414,6 +4659,8 @@ var Director = class {
4414
4659
  { ...opts.config, coordinatorId: this.id },
4415
4660
  { runner: opts.runner }
4416
4661
  );
4662
+ this.coordinator.setFleetBus(this.fleet);
4663
+ this.fleetManager?.setCoordinator(this.coordinator);
4417
4664
  this.taskCompletedListener = (payload) => {
4418
4665
  const r = payload.result;
4419
4666
  this.completed.set(r.taskId, r);
@@ -4448,7 +4695,11 @@ var Director = class {
4448
4695
  title
4449
4696
  }
4450
4697
  );
4451
- this.scheduleManifest();
4698
+ if (this.fleetManager) {
4699
+ this.fleetManager.flushManifest();
4700
+ } else {
4701
+ this.scheduleManifest();
4702
+ }
4452
4703
  };
4453
4704
  this.coordinator.on("task.completed", this.taskCompletedListener);
4454
4705
  const extendCounts = /* @__PURE__ */ new Map();
@@ -4540,10 +4791,17 @@ var Director = class {
4540
4791
  }
4541
4792
  }
4542
4793
  /** Debounced manifest writer. A burst of spawn/assign/complete events
4543
- * collapses into one write. Set `manifestDebounceMs` to 0 to disable. */
4794
+ * collapses into one write. Set `manifestDebounceMs` to 0 to write
4795
+ * synchronously (no debounce); set to negative to disable entirely. */
4544
4796
  scheduleManifest() {
4545
- if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
4546
- if (this.manifestTimer) return;
4797
+ if (!this.manifestPath) return;
4798
+ if (this.manifestDebounceMs === 0) {
4799
+ void this.writeManifest().catch(
4800
+ (err) => this.logShutdownError("manifest_write_debounced", err)
4801
+ );
4802
+ return;
4803
+ }
4804
+ if (this.manifestDebounceMs < 0) return;
4547
4805
  this.manifestTimer = setTimeout(() => {
4548
4806
  this.manifestTimer = null;
4549
4807
  void this.writeManifest().catch(
@@ -4592,7 +4850,9 @@ var Director = class {
4592
4850
  provider: config.provider,
4593
4851
  model: config.model
4594
4852
  });
4595
- if (priceLookup) this.priceLookups.set(result.subagentId, priceLookup);
4853
+ if (priceLookup && config.provider && config.model) {
4854
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
4855
+ }
4596
4856
  }
4597
4857
  const subagentBridge = new InMemoryAgentBridge(
4598
4858
  { agentId: result.subagentId, coordinatorId: this.id },
@@ -6110,129 +6370,6 @@ function makeDirectorSessionFactory(opts) {
6110
6370
  };
6111
6371
  }
6112
6372
 
6113
- // src/coordination/dispatcher.ts
6114
- var DEFAULT_DISPATCH_ROLE = "executor";
6115
- function normalize(text) {
6116
- return ` ${text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
6117
- }
6118
- function scoreAgents(task, catalog = AGENT_CATALOG) {
6119
- const hay = normalize(task);
6120
- const out = [];
6121
- for (const def of Object.values(catalog)) {
6122
- const role = def.config.role;
6123
- if (!role) continue;
6124
- let score = 0;
6125
- const matched = [];
6126
- for (const kw of def.capability.keywords) {
6127
- const needle = normalize(kw);
6128
- if (hay.includes(needle.trimEnd() + " ") || hay.includes(" " + needle.trimStart())) {
6129
- const words = kw.trim().split(/\s+/).length;
6130
- score += words;
6131
- matched.push(kw);
6132
- }
6133
- }
6134
- if (score > 0) {
6135
- out.push({ role, name: def.config.name, score, matched });
6136
- }
6137
- }
6138
- out.sort((a, b) => b.score - a.score);
6139
- return out;
6140
- }
6141
- function heuristicConfidence(candidates) {
6142
- if (candidates.length === 0) return 0;
6143
- const top = candidates[0].score;
6144
- const second = candidates[1]?.score ?? 0;
6145
- const strength = Math.min(1, top / 3);
6146
- const margin = (top - second + 1) / (top + 1);
6147
- return Math.min(1, strength * margin);
6148
- }
6149
- async function dispatchAgent(task, opts = {}) {
6150
- const catalog = opts.catalog ?? AGENT_CATALOG;
6151
- const threshold = opts.confidenceThreshold ?? 0.4;
6152
- const maxCandidates = opts.maxCandidates ?? 6;
6153
- const candidates = scoreAgents(task, catalog);
6154
- const confidence = heuristicConfidence(candidates);
6155
- const top = candidates[0];
6156
- if (top && confidence >= threshold) {
6157
- return {
6158
- role: top.role,
6159
- definition: catalog[top.role],
6160
- confidence,
6161
- method: "heuristic",
6162
- reason: `Matched keywords: ${top.matched.slice(0, 4).join(", ")}`,
6163
- alternatives: candidates.slice(1, maxCandidates)
6164
- };
6165
- }
6166
- if (opts.classifier) {
6167
- const pool = (candidates.length > 0 ? candidates.slice(0, maxCandidates).map((c) => catalog[c.role]) : ALL_AGENT_DEFINITIONS).map((d) => ({
6168
- role: d.config.role,
6169
- name: d.config.name,
6170
- summary: d.capability.summary
6171
- }));
6172
- try {
6173
- const choice = await opts.classifier(task, pool);
6174
- if (choice && catalog[choice.role]) {
6175
- return {
6176
- role: choice.role,
6177
- definition: catalog[choice.role],
6178
- confidence: 1,
6179
- method: "llm",
6180
- reason: choice.reason ?? "Selected by LLM classifier",
6181
- alternatives: candidates.slice(0, maxCandidates).filter((c) => c.role !== choice.role)
6182
- };
6183
- }
6184
- } catch {
6185
- }
6186
- }
6187
- if (top) {
6188
- return {
6189
- role: top.role,
6190
- definition: catalog[top.role],
6191
- confidence,
6192
- method: "heuristic",
6193
- reason: `Weak match (${top.matched.slice(0, 3).join(", ") || "low signal"})`,
6194
- alternatives: candidates.slice(1, maxCandidates)
6195
- };
6196
- }
6197
- const fallbackRole = catalog[DEFAULT_DISPATCH_ROLE] ? DEFAULT_DISPATCH_ROLE : Object.keys(catalog)[0];
6198
- return {
6199
- role: fallbackRole,
6200
- definition: catalog[fallbackRole],
6201
- confidence: 0,
6202
- method: "fallback",
6203
- reason: "No keyword signal; defaulting to the generalist Executor",
6204
- alternatives: []
6205
- };
6206
- }
6207
- function makeLLMClassifier(complete) {
6208
- return async (task, candidates) => {
6209
- const list = candidates.map((c, i) => `${i + 1}. ${c.role} \u2014 ${c.summary}`).join("\n");
6210
- const prompt = `You are an agent router. Pick the single best agent for the task.
6211
-
6212
- Task:
6213
- ${task}
6214
-
6215
- Agents:
6216
- ${list}
6217
-
6218
- Reply with ONLY a compact JSON object: {"role":"<one role id from the list>","reason":"<short why>"}.
6219
- Do not add prose, markdown, or code fences.`;
6220
- const raw = (await complete(prompt)).trim();
6221
- const match = raw.match(/\{[\s\S]*\}/);
6222
- if (!match) return null;
6223
- try {
6224
- const parsed = JSON.parse(match[0]);
6225
- if (typeof parsed.role !== "string") return null;
6226
- const role = parsed.role.trim();
6227
- const valid = candidates.some((c) => c.role === role);
6228
- if (!valid) return null;
6229
- return { role, reason: typeof parsed.reason === "string" ? parsed.reason : void 0 };
6230
- } catch {
6231
- return null;
6232
- }
6233
- };
6234
- }
6235
-
6236
6373
  // src/coordination/auto-extend.ts
6237
6374
  var DEFAULT_CEILING = {
6238
6375
  maxIterations: 5e4,
@@ -6323,6 +6460,8 @@ var FleetManager = class {
6323
6460
  pendingTasks = /* @__PURE__ */ new Map();
6324
6461
  subagentMeta = /* @__PURE__ */ new Map();
6325
6462
  priceLookups = /* @__PURE__ */ new Map();
6463
+ /** The coordinator (wired via setCoordinator by Director after construction). */
6464
+ coordinator = null;
6326
6465
  constructor(opts = {}) {
6327
6466
  this.manifestPath = opts.manifestPath;
6328
6467
  this.sessionsRoot = opts.sessionsRoot;
@@ -6347,7 +6486,10 @@ var FleetManager = class {
6347
6486
  this.fleet = new FleetBus();
6348
6487
  this.usage = new FleetUsageAggregator(
6349
6488
  this.fleet,
6350
- (id) => this.priceLookups.get(id),
6489
+ (id, provider, model) => {
6490
+ if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
6491
+ return void 0;
6492
+ },
6351
6493
  (id) => this.subagentMeta.get(id)
6352
6494
  );
6353
6495
  }
@@ -6357,11 +6499,26 @@ var FleetManager = class {
6357
6499
  get fleetBus() {
6358
6500
  return this.fleet;
6359
6501
  }
6502
+ /**
6503
+ * Wire the coordinator after Director construction. The coordinator
6504
+ * is not available when FleetManager is constructed standalone.
6505
+ * Once set, `getFleetStats()` delegates to `coordinator.getStats()`.
6506
+ */
6507
+ setCoordinator(coordinator) {
6508
+ this.coordinator = coordinator;
6509
+ }
6360
6510
  snapshot() {
6361
6511
  return this.usage.snapshot();
6362
6512
  }
6363
6513
  getSubagentMeta(id) {
6364
- return this.subagentMeta.get(id);
6514
+ const meta = this.subagentMeta.get(id);
6515
+ const manifest = this.manifestEntries.get(id);
6516
+ if (!meta && !manifest) return void 0;
6517
+ return {
6518
+ provider: meta?.provider ?? manifest?.provider,
6519
+ model: meta?.model ?? manifest?.model,
6520
+ name: manifest?.name
6521
+ };
6365
6522
  }
6366
6523
  /**
6367
6524
  * Returns null if the spawn is allowed, or an object describing
@@ -6398,7 +6555,9 @@ var FleetManager = class {
6398
6555
  provider: config.provider,
6399
6556
  model: config.model
6400
6557
  });
6401
- if (priceLookup) this.priceLookups.set(subagentId, priceLookup);
6558
+ if (priceLookup && config.provider && config.model) {
6559
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
6560
+ }
6402
6561
  this.manifestEntries.set(subagentId, {
6403
6562
  subagentId,
6404
6563
  name: config.name,
@@ -6454,9 +6613,21 @@ var FleetManager = class {
6454
6613
  /**
6455
6614
  * Debounced manifest write. Call after any state mutation
6456
6615
  * (spawn, assign, complete) so a burst collapses into one write.
6616
+ * When `manifestDebounceMs` is 0, writes are synchronous (no debounce).
6457
6617
  */
6458
6618
  scheduleManifest() {
6459
- if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
6619
+ if (!this.manifestPath) return;
6620
+ if (this.manifestDebounceMs === 0) {
6621
+ void this.writeManifest().catch((err) => {
6622
+ const detail = err instanceof Error ? err.message : String(err);
6623
+ process.emitWarning(
6624
+ `FleetManager manifest write failed: ${detail}`,
6625
+ "FleetManagerWarning"
6626
+ );
6627
+ });
6628
+ return;
6629
+ }
6630
+ if (this.manifestDebounceMs < 0) return;
6460
6631
  if (this.manifestTimer) return;
6461
6632
  this.manifestTimer = setTimeout(() => {
6462
6633
  this.manifestTimer = null;
@@ -6469,6 +6640,24 @@ var FleetManager = class {
6469
6640
  });
6470
6641
  }, this.manifestDebounceMs);
6471
6642
  }
6643
+ /**
6644
+ * Bypass the debounce timer and write the manifest immediately.
6645
+ * Clears any pending debounce timer before writing.
6646
+ */
6647
+ async flushManifest() {
6648
+ if (!this.manifestPath) return;
6649
+ if (this.manifestTimer) {
6650
+ clearTimeout(this.manifestTimer);
6651
+ this.manifestTimer = null;
6652
+ }
6653
+ await this.writeManifest().catch((err) => {
6654
+ const detail = err instanceof Error ? err.message : String(err);
6655
+ process.emitWarning(
6656
+ `FleetManager manifest write failed: ${detail}`,
6657
+ "FleetManagerWarning"
6658
+ );
6659
+ });
6660
+ }
6472
6661
  /** Best-effort session event writer. Swallows failures. */
6473
6662
  async appendSessionEvent(event) {
6474
6663
  if (!this.sessionWriter) return;
@@ -6486,6 +6675,31 @@ var FleetManager = class {
6486
6675
  removePendingTask(taskId) {
6487
6676
  this.pendingTasks.delete(taskId);
6488
6677
  }
6678
+ getFleetStats() {
6679
+ if (!this.coordinator) {
6680
+ return {
6681
+ total: 0,
6682
+ running: 0,
6683
+ idle: 0,
6684
+ stopped: 0,
6685
+ inFlight: 0,
6686
+ pending: 0,
6687
+ completed: 0,
6688
+ subagentStatuses: []
6689
+ };
6690
+ }
6691
+ const stats = this.coordinator.getStats();
6692
+ const subagentStatuses = [];
6693
+ for (const [subagentId, s] of this.coordinator["subagents"]) {
6694
+ subagentStatuses.push({
6695
+ subagentId,
6696
+ taskId: s.currentTask ?? "",
6697
+ status: s.status,
6698
+ assigned: s.context.parentBridge !== null
6699
+ });
6700
+ }
6701
+ return { ...stats, subagentStatuses };
6702
+ }
6489
6703
  getFleetStatus() {
6490
6704
  const pending = Array.from(this.pendingTasks.entries()).map(([taskId, v]) => ({
6491
6705
  taskId,