@wrongstack/core 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{agent-bridge-6KPqsFx6.d.ts → agent-bridge-DmBiCipY.d.ts} +1 -1
  2. package/dist/{compactor-B4mQZXf2.d.ts → compactor-DSl2FK7a.d.ts} +1 -1
  3. package/dist/{config-BU9f_5yH.d.ts → config-DXrqb41m.d.ts} +1 -1
  4. package/dist/{context-BmM2xGUZ.d.ts → context-u0bryklF.d.ts} +8 -0
  5. package/dist/coordination/index.d.ts +210 -12
  6. package/dist/coordination/index.js +941 -67
  7. package/dist/coordination/index.js.map +1 -1
  8. package/dist/defaults/index.d.ts +18 -18
  9. package/dist/defaults/index.js +953 -41
  10. package/dist/defaults/index.js.map +1 -1
  11. package/dist/{events-BMNaEFZl.d.ts → events-B6Q03pTu.d.ts} +73 -1
  12. package/dist/execution/index.d.ts +11 -11
  13. package/dist/index.d.ts +61 -28
  14. package/dist/index.js +1077 -48
  15. package/dist/index.js.map +1 -1
  16. package/dist/infrastructure/index.d.ts +6 -6
  17. package/dist/kernel/index.d.ts +9 -9
  18. package/dist/kernel/index.js.map +1 -1
  19. package/dist/{mcp-servers-Dzgg4x1w.d.ts → mcp-servers-BA1Ofmfj.d.ts} +3 -3
  20. package/dist/models/index.d.ts +2 -2
  21. package/dist/{multi-agent-fmkRHtof.d.ts → multi-agent-BDfkxL5C.d.ts} +71 -3
  22. package/dist/observability/index.d.ts +2 -2
  23. package/dist/{path-resolver-DBjaoXFq.d.ts → path-resolver-Crkt8wTQ.d.ts} +2 -2
  24. package/dist/{plugin-DJk6LL8B.d.ts → plugin-CoYYZKdn.d.ts} +19 -6
  25. package/dist/{renderer-rk_1Swwc.d.ts → renderer-0A2ZEtca.d.ts} +1 -1
  26. package/dist/sdd/index.d.ts +3 -3
  27. package/dist/{secret-scrubber-CicHLN4G.d.ts → secret-scrubber-3TLUkiCV.d.ts} +1 -1
  28. package/dist/{secret-scrubber-DF88luOe.d.ts → secret-scrubber-CwYliRWd.d.ts} +1 -1
  29. package/dist/security/index.d.ts +20 -4
  30. package/dist/security/index.js +13 -1
  31. package/dist/security/index.js.map +1 -1
  32. package/dist/{selector-BbJqiEP4.d.ts → selector-BRqzvugb.d.ts} +1 -1
  33. package/dist/{session-reader-Drq8RvJu.d.ts → session-reader-C3x96CDR.d.ts} +1 -1
  34. package/dist/{skill-DhfSizKv.d.ts → skill-Bx8jxznf.d.ts} +1 -1
  35. package/dist/storage/index.d.ts +164 -6
  36. package/dist/storage/index.js +273 -1
  37. package/dist/storage/index.js.map +1 -1
  38. package/dist/{system-prompt-BC_8ypCG.d.ts → system-prompt-CG9jU5-5.d.ts} +9 -1
  39. package/dist/{tool-executor-CpuJPYm9.d.ts → tool-executor-CYdZdtno.d.ts} +4 -4
  40. package/dist/types/index.d.ts +15 -15
  41. package/dist/utils/index.d.ts +1 -1
  42. package/package.json +1 -1
@@ -406,6 +406,12 @@ var FileSessionWriter = class {
406
406
  tokenIn = 0;
407
407
  tokenOut = 0;
408
408
  filePath;
409
+ /** Public accessor for the JSONL path — required by SessionWriter so
410
+ * observability surfaces (`/fleet log`, FleetPanel) can locate the
411
+ * transcript without recomputing the path from session metadata. */
412
+ get transcriptPath() {
413
+ return this.filePath || void 0;
414
+ }
409
415
  initDone = false;
410
416
  resumed;
411
417
  appendFailCount = 0;
@@ -1796,6 +1802,272 @@ var SessionAnalyzer = class {
1796
1802
  return last - first;
1797
1803
  }
1798
1804
  };
1805
+ async function loadTodosCheckpoint(filePath) {
1806
+ let raw;
1807
+ try {
1808
+ raw = await fsp.readFile(filePath, "utf8");
1809
+ } catch {
1810
+ return null;
1811
+ }
1812
+ try {
1813
+ const parsed = JSON.parse(raw);
1814
+ if (parsed?.version !== 1 || !Array.isArray(parsed.todos)) return null;
1815
+ return parsed.todos.filter(
1816
+ (t) => !!t && typeof t.id === "string" && typeof t.content === "string" && typeof t.status === "string"
1817
+ );
1818
+ } catch {
1819
+ return null;
1820
+ }
1821
+ }
1822
+ async function saveTodosCheckpoint(filePath, sessionId, todos) {
1823
+ const payload = {
1824
+ version: 1,
1825
+ sessionId,
1826
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1827
+ todos: [...todos]
1828
+ };
1829
+ try {
1830
+ await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
1831
+ } catch (err) {
1832
+ console.warn(
1833
+ "[todos-checkpoint] save failed:",
1834
+ err instanceof Error ? err.message : String(err)
1835
+ );
1836
+ }
1837
+ }
1838
+ function attachTodosCheckpoint(state, filePath, sessionId) {
1839
+ let timer = null;
1840
+ let pending = null;
1841
+ const flush = () => {
1842
+ timer = null;
1843
+ if (pending) {
1844
+ void saveTodosCheckpoint(filePath, sessionId, pending);
1845
+ pending = null;
1846
+ }
1847
+ };
1848
+ const unsubscribe = state.onChange((change) => {
1849
+ if (change.kind !== "todos_replaced") return;
1850
+ pending = change.todos;
1851
+ if (timer) clearTimeout(timer);
1852
+ timer = setTimeout(flush, 150);
1853
+ });
1854
+ return () => {
1855
+ unsubscribe();
1856
+ if (timer) {
1857
+ clearTimeout(timer);
1858
+ flush();
1859
+ }
1860
+ };
1861
+ }
1862
+ async function loadPlan(filePath) {
1863
+ let raw;
1864
+ try {
1865
+ raw = await fsp.readFile(filePath, "utf8");
1866
+ } catch {
1867
+ return null;
1868
+ }
1869
+ try {
1870
+ const parsed = JSON.parse(raw);
1871
+ if (parsed?.version !== 1 || !Array.isArray(parsed.items)) return null;
1872
+ return parsed;
1873
+ } catch {
1874
+ return null;
1875
+ }
1876
+ }
1877
+ async function savePlan(filePath, plan) {
1878
+ try {
1879
+ await atomicWrite(filePath, JSON.stringify(plan, null, 2), { mode: 384 });
1880
+ } catch (err) {
1881
+ console.warn(
1882
+ "[plan-store] save failed:",
1883
+ err instanceof Error ? err.message : String(err)
1884
+ );
1885
+ }
1886
+ }
1887
+ function emptyPlan(sessionId, title) {
1888
+ return {
1889
+ version: 1,
1890
+ sessionId,
1891
+ title,
1892
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1893
+ items: []
1894
+ };
1895
+ }
1896
+ function addPlanItem(plan, title, details) {
1897
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1898
+ const item = {
1899
+ id: `plan_${Date.now()}_${randomUUID().slice(0, 6)}`,
1900
+ title,
1901
+ details,
1902
+ status: "open",
1903
+ createdAt: now,
1904
+ updatedAt: now
1905
+ };
1906
+ return {
1907
+ plan: { ...plan, items: [...plan.items, item], updatedAt: now },
1908
+ item
1909
+ };
1910
+ }
1911
+ function removePlanItem(plan, idOrIndex) {
1912
+ const idx = matchIndex(plan, idOrIndex);
1913
+ if (idx === -1) return plan;
1914
+ return {
1915
+ ...plan,
1916
+ items: plan.items.filter((_, i) => i !== idx),
1917
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1918
+ };
1919
+ }
1920
+ function setPlanItemStatus(plan, idOrIndex, status) {
1921
+ const idx = matchIndex(plan, idOrIndex);
1922
+ if (idx === -1) return plan;
1923
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1924
+ const items = plan.items.map(
1925
+ (it, i) => i === idx ? { ...it, status, updatedAt: now } : it
1926
+ );
1927
+ return { ...plan, items, updatedAt: now };
1928
+ }
1929
+ function clearPlan(plan) {
1930
+ return { ...plan, items: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1931
+ }
1932
+ function formatPlan(plan) {
1933
+ if (plan.items.length === 0) return "Plan is empty.";
1934
+ const lines = [];
1935
+ if (plan.title) lines.push(`# ${plan.title}`);
1936
+ plan.items.forEach((it, i) => {
1937
+ const mark = it.status === "done" ? "[x]" : it.status === "in_progress" ? "[~]" : "[ ]";
1938
+ lines.push(`${i + 1}. ${mark} ${it.title}`);
1939
+ if (it.details) {
1940
+ for (const line of it.details.split("\n")) lines.push(` ${line}`);
1941
+ }
1942
+ });
1943
+ return lines.join("\n");
1944
+ }
1945
+ function matchIndex(plan, idOrIndex) {
1946
+ const asNum = Number.parseInt(idOrIndex, 10);
1947
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= plan.items.length) return asNum - 1;
1948
+ const byId = plan.items.findIndex((it) => it.id === idOrIndex);
1949
+ if (byId !== -1) return byId;
1950
+ const lower = idOrIndex.toLowerCase();
1951
+ return plan.items.findIndex((it) => it.title.toLowerCase().includes(lower));
1952
+ }
1953
+ function attachPlanCheckpoint(_state, _filePath, _sessionId) {
1954
+ return () => void 0;
1955
+ }
1956
+ async function loadDirectorState(filePath) {
1957
+ let raw;
1958
+ try {
1959
+ raw = await fsp.readFile(filePath, "utf8");
1960
+ } catch {
1961
+ return null;
1962
+ }
1963
+ try {
1964
+ const parsed = JSON.parse(raw);
1965
+ if (parsed?.version !== 1) return null;
1966
+ return parsed;
1967
+ } catch {
1968
+ return null;
1969
+ }
1970
+ }
1971
+ var DirectorStateCheckpoint = class {
1972
+ snapshot;
1973
+ filePath;
1974
+ timer = null;
1975
+ debounceMs;
1976
+ writing = false;
1977
+ rewriteRequested = false;
1978
+ constructor(filePath, init, debounceMs = 250) {
1979
+ this.filePath = filePath;
1980
+ this.debounceMs = debounceMs;
1981
+ this.snapshot = {
1982
+ version: 1,
1983
+ directorRunId: init.directorRunId,
1984
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1985
+ spawnCount: 0,
1986
+ maxSpawns: init.maxSpawns,
1987
+ spawnDepth: init.spawnDepth,
1988
+ maxSpawnDepth: init.maxSpawnDepth,
1989
+ subagents: [],
1990
+ tasks: []
1991
+ };
1992
+ }
1993
+ current() {
1994
+ return this.snapshot;
1995
+ }
1996
+ recordSpawn(sub, spawnCount) {
1997
+ this.snapshot = {
1998
+ ...this.snapshot,
1999
+ spawnCount,
2000
+ subagents: [...this.snapshot.subagents.filter((s) => s.id !== sub.id), sub]
2001
+ };
2002
+ this.bumpUpdatedAt();
2003
+ this.schedule();
2004
+ }
2005
+ recordTaskAssigned(task) {
2006
+ const exists = this.snapshot.tasks.some((t) => t.taskId === task.taskId);
2007
+ this.snapshot = {
2008
+ ...this.snapshot,
2009
+ tasks: exists ? this.snapshot.tasks.map((t) => t.taskId === task.taskId ? { ...t, ...task } : t) : [...this.snapshot.tasks, task]
2010
+ };
2011
+ this.bumpUpdatedAt();
2012
+ this.schedule();
2013
+ }
2014
+ recordTaskStatus(taskId, patch) {
2015
+ this.snapshot = {
2016
+ ...this.snapshot,
2017
+ tasks: this.snapshot.tasks.map(
2018
+ (t) => t.taskId === taskId ? { ...t, ...patch } : t
2019
+ )
2020
+ };
2021
+ this.bumpUpdatedAt();
2022
+ this.schedule();
2023
+ }
2024
+ setUsage(usage) {
2025
+ this.snapshot = { ...this.snapshot, usage };
2026
+ this.bumpUpdatedAt();
2027
+ this.schedule();
2028
+ }
2029
+ /** Force a synchronous flush — used by Director.shutdown(). */
2030
+ async flush() {
2031
+ if (this.timer) {
2032
+ clearTimeout(this.timer);
2033
+ this.timer = null;
2034
+ }
2035
+ await this.persist();
2036
+ }
2037
+ bumpUpdatedAt() {
2038
+ this.snapshot = { ...this.snapshot, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2039
+ }
2040
+ schedule() {
2041
+ if (this.timer) return;
2042
+ this.timer = setTimeout(() => {
2043
+ this.timer = null;
2044
+ void this.persist();
2045
+ }, this.debounceMs);
2046
+ }
2047
+ async persist() {
2048
+ if (this.writing) {
2049
+ this.rewriteRequested = true;
2050
+ return;
2051
+ }
2052
+ this.writing = true;
2053
+ try {
2054
+ await atomicWrite(this.filePath, JSON.stringify(this.snapshot, null, 2), {
2055
+ mode: 384
2056
+ });
2057
+ } catch (err) {
2058
+ console.warn(
2059
+ "[director-state] checkpoint write failed:",
2060
+ err instanceof Error ? err.message : String(err)
2061
+ );
2062
+ } finally {
2063
+ this.writing = false;
2064
+ if (this.rewriteRequested) {
2065
+ this.rewriteRequested = false;
2066
+ this.schedule();
2067
+ }
2068
+ }
2069
+ }
2070
+ };
1799
2071
 
1800
2072
  // src/security/secret-scrubber.ts
1801
2073
  var PATTERNS = [
@@ -2071,6 +2343,18 @@ var DefaultPermissionPolicy = class {
2071
2343
  return void 0;
2072
2344
  }
2073
2345
  };
2346
+ var AutoApprovePermissionPolicy = class {
2347
+ async evaluate(tool) {
2348
+ if (tool.permission === "deny") {
2349
+ return { permission: "deny", source: "default", reason: "tool default deny" };
2350
+ }
2351
+ return { permission: "auto", source: "yolo" };
2352
+ }
2353
+ async trust() {
2354
+ }
2355
+ async reload() {
2356
+ }
2357
+ };
2074
2358
 
2075
2359
  // src/types/errors.ts
2076
2360
  var WrongStackError = class extends Error {
@@ -3946,6 +4230,10 @@ var FleetBus = class {
3946
4230
  "iteration.started",
3947
4231
  "iteration.completed",
3948
4232
  "provider.text_delta",
4233
+ // Subagent extended-thinking output. Forwarded so the FleetPanel /
4234
+ // /fleet log can surface "the planner is thinking…" instead of a
4235
+ // silent gap between iteration.started and the first text_delta.
4236
+ "provider.thinking_delta",
3949
4237
  "provider.response",
3950
4238
  "provider.retry",
3951
4239
  "provider.error",
@@ -4194,14 +4482,34 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4194
4482
  completedResults = [];
4195
4483
  totalIterations = 0;
4196
4484
  inFlight = 0;
4485
+ /**
4486
+ * Subagents currently being stopped. Set on entry to `stop()`, cleared
4487
+ * once `recordCompletion` lands the terminal TaskResult. Used by
4488
+ * `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
4489
+ * to a subagent the caller has already asked to terminate — closes the
4490
+ * assign+terminate race where a fresh task could land on a worker that
4491
+ * was about to be killed.
4492
+ */
4493
+ terminating = /* @__PURE__ */ new Set();
4197
4494
  constructor(config, options = {}) {
4198
4495
  super();
4199
4496
  this.coordinatorId = config.coordinatorId;
4200
4497
  this.config = config;
4201
4498
  this.runner = options.runner;
4202
4499
  }
4500
+ /**
4501
+ * Replace the runner after construction. Used when the runner depends
4502
+ * on infrastructure (e.g. FleetBus) that isn't available until after
4503
+ * the coordinator's owning Director is built.
4504
+ */
4505
+ setRunner(runner) {
4506
+ this.runner = runner;
4507
+ }
4203
4508
  async spawn(subagent) {
4204
4509
  const id = subagent.id || randomUUID();
4510
+ if (this.subagents.has(id)) {
4511
+ throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
4512
+ }
4205
4513
  const context = {
4206
4514
  subagentId: id,
4207
4515
  tasks: [],
@@ -4245,6 +4553,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4245
4553
  async stop(subagentId) {
4246
4554
  const subagent = this.subagents.get(subagentId);
4247
4555
  if (!subagent) return;
4556
+ this.terminating.add(subagentId);
4248
4557
  subagent.abortController.abort();
4249
4558
  subagent.status = "stopped";
4250
4559
  subagent.currentTask = void 0;
@@ -4252,6 +4561,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4252
4561
  this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
4253
4562
  }
4254
4563
  async stopAll() {
4564
+ this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
4255
4565
  await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
4256
4566
  }
4257
4567
  getStatus() {
@@ -4285,7 +4595,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4285
4595
  tryDispatchNext() {
4286
4596
  while (this.canDispatch()) {
4287
4597
  const subagentId = this.findIdleSubagent();
4288
- if (!subagentId) return;
4598
+ if (!subagentId) {
4599
+ if (this.pendingTasks.length > 0 && !this.hasLiveSubagent()) {
4600
+ this.drainPendingAsAborted(
4601
+ "No live subagent available \u2014 all stopped or mid-termination"
4602
+ );
4603
+ }
4604
+ return;
4605
+ }
4289
4606
  const task = this.pendingTasks.shift();
4290
4607
  if (!task) return;
4291
4608
  this.runDispatched(subagentId, task).catch((err) => {
@@ -4293,7 +4610,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4293
4610
  subagentId,
4294
4611
  taskId: task.id,
4295
4612
  status: "failed",
4296
- error: err instanceof Error ? err.message : String(err),
4613
+ error: classifySubagentError(err),
4297
4614
  iterations: 0,
4298
4615
  toolCalls: 0,
4299
4616
  durationMs: 0
@@ -4307,13 +4624,76 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4307
4624
  }
4308
4625
  findIdleSubagent() {
4309
4626
  for (const [id, s] of this.subagents) {
4310
- if (s.status === "idle") return id;
4627
+ if (s.status === "idle" && !this.terminating.has(id)) return id;
4311
4628
  }
4312
4629
  return null;
4313
4630
  }
4631
+ /**
4632
+ * Returns true iff at least one spawned subagent could still
4633
+ * process a task. A "live" subagent is one that is not stopped
4634
+ * AND not mid-termination — `running` workers count because they
4635
+ * will eventually finish and become idle.
4636
+ *
4637
+ * When no subagent has ever been spawned, returns `true` so a
4638
+ * pre-spawn `assign()` simply queues (legacy behaviour). The
4639
+ * dead-end detection only fires after `stop()` has retired every
4640
+ * spawned worker.
4641
+ *
4642
+ * Used by `tryDispatchNext` to detect a dead-end pending queue.
4643
+ */
4644
+ hasLiveSubagent() {
4645
+ if (this.subagents.size === 0) return true;
4646
+ for (const [id, s] of this.subagents) {
4647
+ if (s.status !== "stopped" && !this.terminating.has(id)) return true;
4648
+ }
4649
+ return false;
4650
+ }
4651
+ /**
4652
+ * Drain every pending task with a synthetic `aborted_by_parent`
4653
+ * completion event. Same shape as the `stopAll()` drain — we go
4654
+ * around `recordCompletion` because pending tasks were never
4655
+ * counted in `inFlight` and routing them through would trip the
4656
+ * underflow guard on every task after the first.
4657
+ */
4658
+ drainPendingAsAborted(message) {
4659
+ const dropped = this.pendingTasks.splice(0, this.pendingTasks.length);
4660
+ for (const t of dropped) {
4661
+ const synthetic = {
4662
+ subagentId: t.subagentId ?? "unassigned",
4663
+ taskId: t.id,
4664
+ status: "stopped",
4665
+ error: {
4666
+ kind: "aborted_by_parent",
4667
+ message,
4668
+ retryable: false
4669
+ },
4670
+ iterations: 0,
4671
+ toolCalls: 0,
4672
+ durationMs: 0
4673
+ };
4674
+ this.completedResults.push(synthetic);
4675
+ this.emit("task.completed", { task: t, result: synthetic });
4676
+ }
4677
+ }
4314
4678
  async runDispatched(subagentId, task) {
4315
4679
  const subagent = this.subagents.get(subagentId);
4316
4680
  if (!subagent) return;
4681
+ if (this.terminating.has(subagentId) || subagent.status === "stopped") {
4682
+ this.recordCompletion({
4683
+ subagentId,
4684
+ taskId: task.id,
4685
+ status: "stopped",
4686
+ error: {
4687
+ kind: "aborted_by_parent",
4688
+ message: "Subagent was terminated before task could start",
4689
+ retryable: false
4690
+ },
4691
+ iterations: 0,
4692
+ toolCalls: 0,
4693
+ durationMs: 0
4694
+ });
4695
+ return;
4696
+ }
4317
4697
  subagent.status = "running";
4318
4698
  subagent.currentTask = task.id;
4319
4699
  task.subagentId = subagentId;
@@ -4359,7 +4739,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4359
4739
  subagentId,
4360
4740
  taskId: task.id,
4361
4741
  status,
4362
- error: err instanceof Error ? err.message : String(err),
4742
+ error: classifySubagentError(err, {
4743
+ parentAborted: subagent.abortController.signal.aborted
4744
+ }),
4363
4745
  iterations: usage.iterations,
4364
4746
  toolCalls: usage.toolCalls,
4365
4747
  durationMs: Date.now() - startTime
@@ -4398,19 +4780,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4398
4780
  }
4399
4781
  const subagent = this.subagents.get(result.subagentId);
4400
4782
  if (subagent && subagent.status !== "stopped") {
4401
- const failed = result.status === "failed" || result.status === "timeout";
4402
- subagent.status = failed ? "error" : "idle";
4783
+ result.status === "failed" || result.status === "timeout";
4784
+ subagent.status = "idle";
4403
4785
  subagent.currentTask = void 0;
4404
4786
  if (subagent.abortController.signal.aborted) {
4405
4787
  subagent.abortController = new AbortController();
4406
4788
  }
4407
- if (subagent.status === "error") {
4408
- queueMicrotask(() => {
4409
- if (subagent.status === "error") subagent.status = "idle";
4410
- this.tryDispatchNext();
4411
- });
4412
- }
4413
4789
  }
4790
+ this.terminating.delete(result.subagentId);
4414
4791
  this.emit("task.completed", {
4415
4792
  task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
4416
4793
  result
@@ -4433,6 +4810,99 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
4433
4810
  return false;
4434
4811
  }
4435
4812
  };
4813
+ function classifySubagentError(err, hints = {}) {
4814
+ const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
4815
+ const baseMessage = err instanceof Error ? err.message : String(err);
4816
+ if (err instanceof ProviderError) {
4817
+ return providerErrorToSubagentError(err, baseMessage, cause);
4818
+ }
4819
+ if (err instanceof BudgetExceededError) {
4820
+ const map = {
4821
+ iterations: "budget_iterations",
4822
+ tool_calls: "budget_tool_calls",
4823
+ tokens: "budget_tokens",
4824
+ cost: "budget_cost",
4825
+ timeout: "budget_timeout"
4826
+ };
4827
+ return {
4828
+ kind: map[err.kind],
4829
+ message: baseMessage,
4830
+ // Budgets are user-configured ceilings, not transient failures —
4831
+ // retrying with the same budget will hit the same ceiling. The
4832
+ // orchestrator must raise the budget or narrow the task first.
4833
+ retryable: false,
4834
+ cause
4835
+ };
4836
+ }
4837
+ if (hints.parentAborted) {
4838
+ return {
4839
+ kind: "aborted_by_parent",
4840
+ message: baseMessage,
4841
+ retryable: false,
4842
+ cause
4843
+ };
4844
+ }
4845
+ const lower = baseMessage.toLowerCase();
4846
+ if (/agent aborted$/i.test(baseMessage)) {
4847
+ return {
4848
+ kind: "aborted_by_parent",
4849
+ message: baseMessage,
4850
+ retryable: false,
4851
+ cause
4852
+ };
4853
+ }
4854
+ if (/agent exhausted iteration limit$/i.test(baseMessage)) {
4855
+ return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
4856
+ }
4857
+ if (/empty response$/i.test(baseMessage)) {
4858
+ return { kind: "empty_response", message: baseMessage, retryable: false, cause };
4859
+ }
4860
+ if (/^tool failed: /i.test(baseMessage)) {
4861
+ return { kind: "tool_failed", message: baseMessage, retryable: false, cause };
4862
+ }
4863
+ if (lower.includes("bridge transport") || /bridge.*(closed|disconnect)/i.test(baseMessage)) {
4864
+ return { kind: "bridge_failed", message: baseMessage, retryable: false, cause };
4865
+ }
4866
+ if (/context length|max.*tokens?.*exceeded|prompt is too long/i.test(baseMessage)) {
4867
+ return { kind: "context_overflow", message: baseMessage, retryable: false, cause };
4868
+ }
4869
+ return {
4870
+ kind: "unknown",
4871
+ message: baseMessage,
4872
+ retryable: false,
4873
+ cause
4874
+ };
4875
+ }
4876
+ function providerErrorToSubagentError(err, message, cause) {
4877
+ const status = err.status;
4878
+ if (status === 429 || err.body?.type === "rate_limit_error") {
4879
+ return {
4880
+ kind: "provider_rate_limit",
4881
+ message,
4882
+ retryable: true,
4883
+ // Conservative default: 5s. Provider-specific code can override
4884
+ // by emitting an error whose body carries an explicit hint.
4885
+ backoffMs: 5e3,
4886
+ cause
4887
+ };
4888
+ }
4889
+ if (status === 401 || status === 403 || err.body?.type === "authentication_error") {
4890
+ return { kind: "provider_auth", message, retryable: false, cause };
4891
+ }
4892
+ if (status === 408 || status === 0) {
4893
+ return { kind: "provider_timeout", message, retryable: true, cause };
4894
+ }
4895
+ if (status >= 500 && status < 600) {
4896
+ return {
4897
+ kind: "provider_5xx",
4898
+ message,
4899
+ retryable: true,
4900
+ backoffMs: 3e3,
4901
+ cause
4902
+ };
4903
+ }
4904
+ return { kind: "unknown", message, retryable: err.retryable, cause };
4905
+ }
4436
4906
 
4437
4907
  // src/coordination/director.ts
4438
4908
  var DirectorBudgetError = class extends Error {
@@ -4496,6 +4966,27 @@ var Director = class {
4496
4966
  spawnDepth;
4497
4967
  /** Live spawn counter for `maxSpawns` enforcement. */
4498
4968
  spawnCount = 0;
4969
+ /** Optional checkpoint mirror — writes the live task graph + roster to disk. */
4970
+ stateCheckpoint;
4971
+ /** Optional session writer for emitting task_* / agent_* lifecycle events. */
4972
+ sessionWriter;
4973
+ /** Debounce timer for periodic manifest writes. */
4974
+ manifestTimer = null;
4975
+ manifestDebounceMs;
4976
+ /** Resolves task descriptions back from `assign()` so completion events
4977
+ * can also carry a human-readable title. */
4978
+ taskDescriptions = /* @__PURE__ */ new Map();
4979
+ /** Snapshot of which subagent owns each task — drives state-checkpoint
4980
+ * status updates without re-walking the manifest. */
4981
+ taskOwners = /* @__PURE__ */ new Map();
4982
+ /**
4983
+ * Handle to the coordinator-side `task.completed` listener so we can
4984
+ * unsubscribe in `shutdown()`. Without this, repeated Director
4985
+ * construction (e.g. tests, hot reloads) accumulates listeners on a
4986
+ * cached coordinator and slowly drifts the EventEmitter past its
4987
+ * default cap.
4988
+ */
4989
+ taskCompletedListener = null;
4499
4990
  constructor(opts) {
4500
4991
  this.id = opts.config.coordinatorId || randomUUID();
4501
4992
  this.manifestPath = opts.manifestPath;
@@ -4506,6 +4997,14 @@ var Director = class {
4506
4997
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
4507
4998
  this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
4508
4999
  this.spawnDepth = opts.spawnDepth ?? 0;
5000
+ this.sessionWriter = opts.sessionWriter ?? null;
5001
+ this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
5002
+ this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
5003
+ directorRunId: this.id,
5004
+ maxSpawns: opts.maxSpawns,
5005
+ spawnDepth: this.spawnDepth,
5006
+ maxSpawnDepth: this.maxSpawnDepth
5007
+ }) : null;
4509
5008
  if (this.sharedScratchpadPath) {
4510
5009
  void fsp.mkdir(this.sharedScratchpadPath, { recursive: true }).catch(() => void 0);
4511
5010
  }
@@ -4524,7 +5023,7 @@ var Director = class {
4524
5023
  { ...opts.config, coordinatorId: this.id },
4525
5024
  { runner: opts.runner }
4526
5025
  );
4527
- this.coordinator.on("task.completed", (payload) => {
5026
+ this.taskCompletedListener = (payload) => {
4528
5027
  const r = payload.result;
4529
5028
  this.completed.set(r.taskId, r);
4530
5029
  const waiter = this.taskWaiters.get(r.taskId);
@@ -4532,7 +5031,54 @@ var Director = class {
4532
5031
  waiter.resolve(r);
4533
5032
  this.taskWaiters.delete(r.taskId);
4534
5033
  }
4535
- });
5034
+ const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
5035
+ const failed = r.status !== "success";
5036
+ const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
5037
+ this.stateCheckpoint?.recordTaskStatus(r.taskId, {
5038
+ status: failed ? r.status : "completed",
5039
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5040
+ iterations: r.iterations,
5041
+ toolCalls: r.toolCalls,
5042
+ durationMs: r.durationMs,
5043
+ error: errorString
5044
+ });
5045
+ this.stateCheckpoint?.setUsage(this.usage.snapshot());
5046
+ void this.appendSessionEvent(
5047
+ failed ? {
5048
+ type: "task_failed",
5049
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5050
+ taskId: r.taskId,
5051
+ title,
5052
+ error: errorString ?? r.status
5053
+ } : {
5054
+ type: "task_completed",
5055
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5056
+ taskId: r.taskId,
5057
+ title
5058
+ }
5059
+ );
5060
+ this.scheduleManifest();
5061
+ };
5062
+ this.coordinator.on("task.completed", this.taskCompletedListener);
5063
+ }
5064
+ /** Best-effort session-writer append. Swallows failures — the director
5065
+ * must not break a fleet run because the session JSONL handle closed. */
5066
+ async appendSessionEvent(event) {
5067
+ if (!this.sessionWriter) return;
5068
+ try {
5069
+ await this.sessionWriter.append(event);
5070
+ } catch {
5071
+ }
5072
+ }
5073
+ /** Debounced manifest writer. A burst of spawn/assign/complete events
5074
+ * collapses into one write. Set `manifestDebounceMs` to 0 to disable. */
5075
+ scheduleManifest() {
5076
+ if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
5077
+ if (this.manifestTimer) return;
5078
+ this.manifestTimer = setTimeout(() => {
5079
+ this.manifestTimer = null;
5080
+ void this.writeManifest().catch(() => void 0);
5081
+ }, this.manifestDebounceMs);
4536
5082
  }
4537
5083
  /**
4538
5084
  * Spawn a subagent. Identical to the coordinator's `spawn()` but
@@ -4571,6 +5117,25 @@ var Director = class {
4571
5117
  model: config.model,
4572
5118
  taskIds: []
4573
5119
  });
5120
+ const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
5121
+ this.stateCheckpoint?.recordSpawn(
5122
+ {
5123
+ id: result.subagentId,
5124
+ name: config.name,
5125
+ role: config.role,
5126
+ provider: config.provider,
5127
+ model: config.model,
5128
+ spawnedAt
5129
+ },
5130
+ this.spawnCount
5131
+ );
5132
+ void this.appendSessionEvent({
5133
+ type: "agent_spawned",
5134
+ ts: spawnedAt,
5135
+ agentId: result.subagentId,
5136
+ role: config.role ?? config.name
5137
+ });
5138
+ this.scheduleManifest();
4574
5139
  return result.subagentId;
4575
5140
  }
4576
5141
  /**
@@ -4691,13 +5256,42 @@ var Director = class {
4691
5256
  * — calling shutdown twice is a no-op on the second invocation.
4692
5257
  */
4693
5258
  async shutdown() {
5259
+ if (this.manifestTimer) {
5260
+ clearTimeout(this.manifestTimer);
5261
+ this.manifestTimer = null;
5262
+ }
5263
+ if (this.taskCompletedListener) {
5264
+ this.coordinator.off("task.completed", this.taskCompletedListener);
5265
+ this.taskCompletedListener = null;
5266
+ }
4694
5267
  await this.coordinator.stopAll();
4695
5268
  for (const b of this.subagentBridges.values()) {
4696
- await b.stop().catch(() => void 0);
5269
+ await b.stop().catch((err) => this.logShutdownError("subagent_bridge_stop", err));
4697
5270
  }
4698
5271
  this.subagentBridges.clear();
4699
- await this.bridge.stop().catch(() => void 0);
4700
- if (this.manifestPath) await this.writeManifest().catch(() => void 0);
5272
+ await this.bridge.stop().catch((err) => this.logShutdownError("director_bridge_stop", err));
5273
+ if (this.manifestPath)
5274
+ await this.writeManifest().catch((err) => this.logShutdownError("manifest_write", err));
5275
+ if (this.stateCheckpoint) {
5276
+ this.stateCheckpoint.setUsage(this.usage.snapshot());
5277
+ await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
5278
+ }
5279
+ }
5280
+ /**
5281
+ * Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
5282
+ * called from process-exit paths where an uncaught throw would lose
5283
+ * the manifest write that comes after. But we MUST NOT silently
5284
+ * swallow either — a persistent bridge-close failure would otherwise
5285
+ * mask a real bug. `process.emitWarning` is the right tier:
5286
+ * surfaces on stderr by default, lets the host plug a warning
5287
+ * listener for structured collection, and never affects exit code.
5288
+ */
5289
+ logShutdownError(phase, err) {
5290
+ const detail = err instanceof Error ? err.message : String(err);
5291
+ process.emitWarning(
5292
+ `Director shutdown phase "${phase}" failed: ${detail}`,
5293
+ "DirectorShutdownWarning"
5294
+ );
4701
5295
  }
4702
5296
  /**
4703
5297
  * Hand a task to the coordinator. Returns the assigned task id so
@@ -4711,6 +5305,23 @@ var Director = class {
4711
5305
  if (entry) entry.taskIds.push(taskWithId.id);
4712
5306
  }
4713
5307
  await this.coordinator.assign(taskWithId);
5308
+ this.taskDescriptions.set(taskWithId.id, taskWithId.description);
5309
+ if (taskWithId.subagentId) this.taskOwners.set(taskWithId.id, taskWithId.subagentId);
5310
+ const assignedAt = (/* @__PURE__ */ new Date()).toISOString();
5311
+ this.stateCheckpoint?.recordTaskAssigned({
5312
+ taskId: taskWithId.id,
5313
+ subagentId: taskWithId.subagentId,
5314
+ description: taskWithId.description,
5315
+ status: "running",
5316
+ assignedAt
5317
+ });
5318
+ void this.appendSessionEvent({
5319
+ type: "task_created",
5320
+ ts: assignedAt,
5321
+ taskId: taskWithId.id,
5322
+ title: taskWithId.description
5323
+ });
5324
+ this.scheduleManifest();
4714
5325
  return taskWithId.id;
4715
5326
  }
4716
5327
  /**
@@ -4770,6 +5381,23 @@ var Director = class {
4770
5381
  snapshot() {
4771
5382
  return this.usage.snapshot();
4772
5383
  }
5384
+ /**
5385
+ * Look up provider/model metadata for a spawned subagent. Returns
5386
+ * undefined when the subagent id is unknown (not yet spawned, or
5387
+ * already torn down). Callers — notably the TUI fleet panel — use
5388
+ * this to render human-readable provider/model tags next to each
5389
+ * subagent row without reaching into private state.
5390
+ */
5391
+ getSubagentMeta(id) {
5392
+ const usage = this.subagentMeta.get(id);
5393
+ const manifest = this.manifestEntries.get(id);
5394
+ if (!usage && !manifest) return void 0;
5395
+ return {
5396
+ provider: usage?.provider ?? manifest?.provider,
5397
+ model: usage?.model ?? manifest?.model,
5398
+ name: manifest?.name
5399
+ };
5400
+ }
4773
5401
  /**
4774
5402
  * Compose the leader/director-agent system prompt: fleet preamble +
4775
5403
  * (optional) roster summary + user base prompt. Pass the result to your
@@ -5069,12 +5697,260 @@ function makeFleetUsageTool(director) {
5069
5697
  }
5070
5698
  };
5071
5699
  }
5700
+ function createDelegateTool(opts) {
5701
+ const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
5702
+ const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
5703
+ const inputSchema = {
5704
+ type: "object",
5705
+ properties: {
5706
+ task: {
5707
+ type: "string",
5708
+ description: "What the subagent should do \u2014 natural language, complete sentence(s). The subagent has its own tool slice, its own LLM call, and returns when its task is done."
5709
+ },
5710
+ role: {
5711
+ type: "string",
5712
+ description: rosterIds.length > 0 ? `Roster role (preferred). One of: ${rosterIds.join(", ")}. Picks a pre-tuned config (prompt, budgets, tools) for that role.` : "No roster is configured \u2014 pass `name` instead.",
5713
+ enum: rosterIds.length > 0 ? rosterIds : void 0
5714
+ },
5715
+ name: {
5716
+ type: "string",
5717
+ description: "Display name for the subagent when not using a roster role. Required when `role` is omitted."
5718
+ },
5719
+ provider: {
5720
+ type: "string",
5721
+ description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the host provider when omitted.'
5722
+ },
5723
+ model: {
5724
+ type: "string",
5725
+ description: "Model id within the provider. Defaults to the host model when omitted."
5726
+ },
5727
+ systemPromptOverride: {
5728
+ type: "string",
5729
+ description: "Optional extra prompt text appended to the role baseline."
5730
+ },
5731
+ timeoutMs: {
5732
+ type: "number",
5733
+ description: `Wall-clock budget for this delegate in milliseconds. No hard cap \u2014 set as high as the task realistically needs (a monorepo audit can take hours, a single-file lint takes seconds). Default ${Math.round(defaultTimeoutMs / 1e3 / 60)} minutes.`
5734
+ },
5735
+ maxIterations: {
5736
+ type: "number",
5737
+ description: "Maximum LLM iterations the subagent may take. Unset = use the role/coordinator default. Raise this for tasks with many tool-think-tool cycles (deep code analysis, multi-file refactors)."
5738
+ },
5739
+ maxToolCalls: {
5740
+ type: "number",
5741
+ description: "Maximum number of tool invocations the subagent may make. Unset = use the role/coordinator default. Raise this for tasks that touch many files (large grep + read + report)."
5742
+ }
5743
+ },
5744
+ required: ["task"]
5745
+ };
5746
+ return {
5747
+ name: "delegate",
5748
+ description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and its own budget \u2014 use this when a task is self-contained, would otherwise blow up your context, or benefits from a specialized role (bug-hunter, security-scanner, refactor-planner, audit-log). YOU decide how big the budget is: pass `timeoutMs`, `maxIterations`, and `maxToolCalls` sized to the actual work. There is no hidden cap forcing a 3-minute / 80-iteration limit \u2014 if a monorepo audit needs 2 hours and 500 tool calls, ask for that. Call multiple delegates in parallel through the provider's parallel-tool-call surface to fan work out across roles.",
5749
+ usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster or pass `name` + `provider` + `model`. For non-trivial work, also pass `timeoutMs` (the wall-clock budget you actually need), `maxIterations`, and `maxToolCalls` \u2014 defaults are intentionally generous (4 hours) but the right values depend on scope. Returns the subagent's `TaskResult` \u2014 including the textual `result`, iteration count, tool count, and duration. Auto-promotes the host into director mode on first call.",
5750
+ permission: "auto",
5751
+ mutating: false,
5752
+ inputSchema,
5753
+ async execute(input) {
5754
+ const i = input ?? {};
5755
+ if (typeof i.task !== "string" || !i.task.trim()) {
5756
+ return { ok: false, error: "`task` is required." };
5757
+ }
5758
+ let director = await opts.host.ensureDirector();
5759
+ if (!director) {
5760
+ director = await opts.host.promoteToDirector();
5761
+ }
5762
+ if (!director) {
5763
+ const reason = opts.host.getPromotionBlockReason?.();
5764
+ return {
5765
+ ok: false,
5766
+ error: reason ?? "Director could not be activated \u2014 multi-agent host already running in legacy non-director mode. Restart with `--director` for fleet support."
5767
+ };
5768
+ }
5769
+ const timeoutMs = i.timeoutMs ?? defaultTimeoutMs;
5770
+ let cfg;
5771
+ if (i.role) {
5772
+ const base = opts.roster?.[i.role];
5773
+ if (!base) {
5774
+ return {
5775
+ ok: false,
5776
+ error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
5777
+ };
5778
+ }
5779
+ cfg = { ...base };
5780
+ if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
5781
+ if (i.provider) cfg.provider = i.provider;
5782
+ if (i.model) cfg.model = i.model;
5783
+ } else {
5784
+ if (!i.name) {
5785
+ return {
5786
+ ok: false,
5787
+ error: "Either `role` (from the roster) or `name` is required."
5788
+ };
5789
+ }
5790
+ cfg = {
5791
+ name: i.name,
5792
+ provider: i.provider,
5793
+ model: i.model,
5794
+ systemPromptOverride: i.systemPromptOverride
5795
+ };
5796
+ }
5797
+ if (typeof i.maxIterations === "number" && i.maxIterations > 0) {
5798
+ cfg.maxIterations = i.maxIterations;
5799
+ }
5800
+ if (typeof i.maxToolCalls === "number" && i.maxToolCalls > 0) {
5801
+ cfg.maxToolCalls = i.maxToolCalls;
5802
+ }
5803
+ const SUBAGENT_TIMEOUT_BUFFER_MS = 3e4;
5804
+ const desiredSubTimeout = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
5805
+ if (!cfg.timeoutMs || cfg.timeoutMs > desiredSubTimeout) {
5806
+ cfg.timeoutMs = desiredSubTimeout;
5807
+ }
5808
+ try {
5809
+ const subagentId = await director.spawn(cfg);
5810
+ const taskId = await director.assign({
5811
+ id: "",
5812
+ description: i.task,
5813
+ subagentId
5814
+ });
5815
+ const result = await Promise.race([
5816
+ director.awaitTasks([taskId]).then((r) => r[0]),
5817
+ new Promise(
5818
+ (resolve2) => setTimeout(() => resolve2({ __timeout: true }), timeoutMs)
5819
+ )
5820
+ ]);
5821
+ if ("__timeout" in result) {
5822
+ const partial2 = await readSubagentPartial(opts, subagentId);
5823
+ return {
5824
+ ok: false,
5825
+ stopReason: "host_timeout",
5826
+ error: `Subagent did not finish within ${timeoutMs}ms.`,
5827
+ hint: "Reduce scope of the next delegate, raise timeoutMs, or use spawn_subagent + await_tasks for long-running work.",
5828
+ subagentId,
5829
+ taskId,
5830
+ partial: partial2
5831
+ };
5832
+ }
5833
+ const baseStopReason = result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
5834
+ const partial = result.status === "success" ? void 0 : await readSubagentPartial(opts, subagentId);
5835
+ const errorKind = result.error?.kind;
5836
+ const retryable = result.error?.retryable;
5837
+ const backoffMs = result.error?.backoffMs;
5838
+ return {
5839
+ ok: result.status === "success",
5840
+ status: result.status,
5841
+ stopReason: baseStopReason,
5842
+ errorKind,
5843
+ retryable,
5844
+ backoffMs,
5845
+ subagentId: result.subagentId,
5846
+ taskId: result.taskId,
5847
+ result: result.result,
5848
+ error: result.error,
5849
+ iterations: result.iterations,
5850
+ toolCalls: result.toolCalls,
5851
+ durationMs: result.durationMs,
5852
+ ...partial ? { partial } : {},
5853
+ ...hintForKind(errorKind, retryable, backoffMs) ? { hint: hintForKind(errorKind, retryable, backoffMs) } : {}
5854
+ };
5855
+ } catch (err) {
5856
+ return {
5857
+ ok: false,
5858
+ stopReason: "error",
5859
+ error: err instanceof Error ? err.message : String(err)
5860
+ };
5861
+ }
5862
+ }
5863
+ };
5864
+ }
5865
+ function hintForKind(kind, retryable, backoffMs) {
5866
+ if (!kind) return void 0;
5867
+ switch (kind) {
5868
+ case "provider_rate_limit":
5869
+ return `Provider rate-limited. Retry safe after ${backoffMs ?? 5e3}ms backoff. Consider a smaller model or fewer parallel delegates.`;
5870
+ case "provider_5xx":
5871
+ return `Provider server error. Retry safe after ${backoffMs ?? 3e3}ms backoff \u2014 usually transient.`;
5872
+ case "provider_timeout":
5873
+ return "Provider network timeout. Retry safe; reduce input size if it persists.";
5874
+ case "provider_auth":
5875
+ return "Provider rejected credentials. Cannot retry \u2014 fix the API key / config and re-invoke.";
5876
+ case "context_overflow":
5877
+ return "Subagent context exceeded the model limit. Narrow the task, use a larger-context model, or split into multiple delegates.";
5878
+ case "budget_iterations":
5879
+ case "budget_tool_calls":
5880
+ case "budget_tokens":
5881
+ case "budget_cost":
5882
+ return "Subagent exhausted its budget. Raise the matching `max*` field on the next delegate or narrow task scope.";
5883
+ case "budget_timeout":
5884
+ return "Subagent hit its wall-clock budget. Raise `timeoutMs` on the next delegate or split the task.";
5885
+ case "aborted_by_parent":
5886
+ return "Subagent was aborted (user Ctrl+C, parent unwound, or sibling failure cascade). Not retryable until the abort condition is resolved.";
5887
+ case "empty_response":
5888
+ return "Subagent ended its turn with no text and no tool calls. Almost always a prompt / config issue \u2014 clarify the task or check the model.";
5889
+ case "tool_failed":
5890
+ return "A tool inside the subagent returned ok:false. Inspect `partial.lastAssistantText` for the agent reasoning, then retry with corrected inputs.";
5891
+ case "bridge_failed":
5892
+ return "Parent-child bridge transport failed. This is rare \u2014 restart the session and retry.";
5893
+ default:
5894
+ return retryable ? "Failure classified as retryable. Try again with the same input." : void 0;
5895
+ }
5896
+ }
5897
+ async function readSubagentPartial(opts, subagentId) {
5898
+ if (!opts.sessionsRoot) return void 0;
5899
+ const candidates = [];
5900
+ if (opts.directorRunId) {
5901
+ candidates.push(path3.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
5902
+ } else {
5903
+ try {
5904
+ const runDirs = await fsp.readdir(opts.sessionsRoot);
5905
+ for (const r of runDirs) {
5906
+ candidates.push(path3.join(opts.sessionsRoot, r, `${subagentId}.jsonl`));
5907
+ }
5908
+ } catch {
5909
+ return void 0;
5910
+ }
5911
+ }
5912
+ for (const file of candidates) {
5913
+ let raw;
5914
+ try {
5915
+ raw = await fsp.readFile(file, "utf8");
5916
+ } catch {
5917
+ continue;
5918
+ }
5919
+ const lines = raw.split("\n").filter((l) => l.trim());
5920
+ let lastAssistantText;
5921
+ let lastStopReason;
5922
+ let toolUses = 0;
5923
+ for (const line of lines) {
5924
+ try {
5925
+ const ev = JSON.parse(line);
5926
+ if (ev.type === "tool_use") toolUses += 1;
5927
+ if (ev.type === "llm_response") {
5928
+ if (typeof ev.stopReason === "string") lastStopReason = ev.stopReason;
5929
+ if (Array.isArray(ev.content)) {
5930
+ const txt = ev.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n").trim();
5931
+ if (txt) lastAssistantText = txt;
5932
+ }
5933
+ }
5934
+ } catch {
5935
+ }
5936
+ }
5937
+ return {
5938
+ lastAssistantText,
5939
+ lastStopReason,
5940
+ toolUsesObserved: toolUses,
5941
+ events: lines.length
5942
+ };
5943
+ }
5944
+ return void 0;
5945
+ }
5072
5946
 
5073
5947
  // src/coordination/agent-subagent-runner.ts
5074
5948
  function makeAgentSubagentRunner(opts) {
5075
5949
  const format = opts.formatTaskInput ?? defaultFormatTaskInput;
5076
5950
  return async (task, ctx) => {
5077
- const { agent, events } = await opts.factory(ctx.config);
5951
+ const factoryResult = await opts.factory(ctx.config);
5952
+ const { agent, events } = factoryResult;
5953
+ const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
5078
5954
  const aborter = new AbortController();
5079
5955
  let budgetError = null;
5080
5956
  const onBudgetError = (err) => {
@@ -5088,13 +5964,19 @@ function makeAgentSubagentRunner(opts) {
5088
5964
  budgetError.message += ` (caused by: ${err.message})`;
5089
5965
  }
5090
5966
  };
5967
+ let lastToolFailed = null;
5091
5968
  const unsub = [];
5092
5969
  unsub.push(
5093
- events.on("tool.started", () => {
5970
+ events.on("tool.executed", (e) => {
5094
5971
  try {
5095
5972
  ctx.budget.recordToolCall();
5096
- } catch (e) {
5097
- onBudgetError(e);
5973
+ } catch (eb) {
5974
+ onBudgetError(eb);
5975
+ }
5976
+ if (e.ok === false) {
5977
+ lastToolFailed = e.name;
5978
+ } else if (e.ok === true) {
5979
+ lastToolFailed = null;
5098
5980
  }
5099
5981
  }),
5100
5982
  events.on("provider.response", (e) => {
@@ -5111,6 +5993,26 @@ function makeAgentSubagentRunner(opts) {
5111
5993
  } catch (e) {
5112
5994
  onBudgetError(e);
5113
5995
  }
5996
+ }),
5997
+ // D3: cooperative timeout enforcement DURING a long tool call.
5998
+ // The iteration-loop checkTimeout() only fires between agent
5999
+ // iterations — a single `bash sleep 3600` call would otherwise
6000
+ // park inside one tool execution while the timeout silently
6001
+ // passes, relying solely on the coordinator's hard Promise.race
6002
+ // to interrupt. Tools that emit `tool.progress` (bash chunks,
6003
+ // fetch byte progress, spawn-stream stdout) give us a heartbeat
6004
+ // we can hang the check on. When the budget trips here:
6005
+ // 1. onBudgetError sets budgetError + aborter.abort()
6006
+ // 2. aborter signal propagates to agent.run → tool executor
6007
+ // 3. tool's own signal listener kills the child process
6008
+ // Cheap: O(1) per progress event, and the budget short-circuits
6009
+ // when timeoutMs is unset (most subagents have one set anyway).
6010
+ events.on("tool.progress", () => {
6011
+ try {
6012
+ ctx.budget.checkTimeout();
6013
+ } catch (e) {
6014
+ onBudgetError(e);
6015
+ }
5114
6016
  })
5115
6017
  );
5116
6018
  const onParentAbort = () => aborter.abort();
@@ -5119,8 +6021,15 @@ function makeAgentSubagentRunner(opts) {
5119
6021
  try {
5120
6022
  result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
5121
6023
  } finally {
6024
+ detachFleet?.();
5122
6025
  ctx.signal.removeEventListener("abort", onParentAbort);
5123
6026
  for (const u of unsub) u();
6027
+ if (factoryResult.dispose) {
6028
+ try {
6029
+ await factoryResult.dispose();
6030
+ } catch {
6031
+ }
6032
+ }
5124
6033
  }
5125
6034
  if (budgetError) throw budgetError;
5126
6035
  if (result.status === "failed") {
@@ -5133,6 +6042,13 @@ function makeAgentSubagentRunner(opts) {
5133
6042
  throw new Error("agent exhausted iteration limit");
5134
6043
  }
5135
6044
  const usage = ctx.budget.usage();
6045
+ const finalText = (result.finalText ?? "").trim();
6046
+ if (finalText.length === 0 && usage.toolCalls === 0) {
6047
+ throw new Error("empty response");
6048
+ }
6049
+ if (finalText.length === 0 && lastToolFailed !== null) {
6050
+ throw new Error(`tool failed: ${lastToolFailed}`);
6051
+ }
5136
6052
  return {
5137
6053
  result: result.finalText,
5138
6054
  iterations: result.iterations,
@@ -5198,10 +6114,12 @@ Working rules:
5198
6114
  - Never fabricate numbers \u2014 read the actual logs first
5199
6115
  - Always include file:line references for errors
5200
6116
  - If sessionPath is missing, ask the director to provide it
5201
- - Report confidence level: high (>90% accuracy), medium, low`,
5202
- maxIterations: 50,
5203
- maxToolCalls: 200,
5204
- timeoutMs: 12e4
6117
+ - Report confidence level: high (>90% accuracy), medium, low`
6118
+ // No hardcoded budgets — the orchestrator (delegate tool or
6119
+ // spawn_subagent) decides per-task how much room a subagent gets.
6120
+ // A monorepo audit needs hours; a single-file lint check needs
6121
+ // seconds. Pinning a number here forces the orchestrator to fight
6122
+ // the role's default instead of just asking for what it needs.
5205
6123
  };
5206
6124
  var BUG_HUNTER_AGENT = {
5207
6125
  id: "bug-hunter",
@@ -5241,10 +6159,8 @@ Working rules:
5241
6159
  - Never scan node_modules \u2014 it's noise
5242
6160
  - Always include file:line for every finding
5243
6161
  - If >30% of findings are false positives, note the confidence level
5244
- - Ask director for clarification if paths are ambiguous`,
5245
- maxIterations: 80,
5246
- maxToolCalls: 300,
5247
- timeoutMs: 18e4
6162
+ - Ask director for clarification if paths are ambiguous`
6163
+ // Budgets are set by the orchestrator per task — see fleet.ts header.
5248
6164
  };
5249
6165
  var REFACTOR_PLANNER_AGENT = {
5250
6166
  id: "refactor-planner",
@@ -5284,10 +6200,8 @@ Working rules:
5284
6200
  - Always include rollback strategy \u2014 every refactor can fail
5285
6201
  - Merge tasks that take <1h into a single phase
5286
6202
  - Respect team constraints (reviewer availability, parallelization)
5287
- - Never plan without analyzing the actual code first`,
5288
- maxIterations: 60,
5289
- maxToolCalls: 250,
5290
- timeoutMs: 15e4
6203
+ - Never plan without analyzing the actual code first`
6204
+ // Budgets are set by the orchestrator per task — see fleet.ts header.
5291
6205
  };
5292
6206
  var SECURITY_SCANNER_AGENT = {
5293
6207
  id: "security-scanner",
@@ -5335,10 +6249,8 @@ Working rules:
5335
6249
  - Never scan node_modules \u2014 use npm audit instead
5336
6250
  - Always provide remediation steps, not just findings
5337
6251
  - Verify regex-based secrets before flagging (false positive risk)
5338
- - When in doubt, flag as medium rather than ignoring potential issues`,
5339
- maxIterations: 70,
5340
- maxToolCalls: 280,
5341
- timeoutMs: 16e4
6252
+ - When in doubt, flag as medium rather than ignoring potential issues`
6253
+ // Budgets are set by the orchestrator per task — see fleet.ts header.
5342
6254
  };
5343
6255
  var FLEET_ROSTER = {
5344
6256
  "audit-log": AUDIT_LOG_AGENT,
@@ -6959,7 +7871,7 @@ async function startMetricsServer(opts) {
6959
7871
  const tls = opts.tls;
6960
7872
  const useHttps = !!(tls?.cert && tls?.key);
6961
7873
  const host = opts.host ?? "127.0.0.1";
6962
- const path14 = opts.path ?? "/metrics";
7874
+ const path15 = opts.path ?? "/metrics";
6963
7875
  const healthPath = opts.healthPath ?? "/healthz";
6964
7876
  const healthRegistry = opts.healthRegistry;
6965
7877
  const listener = (req, res) => {
@@ -6969,7 +7881,7 @@ async function startMetricsServer(opts) {
6969
7881
  return;
6970
7882
  }
6971
7883
  const url = req.url.split("?")[0];
6972
- if (url === path14) {
7884
+ if (url === path15) {
6973
7885
  let body;
6974
7886
  try {
6975
7887
  body = renderPrometheus(opts.sink.snapshot());
@@ -7033,7 +7945,7 @@ async function startMetricsServer(opts) {
7033
7945
  const protocol = useHttps ? "https" : "http";
7034
7946
  return {
7035
7947
  port: boundPort,
7036
- url: `${protocol}://${host}:${boundPort}${path14}`,
7948
+ url: `${protocol}://${host}:${boundPort}${path15}`,
7037
7949
  close: () => new Promise((resolve2, reject) => {
7038
7950
  server.close((err) => err ? reject(err) : resolve2());
7039
7951
  })
@@ -7579,6 +8491,6 @@ var allServers = () => ({
7579
8491
  sentinel: { ...sentinelServer(), enabled: false }
7580
8492
  });
7581
8493
 
7582
- export { ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoCompactionMiddleware, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorBudgetError, DoneConditionChecker, FLEET_ROSTER, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SubagentBudget, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, allServers, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, everArtServer, filesystemServer, githubServer, googleMapsServer, loadProjectModes, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, migratePlaintextSecrets, renderPrometheus, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, wireMetricsToEvents };
8494
+ export { ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, FLEET_ROSTER, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SubagentBudget, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, addPlanItem, allServers, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createContextManagerTool, createDelegateTool, createMessage, decryptConfigSecrets, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, githubServer, googleMapsServer, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, migratePlaintextSecrets, removePlanItem, renderPrometheus, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, wireMetricsToEvents };
7583
8495
  //# sourceMappingURL=index.js.map
7584
8496
  //# sourceMappingURL=index.js.map