@oni.bot/core 1.0.2 → 1.1.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 (293) hide show
  1. package/CHANGELOG.md +146 -146
  2. package/dist/agents/define-agent.d.ts.map +1 -1
  3. package/dist/agents/define-agent.js +7 -2
  4. package/dist/agents/define-agent.js.map +1 -1
  5. package/dist/checkpoint.d.ts.map +1 -1
  6. package/dist/checkpoint.js +7 -2
  7. package/dist/checkpoint.js.map +1 -1
  8. package/dist/checkpointers/postgres.d.ts.map +1 -1
  9. package/dist/checkpointers/postgres.js +45 -28
  10. package/dist/checkpointers/postgres.js.map +1 -1
  11. package/dist/circuit-breaker.d.ts +1 -0
  12. package/dist/circuit-breaker.d.ts.map +1 -1
  13. package/dist/circuit-breaker.js +13 -0
  14. package/dist/circuit-breaker.js.map +1 -1
  15. package/dist/cli/dev.d.ts.map +1 -1
  16. package/dist/cli/dev.js +0 -1
  17. package/dist/cli/dev.js.map +1 -1
  18. package/dist/cli/inspect.d.ts.map +1 -1
  19. package/dist/cli/inspect.js +4 -2
  20. package/dist/cli/inspect.js.map +1 -1
  21. package/dist/cli/run.d.ts.map +1 -1
  22. package/dist/cli/run.js +0 -1
  23. package/dist/cli/run.js.map +1 -1
  24. package/dist/config/loader.d.ts +1 -1
  25. package/dist/config/loader.js +12 -4
  26. package/dist/config/loader.js.map +1 -1
  27. package/dist/coordination/pubsub.d.ts +1 -0
  28. package/dist/coordination/pubsub.d.ts.map +1 -1
  29. package/dist/coordination/pubsub.js +31 -16
  30. package/dist/coordination/pubsub.js.map +1 -1
  31. package/dist/coordination/request-reply.d.ts +17 -2
  32. package/dist/coordination/request-reply.d.ts.map +1 -1
  33. package/dist/coordination/request-reply.js +56 -14
  34. package/dist/coordination/request-reply.js.map +1 -1
  35. package/dist/events/bus.d.ts +1 -0
  36. package/dist/events/bus.d.ts.map +1 -1
  37. package/dist/events/bus.js +17 -10
  38. package/dist/events/bus.js.map +1 -1
  39. package/dist/functional.d.ts.map +1 -1
  40. package/dist/functional.js +3 -0
  41. package/dist/functional.js.map +1 -1
  42. package/dist/graph.d.ts +11 -1
  43. package/dist/graph.d.ts.map +1 -1
  44. package/dist/graph.js +9 -4
  45. package/dist/graph.js.map +1 -1
  46. package/dist/guardrails/audit.d.ts +4 -1
  47. package/dist/guardrails/audit.d.ts.map +1 -1
  48. package/dist/guardrails/audit.js +18 -1
  49. package/dist/guardrails/audit.js.map +1 -1
  50. package/dist/harness/agent-loop.d.ts +1 -7
  51. package/dist/harness/agent-loop.d.ts.map +1 -1
  52. package/dist/harness/agent-loop.js +2 -523
  53. package/dist/harness/agent-loop.js.map +1 -1
  54. package/dist/harness/context-compactor.d.ts +1 -0
  55. package/dist/harness/context-compactor.d.ts.map +1 -1
  56. package/dist/harness/context-compactor.js +43 -1
  57. package/dist/harness/context-compactor.js.map +1 -1
  58. package/dist/harness/harness.d.ts +6 -0
  59. package/dist/harness/harness.d.ts.map +1 -1
  60. package/dist/harness/harness.js +32 -5
  61. package/dist/harness/harness.js.map +1 -1
  62. package/dist/harness/hooks-engine.d.ts.map +1 -1
  63. package/dist/harness/hooks-engine.js +12 -10
  64. package/dist/harness/hooks-engine.js.map +1 -1
  65. package/dist/harness/index.d.ts +3 -1
  66. package/dist/harness/index.d.ts.map +1 -1
  67. package/dist/harness/index.js +2 -0
  68. package/dist/harness/index.js.map +1 -1
  69. package/dist/harness/loop/hooks.d.ts +7 -0
  70. package/dist/harness/loop/hooks.d.ts.map +1 -0
  71. package/dist/harness/loop/hooks.js +46 -0
  72. package/dist/harness/loop/hooks.js.map +1 -0
  73. package/dist/harness/loop/index.d.ts +8 -0
  74. package/dist/harness/loop/index.d.ts.map +1 -0
  75. package/dist/harness/loop/index.js +257 -0
  76. package/dist/harness/loop/index.js.map +1 -0
  77. package/dist/harness/loop/inference.d.ts +19 -0
  78. package/dist/harness/loop/inference.d.ts.map +1 -0
  79. package/dist/harness/loop/inference.js +121 -0
  80. package/dist/harness/loop/inference.js.map +1 -0
  81. package/dist/harness/loop/memory.d.ts +22 -0
  82. package/dist/harness/loop/memory.d.ts.map +1 -0
  83. package/dist/harness/loop/memory.js +73 -0
  84. package/dist/harness/loop/memory.js.map +1 -0
  85. package/dist/harness/loop/safety.d.ts +8 -0
  86. package/dist/harness/loop/safety.d.ts.map +1 -0
  87. package/dist/harness/loop/safety.js +21 -0
  88. package/dist/harness/loop/safety.js.map +1 -0
  89. package/dist/harness/loop/tools.d.ts +24 -0
  90. package/dist/harness/loop/tools.d.ts.map +1 -0
  91. package/dist/harness/loop/tools.js +184 -0
  92. package/dist/harness/loop/tools.js.map +1 -0
  93. package/dist/harness/loop/types.d.ts +7 -0
  94. package/dist/harness/loop/types.d.ts.map +1 -0
  95. package/dist/harness/loop/types.js +9 -0
  96. package/dist/harness/loop/types.js.map +1 -0
  97. package/dist/harness/memory/fs-compat.d.ts +3 -0
  98. package/dist/harness/memory/fs-compat.d.ts.map +1 -0
  99. package/dist/harness/memory/fs-compat.js +26 -0
  100. package/dist/harness/memory/fs-compat.js.map +1 -0
  101. package/dist/harness/memory/index.d.ts +105 -0
  102. package/dist/harness/memory/index.d.ts.map +1 -0
  103. package/dist/harness/memory/index.js +491 -0
  104. package/dist/harness/memory/index.js.map +1 -0
  105. package/dist/harness/memory/prompter.d.ts +7 -0
  106. package/dist/harness/memory/prompter.d.ts.map +1 -0
  107. package/dist/harness/memory/prompter.js +24 -0
  108. package/dist/harness/memory/prompter.js.map +1 -0
  109. package/dist/harness/memory/ranker.d.ts +15 -0
  110. package/dist/harness/memory/ranker.d.ts.map +1 -0
  111. package/dist/harness/memory/ranker.js +72 -0
  112. package/dist/harness/memory/ranker.js.map +1 -0
  113. package/dist/harness/memory/scanner.d.ts +26 -0
  114. package/dist/harness/memory/scanner.d.ts.map +1 -0
  115. package/dist/harness/memory/scanner.js +187 -0
  116. package/dist/harness/memory/scanner.js.map +1 -0
  117. package/dist/harness/memory/types.d.ts +50 -0
  118. package/dist/harness/memory/types.d.ts.map +1 -0
  119. package/dist/harness/memory/types.js +7 -0
  120. package/dist/harness/memory/types.js.map +1 -0
  121. package/dist/harness/memory-loader.d.ts +3 -0
  122. package/dist/harness/memory-loader.d.ts.map +1 -0
  123. package/dist/harness/memory-loader.js +2 -0
  124. package/dist/harness/memory-loader.js.map +1 -0
  125. package/dist/harness/safety-gate.d.ts.map +1 -1
  126. package/dist/harness/safety-gate.js +47 -26
  127. package/dist/harness/safety-gate.js.map +1 -1
  128. package/dist/harness/skill-loader.d.ts +7 -0
  129. package/dist/harness/skill-loader.d.ts.map +1 -1
  130. package/dist/harness/skill-loader.js +24 -8
  131. package/dist/harness/skill-loader.js.map +1 -1
  132. package/dist/harness/todo-module.d.ts.map +1 -1
  133. package/dist/harness/todo-module.js +13 -6
  134. package/dist/harness/todo-module.js.map +1 -1
  135. package/dist/harness/types.d.ts +7 -0
  136. package/dist/harness/types.d.ts.map +1 -1
  137. package/dist/harness/types.js.map +1 -1
  138. package/dist/harness/validate-args.js +18 -3
  139. package/dist/harness/validate-args.js.map +1 -1
  140. package/dist/hitl/interrupt.d.ts +2 -2
  141. package/dist/hitl/interrupt.d.ts.map +1 -1
  142. package/dist/hitl/interrupt.js +8 -5
  143. package/dist/hitl/interrupt.js.map +1 -1
  144. package/dist/hitl/resume.d.ts +10 -0
  145. package/dist/hitl/resume.d.ts.map +1 -1
  146. package/dist/hitl/resume.js +31 -0
  147. package/dist/hitl/resume.js.map +1 -1
  148. package/dist/injected.d.ts.map +1 -1
  149. package/dist/injected.js.map +1 -1
  150. package/dist/inspect.d.ts.map +1 -1
  151. package/dist/inspect.js +28 -8
  152. package/dist/inspect.js.map +1 -1
  153. package/dist/lsp/client.d.ts +2 -0
  154. package/dist/lsp/client.d.ts.map +1 -1
  155. package/dist/lsp/client.js +62 -17
  156. package/dist/lsp/client.js.map +1 -1
  157. package/dist/lsp/index.d.ts.map +1 -1
  158. package/dist/lsp/index.js.map +1 -1
  159. package/dist/mcp/client.d.ts +2 -0
  160. package/dist/mcp/client.d.ts.map +1 -1
  161. package/dist/mcp/client.js +44 -13
  162. package/dist/mcp/client.js.map +1 -1
  163. package/dist/mcp/convert.js +1 -1
  164. package/dist/mcp/convert.js.map +1 -1
  165. package/dist/mcp/transport.d.ts +2 -0
  166. package/dist/mcp/transport.d.ts.map +1 -1
  167. package/dist/mcp/transport.js +33 -8
  168. package/dist/mcp/transport.js.map +1 -1
  169. package/dist/messages/index.d.ts.map +1 -1
  170. package/dist/messages/index.js +7 -1
  171. package/dist/messages/index.js.map +1 -1
  172. package/dist/models/anthropic.d.ts.map +1 -1
  173. package/dist/models/anthropic.js +25 -15
  174. package/dist/models/anthropic.js.map +1 -1
  175. package/dist/models/google.d.ts.map +1 -1
  176. package/dist/models/google.js +23 -7
  177. package/dist/models/google.js.map +1 -1
  178. package/dist/models/ollama.d.ts.map +1 -1
  179. package/dist/models/ollama.js +11 -1
  180. package/dist/models/ollama.js.map +1 -1
  181. package/dist/models/openai.d.ts.map +1 -1
  182. package/dist/models/openai.js +15 -3
  183. package/dist/models/openai.js.map +1 -1
  184. package/dist/models/openrouter.d.ts.map +1 -1
  185. package/dist/models/openrouter.js +14 -3
  186. package/dist/models/openrouter.js.map +1 -1
  187. package/dist/prebuilt/react-agent.d.ts.map +1 -1
  188. package/dist/prebuilt/react-agent.js +7 -2
  189. package/dist/prebuilt/react-agent.js.map +1 -1
  190. package/dist/pregel/checkpointing.d.ts +12 -0
  191. package/dist/pregel/checkpointing.d.ts.map +1 -0
  192. package/dist/pregel/checkpointing.js +60 -0
  193. package/dist/pregel/checkpointing.js.map +1 -0
  194. package/dist/pregel/execution.d.ts +7 -0
  195. package/dist/pregel/execution.d.ts.map +1 -0
  196. package/dist/pregel/execution.js +178 -0
  197. package/dist/pregel/execution.js.map +1 -0
  198. package/dist/pregel/index.d.ts +61 -0
  199. package/dist/pregel/index.d.ts.map +1 -0
  200. package/dist/pregel/index.js +154 -0
  201. package/dist/pregel/index.js.map +1 -0
  202. package/dist/pregel/interrupts.d.ts +3 -0
  203. package/dist/pregel/interrupts.d.ts.map +1 -0
  204. package/dist/pregel/interrupts.js +7 -0
  205. package/dist/pregel/interrupts.js.map +1 -0
  206. package/dist/pregel/state-helpers.d.ts +12 -0
  207. package/dist/pregel/state-helpers.d.ts.map +1 -0
  208. package/dist/pregel/state-helpers.js +71 -0
  209. package/dist/pregel/state-helpers.js.map +1 -0
  210. package/dist/pregel/streaming.d.ts +5 -0
  211. package/dist/pregel/streaming.d.ts.map +1 -0
  212. package/dist/pregel/streaming.js +462 -0
  213. package/dist/pregel/streaming.js.map +1 -0
  214. package/dist/pregel/types.d.ts +48 -0
  215. package/dist/pregel/types.d.ts.map +1 -0
  216. package/dist/pregel/types.js +5 -0
  217. package/dist/pregel/types.js.map +1 -0
  218. package/dist/pregel.d.ts +1 -63
  219. package/dist/pregel.d.ts.map +1 -1
  220. package/dist/pregel.js +2 -804
  221. package/dist/pregel.js.map +1 -1
  222. package/dist/retry.d.ts.map +1 -1
  223. package/dist/retry.js +7 -6
  224. package/dist/retry.js.map +1 -1
  225. package/dist/store/index.d.ts.map +1 -1
  226. package/dist/store/index.js +36 -9
  227. package/dist/store/index.js.map +1 -1
  228. package/dist/stream-events.d.ts.map +1 -1
  229. package/dist/stream-events.js +3 -9
  230. package/dist/stream-events.js.map +1 -1
  231. package/dist/swarm/agent-node.d.ts +11 -0
  232. package/dist/swarm/agent-node.d.ts.map +1 -0
  233. package/dist/swarm/agent-node.js +156 -0
  234. package/dist/swarm/agent-node.js.map +1 -0
  235. package/dist/swarm/compile-ext.d.ts +5 -0
  236. package/dist/swarm/compile-ext.d.ts.map +1 -0
  237. package/dist/swarm/compile-ext.js +126 -0
  238. package/dist/swarm/compile-ext.js.map +1 -0
  239. package/dist/swarm/config.d.ts +147 -0
  240. package/dist/swarm/config.d.ts.map +1 -0
  241. package/dist/swarm/config.js +17 -0
  242. package/dist/swarm/config.js.map +1 -0
  243. package/dist/swarm/factories.d.ts +37 -0
  244. package/dist/swarm/factories.d.ts.map +1 -0
  245. package/dist/swarm/factories.js +703 -0
  246. package/dist/swarm/factories.js.map +1 -0
  247. package/dist/swarm/graph.d.ts +16 -136
  248. package/dist/swarm/graph.d.ts.map +1 -1
  249. package/dist/swarm/graph.js +39 -897
  250. package/dist/swarm/graph.js.map +1 -1
  251. package/dist/swarm/index.d.ts +2 -1
  252. package/dist/swarm/index.d.ts.map +1 -1
  253. package/dist/swarm/index.js.map +1 -1
  254. package/dist/swarm/mailbox.d.ts.map +1 -1
  255. package/dist/swarm/mailbox.js +3 -1
  256. package/dist/swarm/mailbox.js.map +1 -1
  257. package/dist/swarm/mermaid.d.ts +2 -1
  258. package/dist/swarm/mermaid.d.ts.map +1 -1
  259. package/dist/swarm/mermaid.js +6 -3
  260. package/dist/swarm/mermaid.js.map +1 -1
  261. package/dist/swarm/pool.d.ts.map +1 -1
  262. package/dist/swarm/pool.js +18 -1
  263. package/dist/swarm/pool.js.map +1 -1
  264. package/dist/swarm/registry.d.ts.map +1 -1
  265. package/dist/swarm/registry.js +7 -0
  266. package/dist/swarm/registry.js.map +1 -1
  267. package/dist/swarm/scaling.d.ts +10 -1
  268. package/dist/swarm/scaling.d.ts.map +1 -1
  269. package/dist/swarm/scaling.js +85 -14
  270. package/dist/swarm/scaling.js.map +1 -1
  271. package/dist/swarm/snapshot.d.ts.map +1 -1
  272. package/dist/swarm/snapshot.js +10 -1
  273. package/dist/swarm/snapshot.js.map +1 -1
  274. package/dist/swarm/supervisor.js +20 -12
  275. package/dist/swarm/supervisor.js.map +1 -1
  276. package/dist/swarm/tracer.d.ts +3 -1
  277. package/dist/swarm/tracer.d.ts.map +1 -1
  278. package/dist/swarm/tracer.js +66 -15
  279. package/dist/swarm/tracer.js.map +1 -1
  280. package/dist/swarm/types.d.ts +1 -6
  281. package/dist/swarm/types.d.ts.map +1 -1
  282. package/dist/testing/index.d.ts +4 -4
  283. package/dist/testing/index.d.ts.map +1 -1
  284. package/dist/testing/index.js +3 -2
  285. package/dist/testing/index.js.map +1 -1
  286. package/dist/tools/define.d.ts +2 -1
  287. package/dist/tools/define.d.ts.map +1 -1
  288. package/dist/tools/define.js +3 -1
  289. package/dist/tools/define.js.map +1 -1
  290. package/dist/tools/types.d.ts.map +1 -1
  291. package/dist/types.d.ts +3 -1
  292. package/dist/types.d.ts.map +1 -1
  293. package/package.json +1 -1
@@ -8,37 +8,26 @@
8
8
  // - compile() returns a full ONISkeleton
9
9
  // ============================================================
10
10
  import { StateGraph } from "../graph.js";
11
- import { START, END, appendList, lastValue, mergeObject } from "../types.js";
12
- import { Command, Send } from "../types.js";
13
- import { Handoff } from "./types.js";
11
+ import { START, END } from "../types.js";
14
12
  import { AgentRegistry } from "./registry.js";
13
+ import { baseSwarmChannels } from "./config.js";
15
14
  import { createSupervisorNode } from "./supervisor.js";
16
- import { getInbox } from "./mailbox.js";
17
15
  import { RequestReplyBroker } from "../coordination/request-reply.js";
18
16
  import { PubSub } from "../coordination/pubsub.js";
19
- import { runWithTimeout } from "../internal/timeout.js";
20
- export const baseSwarmChannels = {
21
- task: lastValue(() => ""),
22
- context: mergeObject(() => ({})),
23
- agentResults: mergeObject(() => ({})),
24
- messages: appendList(() => []),
25
- swarmMessages: appendList(() => []),
26
- supervisorRound: lastValue(() => 0),
27
- currentAgent: lastValue(() => null),
28
- done: lastValue(() => false),
29
- handoffHistory: appendList(() => []),
30
- };
17
+ import { createAgentNode } from "./agent-node.js";
18
+ import { buildSwarmExtensions } from "./compile-ext.js";
19
+ import { buildHierarchical, buildFanOut, buildPipeline, buildPeerNetwork, buildMapReduce, buildDebate, buildHierarchicalMesh, buildRace, buildDag, buildPool, buildCompose, } from "./factories.js";
31
20
  // ----------------------------------------------------------------
32
21
  // SwarmGraph
33
22
  // ----------------------------------------------------------------
34
23
  export class SwarmGraph {
35
- inner;
36
- registry;
37
- channels;
38
- agentIds = new Set();
39
- supervisorNodeName = "__supervisor__";
40
- hasSupervisor = false;
41
- onErrorPolicy = "fallback";
24
+ /** @internal */ inner;
25
+ /** @internal */ registry;
26
+ /** @internal */ channels;
27
+ /** @internal */ agentIds = new Set();
28
+ /** @internal */ supervisorNodeName = "__supervisor__";
29
+ /** @internal */ hasSupervisor = false;
30
+ /** @internal */ onErrorPolicy = "fallback";
42
31
  _broker;
43
32
  _pubsub;
44
33
  get broker() {
@@ -62,23 +51,7 @@ export class SwarmGraph {
62
51
  * ready-to-compile SwarmGraph.
63
52
  */
64
53
  static hierarchical(config) {
65
- const swarm = new SwarmGraph(config.channels);
66
- for (const agentDef of config.agents) {
67
- swarm.addAgent(agentDef);
68
- }
69
- swarm.addSupervisor({
70
- model: config.supervisor.model,
71
- strategy: config.supervisor.strategy,
72
- taskField: "task",
73
- contextField: "context",
74
- rules: config.supervisor.rules,
75
- systemPrompt: config.supervisor.systemPrompt,
76
- maxRounds: config.supervisor.maxRounds,
77
- deadlineMs: config.supervisor.deadlineMs,
78
- autoRecover: config.supervisor.autoRecover,
79
- });
80
- swarm.onErrorPolicy = config.onError ?? "fallback";
81
- return swarm;
54
+ return buildHierarchical(config, new SwarmGraph(config.channels));
82
55
  }
83
56
  // ---- Static factory: fan-out template ----
84
57
  /**
@@ -86,68 +59,7 @@ export class SwarmGraph {
86
59
  * and collects results through a reducer node.
87
60
  */
88
61
  static fanOut(config) {
89
- const swarm = new SwarmGraph(config.channels);
90
- const agentIds = config.agents.map((a) => a.id);
91
- const maxConcurrency = config.maxConcurrency;
92
- const timeoutMs = config.timeoutMs;
93
- const weights = config.weights;
94
- // Register agents in registry (but don't use addAgent — we wire manually)
95
- for (const agentDef of config.agents) {
96
- swarm.registry.register(agentDef);
97
- swarm.agentIds.add(agentDef.id);
98
- }
99
- // Single orchestrator node that runs agents with concurrency/timeout control
100
- swarm.inner.addNode("__fanout_runner__", async (state, cfg) => {
101
- const agentMap = new Map(config.agents.map((a) => [a.id, a]));
102
- async function runAgent(id) {
103
- const agent = agentMap.get(id);
104
- try {
105
- await agent.hooks?.onStart?.(id, state);
106
- const result = await runWithTimeout(() => agent.skeleton.invoke({ ...state }, { ...cfg, agentId: id }), timeoutMs, () => new Error(`Agent "${id}" timed out after ${timeoutMs}ms`));
107
- await agent.hooks?.onComplete?.(id, result);
108
- return { id, result, error: null };
109
- }
110
- catch (err) {
111
- await agent.hooks?.onError?.(id, err);
112
- return { id, result: null, error: err };
113
- }
114
- }
115
- let allResults;
116
- if (maxConcurrency != null && maxConcurrency > 0) {
117
- // Batched execution with concurrency limit
118
- allResults = [];
119
- const remaining = [...agentIds];
120
- while (remaining.length > 0) {
121
- const batch = remaining.splice(0, maxConcurrency);
122
- const batchResults = await Promise.all(batch.map(runAgent));
123
- allResults.push(...batchResults);
124
- }
125
- }
126
- else {
127
- // All in parallel
128
- allResults = await Promise.all(agentIds.map(runAgent));
129
- }
130
- // Collect results
131
- const agentResults = { ...(state.agentResults ?? {}) };
132
- for (const { id, result, error } of allResults) {
133
- if (error) {
134
- agentResults[id] = {
135
- _error: true,
136
- agent: id,
137
- error: String(error instanceof Error ? error.message : error),
138
- };
139
- }
140
- else {
141
- agentResults[id] = result;
142
- }
143
- }
144
- // Run reducer with weights
145
- const reduced = config.reducer(agentResults, weights);
146
- return { ...reduced, agentResults };
147
- });
148
- swarm.inner.addEdge(START, "__fanout_runner__");
149
- swarm.inner.addEdge("__fanout_runner__", END);
150
- return swarm;
62
+ return buildFanOut(config, new SwarmGraph(config.channels));
151
63
  }
152
64
  // ---- Static factory: pipeline template ----
153
65
  /**
@@ -156,35 +68,7 @@ export class SwarmGraph {
156
68
  * transition (e.g. loop back for review cycles).
157
69
  */
158
70
  static pipeline(config) {
159
- if (config.stages.length === 0) {
160
- throw new Error("SwarmGraph.pipeline: stages must contain at least one agent.");
161
- }
162
- const swarm = new SwarmGraph(config.channels);
163
- const ids = config.stages.map((a) => a.id);
164
- for (const agentDef of config.stages) {
165
- swarm.addAgent(agentDef);
166
- }
167
- // Wire: START → first stage
168
- swarm.inner.addEdge(START, ids[0]);
169
- // Wire each stage to the next (or conditional)
170
- for (let i = 0; i < ids.length - 1; i++) {
171
- const id = ids[i];
172
- if (config.transitions?.[id]) {
173
- swarm.addConditionalHandoff(id, config.transitions[id]);
174
- }
175
- else {
176
- swarm.inner.addEdge(id, ids[i + 1]);
177
- }
178
- }
179
- // Wire last stage
180
- const lastId = ids[ids.length - 1];
181
- if (config.transitions?.[lastId]) {
182
- swarm.addConditionalHandoff(lastId, config.transitions[lastId]);
183
- }
184
- else {
185
- swarm.inner.addEdge(lastId, END);
186
- }
187
- return swarm;
71
+ return buildPipeline(config, new SwarmGraph(config.channels));
188
72
  }
189
73
  // ---- Static factory: peer network template ----
190
74
  /**
@@ -192,17 +76,7 @@ export class SwarmGraph {
192
76
  * via conditional handoffs, with no supervisor.
193
77
  */
194
78
  static peerNetwork(config) {
195
- const swarm = new SwarmGraph(config.channels);
196
- for (const agentDef of config.agents) {
197
- swarm.addAgent(agentDef);
198
- }
199
- // Wire: START → entrypoint
200
- swarm.inner.addEdge(START, config.entrypoint);
201
- // Wire each agent's conditional handoff
202
- for (const [agentId, handoffFn] of Object.entries(config.handoffs)) {
203
- swarm.addConditionalHandoff(agentId, handoffFn);
204
- }
205
- return swarm;
79
+ return buildPeerNetwork(config, new SwarmGraph(config.channels));
206
80
  }
207
81
  // ---- Static factory: map-reduce template ----
208
82
  /**
@@ -210,74 +84,7 @@ export class SwarmGraph {
210
84
  * fan-out via Send, and collect results through a reducer node.
211
85
  */
212
86
  static mapReduce(config) {
213
- const poolSize = config.poolSize ?? 1;
214
- if (poolSize < 1) {
215
- throw new Error("SwarmGraph.mapReduce: poolSize must be at least 1.");
216
- }
217
- const swarm = new SwarmGraph(config.channels);
218
- // Register poolSize copies of the mapper agent
219
- const mapperIds = [];
220
- for (let i = 0; i < poolSize; i++) {
221
- const id = poolSize === 1 ? config.mapper.id : `${config.mapper.id}_${i}`;
222
- mapperIds.push(id);
223
- swarm.addAgent({
224
- ...config.mapper,
225
- id,
226
- });
227
- }
228
- // Splitter node: passthrough — the conditional edge handles fan-out via Send
229
- swarm.inner.addNode("__splitter__", (_state) => {
230
- return {};
231
- });
232
- // Reducer node: collects agentResults and applies user reducer
233
- swarm.inner.addNode("__reducer__", (state) => {
234
- return config.reducer(state.agentResults ?? {});
235
- });
236
- // Wiring: START → __splitter__
237
- swarm.inner.addEdge(START, "__splitter__");
238
- // __splitter__ → Send to mapper agents using the configured poolStrategy
239
- const inputField = config.inputField;
240
- const strategy = config.poolStrategy ?? "round-robin";
241
- swarm.inner.addConditionalEdges("__splitter__", ((state) => {
242
- const items = state[inputField];
243
- if (!Array.isArray(items) || items.length === 0) {
244
- return "__reducer__";
245
- }
246
- // Per-batch assignment counts — used by least-busy strategy
247
- const assignCounts = new Map(mapperIds.map((id) => [id, 0]));
248
- return items.map((item, idx) => {
249
- let targetId;
250
- if (strategy === "random") {
251
- targetId = mapperIds[Math.floor(Math.random() * mapperIds.length)];
252
- }
253
- else if (strategy === "least-busy") {
254
- // Pick the mapper with fewest assignments in this batch so far
255
- let minCount = Infinity;
256
- let minId = mapperIds[0];
257
- for (const id of mapperIds) {
258
- const c = assignCounts.get(id) ?? 0;
259
- if (c < minCount) {
260
- minCount = c;
261
- minId = id;
262
- }
263
- }
264
- targetId = minId;
265
- }
266
- else {
267
- // round-robin (default)
268
- targetId = mapperIds[idx % mapperIds.length];
269
- }
270
- assignCounts.set(targetId, (assignCounts.get(targetId) ?? 0) + 1);
271
- return new Send(targetId, { ...state, task: String(item) });
272
- });
273
- }));
274
- // Each mapper → __reducer__
275
- for (const id of mapperIds) {
276
- swarm.inner.addEdge(id, "__reducer__");
277
- }
278
- // __reducer__ → END
279
- swarm.inner.addEdge("__reducer__", END);
280
- return swarm;
87
+ return buildMapReduce(config, new SwarmGraph(config.channels));
281
88
  }
282
89
  // ---- Static factory: debate template ----
283
90
  /**
@@ -286,108 +93,7 @@ export class SwarmGraph {
286
93
  * and decides whether consensus has been reached.
287
94
  */
288
95
  static debate(config) {
289
- if (config.debaters.length === 0) {
290
- throw new Error("SwarmGraph.debate: debaters must contain at least one agent.");
291
- }
292
- const swarm = new SwarmGraph(config.channels);
293
- const debaterIds = config.debaters.map((d) => d.id);
294
- const consensusKeyword = config.judge.consensusKeyword ?? "CONSENSUS";
295
- for (const debater of config.debaters) {
296
- swarm.addAgent(debater);
297
- }
298
- const scoreDebaters = config.judge.scoreDebaters ?? false;
299
- const consensusThreshold = config.judge.consensusThreshold;
300
- // Judge node: evaluates arguments, decides continue or consensus
301
- swarm.inner.addNode("__judge__", async (state) => {
302
- const round = state.supervisorRound ?? 0;
303
- // If no agent results yet (first round), just kick off debaters
304
- const results = state.agentResults ?? {};
305
- if (round === 0 && Object.keys(results).length === 0) {
306
- return {
307
- supervisorRound: round + 1,
308
- };
309
- }
310
- // Evaluate arguments via judge model
311
- const argsText = Object.entries(results)
312
- .map(([id, r]) => `${id}: ${JSON.stringify(r)}`)
313
- .join("\n\n");
314
- const scoreInstruction = scoreDebaters
315
- ? `\nAlso provide a JSON object with scores for each debater and a verdict. Format: {"scores": {"debater_id": score}, "verdict": "CONTINUE" or "${consensusKeyword}"}`
316
- : "";
317
- const response = await config.judge.model.chat({
318
- messages: [{
319
- role: "user",
320
- content: `Round ${round}. Evaluate these arguments:\n\n${argsText}\n\nRespond "${consensusKeyword}" if consensus reached, otherwise "CONTINUE".${scoreInstruction}`,
321
- }],
322
- systemPrompt: config.judge.systemPrompt ?? "You are a debate judge.",
323
- });
324
- let isConsensus = false;
325
- let roundScores;
326
- const existingScores = (state.context.debateScores ?? []);
327
- // Try to parse structured response (JSON with scores + verdict)
328
- if (scoreDebaters) {
329
- try {
330
- const parsed = JSON.parse(response.content);
331
- if (parsed.scores) {
332
- roundScores = parsed.scores;
333
- }
334
- if (parsed.verdict) {
335
- isConsensus = parsed.verdict.includes(consensusKeyword);
336
- }
337
- }
338
- catch {
339
- // Fallback to keyword detection
340
- isConsensus = response.content.includes(consensusKeyword);
341
- }
342
- // Check consensus threshold if scores available
343
- if (!isConsensus && roundScores && consensusThreshold != null) {
344
- const scoreValues = Object.values(roundScores);
345
- if (scoreValues.length >= 2) {
346
- const spread = Math.max(...scoreValues) - Math.min(...scoreValues);
347
- if (spread <= consensusThreshold) {
348
- isConsensus = true;
349
- }
350
- }
351
- }
352
- }
353
- else {
354
- isConsensus = response.content.includes(consensusKeyword);
355
- }
356
- const nextRound = round + 1;
357
- // Force done if consensus or max rounds exhausted
358
- const isDone = isConsensus || nextRound > config.judge.maxRounds;
359
- const updatedScores = roundScores
360
- ? [...existingScores, { round, scores: roundScores }]
361
- : existingScores;
362
- return {
363
- done: isDone,
364
- supervisorRound: nextRound,
365
- context: {
366
- ...(state.context ?? {}),
367
- ...(scoreDebaters ? { debateScores: updatedScores } : {}),
368
- },
369
- messages: [{ role: "system", content: `Judge round ${round}: ${isDone ? "Consensus" : "Continue"}` }],
370
- };
371
- });
372
- // Fan-out node: passthrough — conditional edges handle the Send dispatch
373
- swarm.inner.addNode("__fanout__", (_state) => {
374
- return {};
375
- });
376
- // Wiring: START → __judge__
377
- swarm.inner.addEdge(START, "__judge__");
378
- // __judge__ → conditional (done → END, else → __fanout__)
379
- swarm.inner.addConditionalEdges("__judge__", (state) => {
380
- if (state.done)
381
- return END;
382
- return "__fanout__";
383
- });
384
- // __fanout__ → Send to all debaters in parallel
385
- swarm.inner.addConditionalEdges("__fanout__", ((state) => debaterIds.map((id) => new Send(id, { ...state }))));
386
- // Each debater → __judge__
387
- for (const id of debaterIds) {
388
- swarm.inner.addEdge(id, "__judge__");
389
- }
390
- return swarm;
96
+ return buildDebate(config, new SwarmGraph(config.channels));
391
97
  }
392
98
  // ---- Static factory: hierarchical mesh template ----
393
99
  /**
@@ -396,108 +102,7 @@ export class SwarmGraph {
396
102
  * and compiled into a single node in the outer graph.
397
103
  */
398
104
  static hierarchicalMesh(config) {
399
- const swarm = new SwarmGraph(config.channels);
400
- const teamIds = Object.keys(config.teams);
401
- const maxRounds = config.coordinator.maxRounds ?? 10;
402
- // Build each team as a compiled sub-skeleton and mount as a node
403
- for (const [teamId, teamConfig] of Object.entries(config.teams)) {
404
- let teamSwarm;
405
- if (teamConfig.topology === "pipeline") {
406
- teamSwarm = SwarmGraph.pipeline({ stages: teamConfig.agents });
407
- }
408
- else {
409
- teamSwarm = SwarmGraph.peerNetwork({
410
- agents: teamConfig.agents,
411
- entrypoint: teamConfig.agents[0].id,
412
- handoffs: teamConfig.handoffs ?? {},
413
- });
414
- }
415
- const teamSkeleton = teamSwarm.compile();
416
- // Mount team as a single node in the outer graph.
417
- // Spread all top-level fields from teamResult (including done, context)
418
- // so they propagate to the outer coordinator state.
419
- swarm.inner.addNode(teamId, async (state, cfg) => {
420
- const teamResult = await teamSkeleton.invoke(state, cfg);
421
- return {
422
- ...teamResult,
423
- agentResults: {
424
- ...(state.agentResults ?? {}),
425
- [teamId]: teamResult,
426
- },
427
- };
428
- });
429
- }
430
- // Coordinator node: routes to teams
431
- swarm.inner.addNode("__coordinator__", async (state) => {
432
- const round = state.supervisorRound ?? 0;
433
- if (round >= maxRounds || state.done) {
434
- return { done: true };
435
- }
436
- if (config.coordinator.strategy === "llm" && config.coordinator.model) {
437
- const teamList = teamIds.map((id) => `- ${id}`).join("\n");
438
- const response = await config.coordinator.model.chat({
439
- messages: [{
440
- role: "user",
441
- content: `Task: ${state.task}\n\nAvailable teams:\n${teamList}\n\nRespond with the team name to route to, or "DONE" if complete.`,
442
- }],
443
- systemPrompt: config.coordinator.systemPrompt ?? "You coordinate teams.",
444
- });
445
- const picked = response.content.trim();
446
- if (picked === "DONE" || !teamIds.includes(picked)) {
447
- return { done: true, supervisorRound: round + 1 };
448
- }
449
- return new Command({
450
- update: { supervisorRound: round + 1, currentAgent: picked },
451
- goto: picked,
452
- });
453
- }
454
- // Round-robin strategy: route to teams in order, mark done after all visited
455
- if (config.coordinator.strategy === "round-robin") {
456
- const target = teamIds[round % teamIds.length];
457
- if (round >= teamIds.length) {
458
- return { done: true };
459
- }
460
- return new Command({
461
- update: { supervisorRound: round + 1 },
462
- goto: target,
463
- });
464
- }
465
- // Rule-based strategy: evaluate rules in order, route to the first match
466
- if (config.coordinator.strategy === "rule") {
467
- const rules = config.coordinator.rules ?? [];
468
- const task = String(state.task ?? "");
469
- const ctx = (state.context ?? {});
470
- for (const rule of rules) {
471
- if (rule.condition(task, ctx)) {
472
- return new Command({
473
- update: { supervisorRound: round + 1, currentAgent: rule.agentId },
474
- goto: rule.agentId,
475
- });
476
- }
477
- }
478
- // No matching rule — done
479
- return { done: true, supervisorRound: round + 1 };
480
- }
481
- return { done: true };
482
- });
483
- // Wiring: START → coordinator
484
- swarm.inner.addEdge(START, "__coordinator__");
485
- // Each team → coordinator (loop back unless done)
486
- for (const teamId of teamIds) {
487
- swarm.inner.addConditionalEdges(teamId, (state) => {
488
- if (state.done)
489
- return END;
490
- return "__coordinator__";
491
- });
492
- }
493
- // Coordinator → conditional (done → END, else handled by Command.goto)
494
- swarm.inner.addConditionalEdges("__coordinator__", (state) => {
495
- if (state.done)
496
- return END;
497
- // Command.goto handles routing — this is the fallback
498
- return END;
499
- });
500
- return swarm;
105
+ return buildHierarchicalMesh(config, new SwarmGraph(config.channels), (ch) => new SwarmGraph(ch));
501
106
  }
502
107
  // ---- Static factory: race template ----
503
108
  /**
@@ -505,67 +110,7 @@ export class SwarmGraph {
505
110
  * Optionally filter results with an `accept` predicate.
506
111
  */
507
112
  static race(config) {
508
- const swarm = new SwarmGraph(config.channels);
509
- const agentIds = config.agents.map((a) => a.id);
510
- const accept = config.accept ?? (() => true);
511
- const timeoutMs = config.timeoutMs;
512
- // Register agents so they appear in the registry
513
- for (const agentDef of config.agents) {
514
- swarm.registry.register(agentDef);
515
- swarm.agentIds.add(agentDef.id);
516
- }
517
- // Single node that races all agents — resolves as soon as one produces
518
- // an acceptable result (true Promise.race semantics, not Promise.all).
519
- swarm.inner.addNode("__race__", async (state, cfg) => {
520
- const agentPromises = config.agents.map((agent) => {
521
- const p = agent.skeleton
522
- .invoke({ ...state }, { ...cfg, agentId: agent.id })
523
- .then((result) => ({ id: agent.id, result, error: null }), (err) => ({ id: agent.id, result: null, error: err }));
524
- if (timeoutMs != null) {
525
- return Promise.race([
526
- p,
527
- new Promise((resolve) => setTimeout(() => resolve({ id: agent.id, result: null, error: new Error("timeout") }), timeoutMs)),
528
- ]);
529
- }
530
- return p;
531
- });
532
- // Resolve as soon as the first acceptable result arrives.
533
- const winner = await new Promise((resolve) => {
534
- let remaining = agentPromises.length;
535
- if (remaining === 0) {
536
- resolve(null);
537
- return;
538
- }
539
- for (const p of agentPromises) {
540
- p.then((r) => {
541
- if (!r.error && accept(r.result)) {
542
- resolve(r);
543
- }
544
- else {
545
- remaining--;
546
- if (remaining === 0)
547
- resolve(null);
548
- }
549
- });
550
- }
551
- });
552
- if (winner) {
553
- return {
554
- agentResults: { [winner.id]: winner.result },
555
- context: { ...(state.context ?? {}), raceWinner: winner.id },
556
- };
557
- }
558
- // No acceptable result
559
- return {
560
- context: {
561
- ...(state.context ?? {}),
562
- raceError: "No agent produced an acceptable result",
563
- },
564
- };
565
- });
566
- swarm.inner.addEdge(START, "__race__");
567
- swarm.inner.addEdge("__race__", END);
568
- return swarm;
113
+ return buildRace(config, new SwarmGraph(config.channels));
569
114
  }
570
115
  // ---- Static factory: dag template ----
571
116
  /**
@@ -573,82 +118,7 @@ export class SwarmGraph {
573
118
  * Agents with no dependencies run in parallel.
574
119
  */
575
120
  static dag(config) {
576
- const agentMap = new Map(config.agents.map((a) => [a.id, a]));
577
- // Validate dependencies
578
- for (const [node, deps] of Object.entries(config.dependencies)) {
579
- for (const dep of deps) {
580
- if (!agentMap.has(dep)) {
581
- throw new Error(`Dependency "${dep}" not found in agents list.`);
582
- }
583
- }
584
- }
585
- // Cycle detection via topological sort
586
- const visited = new Set();
587
- const visiting = new Set();
588
- const sorted = [];
589
- function visit(id) {
590
- if (visited.has(id))
591
- return;
592
- if (visiting.has(id))
593
- throw new Error(`Cycle detected involving "${id}".`);
594
- visiting.add(id);
595
- for (const dep of config.dependencies[id] ?? []) {
596
- visit(dep);
597
- }
598
- visiting.delete(id);
599
- visited.add(id);
600
- sorted.push(id);
601
- }
602
- for (const agent of config.agents) {
603
- visit(agent.id);
604
- }
605
- // Build a standard swarm with wiring based on topological sort
606
- const swarm = new SwarmGraph(config.channels);
607
- for (const agent of config.agents) {
608
- swarm.addAgent(agent);
609
- }
610
- // Group agents into layers based on dependencies
611
- const deps = config.dependencies;
612
- const rootIds = config.agents.filter((a) => !deps[a.id]?.length).map((a) => a.id);
613
- const nonRootIds = config.agents.filter((a) => deps[a.id]?.length).map((a) => a.id);
614
- // For DAG execution: use a single orchestrator node
615
- swarm.inner.addNode("__dag_runner__", async (state, cfg) => {
616
- const results = {};
617
- const completed = new Set();
618
- // Process in topological order
619
- // Group into parallel batches
620
- const remaining = new Set(sorted);
621
- while (remaining.size > 0) {
622
- // Find all nodes whose deps are satisfied
623
- const ready = [];
624
- for (const id of remaining) {
625
- const idDeps = deps[id] ?? [];
626
- if (idDeps.every((d) => completed.has(d))) {
627
- ready.push(id);
628
- }
629
- }
630
- // Execute ready nodes in parallel
631
- const batchResults = await Promise.all(ready.map(async (id) => {
632
- const agent = agentMap.get(id);
633
- const result = await agent.skeleton.invoke({ ...state, agentResults: { ...(state.agentResults ?? {}), ...results } }, { ...cfg, agentId: id });
634
- return { id, result };
635
- }));
636
- for (const { id, result } of batchResults) {
637
- results[id] = result;
638
- completed.add(id);
639
- remaining.delete(id);
640
- }
641
- }
642
- return {
643
- agentResults: { ...(state.agentResults ?? {}), ...results },
644
- };
645
- });
646
- // Wire: START → __dag_runner__ → END
647
- // Remove any edges added by addAgent
648
- swarm.inner.edges = [];
649
- swarm.inner.addEdge(START, "__dag_runner__");
650
- swarm.inner.addEdge("__dag_runner__", END);
651
- return swarm;
121
+ return buildDag(config, new SwarmGraph(config.channels));
652
122
  }
653
123
  // ---- Static factory: pool template ----
654
124
  /**
@@ -656,90 +126,7 @@ export class SwarmGraph {
656
126
  * then reduce all results.
657
127
  */
658
128
  static pool(config) {
659
- const swarm = new SwarmGraph(config.channels);
660
- const poolSize = config.poolSize;
661
- // Create pool copies
662
- const poolIds = [];
663
- for (let i = 0; i < poolSize; i++) {
664
- const id = poolSize === 1 ? config.agent.id : `${config.agent.id}_${i}`;
665
- poolIds.push(id);
666
- swarm.addAgent({ ...config.agent, id });
667
- }
668
- // Orchestrator node: dispatches items to pool agents and reduces
669
- swarm.inner.addNode("__pool_runner__", async (state, cfg) => {
670
- const items = state[config.inputField];
671
- if (!Array.isArray(items) || items.length === 0) {
672
- return config.reducer({});
673
- }
674
- // Semaphore for concurrency control
675
- let running = 0;
676
- const results = {};
677
- const queue = items.map((item, idx) => ({
678
- item,
679
- idx,
680
- targetId: poolIds[idx % poolIds.length],
681
- }));
682
- await new Promise((resolve, reject) => {
683
- let completed = 0;
684
- const total = queue.length;
685
- function processNext() {
686
- while (running < poolSize && queue.length > 0) {
687
- const work = queue.shift();
688
- // Respect removeAgent() — if the assigned slot was removed, redirect
689
- // to an active pool slot; if none remain, mark the item as failed.
690
- let agentDef = swarm.registry.getDef(work.targetId);
691
- if (!agentDef) {
692
- const activeIds = poolIds.filter((id) => !!swarm.registry.getDef(id));
693
- if (activeIds.length > 0) {
694
- work.targetId = activeIds[work.idx % activeIds.length];
695
- agentDef = swarm.registry.getDef(work.targetId);
696
- }
697
- else {
698
- results[`item_${work.idx}`] = { _error: `Pool slot removed; no active agents remain` };
699
- completed++;
700
- if (completed === total)
701
- resolve();
702
- continue;
703
- }
704
- }
705
- running++;
706
- // Use the full wrapped agentNode (hooks, retries, timeout) stored
707
- // by addAgent() in swarm.inner.nodes rather than raw skeleton.invoke.
708
- const wrappedFn = swarm.inner.nodes.get(work.targetId)?.fn;
709
- const invocation = wrappedFn
710
- ? wrappedFn({ ...state, task: String(work.item) }, { ...cfg, agentId: work.targetId })
711
- : agentDef.skeleton.invoke({ ...state, task: String(work.item) }, { ...cfg, agentId: work.targetId });
712
- invocation.then((result) => {
713
- results[`item_${work.idx}`] = result;
714
- running--;
715
- completed++;
716
- if (completed === total)
717
- resolve();
718
- else
719
- processNext();
720
- }, (err) => {
721
- results[`item_${work.idx}`] = { _error: String(err) };
722
- running--;
723
- completed++;
724
- if (completed === total)
725
- resolve();
726
- else
727
- processNext();
728
- });
729
- }
730
- }
731
- if (total === 0)
732
- resolve();
733
- else
734
- processNext();
735
- });
736
- return config.reducer(results);
737
- });
738
- // Wire: START → __pool_runner__ → END (bypass agent edges)
739
- swarm.inner.edges = [];
740
- swarm.inner.addEdge(START, "__pool_runner__");
741
- swarm.inner.addEdge("__pool_runner__", END);
742
- return swarm;
129
+ return buildPool(config, new SwarmGraph(config.channels));
743
130
  }
744
131
  // ---- Static factory: compose template ----
745
132
  /**
@@ -747,179 +134,13 @@ export class SwarmGraph {
747
134
  * Each stage runs a compiled sub-swarm, passing state through.
748
135
  */
749
136
  static compose(config) {
750
- if (config.stages.length === 0) {
751
- throw new Error("SwarmGraph.compose: stages must contain at least one sub-swarm.");
752
- }
753
- const swarm = new SwarmGraph(config.channels);
754
- const stageIds = config.stages.map((s) => s.id);
755
- for (const stage of config.stages) {
756
- const compiled = stage.swarm.compile();
757
- swarm.inner.addNode(stage.id, async (state, cfg) => {
758
- const stageResult = await compiled.invoke(state, cfg);
759
- return {
760
- ...stageResult,
761
- agentResults: {
762
- ...(state.agentResults ?? {}),
763
- [stage.id]: stageResult,
764
- },
765
- };
766
- });
767
- }
768
- // Wire pipeline: START → stage1 → stage2 → ... → END
769
- swarm.inner.addEdge(START, stageIds[0]);
770
- for (let i = 0; i < stageIds.length - 1; i++) {
771
- swarm.inner.addEdge(stageIds[i], stageIds[i + 1]);
772
- }
773
- swarm.inner.addEdge(stageIds[stageIds.length - 1], END);
774
- return swarm;
137
+ return buildCompose(config, new SwarmGraph(config.channels));
775
138
  }
776
139
  // ---- Agent registration ----
777
140
  addAgent(def) {
778
141
  this.registry.register(def);
779
142
  this.agentIds.add(def.id);
780
- // Wrap the agent's skeleton as a node
781
- // The node executes the agent's skeleton and handles Handoff returns
782
- const agentNode = async (state, config) => {
783
- this.registry.markBusy(def.id);
784
- // Fire onStart hook
785
- await def.hooks?.onStart?.(def.id, state);
786
- const maxRetries = def.maxRetries ?? 2;
787
- const retryDelayMs = def.retryDelayMs ?? 0;
788
- let lastError;
789
- let lastAttempt = 0;
790
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
791
- lastAttempt = attempt;
792
- try {
793
- // Get inbox and filter out already-consumed messages
794
- const consumedIds = (state.context?.__consumedMsgIds ?? []);
795
- const consumedSet = new Set(consumedIds);
796
- const allInbox = getInbox(state.swarmMessages ?? [], def.id);
797
- const inbox = allInbox.filter((m) => !consumedSet.has(m.id));
798
- // Track consumed message IDs
799
- const newConsumedIds = [...consumedIds, ...inbox.map((m) => m.id)];
800
- const agentInput = {
801
- ...state,
802
- context: {
803
- ...(state.context ?? {}),
804
- inbox,
805
- __consumedMsgIds: newConsumedIds,
806
- },
807
- };
808
- let effectiveTimeout = def.timeout;
809
- // Clamp agent timeout to remaining deadline (if set)
810
- const deadlineAbs = state.context?.__deadlineAbsolute;
811
- if (deadlineAbs != null) {
812
- const remaining = deadlineAbs - Date.now();
813
- if (remaining <= 0) {
814
- throw new Error(`Agent "${def.id}" deadline expired`);
815
- }
816
- if (effectiveTimeout == null || remaining < effectiveTimeout) {
817
- effectiveTimeout = remaining;
818
- }
819
- }
820
- const result = await runWithTimeout(() => def.skeleton.invoke(agentInput, {
821
- ...config,
822
- agentId: def.id,
823
- }), effectiveTimeout, () => new Error(`Agent "${def.id}" timed out after ${effectiveTimeout}ms`));
824
- this.registry.markIdle(def.id);
825
- // ---- Handoff detection ----
826
- if (result instanceof Handoff || (result && result.isHandoff)) {
827
- const handoff = result instanceof Handoff
828
- ? result
829
- : new Handoff(result.opts);
830
- // Fire onComplete for handoffs too
831
- await def.hooks?.onComplete?.(def.id, result);
832
- return new Command({
833
- update: {
834
- context: { ...(state.context ?? {}), ...(handoff.context ?? {}) },
835
- handoffHistory: [{
836
- from: def.id,
837
- to: handoff.to,
838
- message: handoff.message,
839
- step: state.supervisorRound ?? 0,
840
- timestamp: Date.now(),
841
- resume: handoff.resume,
842
- }],
843
- currentAgent: handoff.to,
844
- },
845
- goto: handoff.to,
846
- });
847
- }
848
- // Fire onComplete hook
849
- await def.hooks?.onComplete?.(def.id, result);
850
- // ---- Normal result ----
851
- return {
852
- ...result,
853
- context: {
854
- ...(result.context ?? {}),
855
- __consumedMsgIds: newConsumedIds,
856
- },
857
- agentResults: {
858
- ...(state.agentResults ?? {}),
859
- [def.id]: result,
860
- },
861
- handoffHistory: [
862
- ...(state.handoffHistory ?? []),
863
- {
864
- from: def.id,
865
- to: "__completed__",
866
- message: "Agent completed",
867
- step: state.supervisorRound ?? 0,
868
- timestamp: Date.now(),
869
- },
870
- ],
871
- };
872
- }
873
- catch (err) {
874
- lastError = err;
875
- this.registry.markError(def.id);
876
- if (attempt < maxRetries) {
877
- // Backoff delay between retries — clamped to remaining deadline
878
- if (retryDelayMs > 0) {
879
- let delay = retryDelayMs;
880
- const dl = state.context?.__deadlineAbsolute;
881
- if (dl != null) {
882
- const remaining = dl - Date.now();
883
- if (remaining <= 0)
884
- break; // deadline expired, stop retrying
885
- delay = Math.min(delay, remaining);
886
- }
887
- await new Promise((r) => setTimeout(r, delay));
888
- }
889
- continue; // retry
890
- }
891
- }
892
- }
893
- // All retries exhausted — fire onError hook
894
- await def.hooks?.onError?.(def.id, lastError);
895
- // Keep agent in error status (don't reset to idle)
896
- // Build structured error context
897
- const errStr = String(lastError instanceof Error ? lastError.message : lastError);
898
- const errType = errStr.includes("timeout") ? "timeout" :
899
- errStr.includes("model") ? "model_error" :
900
- errStr.includes("tool") ? "tool_error" :
901
- "unknown";
902
- if (this.hasSupervisor && this.onErrorPolicy !== "throw") {
903
- return new Command({
904
- update: {
905
- context: {
906
- ...(state.context ?? {}),
907
- lastAgentError: {
908
- agent: def.id,
909
- error: errStr,
910
- type: errType,
911
- attempt: lastAttempt,
912
- maxRetries,
913
- },
914
- },
915
- },
916
- goto: this.supervisorNodeName,
917
- });
918
- }
919
- // No supervisor, or onError: "throw" — rethrow
920
- throw lastError;
921
- };
922
- this.inner.addNode(def.id, agentNode);
143
+ this.inner.addNode(def.id, createAgentNode(def, this.registry, this));
923
144
  return this;
924
145
  }
925
146
  // ---- Supervisor ----
@@ -976,7 +197,8 @@ export class SwarmGraph {
976
197
  return issues;
977
198
  // For non-supervised swarms, check that every agent has at least one incoming edge
978
199
  const edgeTargets = new Set();
979
- for (const edge of this.inner.edges) {
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ for (const edge of this.inner.edges) { // SAFE: external boundary — accessing private StateGraph.edges for topology validation
980
202
  if (edge.type === "static") {
981
203
  edgeTargets.add(edge.to);
982
204
  }
@@ -1011,99 +233,18 @@ export class SwarmGraph {
1011
233
  // ---- Compile ----
1012
234
  compile(opts = {}) {
1013
235
  const skeleton = this.inner.compile({
1014
- ...(opts.checkpointer ? { checkpointer: opts.checkpointer } : {}),
1015
- ...(opts.interruptBefore ? { interruptBefore: opts.interruptBefore } : {}),
1016
- ...(opts.interruptAfter ? { interruptAfter: opts.interruptAfter } : {}),
1017
- });
1018
- const registry = this.registry;
1019
- const inner = this.inner;
1020
- const hasSupervisor = this.hasSupervisor;
1021
- const supervisorNodeName = this.supervisorNodeName;
1022
- const self = this;
1023
- // Attach swarm-specific extensions
1024
- const extensions = {
1025
- registry,
1026
- agentStats: () => registry.stats(),
1027
- toMermaid: () => inner.toMermaid(),
1028
- spawnAgent(def) {
1029
- // spawnAgent requires a supervisor to route the spawned agent back into the graph.
1030
- // In unsupervised topologies there is no return path, so the agent would never execute.
1031
- if (!hasSupervisor) {
1032
- throw new Error(`spawnAgent() requires a supervisor node. ` +
1033
- `Use SwarmGraph.hierarchical() to add a supervisor, or call addAgent() before compile() for static topologies.`);
1034
- }
1035
- // Register in the registry
1036
- registry.register(def);
1037
- // Create the agent node function (same logic as addAgent)
1038
- const agentNode = async (state, config) => {
1039
- registry.markBusy(def.id);
1040
- await def.hooks?.onStart?.(def.id, state);
1041
- const maxRetries = def.maxRetries ?? 2;
1042
- let lastError;
1043
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1044
- try {
1045
- const result = await def.skeleton.invoke({ ...state, context: { ...(state.context ?? {}), inbox: getInbox(state.swarmMessages ?? [], def.id) } }, { ...config, agentId: def.id });
1046
- registry.markIdle(def.id);
1047
- await def.hooks?.onComplete?.(def.id, result);
1048
- return {
1049
- ...result,
1050
- agentResults: { ...(state.agentResults ?? {}), [def.id]: result },
1051
- };
1052
- }
1053
- catch (err) {
1054
- lastError = err;
1055
- registry.markError(def.id);
1056
- if (attempt < maxRetries)
1057
- continue;
1058
- }
1059
- }
1060
- await def.hooks?.onError?.(def.id, lastError);
1061
- // Keep agent in error status (don't reset to idle)
1062
- if (hasSupervisor) {
1063
- return new Command({
1064
- update: { context: { ...(state.context ?? {}), lastAgentError: { agent: def.id, error: String(lastError) } } },
1065
- goto: supervisorNodeName,
1066
- });
1067
- }
1068
- throw lastError;
1069
- };
1070
- // Add node to the compiled runner's nodes map
1071
- const runner = skeleton._runner;
1072
- if (runner?.nodes) {
1073
- runner.nodes.set(def.id, { name: def.id, fn: agentNode });
1074
- }
1075
- // In supervised swarms, add a conditional return edge so the runner
1076
- // knows to send this agent's output back to the supervisor (or END).
1077
- if (hasSupervisor && runner) {
1078
- const returnEdge = {
1079
- type: "conditional",
1080
- from: def.id,
1081
- condition: (st) => st.done ? END : supervisorNodeName,
1082
- };
1083
- const edgesBySource = runner._edgesBySource;
1084
- const list = edgesBySource.get(def.id) ?? [];
1085
- list.push(returnEdge);
1086
- edgesBySource.set(def.id, list);
1087
- }
1088
- },
1089
- removeAgent(agentId) {
1090
- registry.deregister(agentId);
1091
- const runner = skeleton._runner;
1092
- if (runner?.nodes)
1093
- runner.nodes.delete(agentId);
1094
- // Remove stale edges pointing TO the removed agent so Pregel doesn't try to route to it.
1095
- // Also remove edges FROM the agent (it won't execute, but keeps _edgesBySource clean).
1096
- if (runner?._edgesBySource) {
1097
- const edgesBySource = runner._edgesBySource;
1098
- for (const [from, edges] of edgesBySource) {
1099
- const filtered = edges.filter((e) => !(e.type === "static" && e.to === agentId));
1100
- if (filtered.length !== edges.length)
1101
- edgesBySource.set(from, filtered);
1102
- }
1103
- edgesBySource.delete(agentId);
1104
- }
1105
- },
1106
- };
236
+ ...(opts.checkpointer !== undefined ? { checkpointer: opts.checkpointer } : {}),
237
+ ...(opts.interruptBefore !== undefined ? { interruptBefore: opts.interruptBefore } : {}),
238
+ ...(opts.interruptAfter !== undefined ? { interruptAfter: opts.interruptAfter } : {}),
239
+ ...(opts.store !== undefined ? { store: opts.store } : {}),
240
+ ...(opts.guardrails !== undefined ? { guardrails: opts.guardrails } : {}),
241
+ ...(opts.listeners !== undefined ? { listeners: opts.listeners } : {}),
242
+ ...(opts.defaults !== undefined ? { defaults: opts.defaults } : {}),
243
+ ...(opts.deadLetterQueue !== undefined ? { deadLetterQueue: opts.deadLetterQueue } : {}),
244
+ ...(opts.tracer !== undefined ? { tracer: opts.tracer } : {}),
245
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
+ }); // SAFE: CompileOptions conditional spread — TypeScript can't narrow the intersection type
247
+ const extensions = buildSwarmExtensions(skeleton, this.registry, this.inner, this.hasSupervisor, this.supervisorNodeName, this.onErrorPolicy);
1107
248
  return Object.assign(skeleton, extensions);
1108
249
  }
1109
250
  }
@@ -1134,4 +275,5 @@ export function quickAgent(id, fn, opts) {
1134
275
  retryDelayMs: opts?.retryDelayMs,
1135
276
  };
1136
277
  }
278
+ export { baseSwarmChannels } from "./config.js";
1137
279
  //# sourceMappingURL=graph.js.map