@jackchen_me/open-multi-agent 0.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.
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/agent/agent.d.ts +121 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/agent.js +294 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/pool.d.ts +128 -0
- package/dist/agent/pool.d.ts.map +1 -0
- package/dist/agent/pool.js +236 -0
- package/dist/agent/pool.js.map +1 -0
- package/dist/agent/runner.d.ts +120 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +274 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/adapter.d.ts +38 -0
- package/dist/llm/adapter.d.ts.map +1 -0
- package/dist/llm/adapter.js +46 -0
- package/dist/llm/adapter.js.map +1 -0
- package/dist/llm/anthropic.d.ts +56 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +307 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/openai.d.ts +62 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +424 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/memory/shared.d.ts +86 -0
- package/dist/memory/shared.d.ts.map +1 -0
- package/dist/memory/shared.js +155 -0
- package/dist/memory/shared.js.map +1 -0
- package/dist/memory/store.d.ts +64 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +103 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +173 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.js +698 -0
- package/dist/orchestrator/orchestrator.js.map +1 -0
- package/dist/orchestrator/scheduler.d.ts +112 -0
- package/dist/orchestrator/scheduler.d.ts.map +1 -0
- package/dist/orchestrator/scheduler.js +282 -0
- package/dist/orchestrator/scheduler.js.map +1 -0
- package/dist/task/queue.d.ts +160 -0
- package/dist/task/queue.d.ts.map +1 -0
- package/dist/task/queue.js +337 -0
- package/dist/task/queue.js.map +1 -0
- package/dist/task/task.d.ts +86 -0
- package/dist/task/task.d.ts.map +1 -0
- package/dist/task/task.js +201 -0
- package/dist/task/task.js.map +1 -0
- package/dist/team/messaging.d.ts +106 -0
- package/dist/team/messaging.d.ts.map +1 -0
- package/dist/team/messaging.js +182 -0
- package/dist/team/messaging.js.map +1 -0
- package/dist/team/team.d.ts +141 -0
- package/dist/team/team.d.ts.map +1 -0
- package/dist/team/team.js +282 -0
- package/dist/team/team.js.map +1 -0
- package/dist/tool/built-in/bash.d.ts +12 -0
- package/dist/tool/built-in/bash.d.ts.map +1 -0
- package/dist/tool/built-in/bash.js +133 -0
- package/dist/tool/built-in/bash.js.map +1 -0
- package/dist/tool/built-in/file-edit.d.ts +14 -0
- package/dist/tool/built-in/file-edit.d.ts.map +1 -0
- package/dist/tool/built-in/file-edit.js +130 -0
- package/dist/tool/built-in/file-edit.js.map +1 -0
- package/dist/tool/built-in/file-read.d.ts +12 -0
- package/dist/tool/built-in/file-read.d.ts.map +1 -0
- package/dist/tool/built-in/file-read.js +82 -0
- package/dist/tool/built-in/file-read.js.map +1 -0
- package/dist/tool/built-in/file-write.d.ts +11 -0
- package/dist/tool/built-in/file-write.d.ts.map +1 -0
- package/dist/tool/built-in/file-write.js +70 -0
- package/dist/tool/built-in/file-write.js.map +1 -0
- package/dist/tool/built-in/grep.d.ts +15 -0
- package/dist/tool/built-in/grep.d.ts.map +1 -0
- package/dist/tool/built-in/grep.js +287 -0
- package/dist/tool/built-in/grep.js.map +1 -0
- package/dist/tool/built-in/index.d.ts +36 -0
- package/dist/tool/built-in/index.d.ts.map +1 -0
- package/dist/tool/built-in/index.js +45 -0
- package/dist/tool/built-in/index.js.map +1 -0
- package/dist/tool/executor.d.ts +71 -0
- package/dist/tool/executor.d.ts.map +1 -0
- package/dist/tool/executor.js +116 -0
- package/dist/tool/executor.js.map +1 -0
- package/dist/tool/framework.d.ts +143 -0
- package/dist/tool/framework.d.ts.map +1 -0
- package/dist/tool/framework.js +371 -0
- package/dist/tool/framework.js.map +1 -0
- package/dist/types.d.ts +285 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/semaphore.d.ts +47 -0
- package/dist/utils/semaphore.d.ts.map +1 -0
- package/dist/utils/semaphore.js +85 -0
- package/dist/utils/semaphore.js.map +1 -0
- package/examples/01-single-agent.ts +131 -0
- package/examples/02-team-collaboration.ts +167 -0
- package/examples/03-task-pipeline.ts +201 -0
- package/examples/04-multi-model-team.ts +261 -0
- package/package.json +49 -0
- package/src/agent/agent.ts +364 -0
- package/src/agent/pool.ts +278 -0
- package/src/agent/runner.ts +413 -0
- package/src/index.ts +166 -0
- package/src/llm/adapter.ts +74 -0
- package/src/llm/anthropic.ts +388 -0
- package/src/llm/openai.ts +522 -0
- package/src/memory/shared.ts +181 -0
- package/src/memory/store.ts +124 -0
- package/src/orchestrator/orchestrator.ts +851 -0
- package/src/orchestrator/scheduler.ts +352 -0
- package/src/task/queue.ts +394 -0
- package/src/task/task.ts +232 -0
- package/src/team/messaging.ts +230 -0
- package/src/team/team.ts +334 -0
- package/src/tool/built-in/bash.ts +187 -0
- package/src/tool/built-in/file-edit.ts +154 -0
- package/src/tool/built-in/file-read.ts +105 -0
- package/src/tool/built-in/file-write.ts +81 -0
- package/src/tool/built-in/grep.ts +362 -0
- package/src/tool/built-in/index.ts +50 -0
- package/src/tool/executor.ts +178 -0
- package/src/tool/framework.ts +557 -0
- package/src/types.ts +362 -0
- package/src/utils/semaphore.ts +89 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,851 @@
|
|
|
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
|
+
|
|
44
|
+
import type {
|
|
45
|
+
AgentConfig,
|
|
46
|
+
AgentRunResult,
|
|
47
|
+
OrchestratorConfig,
|
|
48
|
+
OrchestratorEvent,
|
|
49
|
+
Task,
|
|
50
|
+
TaskStatus,
|
|
51
|
+
TeamConfig,
|
|
52
|
+
TeamRunResult,
|
|
53
|
+
TokenUsage,
|
|
54
|
+
} from '../types.js'
|
|
55
|
+
import { Agent } from '../agent/agent.js'
|
|
56
|
+
import { AgentPool } from '../agent/pool.js'
|
|
57
|
+
import { ToolRegistry } from '../tool/framework.js'
|
|
58
|
+
import { ToolExecutor } from '../tool/executor.js'
|
|
59
|
+
import { registerBuiltInTools } from '../tool/built-in/index.js'
|
|
60
|
+
import { Team } from '../team/team.js'
|
|
61
|
+
import { TaskQueue } from '../task/queue.js'
|
|
62
|
+
import { createTask } from '../task/task.js'
|
|
63
|
+
import { Scheduler } from './scheduler.js'
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Internal constants
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const ZERO_USAGE: TokenUsage = { input_tokens: 0, output_tokens: 0 }
|
|
70
|
+
const DEFAULT_MAX_CONCURRENCY = 5
|
|
71
|
+
const DEFAULT_MODEL = 'claude-opus-4-6'
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Internal helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage {
|
|
78
|
+
return {
|
|
79
|
+
input_tokens: a.input_tokens + b.input_tokens,
|
|
80
|
+
output_tokens: a.output_tokens + b.output_tokens,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a minimal {@link Agent} with its own fresh registry/executor.
|
|
86
|
+
* Registers all built-in tools so coordinator/worker agents can use them.
|
|
87
|
+
*/
|
|
88
|
+
function buildAgent(config: AgentConfig): Agent {
|
|
89
|
+
const registry = new ToolRegistry()
|
|
90
|
+
registerBuiltInTools(registry)
|
|
91
|
+
const executor = new ToolExecutor(registry)
|
|
92
|
+
return new Agent(config, registry, executor)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Parsed task spec (result of coordinator decomposition)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
interface ParsedTaskSpec {
|
|
100
|
+
title: string
|
|
101
|
+
description: string
|
|
102
|
+
assignee?: string
|
|
103
|
+
dependsOn?: string[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Attempt to extract a JSON array of task specs from the coordinator's raw
|
|
108
|
+
* output. The coordinator is prompted to emit JSON inside a ```json … ``` fence
|
|
109
|
+
* or as a bare array. Returns `null` when no valid array can be extracted.
|
|
110
|
+
*/
|
|
111
|
+
function parseTaskSpecs(raw: string): ParsedTaskSpec[] | null {
|
|
112
|
+
// Strategy 1: look for a fenced JSON block
|
|
113
|
+
const fenceMatch = raw.match(/```json\s*([\s\S]*?)```/)
|
|
114
|
+
const candidate = fenceMatch ? fenceMatch[1]! : raw
|
|
115
|
+
|
|
116
|
+
// Strategy 2: find the first '[' and last ']'
|
|
117
|
+
const arrayStart = candidate.indexOf('[')
|
|
118
|
+
const arrayEnd = candidate.lastIndexOf(']')
|
|
119
|
+
if (arrayStart === -1 || arrayEnd === -1 || arrayEnd <= arrayStart) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const jsonSlice = candidate.slice(arrayStart, arrayEnd + 1)
|
|
124
|
+
try {
|
|
125
|
+
const parsed: unknown = JSON.parse(jsonSlice)
|
|
126
|
+
if (!Array.isArray(parsed)) return null
|
|
127
|
+
|
|
128
|
+
const specs: ParsedTaskSpec[] = []
|
|
129
|
+
for (const item of parsed) {
|
|
130
|
+
if (typeof item !== 'object' || item === null) continue
|
|
131
|
+
const obj = item as Record<string, unknown>
|
|
132
|
+
if (typeof obj['title'] !== 'string') continue
|
|
133
|
+
if (typeof obj['description'] !== 'string') continue
|
|
134
|
+
|
|
135
|
+
specs.push({
|
|
136
|
+
title: obj['title'],
|
|
137
|
+
description: obj['description'],
|
|
138
|
+
assignee: typeof obj['assignee'] === 'string' ? obj['assignee'] : undefined,
|
|
139
|
+
dependsOn: Array.isArray(obj['dependsOn'])
|
|
140
|
+
? (obj['dependsOn'] as unknown[]).filter((x): x is string => typeof x === 'string')
|
|
141
|
+
: undefined,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return specs.length > 0 ? specs : null
|
|
146
|
+
} catch {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Orchestration loop
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Internal execution context assembled once per `runTeam` / `runTasks` call.
|
|
157
|
+
*/
|
|
158
|
+
interface RunContext {
|
|
159
|
+
readonly team: Team
|
|
160
|
+
readonly pool: AgentPool
|
|
161
|
+
readonly scheduler: Scheduler
|
|
162
|
+
readonly agentResults: Map<string, AgentRunResult>
|
|
163
|
+
readonly config: OrchestratorConfig
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute all tasks in `queue` using agents in `pool`, respecting dependencies
|
|
168
|
+
* and running independent tasks in parallel.
|
|
169
|
+
*
|
|
170
|
+
* The orchestration loop works in rounds:
|
|
171
|
+
* 1. Find all `'pending'` tasks (dependencies satisfied).
|
|
172
|
+
* 2. Dispatch them in parallel via the pool.
|
|
173
|
+
* 3. On completion, the queue automatically unblocks dependents.
|
|
174
|
+
* 4. Repeat until no more pending tasks exist or all remaining tasks are
|
|
175
|
+
* `'failed'`/`'blocked'` (stuck).
|
|
176
|
+
*/
|
|
177
|
+
async function executeQueue(
|
|
178
|
+
queue: TaskQueue,
|
|
179
|
+
ctx: RunContext,
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const { team, pool, scheduler, config } = ctx
|
|
182
|
+
|
|
183
|
+
while (true) {
|
|
184
|
+
// Re-run auto-assignment each iteration so tasks that were unblocked since
|
|
185
|
+
// the last round (and thus have no assignee yet) get assigned before dispatch.
|
|
186
|
+
scheduler.autoAssign(queue, team.getAgents())
|
|
187
|
+
|
|
188
|
+
const pending = queue.getByStatus('pending')
|
|
189
|
+
if (pending.length === 0) {
|
|
190
|
+
// Either all done, or everything remaining is blocked/failed.
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Dispatch all currently-pending tasks as a parallel batch.
|
|
195
|
+
const dispatchPromises = pending.map(async (task): Promise<void> => {
|
|
196
|
+
// Mark in-progress
|
|
197
|
+
queue.update(task.id, { status: 'in_progress' as TaskStatus })
|
|
198
|
+
|
|
199
|
+
const assignee = task.assignee
|
|
200
|
+
if (!assignee) {
|
|
201
|
+
// No assignee — mark failed and continue
|
|
202
|
+
const msg = `Task "${task.title}" has no assignee.`
|
|
203
|
+
queue.fail(task.id, msg)
|
|
204
|
+
config.onProgress?.({
|
|
205
|
+
type: 'error',
|
|
206
|
+
task: task.id,
|
|
207
|
+
data: msg,
|
|
208
|
+
} satisfies OrchestratorEvent)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const agent = pool.get(assignee)
|
|
213
|
+
if (!agent) {
|
|
214
|
+
const msg = `Agent "${assignee}" not found in pool for task "${task.title}".`
|
|
215
|
+
queue.fail(task.id, msg)
|
|
216
|
+
config.onProgress?.({
|
|
217
|
+
type: 'error',
|
|
218
|
+
task: task.id,
|
|
219
|
+
agent: assignee,
|
|
220
|
+
data: msg,
|
|
221
|
+
} satisfies OrchestratorEvent)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
config.onProgress?.({
|
|
226
|
+
type: 'task_start',
|
|
227
|
+
task: task.id,
|
|
228
|
+
agent: assignee,
|
|
229
|
+
data: task,
|
|
230
|
+
} satisfies OrchestratorEvent)
|
|
231
|
+
|
|
232
|
+
config.onProgress?.({
|
|
233
|
+
type: 'agent_start',
|
|
234
|
+
agent: assignee,
|
|
235
|
+
task: task.id,
|
|
236
|
+
data: task,
|
|
237
|
+
} satisfies OrchestratorEvent)
|
|
238
|
+
|
|
239
|
+
// Build the prompt: inject shared memory context + task description
|
|
240
|
+
const prompt = await buildTaskPrompt(task, team)
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const result = await pool.run(assignee, prompt)
|
|
244
|
+
ctx.agentResults.set(`${assignee}:${task.id}`, result)
|
|
245
|
+
|
|
246
|
+
if (result.success) {
|
|
247
|
+
// Persist result into shared memory so other agents can read it
|
|
248
|
+
const sharedMem = team.getSharedMemoryInstance()
|
|
249
|
+
if (sharedMem) {
|
|
250
|
+
await sharedMem.write(assignee, `task:${task.id}:result`, result.output)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
queue.complete(task.id, result.output)
|
|
254
|
+
|
|
255
|
+
config.onProgress?.({
|
|
256
|
+
type: 'task_complete',
|
|
257
|
+
task: task.id,
|
|
258
|
+
agent: assignee,
|
|
259
|
+
data: result,
|
|
260
|
+
} satisfies OrchestratorEvent)
|
|
261
|
+
|
|
262
|
+
config.onProgress?.({
|
|
263
|
+
type: 'agent_complete',
|
|
264
|
+
agent: assignee,
|
|
265
|
+
task: task.id,
|
|
266
|
+
data: result,
|
|
267
|
+
} satisfies OrchestratorEvent)
|
|
268
|
+
} else {
|
|
269
|
+
queue.fail(task.id, result.output)
|
|
270
|
+
config.onProgress?.({
|
|
271
|
+
type: 'error',
|
|
272
|
+
task: task.id,
|
|
273
|
+
agent: assignee,
|
|
274
|
+
data: result,
|
|
275
|
+
} satisfies OrchestratorEvent)
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
279
|
+
queue.fail(task.id, message)
|
|
280
|
+
config.onProgress?.({
|
|
281
|
+
type: 'error',
|
|
282
|
+
task: task.id,
|
|
283
|
+
agent: assignee,
|
|
284
|
+
data: err,
|
|
285
|
+
} satisfies OrchestratorEvent)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// Wait for the entire parallel batch before checking for newly-unblocked tasks.
|
|
290
|
+
await Promise.all(dispatchPromises)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Build the agent prompt for a specific task.
|
|
296
|
+
*
|
|
297
|
+
* Injects:
|
|
298
|
+
* - Task title and description
|
|
299
|
+
* - Dependency results from shared memory (if available)
|
|
300
|
+
* - Any messages addressed to this agent from the team bus
|
|
301
|
+
*/
|
|
302
|
+
async function buildTaskPrompt(task: Task, team: Team): Promise<string> {
|
|
303
|
+
const lines: string[] = [
|
|
304
|
+
`# Task: ${task.title}`,
|
|
305
|
+
'',
|
|
306
|
+
task.description,
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
// Inject shared memory summary so the agent sees its teammates' work
|
|
310
|
+
const sharedMem = team.getSharedMemoryInstance()
|
|
311
|
+
if (sharedMem) {
|
|
312
|
+
const summary = await sharedMem.getSummary()
|
|
313
|
+
if (summary) {
|
|
314
|
+
lines.push('', summary)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Inject messages from other agents addressed to this assignee
|
|
319
|
+
if (task.assignee) {
|
|
320
|
+
const messages = team.getMessages(task.assignee)
|
|
321
|
+
if (messages.length > 0) {
|
|
322
|
+
lines.push('', '## Messages from team members')
|
|
323
|
+
for (const msg of messages) {
|
|
324
|
+
lines.push(`- **${msg.from}**: ${msg.content}`)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return lines.join('\n')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// OpenMultiAgent
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Top-level orchestrator for the open-multi-agent framework.
|
|
338
|
+
*
|
|
339
|
+
* Manages teams, coordinates task execution, and surfaces progress events.
|
|
340
|
+
* Most users will interact with this class exclusively.
|
|
341
|
+
*/
|
|
342
|
+
export class OpenMultiAgent {
|
|
343
|
+
private readonly config: Required<
|
|
344
|
+
Omit<OrchestratorConfig, 'onProgress'>
|
|
345
|
+
> & Pick<OrchestratorConfig, 'onProgress'>
|
|
346
|
+
|
|
347
|
+
private readonly teams: Map<string, Team> = new Map()
|
|
348
|
+
private completedTaskCount = 0
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @param config - Optional top-level configuration.
|
|
352
|
+
*
|
|
353
|
+
* Sensible defaults:
|
|
354
|
+
* - `maxConcurrency`: 5
|
|
355
|
+
* - `defaultModel`: `'claude-opus-4-6'`
|
|
356
|
+
* - `defaultProvider`: `'anthropic'`
|
|
357
|
+
*/
|
|
358
|
+
constructor(config: OrchestratorConfig = {}) {
|
|
359
|
+
this.config = {
|
|
360
|
+
maxConcurrency: config.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
|
|
361
|
+
defaultModel: config.defaultModel ?? DEFAULT_MODEL,
|
|
362
|
+
defaultProvider: config.defaultProvider ?? 'anthropic',
|
|
363
|
+
onProgress: config.onProgress,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
// Team management
|
|
369
|
+
// -------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create and register a {@link Team} with the orchestrator.
|
|
373
|
+
*
|
|
374
|
+
* The team is stored internally so {@link getStatus} can report aggregate
|
|
375
|
+
* agent counts. Returns the new {@link Team} for further configuration.
|
|
376
|
+
*
|
|
377
|
+
* @param name - Unique team identifier. Throws if already registered.
|
|
378
|
+
* @param config - Team configuration (agents, shared memory, concurrency).
|
|
379
|
+
*/
|
|
380
|
+
createTeam(name: string, config: TeamConfig): Team {
|
|
381
|
+
if (this.teams.has(name)) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`OpenMultiAgent: a team named "${name}" already exists. ` +
|
|
384
|
+
`Use a unique name or call shutdown() to clear all teams.`,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
const team = new Team(config)
|
|
388
|
+
this.teams.set(name, team)
|
|
389
|
+
return team
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// -------------------------------------------------------------------------
|
|
393
|
+
// Single-agent convenience
|
|
394
|
+
// -------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Run a single prompt with a one-off agent.
|
|
398
|
+
*
|
|
399
|
+
* Constructs a fresh agent from `config`, runs `prompt` in a single turn,
|
|
400
|
+
* and returns the result. The agent is not registered with any pool or team.
|
|
401
|
+
*
|
|
402
|
+
* Useful for simple one-shot queries that do not need team orchestration.
|
|
403
|
+
*
|
|
404
|
+
* @param config - Agent configuration.
|
|
405
|
+
* @param prompt - The user prompt to send.
|
|
406
|
+
*/
|
|
407
|
+
async runAgent(config: AgentConfig, prompt: string): Promise<AgentRunResult> {
|
|
408
|
+
const agent = buildAgent(config)
|
|
409
|
+
this.config.onProgress?.({
|
|
410
|
+
type: 'agent_start',
|
|
411
|
+
agent: config.name,
|
|
412
|
+
data: { prompt },
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const result = await agent.run(prompt)
|
|
416
|
+
|
|
417
|
+
this.config.onProgress?.({
|
|
418
|
+
type: 'agent_complete',
|
|
419
|
+
agent: config.name,
|
|
420
|
+
data: result,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
if (result.success) {
|
|
424
|
+
this.completedTaskCount++
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// -------------------------------------------------------------------------
|
|
431
|
+
// Auto-orchestrated team run (KILLER FEATURE)
|
|
432
|
+
// -------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Run a team on a high-level goal with full automatic orchestration.
|
|
436
|
+
*
|
|
437
|
+
* This is the flagship method of the framework. It works as follows:
|
|
438
|
+
*
|
|
439
|
+
* 1. A temporary "coordinator" agent receives the goal and the team's agent
|
|
440
|
+
* roster, and is asked to decompose it into an ordered list of tasks with
|
|
441
|
+
* JSON output.
|
|
442
|
+
* 2. The tasks are loaded into a {@link TaskQueue}. Title-based dependency
|
|
443
|
+
* tokens in the coordinator's output are resolved to task IDs.
|
|
444
|
+
* 3. The {@link Scheduler} assigns unassigned tasks to team agents.
|
|
445
|
+
* 4. Tasks are executed in dependency order, with independent tasks running
|
|
446
|
+
* in parallel up to `maxConcurrency`.
|
|
447
|
+
* 5. Results are persisted to shared memory after each task so subsequent
|
|
448
|
+
* agents can read them.
|
|
449
|
+
* 6. The coordinator synthesises a final answer from all task outputs.
|
|
450
|
+
* 7. A {@link TeamRunResult} is returned.
|
|
451
|
+
*
|
|
452
|
+
* @param team - A team created via {@link createTeam} (or `new Team(...)`).
|
|
453
|
+
* @param goal - High-level natural-language goal for the team.
|
|
454
|
+
*/
|
|
455
|
+
async runTeam(team: Team, goal: string): Promise<TeamRunResult> {
|
|
456
|
+
const agentConfigs = team.getAgents()
|
|
457
|
+
|
|
458
|
+
// ------------------------------------------------------------------
|
|
459
|
+
// Step 1: Coordinator decomposes goal into tasks
|
|
460
|
+
// ------------------------------------------------------------------
|
|
461
|
+
const coordinatorConfig: AgentConfig = {
|
|
462
|
+
name: 'coordinator',
|
|
463
|
+
model: this.config.defaultModel,
|
|
464
|
+
provider: this.config.defaultProvider,
|
|
465
|
+
systemPrompt: this.buildCoordinatorSystemPrompt(agentConfigs),
|
|
466
|
+
maxTurns: 3,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const decompositionPrompt = this.buildDecompositionPrompt(goal, agentConfigs)
|
|
470
|
+
const coordinatorAgent = buildAgent(coordinatorConfig)
|
|
471
|
+
|
|
472
|
+
this.config.onProgress?.({
|
|
473
|
+
type: 'agent_start',
|
|
474
|
+
agent: 'coordinator',
|
|
475
|
+
data: { phase: 'decomposition', goal },
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const decompositionResult = await coordinatorAgent.run(decompositionPrompt)
|
|
479
|
+
const agentResults = new Map<string, AgentRunResult>()
|
|
480
|
+
agentResults.set('coordinator:decompose', decompositionResult)
|
|
481
|
+
|
|
482
|
+
// ------------------------------------------------------------------
|
|
483
|
+
// Step 2: Parse tasks from coordinator output
|
|
484
|
+
// ------------------------------------------------------------------
|
|
485
|
+
const taskSpecs = parseTaskSpecs(decompositionResult.output)
|
|
486
|
+
|
|
487
|
+
const queue = new TaskQueue()
|
|
488
|
+
const scheduler = new Scheduler('dependency-first')
|
|
489
|
+
|
|
490
|
+
if (taskSpecs && taskSpecs.length > 0) {
|
|
491
|
+
// Map title-based dependsOn references to real task IDs so we can
|
|
492
|
+
// build the dependency graph before adding tasks to the queue.
|
|
493
|
+
this.loadSpecsIntoQueue(taskSpecs, agentConfigs, queue)
|
|
494
|
+
} else {
|
|
495
|
+
// Coordinator failed to produce structured output — fall back to
|
|
496
|
+
// one task per agent using the goal as the description.
|
|
497
|
+
for (const agentConfig of agentConfigs) {
|
|
498
|
+
const task = createTask({
|
|
499
|
+
title: `${agentConfig.name}: ${goal.slice(0, 80)}`,
|
|
500
|
+
description: goal,
|
|
501
|
+
assignee: agentConfig.name,
|
|
502
|
+
})
|
|
503
|
+
queue.add(task)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ------------------------------------------------------------------
|
|
508
|
+
// Step 3: Auto-assign any unassigned tasks
|
|
509
|
+
// ------------------------------------------------------------------
|
|
510
|
+
scheduler.autoAssign(queue, agentConfigs)
|
|
511
|
+
|
|
512
|
+
// ------------------------------------------------------------------
|
|
513
|
+
// Step 4: Build pool and execute
|
|
514
|
+
// ------------------------------------------------------------------
|
|
515
|
+
const pool = this.buildPool(agentConfigs)
|
|
516
|
+
const ctx: RunContext = {
|
|
517
|
+
team,
|
|
518
|
+
pool,
|
|
519
|
+
scheduler,
|
|
520
|
+
agentResults,
|
|
521
|
+
config: this.config,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
await executeQueue(queue, ctx)
|
|
525
|
+
|
|
526
|
+
// ------------------------------------------------------------------
|
|
527
|
+
// Step 5: Coordinator synthesises final result
|
|
528
|
+
// ------------------------------------------------------------------
|
|
529
|
+
const synthesisPrompt = await this.buildSynthesisPrompt(goal, queue.list(), team)
|
|
530
|
+
const synthesisResult = await coordinatorAgent.run(synthesisPrompt)
|
|
531
|
+
agentResults.set('coordinator', synthesisResult)
|
|
532
|
+
|
|
533
|
+
this.config.onProgress?.({
|
|
534
|
+
type: 'agent_complete',
|
|
535
|
+
agent: 'coordinator',
|
|
536
|
+
data: synthesisResult,
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
// Note: coordinator decompose and synthesis are internal meta-steps.
|
|
540
|
+
// Only actual user tasks (non-coordinator keys) are counted in
|
|
541
|
+
// buildTeamRunResult, so we do not increment completedTaskCount here.
|
|
542
|
+
|
|
543
|
+
return this.buildTeamRunResult(agentResults)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// -------------------------------------------------------------------------
|
|
547
|
+
// Explicit-task team run
|
|
548
|
+
// -------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Run a team with an explicitly provided task list.
|
|
552
|
+
*
|
|
553
|
+
* Simpler than {@link runTeam}: no coordinator agent is involved. Tasks are
|
|
554
|
+
* loaded directly into the queue, unassigned tasks are auto-assigned via the
|
|
555
|
+
* {@link Scheduler}, and execution proceeds in dependency order.
|
|
556
|
+
*
|
|
557
|
+
* @param team - A team created via {@link createTeam}.
|
|
558
|
+
* @param tasks - Array of task descriptors.
|
|
559
|
+
*/
|
|
560
|
+
async runTasks(
|
|
561
|
+
team: Team,
|
|
562
|
+
tasks: ReadonlyArray<{
|
|
563
|
+
title: string
|
|
564
|
+
description: string
|
|
565
|
+
assignee?: string
|
|
566
|
+
dependsOn?: string[]
|
|
567
|
+
}>,
|
|
568
|
+
): Promise<TeamRunResult> {
|
|
569
|
+
const agentConfigs = team.getAgents()
|
|
570
|
+
const queue = new TaskQueue()
|
|
571
|
+
const scheduler = new Scheduler('dependency-first')
|
|
572
|
+
|
|
573
|
+
this.loadSpecsIntoQueue(
|
|
574
|
+
tasks.map((t) => ({
|
|
575
|
+
title: t.title,
|
|
576
|
+
description: t.description,
|
|
577
|
+
assignee: t.assignee,
|
|
578
|
+
dependsOn: t.dependsOn,
|
|
579
|
+
})),
|
|
580
|
+
agentConfigs,
|
|
581
|
+
queue,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
scheduler.autoAssign(queue, agentConfigs)
|
|
585
|
+
|
|
586
|
+
const pool = this.buildPool(agentConfigs)
|
|
587
|
+
const agentResults = new Map<string, AgentRunResult>()
|
|
588
|
+
const ctx: RunContext = {
|
|
589
|
+
team,
|
|
590
|
+
pool,
|
|
591
|
+
scheduler,
|
|
592
|
+
agentResults,
|
|
593
|
+
config: this.config,
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
await executeQueue(queue, ctx)
|
|
597
|
+
|
|
598
|
+
return this.buildTeamRunResult(agentResults)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// -------------------------------------------------------------------------
|
|
602
|
+
// Observability
|
|
603
|
+
// -------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Returns a lightweight status snapshot.
|
|
607
|
+
*
|
|
608
|
+
* - `teams` — Number of teams registered with this orchestrator.
|
|
609
|
+
* - `activeAgents` — Total agents currently in `running` state.
|
|
610
|
+
* - `completedTasks` — Cumulative count of successfully completed tasks
|
|
611
|
+
* (coordinator meta-steps excluded).
|
|
612
|
+
*/
|
|
613
|
+
getStatus(): { teams: number; activeAgents: number; completedTasks: number } {
|
|
614
|
+
return {
|
|
615
|
+
teams: this.teams.size,
|
|
616
|
+
activeAgents: 0, // Pools are ephemeral per-run; no cross-run state to inspect.
|
|
617
|
+
completedTasks: this.completedTaskCount,
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// -------------------------------------------------------------------------
|
|
622
|
+
// Lifecycle
|
|
623
|
+
// -------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Deregister all teams and reset internal counters.
|
|
627
|
+
*
|
|
628
|
+
* Does not cancel in-flight runs. Call this when you want to reuse the
|
|
629
|
+
* orchestrator instance for a fresh set of teams.
|
|
630
|
+
*
|
|
631
|
+
* Async for forward compatibility — shutdown may need to perform async
|
|
632
|
+
* cleanup (e.g. graceful agent drain) in future versions.
|
|
633
|
+
*/
|
|
634
|
+
async shutdown(): Promise<void> {
|
|
635
|
+
this.teams.clear()
|
|
636
|
+
this.completedTaskCount = 0
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// -------------------------------------------------------------------------
|
|
640
|
+
// Private helpers
|
|
641
|
+
// -------------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
/** Build the system prompt given to the coordinator agent. */
|
|
644
|
+
private buildCoordinatorSystemPrompt(agents: AgentConfig[]): string {
|
|
645
|
+
const roster = agents
|
|
646
|
+
.map(
|
|
647
|
+
(a) =>
|
|
648
|
+
`- **${a.name}** (${a.model}): ${a.systemPrompt?.slice(0, 120) ?? 'general purpose agent'}`,
|
|
649
|
+
)
|
|
650
|
+
.join('\n')
|
|
651
|
+
|
|
652
|
+
return [
|
|
653
|
+
'You are a task coordinator responsible for decomposing high-level goals',
|
|
654
|
+
'into concrete, actionable tasks and assigning them to the right team members.',
|
|
655
|
+
'',
|
|
656
|
+
'## Team Roster',
|
|
657
|
+
roster,
|
|
658
|
+
'',
|
|
659
|
+
'## Output Format',
|
|
660
|
+
'When asked to decompose a goal, respond ONLY with a JSON array of task objects.',
|
|
661
|
+
'Each task must have:',
|
|
662
|
+
' - "title": Short descriptive title (string)',
|
|
663
|
+
' - "description": Full task description with context and expected output (string)',
|
|
664
|
+
' - "assignee": One of the agent names listed in the roster (string)',
|
|
665
|
+
' - "dependsOn": Array of titles of tasks this task depends on (string[], may be empty)',
|
|
666
|
+
'',
|
|
667
|
+
'Wrap the JSON in a ```json code fence.',
|
|
668
|
+
'Do not include any text outside the code fence.',
|
|
669
|
+
'',
|
|
670
|
+
'## When synthesising results',
|
|
671
|
+
'You will be given completed task outputs and asked to synthesise a final answer.',
|
|
672
|
+
'Write a clear, comprehensive response that addresses the original goal.',
|
|
673
|
+
].join('\n')
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Build the decomposition prompt for the coordinator. */
|
|
677
|
+
private buildDecompositionPrompt(goal: string, agents: AgentConfig[]): string {
|
|
678
|
+
const names = agents.map((a) => a.name).join(', ')
|
|
679
|
+
return [
|
|
680
|
+
`Decompose the following goal into tasks for your team (${names}).`,
|
|
681
|
+
'',
|
|
682
|
+
`## Goal`,
|
|
683
|
+
goal,
|
|
684
|
+
'',
|
|
685
|
+
'Return ONLY the JSON task array in a ```json code fence.',
|
|
686
|
+
].join('\n')
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Build the synthesis prompt shown to the coordinator after all tasks complete. */
|
|
690
|
+
private async buildSynthesisPrompt(
|
|
691
|
+
goal: string,
|
|
692
|
+
tasks: Task[],
|
|
693
|
+
team: Team,
|
|
694
|
+
): Promise<string> {
|
|
695
|
+
const completedTasks = tasks.filter((t) => t.status === 'completed')
|
|
696
|
+
const failedTasks = tasks.filter((t) => t.status === 'failed')
|
|
697
|
+
|
|
698
|
+
const resultSections = completedTasks.map((t) => {
|
|
699
|
+
const assignee = t.assignee ?? 'unknown'
|
|
700
|
+
return `### ${t.title} (completed by ${assignee})\n${t.result ?? '(no output)'}`
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
const failureSections = failedTasks.map(
|
|
704
|
+
(t) => `### ${t.title} (FAILED)\nError: ${t.result ?? 'unknown error'}`,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
// Also include shared memory summary for additional context
|
|
708
|
+
let memorySummary = ''
|
|
709
|
+
const sharedMem = team.getSharedMemoryInstance()
|
|
710
|
+
if (sharedMem) {
|
|
711
|
+
memorySummary = await sharedMem.getSummary()
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return [
|
|
715
|
+
`## Original Goal`,
|
|
716
|
+
goal,
|
|
717
|
+
'',
|
|
718
|
+
`## Task Results`,
|
|
719
|
+
...resultSections,
|
|
720
|
+
...(failureSections.length > 0 ? ['', '## Failed Tasks', ...failureSections] : []),
|
|
721
|
+
...(memorySummary ? ['', memorySummary] : []),
|
|
722
|
+
'',
|
|
723
|
+
'## Your Task',
|
|
724
|
+
'Synthesise the above results into a comprehensive final answer that addresses the original goal.',
|
|
725
|
+
'If some tasks failed, note any gaps in the result.',
|
|
726
|
+
].join('\n')
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Load a list of task specs into a queue.
|
|
731
|
+
*
|
|
732
|
+
* Handles title-based `dependsOn` references by building a title→id map first,
|
|
733
|
+
* then resolving them to real IDs before adding tasks to the queue.
|
|
734
|
+
*/
|
|
735
|
+
private loadSpecsIntoQueue(
|
|
736
|
+
specs: ReadonlyArray<ParsedTaskSpec>,
|
|
737
|
+
agentConfigs: AgentConfig[],
|
|
738
|
+
queue: TaskQueue,
|
|
739
|
+
): void {
|
|
740
|
+
const agentNames = new Set(agentConfigs.map((a) => a.name))
|
|
741
|
+
|
|
742
|
+
// First pass: create tasks (without dependencies) to get stable IDs.
|
|
743
|
+
const titleToId = new Map<string, string>()
|
|
744
|
+
const createdTasks: Task[] = []
|
|
745
|
+
|
|
746
|
+
for (const spec of specs) {
|
|
747
|
+
const task = createTask({
|
|
748
|
+
title: spec.title,
|
|
749
|
+
description: spec.description,
|
|
750
|
+
assignee: spec.assignee && agentNames.has(spec.assignee)
|
|
751
|
+
? spec.assignee
|
|
752
|
+
: undefined,
|
|
753
|
+
})
|
|
754
|
+
titleToId.set(spec.title.toLowerCase().trim(), task.id)
|
|
755
|
+
createdTasks.push(task)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Second pass: resolve title-based dependsOn to IDs.
|
|
759
|
+
for (let i = 0; i < createdTasks.length; i++) {
|
|
760
|
+
const spec = specs[i]!
|
|
761
|
+
const task = createdTasks[i]!
|
|
762
|
+
|
|
763
|
+
if (!spec.dependsOn || spec.dependsOn.length === 0) {
|
|
764
|
+
queue.add(task)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const resolvedDeps: string[] = []
|
|
769
|
+
for (const depRef of spec.dependsOn) {
|
|
770
|
+
// Accept both raw IDs and title strings
|
|
771
|
+
const byId = createdTasks.find((t) => t.id === depRef)
|
|
772
|
+
const byTitle = titleToId.get(depRef.toLowerCase().trim())
|
|
773
|
+
const resolvedId = byId?.id ?? byTitle
|
|
774
|
+
if (resolvedId) {
|
|
775
|
+
resolvedDeps.push(resolvedId)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const taskWithDeps: Task = {
|
|
780
|
+
...task,
|
|
781
|
+
dependsOn: resolvedDeps.length > 0 ? resolvedDeps : undefined,
|
|
782
|
+
}
|
|
783
|
+
queue.add(taskWithDeps)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** Build an {@link AgentPool} from a list of agent configurations. */
|
|
788
|
+
private buildPool(agentConfigs: AgentConfig[]): AgentPool {
|
|
789
|
+
const pool = new AgentPool(this.config.maxConcurrency)
|
|
790
|
+
for (const config of agentConfigs) {
|
|
791
|
+
const effective: AgentConfig = {
|
|
792
|
+
...config,
|
|
793
|
+
model: config.model,
|
|
794
|
+
provider: config.provider ?? this.config.defaultProvider,
|
|
795
|
+
}
|
|
796
|
+
pool.add(buildAgent(effective))
|
|
797
|
+
}
|
|
798
|
+
return pool
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Aggregate the per-run `agentResults` map into a {@link TeamRunResult}.
|
|
803
|
+
*
|
|
804
|
+
* Merges results keyed as `agentName:taskId` back into a per-agent map
|
|
805
|
+
* by agent name for the public result surface.
|
|
806
|
+
*
|
|
807
|
+
* Only non-coordinator entries are counted toward `completedTaskCount` to
|
|
808
|
+
* avoid double-counting the coordinator's internal decompose/synthesis steps.
|
|
809
|
+
*/
|
|
810
|
+
private buildTeamRunResult(
|
|
811
|
+
agentResults: Map<string, AgentRunResult>,
|
|
812
|
+
): TeamRunResult {
|
|
813
|
+
let totalUsage: TokenUsage = ZERO_USAGE
|
|
814
|
+
let overallSuccess = true
|
|
815
|
+
const collapsed = new Map<string, AgentRunResult>()
|
|
816
|
+
|
|
817
|
+
for (const [key, result] of agentResults) {
|
|
818
|
+
// Strip the `:taskId` suffix to get the agent name
|
|
819
|
+
const agentName = key.includes(':') ? key.split(':')[0]! : key
|
|
820
|
+
|
|
821
|
+
totalUsage = addUsage(totalUsage, result.tokenUsage)
|
|
822
|
+
if (!result.success) overallSuccess = false
|
|
823
|
+
|
|
824
|
+
const existing = collapsed.get(agentName)
|
|
825
|
+
if (!existing) {
|
|
826
|
+
collapsed.set(agentName, result)
|
|
827
|
+
} else {
|
|
828
|
+
// Merge multiple results for the same agent (multi-task case)
|
|
829
|
+
collapsed.set(agentName, {
|
|
830
|
+
success: existing.success && result.success,
|
|
831
|
+
output: [existing.output, result.output].filter(Boolean).join('\n\n---\n\n'),
|
|
832
|
+
messages: [...existing.messages, ...result.messages],
|
|
833
|
+
tokenUsage: addUsage(existing.tokenUsage, result.tokenUsage),
|
|
834
|
+
toolCalls: [...existing.toolCalls, ...result.toolCalls],
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Only count actual user tasks — skip coordinator meta-entries
|
|
839
|
+
// (keys that start with 'coordinator') to avoid double-counting.
|
|
840
|
+
if (result.success && !key.startsWith('coordinator')) {
|
|
841
|
+
this.completedTaskCount++
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
success: overallSuccess,
|
|
847
|
+
agentResults: collapsed,
|
|
848
|
+
totalTokenUsage: totalUsage,
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|