@open-multi-agent/core 1.4.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +373 -0
  3. package/dist/agent/agent.d.ts +153 -0
  4. package/dist/agent/agent.d.ts.map +1 -0
  5. package/dist/agent/agent.js +559 -0
  6. package/dist/agent/agent.js.map +1 -0
  7. package/dist/agent/loop-detector.d.ts +39 -0
  8. package/dist/agent/loop-detector.d.ts.map +1 -0
  9. package/dist/agent/loop-detector.js +122 -0
  10. package/dist/agent/loop-detector.js.map +1 -0
  11. package/dist/agent/pool.d.ts +158 -0
  12. package/dist/agent/pool.d.ts.map +1 -0
  13. package/dist/agent/pool.js +320 -0
  14. package/dist/agent/pool.js.map +1 -0
  15. package/dist/agent/runner.d.ts +242 -0
  16. package/dist/agent/runner.d.ts.map +1 -0
  17. package/dist/agent/runner.js +943 -0
  18. package/dist/agent/runner.js.map +1 -0
  19. package/dist/agent/structured-output.d.ts +33 -0
  20. package/dist/agent/structured-output.d.ts.map +1 -0
  21. package/dist/agent/structured-output.js +116 -0
  22. package/dist/agent/structured-output.js.map +1 -0
  23. package/dist/cli/oma.d.ts +30 -0
  24. package/dist/cli/oma.d.ts.map +1 -0
  25. package/dist/cli/oma.js +433 -0
  26. package/dist/cli/oma.js.map +1 -0
  27. package/dist/dashboard/layout-tasks.d.ts +23 -0
  28. package/dist/dashboard/layout-tasks.d.ts.map +1 -0
  29. package/dist/dashboard/layout-tasks.js +79 -0
  30. package/dist/dashboard/layout-tasks.js.map +1 -0
  31. package/dist/dashboard/render-team-run-dashboard.d.ts +11 -0
  32. package/dist/dashboard/render-team-run-dashboard.d.ts.map +1 -0
  33. package/dist/dashboard/render-team-run-dashboard.js +456 -0
  34. package/dist/dashboard/render-team-run-dashboard.js.map +1 -0
  35. package/dist/errors.d.ts +14 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +20 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/index.d.ts +79 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +92 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/llm/adapter.d.ts +54 -0
  44. package/dist/llm/adapter.d.ts.map +1 -0
  45. package/dist/llm/adapter.js +101 -0
  46. package/dist/llm/adapter.js.map +1 -0
  47. package/dist/llm/anthropic.d.ts +57 -0
  48. package/dist/llm/anthropic.d.ts.map +1 -0
  49. package/dist/llm/anthropic.js +432 -0
  50. package/dist/llm/anthropic.js.map +1 -0
  51. package/dist/llm/azure-openai.d.ts +74 -0
  52. package/dist/llm/azure-openai.d.ts.map +1 -0
  53. package/dist/llm/azure-openai.js +267 -0
  54. package/dist/llm/azure-openai.js.map +1 -0
  55. package/dist/llm/bedrock.d.ts +41 -0
  56. package/dist/llm/bedrock.d.ts.map +1 -0
  57. package/dist/llm/bedrock.js +345 -0
  58. package/dist/llm/bedrock.js.map +1 -0
  59. package/dist/llm/copilot.d.ts +92 -0
  60. package/dist/llm/copilot.d.ts.map +1 -0
  61. package/dist/llm/copilot.js +433 -0
  62. package/dist/llm/copilot.js.map +1 -0
  63. package/dist/llm/deepseek.d.ts +21 -0
  64. package/dist/llm/deepseek.d.ts.map +1 -0
  65. package/dist/llm/deepseek.js +24 -0
  66. package/dist/llm/deepseek.js.map +1 -0
  67. package/dist/llm/gemini.d.ts +65 -0
  68. package/dist/llm/gemini.d.ts.map +1 -0
  69. package/dist/llm/gemini.js +427 -0
  70. package/dist/llm/gemini.js.map +1 -0
  71. package/dist/llm/grok.d.ts +21 -0
  72. package/dist/llm/grok.d.ts.map +1 -0
  73. package/dist/llm/grok.js +24 -0
  74. package/dist/llm/grok.js.map +1 -0
  75. package/dist/llm/minimax.d.ts +21 -0
  76. package/dist/llm/minimax.d.ts.map +1 -0
  77. package/dist/llm/minimax.js +24 -0
  78. package/dist/llm/minimax.js.map +1 -0
  79. package/dist/llm/openai-common.d.ts +65 -0
  80. package/dist/llm/openai-common.d.ts.map +1 -0
  81. package/dist/llm/openai-common.js +286 -0
  82. package/dist/llm/openai-common.js.map +1 -0
  83. package/dist/llm/openai.d.ts +63 -0
  84. package/dist/llm/openai.d.ts.map +1 -0
  85. package/dist/llm/openai.js +256 -0
  86. package/dist/llm/openai.js.map +1 -0
  87. package/dist/llm/qiniu.d.ts +21 -0
  88. package/dist/llm/qiniu.d.ts.map +1 -0
  89. package/dist/llm/qiniu.js +24 -0
  90. package/dist/llm/qiniu.js.map +1 -0
  91. package/dist/mcp.d.ts +3 -0
  92. package/dist/mcp.d.ts.map +1 -0
  93. package/dist/mcp.js +2 -0
  94. package/dist/mcp.js.map +1 -0
  95. package/dist/memory/shared.d.ts +162 -0
  96. package/dist/memory/shared.d.ts.map +1 -0
  97. package/dist/memory/shared.js +294 -0
  98. package/dist/memory/shared.js.map +1 -0
  99. package/dist/memory/store.d.ts +72 -0
  100. package/dist/memory/store.d.ts.map +1 -0
  101. package/dist/memory/store.js +121 -0
  102. package/dist/memory/store.js.map +1 -0
  103. package/dist/orchestrator/orchestrator.d.ts +245 -0
  104. package/dist/orchestrator/orchestrator.d.ts.map +1 -0
  105. package/dist/orchestrator/orchestrator.js +1400 -0
  106. package/dist/orchestrator/orchestrator.js.map +1 -0
  107. package/dist/orchestrator/scheduler.d.ts +112 -0
  108. package/dist/orchestrator/scheduler.d.ts.map +1 -0
  109. package/dist/orchestrator/scheduler.js +256 -0
  110. package/dist/orchestrator/scheduler.js.map +1 -0
  111. package/dist/task/queue.d.ts +191 -0
  112. package/dist/task/queue.d.ts.map +1 -0
  113. package/dist/task/queue.js +408 -0
  114. package/dist/task/queue.js.map +1 -0
  115. package/dist/task/task.d.ts +90 -0
  116. package/dist/task/task.d.ts.map +1 -0
  117. package/dist/task/task.js +206 -0
  118. package/dist/task/task.js.map +1 -0
  119. package/dist/team/messaging.d.ts +106 -0
  120. package/dist/team/messaging.d.ts.map +1 -0
  121. package/dist/team/messaging.js +183 -0
  122. package/dist/team/messaging.js.map +1 -0
  123. package/dist/team/team.d.ts +141 -0
  124. package/dist/team/team.d.ts.map +1 -0
  125. package/dist/team/team.js +293 -0
  126. package/dist/team/team.js.map +1 -0
  127. package/dist/tool/built-in/bash.d.ts +12 -0
  128. package/dist/tool/built-in/bash.d.ts.map +1 -0
  129. package/dist/tool/built-in/bash.js +133 -0
  130. package/dist/tool/built-in/bash.js.map +1 -0
  131. package/dist/tool/built-in/delegate.d.ts +29 -0
  132. package/dist/tool/built-in/delegate.d.ts.map +1 -0
  133. package/dist/tool/built-in/delegate.js +92 -0
  134. package/dist/tool/built-in/delegate.js.map +1 -0
  135. package/dist/tool/built-in/file-edit.d.ts +14 -0
  136. package/dist/tool/built-in/file-edit.d.ts.map +1 -0
  137. package/dist/tool/built-in/file-edit.js +130 -0
  138. package/dist/tool/built-in/file-edit.js.map +1 -0
  139. package/dist/tool/built-in/file-read.d.ts +12 -0
  140. package/dist/tool/built-in/file-read.d.ts.map +1 -0
  141. package/dist/tool/built-in/file-read.js +82 -0
  142. package/dist/tool/built-in/file-read.js.map +1 -0
  143. package/dist/tool/built-in/file-write.d.ts +11 -0
  144. package/dist/tool/built-in/file-write.d.ts.map +1 -0
  145. package/dist/tool/built-in/file-write.js +70 -0
  146. package/dist/tool/built-in/file-write.js.map +1 -0
  147. package/dist/tool/built-in/fs-walk.d.ts +23 -0
  148. package/dist/tool/built-in/fs-walk.d.ts.map +1 -0
  149. package/dist/tool/built-in/fs-walk.js +78 -0
  150. package/dist/tool/built-in/fs-walk.js.map +1 -0
  151. package/dist/tool/built-in/glob.d.ts +12 -0
  152. package/dist/tool/built-in/glob.d.ts.map +1 -0
  153. package/dist/tool/built-in/glob.js +82 -0
  154. package/dist/tool/built-in/glob.js.map +1 -0
  155. package/dist/tool/built-in/grep.d.ts +15 -0
  156. package/dist/tool/built-in/grep.d.ts.map +1 -0
  157. package/dist/tool/built-in/grep.js +218 -0
  158. package/dist/tool/built-in/grep.js.map +1 -0
  159. package/dist/tool/built-in/index.d.ts +48 -0
  160. package/dist/tool/built-in/index.d.ts.map +1 -0
  161. package/dist/tool/built-in/index.js +56 -0
  162. package/dist/tool/built-in/index.js.map +1 -0
  163. package/dist/tool/executor.d.ts +100 -0
  164. package/dist/tool/executor.d.ts.map +1 -0
  165. package/dist/tool/executor.js +184 -0
  166. package/dist/tool/executor.js.map +1 -0
  167. package/dist/tool/framework.d.ts +167 -0
  168. package/dist/tool/framework.d.ts.map +1 -0
  169. package/dist/tool/framework.js +402 -0
  170. package/dist/tool/framework.js.map +1 -0
  171. package/dist/tool/mcp.d.ts +31 -0
  172. package/dist/tool/mcp.d.ts.map +1 -0
  173. package/dist/tool/mcp.js +175 -0
  174. package/dist/tool/mcp.js.map +1 -0
  175. package/dist/tool/text-tool-extractor.d.ts +32 -0
  176. package/dist/tool/text-tool-extractor.d.ts.map +1 -0
  177. package/dist/tool/text-tool-extractor.js +195 -0
  178. package/dist/tool/text-tool-extractor.js.map +1 -0
  179. package/dist/types.d.ts +916 -0
  180. package/dist/types.d.ts.map +1 -0
  181. package/dist/types.js +8 -0
  182. package/dist/types.js.map +1 -0
  183. package/dist/utils/keywords.d.ts +18 -0
  184. package/dist/utils/keywords.d.ts.map +1 -0
  185. package/dist/utils/keywords.js +32 -0
  186. package/dist/utils/keywords.js.map +1 -0
  187. package/dist/utils/semaphore.d.ts +49 -0
  188. package/dist/utils/semaphore.d.ts.map +1 -0
  189. package/dist/utils/semaphore.js +89 -0
  190. package/dist/utils/semaphore.js.map +1 -0
  191. package/dist/utils/tokens.d.ts +7 -0
  192. package/dist/utils/tokens.d.ts.map +1 -0
  193. package/dist/utils/tokens.js +30 -0
  194. package/dist/utils/tokens.js.map +1 -0
  195. package/dist/utils/trace.d.ts +12 -0
  196. package/dist/utils/trace.d.ts.map +1 -0
  197. package/dist/utils/trace.js +30 -0
  198. package/dist/utils/trace.js.map +1 -0
  199. package/docs/DECISIONS.md +49 -0
  200. package/docs/cli.md +265 -0
  201. package/docs/context-management.md +24 -0
  202. package/docs/featured-partner.md +28 -0
  203. package/docs/observability.md +56 -0
  204. package/docs/providers.md +78 -0
  205. package/docs/shared-memory.md +27 -0
  206. package/docs/tool-configuration.md +152 -0
  207. package/package.json +96 -0
@@ -0,0 +1,1400 @@
1
+ /**
2
+ * @fileoverview OpenMultiAgent — the top-level multi-agent orchestration class.
3
+ *
4
+ * {@link OpenMultiAgent} is the primary public API of the open-multi-agent framework.
5
+ * It ties together every subsystem:
6
+ *
7
+ * - {@link Team} — Agent roster, shared memory, inter-agent messaging
8
+ * - {@link TaskQueue} — Dependency-aware work queue
9
+ * - {@link Scheduler} — Task-to-agent assignment strategies
10
+ * - {@link AgentPool} — Concurrency-controlled execution pool
11
+ * - {@link Agent} — Conversation + tool-execution loop
12
+ *
13
+ * ## Quick start
14
+ *
15
+ * ```ts
16
+ * const orchestrator = new OpenMultiAgent({ defaultModel: 'claude-opus-4-6' })
17
+ *
18
+ * const team = orchestrator.createTeam('research', {
19
+ * name: 'research',
20
+ * agents: [
21
+ * { name: 'researcher', model: 'claude-opus-4-6', systemPrompt: 'You are a researcher.' },
22
+ * { name: 'writer', model: 'claude-opus-4-6', systemPrompt: 'You are a technical writer.' },
23
+ * ],
24
+ * sharedMemory: true,
25
+ * })
26
+ *
27
+ * const result = await orchestrator.runTeam(team, 'Produce a report on TypeScript 5.5.')
28
+ * console.log(result.agentResults.get('coordinator')?.output)
29
+ * ```
30
+ *
31
+ * ## Key design decisions
32
+ *
33
+ * - **Coordinator pattern** — `runTeam()` spins up a temporary "coordinator" agent
34
+ * that breaks the high-level goal into tasks, assigns them, and synthesises the
35
+ * final answer. This is the framework's killer feature.
36
+ * - **Parallel-by-default** — Independent tasks (no shared dependency) run in
37
+ * parallel up to `maxConcurrency`.
38
+ * - **Graceful failure** — A failed task marks itself `'failed'` and its direct
39
+ * dependents remain `'blocked'` indefinitely; all non-dependent tasks continue.
40
+ * - **Progress callbacks** — Callers can pass `onProgress` in the config to receive
41
+ * structured {@link OrchestratorEvent}s without polling.
42
+ */
43
+ import { Agent } from '../agent/agent.js';
44
+ import { AgentPool } from '../agent/pool.js';
45
+ import { emitTrace, generateRunId } from '../utils/trace.js';
46
+ import { ToolRegistry } from '../tool/framework.js';
47
+ import { ToolExecutor } from '../tool/executor.js';
48
+ import { registerBuiltInTools } from '../tool/built-in/index.js';
49
+ import { Team } from '../team/team.js';
50
+ import { TaskQueue } from '../task/queue.js';
51
+ import { createTask } from '../task/task.js';
52
+ import { Scheduler } from './scheduler.js';
53
+ import { TokenBudgetExceededError } from '../errors.js';
54
+ import { extractKeywords, keywordScore } from '../utils/keywords.js';
55
+ // ---------------------------------------------------------------------------
56
+ // Internal constants
57
+ // ---------------------------------------------------------------------------
58
+ const ZERO_USAGE = { input_tokens: 0, output_tokens: 0 };
59
+ const DEFAULT_MAX_CONCURRENCY = 5;
60
+ const DEFAULT_MAX_DELEGATION_DEPTH = 3;
61
+ const DEFAULT_MODEL = 'claude-opus-4-6';
62
+ // ---------------------------------------------------------------------------
63
+ // Short-circuit helpers (exported for testability)
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * Regex patterns that indicate a goal requires multi-agent coordination.
67
+ *
68
+ * Each pattern targets a distinct complexity signal:
69
+ * - Sequencing: "first … then", "step 1 / step 2", numbered lists
70
+ * - Coordination: "collaborate", "coordinate", "review each other"
71
+ * - Parallel work: "in parallel", "at the same time", "concurrently"
72
+ * - Multi-phase: "phase", "stage", multiple distinct action verbs joined by connectives
73
+ */
74
+ const COMPLEXITY_PATTERNS = [
75
+ // Explicit sequencing
76
+ /\bfirst\b.{3,60}\bthen\b/i,
77
+ /\bstep\s*\d/i,
78
+ /\bphase\s*\d/i,
79
+ /\bstage\s*\d/i,
80
+ /^\s*\d+[\.\)]/m, // numbered list items ("1. …", "2) …")
81
+ // Coordination language — must be an imperative directive aimed at the agents
82
+ // ("collaborate with X", "coordinate the team", "agents should coordinate"),
83
+ // not a descriptive use ("how does X coordinate with Y" / "what does collaboration mean").
84
+ // Match either an explicit preposition or a noun-phrase that names a group.
85
+ /\bcollaborat(?:e|ing)\b\s+(?:with|on|to)\b/i,
86
+ /\bcoordinat(?:e|ing)\b\s+(?:with|on|across|between|the\s+(?:team|agents?|workers?|effort|work))\b/i,
87
+ /\breview\s+each\s+other/i,
88
+ /\bwork\s+together\b/i,
89
+ // Parallel execution
90
+ /\bin\s+parallel\b/i,
91
+ /\bconcurrently\b/i,
92
+ /\bat\s+the\s+same\s+time\b/i,
93
+ // Multiple deliverables joined by connectives
94
+ // Matches patterns like "build X, then deploy Y and test Z"
95
+ /\b(?:build|create|implement|design|write|develop)\b.{5,80}\b(?:and|then)\b.{5,80}\b(?:build|create|implement|design|write|develop|test|review|deploy)\b/i,
96
+ ];
97
+ /**
98
+ * Maximum goal length (in characters) below which a goal *may* be simple.
99
+ *
100
+ * Goals longer than this threshold almost always contain enough detail to
101
+ * warrant multi-agent decomposition. The value is generous — short-circuit
102
+ * is meant for genuinely simple, single-action goals.
103
+ */
104
+ const SIMPLE_GOAL_MAX_LENGTH = 200;
105
+ /**
106
+ * Determine whether a goal is simple enough to skip coordinator decomposition.
107
+ *
108
+ * A goal is considered "simple" when ALL of the following hold:
109
+ * 1. Its length is ≤ {@link SIMPLE_GOAL_MAX_LENGTH}.
110
+ * 2. It does not match any {@link COMPLEXITY_PATTERNS}.
111
+ *
112
+ * The complexity patterns are deliberately conservative — they only fire on
113
+ * imperative coordination directives (e.g. "collaborate with the team",
114
+ * "coordinate the workers"), so descriptive uses ("how do pods coordinate
115
+ * state", "explain microservice collaboration") remain classified as simple.
116
+ *
117
+ * Exported for unit testing.
118
+ */
119
+ export function isSimpleGoal(goal) {
120
+ if (goal.length > SIMPLE_GOAL_MAX_LENGTH)
121
+ return false;
122
+ return !COMPLEXITY_PATTERNS.some((re) => re.test(goal));
123
+ }
124
+ /**
125
+ * Select the best-matching agent for a goal using keyword affinity scoring.
126
+ *
127
+ * The scoring logic mirrors {@link Scheduler}'s `capability-match` strategy
128
+ * exactly, including its asymmetric use of the agent's `model` field:
129
+ *
130
+ * - `agentKeywords` is computed from `name + systemPrompt + model` so that
131
+ * a goal which mentions a model name (e.g. "haiku") can boost an agent
132
+ * bound to that model.
133
+ * - `agentText` (used for the reverse direction) is computed from
134
+ * `name + systemPrompt` only — model names should not bias the
135
+ * text-vs-goal-keywords match.
136
+ *
137
+ * The two-direction sum (`scoreA + scoreB`) ensures both "agent describes
138
+ * goal" and "goal mentions agent capability" contribute to the final score.
139
+ *
140
+ * Exported for unit testing.
141
+ */
142
+ export function selectBestAgent(goal, agents) {
143
+ if (agents.length <= 1)
144
+ return agents[0];
145
+ const goalKeywords = extractKeywords(goal);
146
+ let bestAgent = agents[0];
147
+ let bestScore = -1;
148
+ for (const agent of agents) {
149
+ const agentText = `${agent.name} ${agent.systemPrompt ?? ''}`;
150
+ // Mirror Scheduler.capability-match: include `model` here only.
151
+ const agentKeywords = extractKeywords(`${agent.name} ${agent.systemPrompt ?? ''} ${agent.model}`);
152
+ const scoreA = keywordScore(agentText, goalKeywords);
153
+ const scoreB = keywordScore(goal, agentKeywords);
154
+ const score = scoreA + scoreB;
155
+ if (score > bestScore) {
156
+ bestScore = score;
157
+ bestAgent = agent;
158
+ }
159
+ }
160
+ return bestAgent;
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Internal helpers
164
+ // ---------------------------------------------------------------------------
165
+ function addUsage(a, b) {
166
+ return {
167
+ input_tokens: a.input_tokens + b.input_tokens,
168
+ output_tokens: a.output_tokens + b.output_tokens,
169
+ };
170
+ }
171
+ function resolveTokenBudget(primary, fallback) {
172
+ if (primary === undefined)
173
+ return fallback;
174
+ if (fallback === undefined)
175
+ return primary;
176
+ return Math.min(primary, fallback);
177
+ }
178
+ /**
179
+ * Build a minimal {@link Agent} with its own fresh registry/executor.
180
+ * Pool workers pass `includeDelegateTool` so `delegate_to_agent` is available during `runTeam` / `runTasks`.
181
+ */
182
+ function buildAgent(config, toolRegistration) {
183
+ const registry = new ToolRegistry();
184
+ registerBuiltInTools(registry, toolRegistration);
185
+ if (config.customTools) {
186
+ for (const tool of config.customTools) {
187
+ registry.register(tool, { runtimeAdded: true });
188
+ }
189
+ }
190
+ const executor = new ToolExecutor(registry, {
191
+ ...(config.maxToolOutputChars !== undefined
192
+ ? { maxToolOutputChars: config.maxToolOutputChars }
193
+ : {}),
194
+ });
195
+ return new Agent(config, registry, executor);
196
+ }
197
+ /** Promise-based delay. */
198
+ function sleep(ms) {
199
+ return new Promise((resolve) => setTimeout(resolve, ms));
200
+ }
201
+ /** Maximum delay cap to prevent runaway exponential backoff (30 seconds). */
202
+ const MAX_RETRY_DELAY_MS = 30_000;
203
+ /**
204
+ * Compute the retry delay for a given attempt, capped at {@link MAX_RETRY_DELAY_MS}.
205
+ */
206
+ export function computeRetryDelay(baseDelay, backoff, attempt) {
207
+ return Math.min(baseDelay * backoff ** (attempt - 1), MAX_RETRY_DELAY_MS);
208
+ }
209
+ /**
210
+ * Execute an agent task with optional retry and exponential backoff.
211
+ *
212
+ * Exported for testability — called internally by {@link executeQueue}.
213
+ *
214
+ * @param run - The function that executes the task (typically `pool.run`).
215
+ * @param task - The task to execute (retry config read from its fields).
216
+ * @param onRetry - Called before each retry sleep with event data.
217
+ * @param delayFn - Injectable delay function (defaults to real `sleep`).
218
+ * @returns The final {@link AgentRunResult} from the last attempt.
219
+ */
220
+ export async function executeWithRetry(run, task, onRetry, delayFn = sleep) {
221
+ const rawRetries = Number.isFinite(task.maxRetries) ? task.maxRetries : 0;
222
+ const maxAttempts = Math.max(0, rawRetries) + 1;
223
+ const baseDelay = Math.max(0, Number.isFinite(task.retryDelayMs) ? task.retryDelayMs : 1000);
224
+ const backoff = Math.max(1, Number.isFinite(task.retryBackoff) ? task.retryBackoff : 2);
225
+ let lastError = '';
226
+ // Accumulate token usage across all attempts so billing/observability
227
+ // reflects the true cost of retries.
228
+ let totalUsage = { input_tokens: 0, output_tokens: 0 };
229
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
230
+ try {
231
+ const result = await run();
232
+ totalUsage = {
233
+ input_tokens: totalUsage.input_tokens + result.tokenUsage.input_tokens,
234
+ output_tokens: totalUsage.output_tokens + result.tokenUsage.output_tokens,
235
+ };
236
+ if (result.success) {
237
+ return { ...result, tokenUsage: totalUsage };
238
+ }
239
+ lastError = result.output;
240
+ // Failure — retry or give up
241
+ if (attempt < maxAttempts) {
242
+ const delay = computeRetryDelay(baseDelay, backoff, attempt);
243
+ onRetry?.({ attempt, maxAttempts, error: lastError, nextDelayMs: delay });
244
+ await delayFn(delay);
245
+ continue;
246
+ }
247
+ return { ...result, tokenUsage: totalUsage };
248
+ }
249
+ catch (err) {
250
+ lastError = err instanceof Error ? err.message : String(err);
251
+ if (attempt < maxAttempts) {
252
+ const delay = computeRetryDelay(baseDelay, backoff, attempt);
253
+ onRetry?.({ attempt, maxAttempts, error: lastError, nextDelayMs: delay });
254
+ await delayFn(delay);
255
+ continue;
256
+ }
257
+ // All retries exhausted — return a failure result
258
+ return {
259
+ success: false,
260
+ output: lastError,
261
+ messages: [],
262
+ tokenUsage: totalUsage,
263
+ toolCalls: [],
264
+ };
265
+ }
266
+ }
267
+ // Should not be reached, but TypeScript needs a return
268
+ return {
269
+ success: false,
270
+ output: lastError,
271
+ messages: [],
272
+ tokenUsage: totalUsage,
273
+ toolCalls: [],
274
+ };
275
+ }
276
+ /**
277
+ * Attempt to extract a JSON array of task specs from the coordinator's raw
278
+ * output. The coordinator is prompted to emit JSON inside a ```json … ``` fence
279
+ * or as a bare array. Returns `null` when no valid array can be extracted.
280
+ */
281
+ function parseTaskSpecs(raw) {
282
+ // Strategy 1: look for a fenced JSON block
283
+ const fenceMatch = raw.match(/```json\s*([\s\S]*?)```/);
284
+ const candidate = fenceMatch ? fenceMatch[1] : raw;
285
+ // Strategy 2: find the first '[' and last ']'
286
+ const arrayStart = candidate.indexOf('[');
287
+ const arrayEnd = candidate.lastIndexOf(']');
288
+ if (arrayStart === -1 || arrayEnd === -1 || arrayEnd <= arrayStart) {
289
+ return null;
290
+ }
291
+ const jsonSlice = candidate.slice(arrayStart, arrayEnd + 1);
292
+ try {
293
+ const parsed = JSON.parse(jsonSlice);
294
+ if (!Array.isArray(parsed))
295
+ return null;
296
+ const specs = [];
297
+ for (const item of parsed) {
298
+ if (typeof item !== 'object' || item === null)
299
+ continue;
300
+ const obj = item;
301
+ if (typeof obj['title'] !== 'string')
302
+ continue;
303
+ if (typeof obj['description'] !== 'string')
304
+ continue;
305
+ specs.push({
306
+ title: obj['title'],
307
+ description: obj['description'],
308
+ assignee: typeof obj['assignee'] === 'string' ? obj['assignee'] : undefined,
309
+ dependsOn: Array.isArray(obj['dependsOn'])
310
+ ? obj['dependsOn'].filter((x) => typeof x === 'string')
311
+ : undefined,
312
+ memoryScope: obj['memoryScope'] === 'all' ? 'all' : undefined,
313
+ maxRetries: typeof obj['maxRetries'] === 'number' ? obj['maxRetries'] : undefined,
314
+ retryDelayMs: typeof obj['retryDelayMs'] === 'number' ? obj['retryDelayMs'] : undefined,
315
+ retryBackoff: typeof obj['retryBackoff'] === 'number' ? obj['retryBackoff'] : undefined,
316
+ });
317
+ }
318
+ return specs.length > 0 ? specs : null;
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ /**
325
+ * Build {@link TeamInfo} for tool context, including nested `runDelegatedAgent`
326
+ * that respects pool capacity to avoid semaphore deadlocks.
327
+ *
328
+ * Delegation always builds a **fresh** Agent instance for the target and runs
329
+ * it via `pool.runEphemeral` — the pool semaphore still gates total concurrency,
330
+ * but the per-agent lock is bypassed. This matches `delegate_to_agent`'s "runs
331
+ * in a fresh conversation for this prompt only" contract and prevents mutual
332
+ * delegation (A→B while B→A) from deadlocking on each other's agent locks.
333
+ */
334
+ function buildTaskAgentTeamInfo(ctx, taskId, traceBase, delegationDepth, delegationChain) {
335
+ const sharedMem = ctx.team.getSharedMemoryInstance();
336
+ const maxDepth = ctx.config.maxDelegationDepth;
337
+ const agentConfigs = ctx.team.getAgents();
338
+ const agentNames = agentConfigs.map((a) => a.name);
339
+ const runDelegatedAgent = async (targetAgent, prompt) => {
340
+ const pool = ctx.pool;
341
+ if (pool.availableRunSlots < 1) {
342
+ return {
343
+ success: false,
344
+ output: 'Agent pool has no free concurrency slot for a delegated run (would deadlock). ' +
345
+ 'Increase maxConcurrency or reduce parallel delegation.',
346
+ messages: [],
347
+ tokenUsage: ZERO_USAGE,
348
+ toolCalls: [],
349
+ };
350
+ }
351
+ const targetConfig = agentConfigs.find((a) => a.name === targetAgent);
352
+ if (!targetConfig) {
353
+ return {
354
+ success: false,
355
+ output: `Unknown agent "${targetAgent}" — not in team roster [${agentNames.join(', ')}].`,
356
+ messages: [],
357
+ tokenUsage: ZERO_USAGE,
358
+ toolCalls: [],
359
+ };
360
+ }
361
+ // Apply orchestrator-level defaults just like buildPool, then construct a
362
+ // one-shot Agent for this delegation only.
363
+ const effective = {
364
+ ...targetConfig,
365
+ provider: targetConfig.provider ?? ctx.config.defaultProvider,
366
+ baseURL: targetConfig.baseURL ?? ctx.config.defaultBaseURL,
367
+ apiKey: targetConfig.apiKey ?? ctx.config.defaultApiKey,
368
+ };
369
+ const tempAgent = buildAgent(effective, { includeDelegateTool: true });
370
+ const nestedTeam = buildTaskAgentTeamInfo(ctx, taskId, traceBase, delegationDepth + 1, [...delegationChain, targetAgent]);
371
+ const childOpts = {
372
+ ...traceBase,
373
+ traceAgent: targetAgent,
374
+ taskId,
375
+ team: nestedTeam,
376
+ };
377
+ return pool.runEphemeral(tempAgent, prompt, childOpts);
378
+ };
379
+ return {
380
+ name: ctx.team.name,
381
+ agents: agentNames,
382
+ ...(sharedMem ? { sharedMemory: sharedMem.getStore() } : {}),
383
+ delegationDepth,
384
+ maxDelegationDepth: maxDepth,
385
+ delegationPool: ctx.pool,
386
+ delegationChain,
387
+ runDelegatedAgent,
388
+ };
389
+ }
390
+ /**
391
+ * Execute all tasks in `queue` using agents in `pool`, respecting dependencies
392
+ * and running independent tasks in parallel.
393
+ *
394
+ * The orchestration loop works in rounds:
395
+ * 1. Find all `'pending'` tasks (dependencies satisfied).
396
+ * 2. Dispatch them in parallel via the pool.
397
+ * 3. On completion, the queue automatically unblocks dependents.
398
+ * 4. Repeat until no more pending tasks exist or all remaining tasks are
399
+ * `'failed'`/`'blocked'` (stuck).
400
+ */
401
+ async function executeQueue(queue, ctx) {
402
+ const { team, pool, scheduler, config } = ctx;
403
+ // Relay queue-level skip events to the orchestrator's onProgress callback.
404
+ const unsubSkipped = config.onProgress
405
+ ? queue.on('task:skipped', (task) => {
406
+ config.onProgress({
407
+ type: 'task_skipped',
408
+ task: task.id,
409
+ data: task,
410
+ });
411
+ })
412
+ : undefined;
413
+ while (true) {
414
+ // Check for cancellation before each dispatch round.
415
+ if (ctx.abortSignal?.aborted) {
416
+ queue.skipRemaining('Skipped: run aborted.');
417
+ break;
418
+ }
419
+ // Re-run auto-assignment each iteration so tasks that were unblocked since
420
+ // the last round (and thus have no assignee yet) get assigned before dispatch.
421
+ scheduler.autoAssign(queue, team.getAgents());
422
+ const pending = queue.getByStatus('pending');
423
+ if (pending.length === 0) {
424
+ // Either all done, or everything remaining is blocked/failed.
425
+ break;
426
+ }
427
+ // Track tasks that complete successfully in this round for the approval gate.
428
+ // Safe to push from concurrent promises: JS is single-threaded, so
429
+ // Array.push calls from resolved microtasks never interleave.
430
+ const completedThisRound = [];
431
+ // Dispatch all currently-pending tasks as a parallel batch.
432
+ const dispatchPromises = pending.map(async (task) => {
433
+ // Mark in-progress
434
+ queue.update(task.id, { status: 'in_progress' });
435
+ const assignee = task.assignee;
436
+ if (!assignee) {
437
+ // No assignee — mark failed and continue
438
+ const msg = `Task "${task.title}" has no assignee.`;
439
+ queue.fail(task.id, msg);
440
+ config.onProgress?.({
441
+ type: 'error',
442
+ task: task.id,
443
+ data: msg,
444
+ });
445
+ return;
446
+ }
447
+ const agent = pool.get(assignee);
448
+ if (!agent) {
449
+ const msg = `Agent "${assignee}" not found in pool for task "${task.title}".`;
450
+ queue.fail(task.id, msg);
451
+ config.onProgress?.({
452
+ type: 'error',
453
+ task: task.id,
454
+ agent: assignee,
455
+ data: msg,
456
+ });
457
+ return;
458
+ }
459
+ config.onProgress?.({
460
+ type: 'task_start',
461
+ task: task.id,
462
+ agent: assignee,
463
+ data: task,
464
+ });
465
+ config.onProgress?.({
466
+ type: 'agent_start',
467
+ agent: assignee,
468
+ task: task.id,
469
+ data: task,
470
+ });
471
+ // Build the prompt: task description + dependency-only context by default.
472
+ const prompt = await buildTaskPrompt(task, team, queue);
473
+ // Trace + abort + team tool context (delegate_to_agent)
474
+ const traceBase = {
475
+ ...(config.onTrace
476
+ ? {
477
+ onTrace: config.onTrace,
478
+ runId: ctx.runId ?? '',
479
+ taskId: task.id,
480
+ traceAgent: assignee,
481
+ }
482
+ : {}),
483
+ ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
484
+ };
485
+ const runOptions = {
486
+ ...traceBase,
487
+ team: buildTaskAgentTeamInfo(ctx, task.id, traceBase, 0, [assignee]),
488
+ };
489
+ const taskStartMs = Date.now();
490
+ let retryCount = 0;
491
+ const result = await executeWithRetry(() => pool.run(assignee, prompt, runOptions, config.onAgentStream
492
+ ? (event) => {
493
+ if (config.onTrace) {
494
+ const streamMs = Date.now();
495
+ emitTrace(config.onTrace, {
496
+ type: 'agent_stream',
497
+ runId: ctx.runId ?? '',
498
+ taskId: task.id,
499
+ agent: assignee,
500
+ streamType: event.type,
501
+ startMs: streamMs,
502
+ endMs: streamMs,
503
+ durationMs: 0,
504
+ });
505
+ }
506
+ config.onAgentStream(assignee, event);
507
+ }
508
+ : undefined), task, (retryData) => {
509
+ retryCount++;
510
+ config.onProgress?.({
511
+ type: 'task_retry',
512
+ task: task.id,
513
+ agent: assignee,
514
+ data: retryData,
515
+ });
516
+ });
517
+ const taskEndMs = Date.now();
518
+ // Emit task trace
519
+ if (config.onTrace) {
520
+ emitTrace(config.onTrace, {
521
+ type: 'task',
522
+ runId: ctx.runId ?? '',
523
+ taskId: task.id,
524
+ taskTitle: task.title,
525
+ agent: assignee,
526
+ success: result.success,
527
+ retries: retryCount,
528
+ startMs: taskStartMs,
529
+ endMs: taskEndMs,
530
+ durationMs: taskEndMs - taskStartMs,
531
+ });
532
+ }
533
+ ctx.agentResults.set(`${assignee}:${task.id}`, result);
534
+ ctx.taskMetrics.set(task.id, {
535
+ startMs: taskStartMs,
536
+ endMs: taskEndMs,
537
+ durationMs: Math.max(0, taskEndMs - taskStartMs),
538
+ tokenUsage: result.tokenUsage,
539
+ toolCalls: result.toolCalls,
540
+ });
541
+ ctx.cumulativeUsage = addUsage(ctx.cumulativeUsage, result.tokenUsage);
542
+ const totalTokens = ctx.cumulativeUsage.input_tokens + ctx.cumulativeUsage.output_tokens;
543
+ if (!ctx.budgetExceededTriggered
544
+ && ctx.maxTokenBudget !== undefined
545
+ && totalTokens > ctx.maxTokenBudget) {
546
+ ctx.budgetExceededTriggered = true;
547
+ const err = new TokenBudgetExceededError('orchestrator', totalTokens, ctx.maxTokenBudget);
548
+ ctx.budgetExceededReason = err.message;
549
+ config.onProgress?.({
550
+ type: 'budget_exceeded',
551
+ agent: assignee,
552
+ task: task.id,
553
+ data: err,
554
+ });
555
+ }
556
+ if (result.success) {
557
+ // Persist result into shared memory so other agents can read it
558
+ const sharedMem = team.getSharedMemoryInstance();
559
+ if (sharedMem) {
560
+ await sharedMem.write(assignee, `task:${task.id}:result`, result.output);
561
+ // Advance the turn counter so any TTL-tagged entries written during
562
+ // this task can be expired by subsequent reads.
563
+ sharedMem.advanceTurn();
564
+ }
565
+ const completedTask = queue.complete(task.id, result.output);
566
+ completedThisRound.push(completedTask);
567
+ config.onProgress?.({
568
+ type: 'task_complete',
569
+ task: task.id,
570
+ agent: assignee,
571
+ data: result,
572
+ });
573
+ config.onProgress?.({
574
+ type: 'agent_complete',
575
+ agent: assignee,
576
+ task: task.id,
577
+ data: result,
578
+ });
579
+ }
580
+ else {
581
+ queue.fail(task.id, result.output);
582
+ config.onProgress?.({
583
+ type: 'error',
584
+ task: task.id,
585
+ agent: assignee,
586
+ data: result,
587
+ });
588
+ }
589
+ });
590
+ // Wait for the entire parallel batch before checking for newly-unblocked tasks.
591
+ await Promise.all(dispatchPromises);
592
+ if (ctx.budgetExceededTriggered) {
593
+ queue.skipRemaining(ctx.budgetExceededReason ?? 'Skipped: token budget exceeded.');
594
+ break;
595
+ }
596
+ // --- Approval gate ---
597
+ // After the batch completes, check if the caller wants to approve
598
+ // the next round before it starts.
599
+ if (config.onApproval && completedThisRound.length > 0) {
600
+ scheduler.autoAssign(queue, team.getAgents());
601
+ const nextPending = queue.getByStatus('pending');
602
+ if (nextPending.length > 0) {
603
+ let approved;
604
+ try {
605
+ approved = await config.onApproval(completedThisRound, nextPending);
606
+ }
607
+ catch (err) {
608
+ const reason = `Skipped: approval callback error — ${err instanceof Error ? err.message : String(err)}`;
609
+ queue.skipRemaining(reason);
610
+ break;
611
+ }
612
+ if (!approved) {
613
+ queue.skipRemaining('Skipped: approval rejected.');
614
+ break;
615
+ }
616
+ }
617
+ }
618
+ }
619
+ unsubSkipped?.();
620
+ }
621
+ /**
622
+ * Build the agent prompt for a specific task.
623
+ *
624
+ * Injects:
625
+ * - Task title and description
626
+ * - Direct dependency task results by default (clean slate when none)
627
+ * - Optional full shared-memory context when `task.memoryScope === 'all'`
628
+ * - Any messages addressed to this agent from the team bus
629
+ */
630
+ async function buildTaskPrompt(task, team, queue) {
631
+ const lines = [
632
+ `# Task: ${task.title}`,
633
+ '',
634
+ task.description,
635
+ ];
636
+ if (task.memoryScope === 'all') {
637
+ // Explicit opt-in for full visibility (legacy/shared-memory behavior).
638
+ const sharedMem = team.getSharedMemoryInstance();
639
+ if (sharedMem) {
640
+ const summary = await sharedMem.getSummary();
641
+ if (summary) {
642
+ lines.push('', summary);
643
+ }
644
+ }
645
+ }
646
+ else if (task.dependsOn && task.dependsOn.length > 0) {
647
+ // Default-deny: inject only explicit prerequisite outputs.
648
+ const depResults = [];
649
+ for (const depId of task.dependsOn) {
650
+ const depTask = queue.get(depId);
651
+ if (depTask?.status === 'completed' && depTask.result) {
652
+ depResults.push(`### ${depTask.title} (by ${depTask.assignee ?? 'unknown'})\n${depTask.result}`);
653
+ }
654
+ }
655
+ if (depResults.length > 0) {
656
+ lines.push('', '## Context from prerequisite tasks', '', ...depResults);
657
+ }
658
+ }
659
+ // Inject messages from other agents addressed to this assignee
660
+ if (task.assignee) {
661
+ const messages = team.getMessages(task.assignee);
662
+ if (messages.length > 0) {
663
+ lines.push('', '## Messages from team members');
664
+ for (const msg of messages) {
665
+ lines.push(`- **${msg.from}**: ${msg.content}`);
666
+ }
667
+ }
668
+ }
669
+ return lines.join('\n');
670
+ }
671
+ // ---------------------------------------------------------------------------
672
+ // OpenMultiAgent
673
+ // ---------------------------------------------------------------------------
674
+ /**
675
+ * Top-level orchestrator for the open-multi-agent framework.
676
+ *
677
+ * Manages teams, coordinates task execution, and surfaces progress events.
678
+ * Most users will interact with this class exclusively.
679
+ */
680
+ export class OpenMultiAgent {
681
+ config;
682
+ teams = new Map();
683
+ completedTaskCount = 0;
684
+ /**
685
+ * @param config - Optional top-level configuration.
686
+ *
687
+ * Sensible defaults:
688
+ * - `maxConcurrency`: 5
689
+ * - `maxDelegationDepth`: 3
690
+ * - `defaultModel`: `'claude-opus-4-6'`
691
+ * - `defaultProvider`: `'anthropic'`
692
+ */
693
+ constructor(config = {}) {
694
+ this.config = {
695
+ maxConcurrency: config.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
696
+ maxDelegationDepth: config.maxDelegationDepth ?? DEFAULT_MAX_DELEGATION_DEPTH,
697
+ defaultModel: config.defaultModel ?? DEFAULT_MODEL,
698
+ defaultProvider: config.defaultProvider ?? 'anthropic',
699
+ defaultBaseURL: config.defaultBaseURL,
700
+ defaultApiKey: config.defaultApiKey,
701
+ maxTokenBudget: config.maxTokenBudget,
702
+ onApproval: config.onApproval,
703
+ onPlanReady: config.onPlanReady,
704
+ onAgentStream: config.onAgentStream,
705
+ onProgress: config.onProgress,
706
+ onTrace: config.onTrace,
707
+ };
708
+ }
709
+ // -------------------------------------------------------------------------
710
+ // Team management
711
+ // -------------------------------------------------------------------------
712
+ /**
713
+ * Create and register a {@link Team} with the orchestrator.
714
+ *
715
+ * The team is stored internally so {@link getStatus} can report aggregate
716
+ * agent counts. Returns the new {@link Team} for further configuration.
717
+ *
718
+ * @param name - Unique team identifier. Throws if already registered.
719
+ * @param config - Team configuration (agents, shared memory, concurrency).
720
+ */
721
+ createTeam(name, config) {
722
+ if (this.teams.has(name)) {
723
+ throw new Error(`OpenMultiAgent: a team named "${name}" already exists. ` +
724
+ `Use a unique name or call shutdown() to clear all teams.`);
725
+ }
726
+ const team = new Team(config);
727
+ this.teams.set(name, team);
728
+ return team;
729
+ }
730
+ // -------------------------------------------------------------------------
731
+ // Single-agent convenience
732
+ // -------------------------------------------------------------------------
733
+ /**
734
+ * Run a single prompt with a one-off agent.
735
+ *
736
+ * Constructs a fresh agent from `config`, runs `prompt` in a single turn,
737
+ * and returns the result. The agent is not registered with any pool or team.
738
+ *
739
+ * Useful for simple one-shot queries that do not need team orchestration.
740
+ *
741
+ * @param config - Agent configuration.
742
+ * @param prompt - The user prompt to send.
743
+ */
744
+ async runAgent(config, prompt, options) {
745
+ const effectiveBudget = resolveTokenBudget(config.maxTokenBudget, this.config.maxTokenBudget);
746
+ const effective = {
747
+ ...config,
748
+ provider: config.provider ?? this.config.defaultProvider,
749
+ baseURL: config.baseURL ?? this.config.defaultBaseURL,
750
+ apiKey: config.apiKey ?? this.config.defaultApiKey,
751
+ maxTokenBudget: effectiveBudget,
752
+ };
753
+ const agent = buildAgent(effective);
754
+ this.config.onProgress?.({
755
+ type: 'agent_start',
756
+ agent: config.name,
757
+ data: { prompt },
758
+ });
759
+ // Build run-time options: trace + optional abort signal. RunOptions has
760
+ // readonly fields, so we assemble the literal in one shot.
761
+ const traceFields = this.config.onTrace
762
+ ? {
763
+ onTrace: this.config.onTrace,
764
+ runId: generateRunId(),
765
+ traceAgent: config.name,
766
+ }
767
+ : null;
768
+ const abortFields = options?.abortSignal ? { abortSignal: options.abortSignal } : null;
769
+ const runOptions = traceFields || abortFields
770
+ ? { ...(traceFields ?? {}), ...(abortFields ?? {}) }
771
+ : undefined;
772
+ const result = await agent.run(prompt, runOptions);
773
+ if (result.budgetExceeded) {
774
+ this.config.onProgress?.({
775
+ type: 'budget_exceeded',
776
+ agent: config.name,
777
+ data: new TokenBudgetExceededError(config.name, result.tokenUsage.input_tokens + result.tokenUsage.output_tokens, effectiveBudget ?? 0),
778
+ });
779
+ }
780
+ this.config.onProgress?.({
781
+ type: 'agent_complete',
782
+ agent: config.name,
783
+ data: result,
784
+ });
785
+ if (result.success) {
786
+ this.completedTaskCount++;
787
+ }
788
+ return result;
789
+ }
790
+ // -------------------------------------------------------------------------
791
+ // Auto-orchestrated team run (KILLER FEATURE)
792
+ // -------------------------------------------------------------------------
793
+ /**
794
+ * Run a team on a high-level goal with full automatic orchestration.
795
+ *
796
+ * This is the flagship method of the framework. It works as follows:
797
+ *
798
+ * 1. A temporary "coordinator" agent receives the goal and the team's agent
799
+ * roster, and is asked to decompose it into an ordered list of tasks with
800
+ * JSON output.
801
+ * 2. The tasks are loaded into a {@link TaskQueue}. Title-based dependency
802
+ * tokens in the coordinator's output are resolved to task IDs.
803
+ * 3. The {@link Scheduler} assigns unassigned tasks to team agents.
804
+ * 4. Tasks are executed in dependency order, with independent tasks running
805
+ * in parallel up to `maxConcurrency`.
806
+ * 5. Results are persisted to shared memory after each task so subsequent
807
+ * agents can read them.
808
+ * 6. The coordinator synthesises a final answer from all task outputs.
809
+ * 7. A {@link TeamRunResult} is returned.
810
+ *
811
+ * @param team - A team created via {@link createTeam} (or `new Team(...)`).
812
+ * @param goal - High-level natural-language goal for the team.
813
+ */
814
+ async runTeam(team, goal, options) {
815
+ const agentConfigs = team.getAgents();
816
+ const coordinatorOverrides = options?.coordinator;
817
+ // ------------------------------------------------------------------
818
+ // Short-circuit: skip coordinator for simple, single-action goals.
819
+ //
820
+ // When the goal is short and contains no multi-step / coordination
821
+ // signals, dispatching it to a single agent is faster and cheaper
822
+ // than spinning up a coordinator for decomposition + synthesis.
823
+ //
824
+ // The best-matching agent is selected via keyword affinity scoring
825
+ // (same algorithm as the `capability-match` scheduler strategy).
826
+ // ------------------------------------------------------------------
827
+ if (!options?.planOnly && agentConfigs.length > 0 && isSimpleGoal(goal)) {
828
+ const bestAgent = selectBestAgent(goal, agentConfigs);
829
+ // Use buildAgent() + agent.run() directly instead of this.runAgent()
830
+ // to avoid duplicate progress events and double completedTaskCount.
831
+ // Events are emitted here; counting is handled by buildTeamRunResult().
832
+ const effectiveBudget = resolveTokenBudget(bestAgent.maxTokenBudget, this.config.maxTokenBudget);
833
+ const effective = {
834
+ ...bestAgent,
835
+ provider: bestAgent.provider ?? this.config.defaultProvider,
836
+ baseURL: bestAgent.baseURL ?? this.config.defaultBaseURL,
837
+ apiKey: bestAgent.apiKey ?? this.config.defaultApiKey,
838
+ maxTokenBudget: effectiveBudget,
839
+ };
840
+ const agent = buildAgent(effective);
841
+ this.config.onProgress?.({
842
+ type: 'agent_start',
843
+ agent: bestAgent.name,
844
+ data: { phase: 'short-circuit', goal },
845
+ });
846
+ const traceFields = this.config.onTrace
847
+ ? { onTrace: this.config.onTrace, runId: generateRunId(), traceAgent: bestAgent.name }
848
+ : null;
849
+ const abortFields = options?.abortSignal ? { abortSignal: options.abortSignal } : null;
850
+ const runOptions = traceFields || abortFields
851
+ ? { ...(traceFields ?? {}), ...(abortFields ?? {}) }
852
+ : undefined;
853
+ const scStartMs = Date.now();
854
+ const result = await agent.run(goal, runOptions);
855
+ const scEndMs = Date.now();
856
+ if (result.budgetExceeded) {
857
+ this.config.onProgress?.({
858
+ type: 'budget_exceeded',
859
+ agent: bestAgent.name,
860
+ data: new TokenBudgetExceededError(bestAgent.name, result.tokenUsage.input_tokens + result.tokenUsage.output_tokens, effectiveBudget ?? 0),
861
+ });
862
+ }
863
+ this.config.onProgress?.({
864
+ type: 'agent_complete',
865
+ agent: bestAgent.name,
866
+ data: { phase: 'short-circuit', result },
867
+ });
868
+ const agentResults = new Map();
869
+ agentResults.set(bestAgent.name, result);
870
+ const tasks = [{
871
+ id: 'short-circuit',
872
+ title: `Short-circuit: ${bestAgent.name}`,
873
+ assignee: bestAgent.name,
874
+ status: result.success ? 'completed' : 'failed',
875
+ dependsOn: [],
876
+ metrics: {
877
+ startMs: scStartMs,
878
+ endMs: scEndMs,
879
+ durationMs: Math.max(0, scEndMs - scStartMs),
880
+ tokenUsage: result.tokenUsage,
881
+ toolCalls: result.toolCalls,
882
+ },
883
+ }];
884
+ return this.buildTeamRunResult(agentResults, goal, tasks);
885
+ }
886
+ // ------------------------------------------------------------------
887
+ // Step 1: Coordinator decomposes goal into tasks
888
+ // ------------------------------------------------------------------
889
+ const coordinatorConfig = {
890
+ name: 'coordinator',
891
+ model: coordinatorOverrides?.model ?? this.config.defaultModel,
892
+ provider: coordinatorOverrides?.provider ?? this.config.defaultProvider,
893
+ baseURL: coordinatorOverrides?.baseURL ?? this.config.defaultBaseURL,
894
+ apiKey: coordinatorOverrides?.apiKey ?? this.config.defaultApiKey,
895
+ systemPrompt: this.buildCoordinatorPrompt(agentConfigs, coordinatorOverrides),
896
+ maxTurns: coordinatorOverrides?.maxTurns ?? 3,
897
+ maxTokens: coordinatorOverrides?.maxTokens,
898
+ temperature: coordinatorOverrides?.temperature,
899
+ topP: coordinatorOverrides?.topP,
900
+ topK: coordinatorOverrides?.topK,
901
+ minP: coordinatorOverrides?.minP,
902
+ parallelToolCalls: coordinatorOverrides?.parallelToolCalls,
903
+ frequencyPenalty: coordinatorOverrides?.frequencyPenalty,
904
+ presencePenalty: coordinatorOverrides?.presencePenalty,
905
+ extraBody: coordinatorOverrides?.extraBody,
906
+ toolPreset: coordinatorOverrides?.toolPreset,
907
+ tools: coordinatorOverrides?.tools,
908
+ disallowedTools: coordinatorOverrides?.disallowedTools,
909
+ loopDetection: coordinatorOverrides?.loopDetection,
910
+ timeoutMs: coordinatorOverrides?.timeoutMs,
911
+ };
912
+ const decompositionPrompt = this.buildDecompositionPrompt(goal, agentConfigs);
913
+ const coordinatorAgent = buildAgent(coordinatorConfig);
914
+ const runId = this.config.onTrace ? generateRunId() : undefined;
915
+ this.config.onProgress?.({
916
+ type: 'agent_start',
917
+ agent: 'coordinator',
918
+ data: { phase: 'decomposition', goal },
919
+ });
920
+ const decompTraceOptions = this.config.onTrace
921
+ ? { onTrace: this.config.onTrace, runId: runId ?? '', traceAgent: 'coordinator', abortSignal: options?.abortSignal }
922
+ : options?.abortSignal ? { abortSignal: options.abortSignal } : undefined;
923
+ const decompositionResult = await coordinatorAgent.run(decompositionPrompt, decompTraceOptions);
924
+ const agentResults = new Map();
925
+ agentResults.set('coordinator:decompose', decompositionResult);
926
+ const maxTokenBudget = this.config.maxTokenBudget;
927
+ let cumulativeUsage = addUsage(ZERO_USAGE, decompositionResult.tokenUsage);
928
+ if (maxTokenBudget !== undefined
929
+ && cumulativeUsage.input_tokens + cumulativeUsage.output_tokens > maxTokenBudget) {
930
+ this.config.onProgress?.({
931
+ type: 'budget_exceeded',
932
+ agent: 'coordinator',
933
+ data: new TokenBudgetExceededError('coordinator', cumulativeUsage.input_tokens + cumulativeUsage.output_tokens, maxTokenBudget),
934
+ });
935
+ return this.buildTeamRunResult(agentResults, goal, []);
936
+ }
937
+ // ------------------------------------------------------------------
938
+ // Step 2: Parse tasks from coordinator output
939
+ // ------------------------------------------------------------------
940
+ const taskSpecs = parseTaskSpecs(decompositionResult.output);
941
+ const queue = new TaskQueue();
942
+ const scheduler = new Scheduler('dependency-first');
943
+ const taskMetrics = new Map();
944
+ if (taskSpecs && taskSpecs.length > 0) {
945
+ // Map title-based dependsOn references to real task IDs so we can
946
+ // build the dependency graph before adding tasks to the queue.
947
+ this.loadSpecsIntoQueue(taskSpecs, agentConfigs, queue);
948
+ }
949
+ else {
950
+ // Coordinator failed to produce structured output — fall back to
951
+ // one task per agent using the goal as the description.
952
+ for (const agentConfig of agentConfigs) {
953
+ const task = createTask({
954
+ title: `${agentConfig.name}: ${goal.slice(0, 80)}`,
955
+ description: goal,
956
+ assignee: agentConfig.name,
957
+ });
958
+ queue.add(task);
959
+ }
960
+ }
961
+ // ------------------------------------------------------------------
962
+ // Step 3: Auto-assign any unassigned tasks
963
+ // ------------------------------------------------------------------
964
+ scheduler.autoAssign(queue, agentConfigs);
965
+ // ------------------------------------------------------------------
966
+ // Step 4: Build pool and execute
967
+ // ------------------------------------------------------------------
968
+ const pool = this.buildPool(agentConfigs);
969
+ const ctx = {
970
+ team,
971
+ pool,
972
+ scheduler,
973
+ agentResults,
974
+ config: this.config,
975
+ runId,
976
+ abortSignal: options?.abortSignal,
977
+ cumulativeUsage,
978
+ maxTokenBudget,
979
+ budgetExceededTriggered: false,
980
+ budgetExceededReason: undefined,
981
+ taskMetrics,
982
+ };
983
+ const planTasks = queue.list();
984
+ const planReadyStartMs = Date.now();
985
+ let approved = true;
986
+ if (this.config.onPlanReady) {
987
+ try {
988
+ approved = await this.config.onPlanReady(planTasks);
989
+ }
990
+ catch {
991
+ approved = false;
992
+ }
993
+ }
994
+ if (this.config.onTrace) {
995
+ const planReadyEndMs = Date.now();
996
+ emitTrace(this.config.onTrace, {
997
+ type: 'plan_ready',
998
+ runId: runId ?? '',
999
+ agent: 'coordinator',
1000
+ taskCount: planTasks.length,
1001
+ approved,
1002
+ startMs: planReadyStartMs,
1003
+ endMs: planReadyEndMs,
1004
+ durationMs: planReadyEndMs - planReadyStartMs,
1005
+ });
1006
+ }
1007
+ if (!approved) {
1008
+ return { ...this.buildTeamRunResult(agentResults, goal, []), success: false };
1009
+ }
1010
+ if (options?.planOnly) {
1011
+ const planOnlyTasks = queue.list().map((task) => ({
1012
+ id: task.id,
1013
+ title: task.title,
1014
+ assignee: task.assignee,
1015
+ status: task.status,
1016
+ dependsOn: task.dependsOn ?? [],
1017
+ metrics: undefined,
1018
+ }));
1019
+ this.config.onProgress?.({
1020
+ type: 'agent_complete',
1021
+ agent: 'coordinator',
1022
+ data: decompositionResult,
1023
+ });
1024
+ return {
1025
+ ...this.buildTeamRunResult(agentResults, goal, planOnlyTasks),
1026
+ planOnly: true,
1027
+ };
1028
+ }
1029
+ await executeQueue(queue, ctx);
1030
+ cumulativeUsage = ctx.cumulativeUsage;
1031
+ const taskRecords = queue.list().map((task) => ({
1032
+ id: task.id,
1033
+ title: task.title,
1034
+ assignee: task.assignee,
1035
+ status: task.status,
1036
+ dependsOn: task.dependsOn ?? [],
1037
+ metrics: taskMetrics.get(task.id),
1038
+ }));
1039
+ // ------------------------------------------------------------------
1040
+ // Step 5: Coordinator synthesises final result
1041
+ // ------------------------------------------------------------------
1042
+ if (maxTokenBudget !== undefined
1043
+ && cumulativeUsage.input_tokens + cumulativeUsage.output_tokens > maxTokenBudget) {
1044
+ return this.buildTeamRunResult(agentResults, goal, taskRecords);
1045
+ }
1046
+ const synthesisPrompt = await this.buildSynthesisPrompt(goal, queue.list(), team);
1047
+ const synthTraceOptions = this.config.onTrace
1048
+ ? { onTrace: this.config.onTrace, runId: runId ?? '', traceAgent: 'coordinator' }
1049
+ : undefined;
1050
+ const synthesisResult = await coordinatorAgent.run(synthesisPrompt, synthTraceOptions);
1051
+ agentResults.set('coordinator', synthesisResult);
1052
+ cumulativeUsage = addUsage(cumulativeUsage, synthesisResult.tokenUsage);
1053
+ if (maxTokenBudget !== undefined
1054
+ && cumulativeUsage.input_tokens + cumulativeUsage.output_tokens > maxTokenBudget) {
1055
+ this.config.onProgress?.({
1056
+ type: 'budget_exceeded',
1057
+ agent: 'coordinator',
1058
+ data: new TokenBudgetExceededError('coordinator', cumulativeUsage.input_tokens + cumulativeUsage.output_tokens, maxTokenBudget),
1059
+ });
1060
+ }
1061
+ this.config.onProgress?.({
1062
+ type: 'agent_complete',
1063
+ agent: 'coordinator',
1064
+ data: synthesisResult,
1065
+ });
1066
+ // Note: coordinator decompose and synthesis are internal meta-steps.
1067
+ // Only actual user tasks (non-coordinator keys) are counted in
1068
+ // buildTeamRunResult, so we do not increment completedTaskCount here.
1069
+ return this.buildTeamRunResult(agentResults, goal, taskRecords);
1070
+ }
1071
+ // -------------------------------------------------------------------------
1072
+ // Explicit-task team run
1073
+ // -------------------------------------------------------------------------
1074
+ /**
1075
+ * Run a team with an explicitly provided task list.
1076
+ *
1077
+ * Simpler than {@link runTeam}: no coordinator agent is involved. Tasks are
1078
+ * loaded directly into the queue, unassigned tasks are auto-assigned via the
1079
+ * {@link Scheduler}, and execution proceeds in dependency order.
1080
+ *
1081
+ * @param team - A team created via {@link createTeam}.
1082
+ * @param tasks - Array of task descriptors.
1083
+ */
1084
+ async runTasks(team, tasks, options) {
1085
+ const agentConfigs = team.getAgents();
1086
+ const queue = new TaskQueue();
1087
+ const scheduler = new Scheduler('dependency-first');
1088
+ this.loadSpecsIntoQueue(tasks.map((t) => ({
1089
+ title: t.title,
1090
+ description: t.description,
1091
+ assignee: t.assignee,
1092
+ dependsOn: t.dependsOn,
1093
+ memoryScope: t.memoryScope,
1094
+ maxRetries: t.maxRetries,
1095
+ retryDelayMs: t.retryDelayMs,
1096
+ retryBackoff: t.retryBackoff,
1097
+ })), agentConfigs, queue);
1098
+ scheduler.autoAssign(queue, agentConfigs);
1099
+ const pool = this.buildPool(agentConfigs);
1100
+ const agentResults = new Map();
1101
+ const ctx = {
1102
+ team,
1103
+ pool,
1104
+ scheduler,
1105
+ agentResults,
1106
+ config: this.config,
1107
+ runId: this.config.onTrace ? generateRunId() : undefined,
1108
+ abortSignal: options?.abortSignal,
1109
+ cumulativeUsage: ZERO_USAGE,
1110
+ maxTokenBudget: this.config.maxTokenBudget,
1111
+ budgetExceededTriggered: false,
1112
+ budgetExceededReason: undefined,
1113
+ taskMetrics: new Map(),
1114
+ };
1115
+ await executeQueue(queue, ctx);
1116
+ const taskRecords = queue.list().map((task) => ({
1117
+ id: task.id,
1118
+ title: task.title,
1119
+ assignee: task.assignee,
1120
+ status: task.status,
1121
+ dependsOn: task.dependsOn ?? [],
1122
+ metrics: ctx.taskMetrics.get(task.id),
1123
+ }));
1124
+ return this.buildTeamRunResult(agentResults, undefined, taskRecords);
1125
+ }
1126
+ // -------------------------------------------------------------------------
1127
+ // Observability
1128
+ // -------------------------------------------------------------------------
1129
+ /**
1130
+ * Returns a lightweight status snapshot.
1131
+ *
1132
+ * - `teams` — Number of teams registered with this orchestrator.
1133
+ * - `activeAgents` — Total agents currently in `running` state.
1134
+ * - `completedTasks` — Cumulative count of successfully completed tasks
1135
+ * (coordinator meta-steps excluded).
1136
+ */
1137
+ getStatus() {
1138
+ return {
1139
+ teams: this.teams.size,
1140
+ activeAgents: 0, // Pools are ephemeral per-run; no cross-run state to inspect.
1141
+ completedTasks: this.completedTaskCount,
1142
+ };
1143
+ }
1144
+ // -------------------------------------------------------------------------
1145
+ // Lifecycle
1146
+ // -------------------------------------------------------------------------
1147
+ /**
1148
+ * Deregister all teams and reset internal counters.
1149
+ *
1150
+ * Does not cancel in-flight runs. Call this when you want to reuse the
1151
+ * orchestrator instance for a fresh set of teams.
1152
+ *
1153
+ * Async for forward compatibility — shutdown may need to perform async
1154
+ * cleanup (e.g. graceful agent drain) in future versions.
1155
+ */
1156
+ async shutdown() {
1157
+ this.teams.clear();
1158
+ this.completedTaskCount = 0;
1159
+ }
1160
+ // -------------------------------------------------------------------------
1161
+ // Private helpers
1162
+ // -------------------------------------------------------------------------
1163
+ /** Build the system prompt given to the coordinator agent. */
1164
+ buildCoordinatorSystemPrompt(agents) {
1165
+ return [
1166
+ 'You are a task coordinator responsible for decomposing high-level goals',
1167
+ 'into concrete, actionable tasks and assigning them to the right team members.',
1168
+ '',
1169
+ this.buildCoordinatorRosterSection(agents),
1170
+ '',
1171
+ this.buildCoordinatorOutputFormatSection(),
1172
+ '',
1173
+ this.buildCoordinatorSynthesisSection(),
1174
+ ].join('\n');
1175
+ }
1176
+ /** Build coordinator system prompt with optional caller overrides. */
1177
+ buildCoordinatorPrompt(agents, config) {
1178
+ if (config?.systemPrompt) {
1179
+ return [
1180
+ config.systemPrompt,
1181
+ '',
1182
+ this.buildCoordinatorRosterSection(agents),
1183
+ '',
1184
+ this.buildCoordinatorOutputFormatSection(),
1185
+ '',
1186
+ this.buildCoordinatorSynthesisSection(),
1187
+ ].join('\n');
1188
+ }
1189
+ const base = this.buildCoordinatorSystemPrompt(agents);
1190
+ if (!config?.instructions) {
1191
+ return base;
1192
+ }
1193
+ return [
1194
+ base,
1195
+ '',
1196
+ '## Additional Instructions',
1197
+ config.instructions,
1198
+ ].join('\n');
1199
+ }
1200
+ /** Build the coordinator team roster section. */
1201
+ buildCoordinatorRosterSection(agents) {
1202
+ const roster = agents
1203
+ .map((a) => `- **${a.name}** (${a.model}): ${a.systemPrompt ?? 'general purpose agent'}`)
1204
+ .join('\n');
1205
+ return [
1206
+ '## Team Roster',
1207
+ roster,
1208
+ ].join('\n');
1209
+ }
1210
+ /** Build the coordinator JSON output-format section. */
1211
+ buildCoordinatorOutputFormatSection() {
1212
+ return [
1213
+ '## Output Format',
1214
+ 'When asked to decompose a goal, respond ONLY with a JSON array of task objects.',
1215
+ 'Each task must have:',
1216
+ ' - "title": Short descriptive title (string)',
1217
+ ' - "description": Full task description with context and expected output (string)',
1218
+ ' - "assignee": One of the agent names listed in the roster (string)',
1219
+ ' - "dependsOn": Array of titles of tasks this task depends on (string[], may be empty).',
1220
+ '',
1221
+ '## Dependency Guidance',
1222
+ 'Prefer the minimum set of upstream tasks each assignee needs. When deciding dependsOn for agent X:',
1223
+ ' 1. Use X\'s system prompt as the primary signal for what inputs it consumes.',
1224
+ ' 2. Lean toward including a task as a dependency only when X\'s system prompt names or describes needing that kind of input.',
1225
+ ' 3. Avoid adding a dependency just because the information "would be useful" or matches general best practice; if X\'s system prompt gives no indication it consumes that input, prefer to leave it out.',
1226
+ ' 4. When uncertain, prefer fewer dependencies over more — extra parents cost parallelism and tokens.',
1227
+ '',
1228
+ 'Wrap the JSON in a ```json code fence.',
1229
+ 'Do not include any text outside the code fence.',
1230
+ ].join('\n');
1231
+ }
1232
+ /** Build the coordinator synthesis guidance section. */
1233
+ buildCoordinatorSynthesisSection() {
1234
+ return [
1235
+ '## When synthesising results',
1236
+ 'You will be given completed task outputs and asked to synthesise a final answer.',
1237
+ 'Write a clear, comprehensive response that addresses the original goal.',
1238
+ ].join('\n');
1239
+ }
1240
+ /** Build the decomposition prompt for the coordinator. */
1241
+ buildDecompositionPrompt(goal, agents) {
1242
+ const names = agents.map((a) => a.name).join(', ');
1243
+ return [
1244
+ `Decompose the following goal into tasks for your team (${names}).`,
1245
+ '',
1246
+ `## Goal`,
1247
+ goal,
1248
+ '',
1249
+ 'Return ONLY the JSON task array in a ```json code fence.',
1250
+ ].join('\n');
1251
+ }
1252
+ /** Build the synthesis prompt shown to the coordinator after all tasks complete. */
1253
+ async buildSynthesisPrompt(goal, tasks, team) {
1254
+ const completedTasks = tasks.filter((t) => t.status === 'completed');
1255
+ const failedTasks = tasks.filter((t) => t.status === 'failed');
1256
+ const skippedTasks = tasks.filter((t) => t.status === 'skipped');
1257
+ const resultSections = completedTasks.map((t) => {
1258
+ const assignee = t.assignee ?? 'unknown';
1259
+ return `### ${t.title} (completed by ${assignee})\n${t.result ?? '(no output)'}`;
1260
+ });
1261
+ const failureSections = failedTasks.map((t) => `### ${t.title} (FAILED)\nError: ${t.result ?? 'unknown error'}`);
1262
+ const skippedSections = skippedTasks.map((t) => `### ${t.title} (SKIPPED)\nReason: ${t.result ?? 'approval rejected'}`);
1263
+ // Also include shared memory summary for additional context
1264
+ let memorySummary = '';
1265
+ const sharedMem = team.getSharedMemoryInstance();
1266
+ if (sharedMem) {
1267
+ memorySummary = await sharedMem.getSummary();
1268
+ }
1269
+ return [
1270
+ `## Original Goal`,
1271
+ goal,
1272
+ '',
1273
+ `## Task Results`,
1274
+ ...resultSections,
1275
+ ...(failureSections.length > 0 ? ['', '## Failed Tasks', ...failureSections] : []),
1276
+ ...(skippedSections.length > 0 ? ['', '## Skipped Tasks', ...skippedSections] : []),
1277
+ ...(memorySummary ? ['', memorySummary] : []),
1278
+ '',
1279
+ '## Your Task',
1280
+ 'Synthesise the above results into a comprehensive final answer that addresses the original goal.',
1281
+ 'If some tasks failed or were skipped, note any gaps in the result.',
1282
+ ].join('\n');
1283
+ }
1284
+ /**
1285
+ * Load a list of task specs into a queue.
1286
+ *
1287
+ * Handles title-based `dependsOn` references by building a title→id map first,
1288
+ * then resolving them to real IDs before adding tasks to the queue.
1289
+ */
1290
+ loadSpecsIntoQueue(specs, agentConfigs, queue) {
1291
+ const agentNames = new Set(agentConfigs.map((a) => a.name));
1292
+ // First pass: create tasks (without dependencies) to get stable IDs.
1293
+ const titleToId = new Map();
1294
+ const createdTasks = [];
1295
+ for (const spec of specs) {
1296
+ const task = createTask({
1297
+ title: spec.title,
1298
+ description: spec.description,
1299
+ assignee: spec.assignee && agentNames.has(spec.assignee)
1300
+ ? spec.assignee
1301
+ : undefined,
1302
+ memoryScope: spec.memoryScope,
1303
+ maxRetries: spec.maxRetries,
1304
+ retryDelayMs: spec.retryDelayMs,
1305
+ retryBackoff: spec.retryBackoff,
1306
+ });
1307
+ titleToId.set(spec.title.toLowerCase().trim(), task.id);
1308
+ createdTasks.push(task);
1309
+ }
1310
+ // Second pass: resolve title-based dependsOn to IDs.
1311
+ for (let i = 0; i < createdTasks.length; i++) {
1312
+ const spec = specs[i];
1313
+ const task = createdTasks[i];
1314
+ if (!spec.dependsOn || spec.dependsOn.length === 0) {
1315
+ queue.add(task);
1316
+ continue;
1317
+ }
1318
+ const resolvedDeps = [];
1319
+ for (const depRef of spec.dependsOn) {
1320
+ // Accept both raw IDs and title strings
1321
+ const byId = createdTasks.find((t) => t.id === depRef);
1322
+ const byTitle = titleToId.get(depRef.toLowerCase().trim());
1323
+ const resolvedId = byId?.id ?? byTitle;
1324
+ if (resolvedId) {
1325
+ resolvedDeps.push(resolvedId);
1326
+ }
1327
+ }
1328
+ const taskWithDeps = {
1329
+ ...task,
1330
+ dependsOn: resolvedDeps.length > 0 ? resolvedDeps : undefined,
1331
+ };
1332
+ queue.add(taskWithDeps);
1333
+ }
1334
+ }
1335
+ /** Build an {@link AgentPool} from a list of agent configurations. */
1336
+ buildPool(agentConfigs) {
1337
+ const pool = new AgentPool(this.config.maxConcurrency);
1338
+ for (const config of agentConfigs) {
1339
+ const effective = {
1340
+ ...config,
1341
+ model: config.model,
1342
+ provider: config.provider ?? this.config.defaultProvider,
1343
+ baseURL: config.baseURL ?? this.config.defaultBaseURL,
1344
+ apiKey: config.apiKey ?? this.config.defaultApiKey,
1345
+ };
1346
+ pool.add(buildAgent(effective, { includeDelegateTool: true }));
1347
+ }
1348
+ return pool;
1349
+ }
1350
+ /**
1351
+ * Aggregate the per-run `agentResults` map into a {@link TeamRunResult}.
1352
+ *
1353
+ * Merges results keyed as `agentName:taskId` back into a per-agent map
1354
+ * by agent name for the public result surface.
1355
+ *
1356
+ * Only non-coordinator entries are counted toward `completedTaskCount` to
1357
+ * avoid double-counting the coordinator's internal decompose/synthesis steps.
1358
+ */
1359
+ buildTeamRunResult(agentResults, goal, tasks) {
1360
+ let totalUsage = ZERO_USAGE;
1361
+ let overallSuccess = true;
1362
+ const collapsed = new Map();
1363
+ for (const [key, result] of agentResults) {
1364
+ // Strip the `:taskId` suffix to get the agent name
1365
+ const agentName = key.includes(':') ? key.split(':')[0] : key;
1366
+ totalUsage = addUsage(totalUsage, result.tokenUsage);
1367
+ if (!result.success)
1368
+ overallSuccess = false;
1369
+ const existing = collapsed.get(agentName);
1370
+ if (!existing) {
1371
+ collapsed.set(agentName, result);
1372
+ }
1373
+ else {
1374
+ // Merge multiple results for the same agent (multi-task case).
1375
+ // Keep the latest `structured` value (last completed task wins).
1376
+ collapsed.set(agentName, {
1377
+ success: existing.success && result.success,
1378
+ output: [existing.output, result.output].filter(Boolean).join('\n\n---\n\n'),
1379
+ messages: [...existing.messages, ...result.messages],
1380
+ tokenUsage: addUsage(existing.tokenUsage, result.tokenUsage),
1381
+ toolCalls: [...existing.toolCalls, ...result.toolCalls],
1382
+ structured: result.structured !== undefined ? result.structured : existing.structured,
1383
+ });
1384
+ }
1385
+ // Only count actual user tasks — skip coordinator meta-entries
1386
+ // (keys that start with 'coordinator') to avoid double-counting.
1387
+ if (result.success && !key.startsWith('coordinator')) {
1388
+ this.completedTaskCount++;
1389
+ }
1390
+ }
1391
+ return {
1392
+ success: overallSuccess,
1393
+ goal,
1394
+ tasks,
1395
+ agentResults: collapsed,
1396
+ totalTokenUsage: totalUsage,
1397
+ };
1398
+ }
1399
+ }
1400
+ //# sourceMappingURL=orchestrator.js.map