@clinebot/agents 0.0.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/README.md +145 -0
- package/dist/agent-input.d.ts +2 -0
- package/dist/agent.d.ts +56 -0
- package/dist/extensions.d.ts +21 -0
- package/dist/hooks/engine.d.ts +42 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/lifecycle.d.ts +5 -0
- package/dist/hooks/node.d.ts +2 -0
- package/dist/hooks/subprocess-runner.d.ts +16 -0
- package/dist/hooks/subprocess.d.ts +268 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +49 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +49 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/policies.d.ts +14 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/types.d.ts +35 -0
- package/dist/message-builder.d.ts +31 -0
- package/dist/prompts/cline.d.ts +1 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/runtime/agent-runtime-bus.d.ts +13 -0
- package/dist/runtime/conversation-store.d.ts +16 -0
- package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
- package/dist/runtime/tool-orchestrator.d.ts +39 -0
- package/dist/runtime/turn-processor.d.ts +21 -0
- package/dist/teams/index.d.ts +3 -0
- package/dist/teams/multi-agent.d.ts +566 -0
- package/dist/teams/spawn-agent-tool.d.ts +85 -0
- package/dist/teams/team-tools.d.ts +51 -0
- package/dist/tools/ask-question.d.ts +12 -0
- package/dist/tools/create.d.ts +59 -0
- package/dist/tools/execution.d.ts +61 -0
- package/dist/tools/formatting.d.ts +20 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/registry.d.ts +26 -0
- package/dist/tools/validation.d.ts +27 -0
- package/dist/types.d.ts +826 -0
- package/package.json +54 -0
- package/src/agent-input.ts +116 -0
- package/src/agent.test.ts +931 -0
- package/src/agent.ts +1050 -0
- package/src/example.test.ts +564 -0
- package/src/extensions.ts +337 -0
- package/src/hooks/engine.test.ts +163 -0
- package/src/hooks/engine.ts +537 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/lifecycle.ts +239 -0
- package/src/hooks/node.ts +18 -0
- package/src/hooks/subprocess-runner.ts +140 -0
- package/src/hooks/subprocess.test.ts +180 -0
- package/src/hooks/subprocess.ts +620 -0
- package/src/index.browser.ts +1 -0
- package/src/index.node.ts +21 -0
- package/src/index.ts +133 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/policies.test.ts +51 -0
- package/src/mcp/policies.ts +53 -0
- package/src/mcp/tools.test.ts +76 -0
- package/src/mcp/tools.ts +60 -0
- package/src/mcp/types.ts +41 -0
- package/src/message-builder.test.ts +175 -0
- package/src/message-builder.ts +429 -0
- package/src/prompts/cline.ts +49 -0
- package/src/prompts/index.ts +1 -0
- package/src/runtime/agent-runtime-bus.ts +53 -0
- package/src/runtime/conversation-store.ts +61 -0
- package/src/runtime/lifecycle-orchestrator.ts +90 -0
- package/src/runtime/tool-orchestrator.ts +177 -0
- package/src/runtime/turn-processor.ts +250 -0
- package/src/streaming.test.ts +197 -0
- package/src/streaming.ts +307 -0
- package/src/teams/index.ts +63 -0
- package/src/teams/multi-agent.lifecycle.test.ts +48 -0
- package/src/teams/multi-agent.ts +1866 -0
- package/src/teams/spawn-agent-tool.test.ts +172 -0
- package/src/teams/spawn-agent-tool.ts +223 -0
- package/src/teams/team-tools.test.ts +448 -0
- package/src/teams/team-tools.ts +929 -0
- package/src/tools/ask-question.ts +78 -0
- package/src/tools/create.ts +104 -0
- package/src/tools/execution.ts +311 -0
- package/src/tools/formatting.ts +73 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/tools.test.ts +292 -0
- package/src/tools/validation.ts +73 -0
- package/src/types.ts +966 -0
|
@@ -0,0 +1,1866 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Coordination
|
|
3
|
+
*
|
|
4
|
+
* Utilities for orchestrating multiple agents working together.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type Agent, createAgent } from "../agent.js";
|
|
8
|
+
import type { AgentConfig, AgentEvent, AgentResult } from "../types.js";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for a team member agent
|
|
16
|
+
*/
|
|
17
|
+
export interface TeamMemberConfig extends AgentConfig {
|
|
18
|
+
/** Optional role description for this agent */
|
|
19
|
+
role?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Task to execute on a specific agent
|
|
24
|
+
*/
|
|
25
|
+
export interface AgentTask {
|
|
26
|
+
/** ID of the agent to run the task */
|
|
27
|
+
agentId: string;
|
|
28
|
+
/** Message to send to the agent */
|
|
29
|
+
message: string;
|
|
30
|
+
/** Optional metadata for the task */
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result from a task execution
|
|
36
|
+
*/
|
|
37
|
+
export interface TaskResult {
|
|
38
|
+
/** ID of the agent that executed the task */
|
|
39
|
+
agentId: string;
|
|
40
|
+
/** The agent result */
|
|
41
|
+
result: AgentResult;
|
|
42
|
+
/** Any error that occurred */
|
|
43
|
+
error?: Error;
|
|
44
|
+
/** Task metadata */
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export enum TeamMessageType {
|
|
49
|
+
TaskStart = "task_start",
|
|
50
|
+
TaskEnd = "task_end",
|
|
51
|
+
AgentEvent = "agent_event",
|
|
52
|
+
TeammateSpawned = "teammate_spawned",
|
|
53
|
+
TeammateShutdown = "teammate_shutdown",
|
|
54
|
+
TeamTaskUpdated = "team_task_updated",
|
|
55
|
+
TeamMessage = "team_message",
|
|
56
|
+
TeamMissionLog = "team_mission_log",
|
|
57
|
+
TeamTaskCompleted = "team_task_completed",
|
|
58
|
+
RunStarted = "run_started",
|
|
59
|
+
RunQueued = "run_queued",
|
|
60
|
+
RunProgress = "run_progress",
|
|
61
|
+
RunCompleted = "run_completed",
|
|
62
|
+
RunFailed = "run_failed",
|
|
63
|
+
RunCancelled = "run_cancelled",
|
|
64
|
+
RunInterrupted = "run_interrupted",
|
|
65
|
+
OutcomeCreated = "outcome_created",
|
|
66
|
+
OutcomeFragmentAttached = "outcome_fragment_attached",
|
|
67
|
+
OutcomeFragmentReviewed = "outcome_fragment_reviewed",
|
|
68
|
+
OutcomeFinalized = "outcome_finalized",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TeammateLifecycleSpec {
|
|
72
|
+
rolePrompt: string;
|
|
73
|
+
modelId?: string;
|
|
74
|
+
maxIterations?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Event emitted during team execution
|
|
79
|
+
*/
|
|
80
|
+
export type TeamEvent =
|
|
81
|
+
| { type: TeamMessageType.TaskStart; agentId: string; message: string }
|
|
82
|
+
| {
|
|
83
|
+
type: TeamMessageType.TaskEnd;
|
|
84
|
+
agentId: string;
|
|
85
|
+
result?: AgentResult;
|
|
86
|
+
error?: Error;
|
|
87
|
+
}
|
|
88
|
+
| { type: TeamMessageType.AgentEvent; agentId: string; event: AgentEvent }
|
|
89
|
+
| {
|
|
90
|
+
type: TeamMessageType.TeammateSpawned;
|
|
91
|
+
agentId: string;
|
|
92
|
+
role?: string;
|
|
93
|
+
teammate: TeammateLifecycleSpec;
|
|
94
|
+
}
|
|
95
|
+
| { type: TeamMessageType.TeammateShutdown; agentId: string; reason?: string }
|
|
96
|
+
| { type: TeamMessageType.TeamTaskUpdated; task: TeamTask }
|
|
97
|
+
| { type: TeamMessageType.TeamMessage; message: TeamMailboxMessage }
|
|
98
|
+
| { type: TeamMessageType.TeamMissionLog; entry: MissionLogEntry }
|
|
99
|
+
| { type: TeamMessageType.RunQueued; run: TeamRunRecord }
|
|
100
|
+
| { type: TeamMessageType.RunStarted; run: TeamRunRecord }
|
|
101
|
+
| { type: TeamMessageType.RunProgress; run: TeamRunRecord; message: string }
|
|
102
|
+
| { type: TeamMessageType.RunCompleted; run: TeamRunRecord }
|
|
103
|
+
| { type: TeamMessageType.RunFailed; run: TeamRunRecord }
|
|
104
|
+
| { type: TeamMessageType.RunCancelled; run: TeamRunRecord; reason?: string }
|
|
105
|
+
| {
|
|
106
|
+
type: TeamMessageType.RunInterrupted;
|
|
107
|
+
run: TeamRunRecord;
|
|
108
|
+
reason?: string;
|
|
109
|
+
}
|
|
110
|
+
| { type: TeamMessageType.OutcomeCreated; outcome: TeamOutcome }
|
|
111
|
+
| {
|
|
112
|
+
type: TeamMessageType.OutcomeFragmentAttached;
|
|
113
|
+
fragment: TeamOutcomeFragment;
|
|
114
|
+
}
|
|
115
|
+
| {
|
|
116
|
+
type: TeamMessageType.OutcomeFragmentReviewed;
|
|
117
|
+
fragment: TeamOutcomeFragment;
|
|
118
|
+
}
|
|
119
|
+
| { type: TeamMessageType.OutcomeFinalized; outcome: TeamOutcome };
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// AgentTeam
|
|
123
|
+
// =============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A team of agents that can work together
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const team = createAgentTeam({
|
|
131
|
+
* coder: {
|
|
132
|
+
* providerId: "anthropic",
|
|
133
|
+
* modelId: "claude-sonnet-4-20250514",
|
|
134
|
+
* systemPrompt: "You are a coding expert.",
|
|
135
|
+
* tools: [readFile, writeFile],
|
|
136
|
+
* },
|
|
137
|
+
* reviewer: {
|
|
138
|
+
* providerId: "openai",
|
|
139
|
+
* modelId: "gpt-4o",
|
|
140
|
+
* systemPrompt: "You are a code reviewer.",
|
|
141
|
+
* tools: [readFile],
|
|
142
|
+
* }
|
|
143
|
+
* })
|
|
144
|
+
*
|
|
145
|
+
* const result = await team.routeTo("coder", "Write a function")
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export class AgentTeam {
|
|
149
|
+
private agents: Map<string, Agent> = new Map();
|
|
150
|
+
private configs: Map<string, TeamMemberConfig> = new Map();
|
|
151
|
+
private onTeamEvent?: (event: TeamEvent) => void;
|
|
152
|
+
|
|
153
|
+
constructor(
|
|
154
|
+
configs?: Record<string, TeamMemberConfig>,
|
|
155
|
+
onTeamEvent?: (event: TeamEvent) => void,
|
|
156
|
+
) {
|
|
157
|
+
this.onTeamEvent = onTeamEvent;
|
|
158
|
+
|
|
159
|
+
if (configs) {
|
|
160
|
+
for (const [id, config] of Object.entries(configs)) {
|
|
161
|
+
this.addAgent(id, config);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add an agent to the team
|
|
168
|
+
*
|
|
169
|
+
* @param id - Unique identifier for the agent
|
|
170
|
+
* @param config - Agent configuration
|
|
171
|
+
*/
|
|
172
|
+
addAgent(id: string, config: TeamMemberConfig): void {
|
|
173
|
+
if (this.agents.has(id)) {
|
|
174
|
+
throw new Error(`Agent with id "${id}" already exists in the team`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Wrap onEvent to emit team events
|
|
178
|
+
const wrappedConfig: AgentConfig = {
|
|
179
|
+
...config,
|
|
180
|
+
onEvent: (event: AgentEvent) => {
|
|
181
|
+
config.onEvent?.(event);
|
|
182
|
+
this.emitEvent({
|
|
183
|
+
type: TeamMessageType.AgentEvent,
|
|
184
|
+
agentId: id,
|
|
185
|
+
event,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const agent = createAgent(wrappedConfig);
|
|
191
|
+
this.agents.set(id, agent);
|
|
192
|
+
this.configs.set(id, config);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Remove an agent from the team
|
|
197
|
+
*/
|
|
198
|
+
removeAgent(id: string): boolean {
|
|
199
|
+
this.configs.delete(id);
|
|
200
|
+
return this.agents.delete(id);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get an agent by ID
|
|
205
|
+
*/
|
|
206
|
+
getAgent(id: string): Agent | undefined {
|
|
207
|
+
return this.agents.get(id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get all agent IDs in the team
|
|
212
|
+
*/
|
|
213
|
+
getAgentIds(): string[] {
|
|
214
|
+
return Array.from(this.agents.keys());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get the number of agents in the team
|
|
219
|
+
*/
|
|
220
|
+
get size(): number {
|
|
221
|
+
return this.agents.size;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Route a message to a specific agent
|
|
226
|
+
*
|
|
227
|
+
* @param agentId - ID of the agent to send the message to
|
|
228
|
+
* @param message - The message to send
|
|
229
|
+
* @returns The agent result
|
|
230
|
+
*/
|
|
231
|
+
async routeTo(agentId: string, message: string): Promise<AgentResult> {
|
|
232
|
+
const agent = this.agents.get(agentId);
|
|
233
|
+
if (!agent) {
|
|
234
|
+
throw new Error(`Agent "${agentId}" not found in team`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.emitEvent({ type: TeamMessageType.TaskStart, agentId, message });
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = await agent.run(message);
|
|
241
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
242
|
+
return result;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
245
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Continue a conversation with a specific agent
|
|
252
|
+
*
|
|
253
|
+
* @param agentId - ID of the agent to continue with
|
|
254
|
+
* @param message - The message to send
|
|
255
|
+
* @returns The agent result
|
|
256
|
+
*/
|
|
257
|
+
async continueTo(agentId: string, message: string): Promise<AgentResult> {
|
|
258
|
+
const agent = this.agents.get(agentId);
|
|
259
|
+
if (!agent) {
|
|
260
|
+
throw new Error(`Agent "${agentId}" not found in team`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.emitEvent({ type: TeamMessageType.TaskStart, agentId, message });
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const result = await agent.continue(message);
|
|
267
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
268
|
+
return result;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
271
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Run multiple tasks in parallel across different agents
|
|
278
|
+
*
|
|
279
|
+
* @param tasks - Array of tasks to execute
|
|
280
|
+
* @returns Array of task results in the same order
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const results = await team.runParallel([
|
|
285
|
+
* { agentId: "coder", message: "Implement feature X" },
|
|
286
|
+
* { agentId: "reviewer", message: "Review the code" },
|
|
287
|
+
* ])
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
async runParallel(tasks: AgentTask[]): Promise<TaskResult[]> {
|
|
291
|
+
const executions = tasks.map(async (task): Promise<TaskResult> => {
|
|
292
|
+
const agent = this.agents.get(task.agentId);
|
|
293
|
+
if (!agent) {
|
|
294
|
+
return {
|
|
295
|
+
agentId: task.agentId,
|
|
296
|
+
result: undefined as unknown as AgentResult,
|
|
297
|
+
error: new Error(`Agent "${task.agentId}" not found in team`),
|
|
298
|
+
metadata: task.metadata,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.emitEvent({
|
|
303
|
+
type: TeamMessageType.TaskStart,
|
|
304
|
+
agentId: task.agentId,
|
|
305
|
+
message: task.message,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const result = await agent.run(task.message);
|
|
310
|
+
this.emitEvent({
|
|
311
|
+
type: TeamMessageType.TaskEnd,
|
|
312
|
+
agentId: task.agentId,
|
|
313
|
+
result,
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
agentId: task.agentId,
|
|
317
|
+
result,
|
|
318
|
+
metadata: task.metadata,
|
|
319
|
+
};
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
322
|
+
this.emitEvent({
|
|
323
|
+
type: TeamMessageType.TaskEnd,
|
|
324
|
+
agentId: task.agentId,
|
|
325
|
+
error: err,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
agentId: task.agentId,
|
|
329
|
+
result: undefined as unknown as AgentResult,
|
|
330
|
+
error: err,
|
|
331
|
+
metadata: task.metadata,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return Promise.all(executions);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Run tasks sequentially across agents
|
|
341
|
+
*
|
|
342
|
+
* Tasks are executed in order, and the result of each task is available
|
|
343
|
+
* to the next task via the context parameter.
|
|
344
|
+
*
|
|
345
|
+
* @param tasks - Array of tasks to execute in order
|
|
346
|
+
* @returns Array of task results in the same order
|
|
347
|
+
*/
|
|
348
|
+
async runSequential(tasks: AgentTask[]): Promise<TaskResult[]> {
|
|
349
|
+
const results: TaskResult[] = [];
|
|
350
|
+
|
|
351
|
+
for (const task of tasks) {
|
|
352
|
+
const agent = this.agents.get(task.agentId);
|
|
353
|
+
if (!agent) {
|
|
354
|
+
results.push({
|
|
355
|
+
agentId: task.agentId,
|
|
356
|
+
result: undefined as unknown as AgentResult,
|
|
357
|
+
error: new Error(`Agent "${task.agentId}" not found in team`),
|
|
358
|
+
metadata: task.metadata,
|
|
359
|
+
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.emitEvent({
|
|
364
|
+
type: TeamMessageType.TaskStart,
|
|
365
|
+
agentId: task.agentId,
|
|
366
|
+
message: task.message,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const result = await agent.run(task.message);
|
|
371
|
+
this.emitEvent({
|
|
372
|
+
type: TeamMessageType.TaskEnd,
|
|
373
|
+
agentId: task.agentId,
|
|
374
|
+
result,
|
|
375
|
+
});
|
|
376
|
+
results.push({
|
|
377
|
+
agentId: task.agentId,
|
|
378
|
+
result,
|
|
379
|
+
metadata: task.metadata,
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
383
|
+
this.emitEvent({
|
|
384
|
+
type: TeamMessageType.TaskEnd,
|
|
385
|
+
agentId: task.agentId,
|
|
386
|
+
error: err,
|
|
387
|
+
});
|
|
388
|
+
results.push({
|
|
389
|
+
agentId: task.agentId,
|
|
390
|
+
result: undefined as unknown as AgentResult,
|
|
391
|
+
error: err,
|
|
392
|
+
metadata: task.metadata,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Run a pipeline of agents where output from one becomes input to the next
|
|
402
|
+
*
|
|
403
|
+
* @param pipeline - Array of agent IDs in pipeline order
|
|
404
|
+
* @param initialMessage - The starting message
|
|
405
|
+
* @param messageTransformer - Optional function to transform output to input
|
|
406
|
+
* @returns Array of all results from the pipeline
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```typescript
|
|
410
|
+
* const results = await team.runPipeline(
|
|
411
|
+
* ["planner", "coder", "reviewer"],
|
|
412
|
+
* "Create a REST API for user management",
|
|
413
|
+
* (prevResult, agentId) => {
|
|
414
|
+
* if (agentId === "coder") {
|
|
415
|
+
* return `Implement this plan:\n${prevResult.text}`
|
|
416
|
+
* }
|
|
417
|
+
* return `Review this code:\n${prevResult.text}`
|
|
418
|
+
* }
|
|
419
|
+
* )
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
async runPipeline(
|
|
423
|
+
pipeline: string[],
|
|
424
|
+
initialMessage: string,
|
|
425
|
+
messageTransformer?: (
|
|
426
|
+
prevResult: AgentResult,
|
|
427
|
+
nextAgentId: string,
|
|
428
|
+
) => string,
|
|
429
|
+
): Promise<TaskResult[]> {
|
|
430
|
+
const results: TaskResult[] = [];
|
|
431
|
+
let currentMessage = initialMessage;
|
|
432
|
+
|
|
433
|
+
for (const agentId of pipeline) {
|
|
434
|
+
const agent = this.agents.get(agentId);
|
|
435
|
+
if (!agent) {
|
|
436
|
+
results.push({
|
|
437
|
+
agentId,
|
|
438
|
+
result: undefined as unknown as AgentResult,
|
|
439
|
+
error: new Error(`Agent "${agentId}" not found in team`),
|
|
440
|
+
});
|
|
441
|
+
break; // Pipeline stops on missing agent
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.emitEvent({
|
|
445
|
+
type: TeamMessageType.TaskStart,
|
|
446
|
+
agentId,
|
|
447
|
+
message: currentMessage,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const result = await agent.run(currentMessage);
|
|
452
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
453
|
+
results.push({ agentId, result });
|
|
454
|
+
|
|
455
|
+
// Transform for next agent if not the last one
|
|
456
|
+
const nextIndex = pipeline.indexOf(agentId) + 1;
|
|
457
|
+
if (nextIndex < pipeline.length) {
|
|
458
|
+
const nextAgentId = pipeline[nextIndex];
|
|
459
|
+
currentMessage = messageTransformer
|
|
460
|
+
? messageTransformer(result, nextAgentId)
|
|
461
|
+
: `Previous agent output:\n${result.text}\n\nPlease continue from here.`;
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
465
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
|
|
466
|
+
results.push({
|
|
467
|
+
agentId,
|
|
468
|
+
result: undefined as unknown as AgentResult,
|
|
469
|
+
error: err,
|
|
470
|
+
});
|
|
471
|
+
break; // Pipeline stops on error
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return results;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Abort all running agents
|
|
480
|
+
*/
|
|
481
|
+
abortAll(): void {
|
|
482
|
+
for (const agent of this.agents.values()) {
|
|
483
|
+
agent.abort();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Clear all agents from the team
|
|
489
|
+
*/
|
|
490
|
+
clear(): void {
|
|
491
|
+
this.abortAll();
|
|
492
|
+
this.agents.clear();
|
|
493
|
+
this.configs.clear();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private emitEvent(event: TeamEvent): void {
|
|
497
|
+
try {
|
|
498
|
+
this.onTeamEvent?.(event);
|
|
499
|
+
} catch {
|
|
500
|
+
// Ignore callback errors
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// =============================================================================
|
|
506
|
+
// Factory Functions
|
|
507
|
+
// =============================================================================
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create a new agent team
|
|
511
|
+
*
|
|
512
|
+
* @param configs - Map of agent ID to configuration
|
|
513
|
+
* @param onTeamEvent - Optional callback for team events
|
|
514
|
+
* @returns A new AgentTeam instance
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```typescript
|
|
518
|
+
* const team = createAgentTeam({
|
|
519
|
+
* coder: {
|
|
520
|
+
* providerId: "anthropic",
|
|
521
|
+
* modelId: "claude-sonnet-4-20250514",
|
|
522
|
+
* systemPrompt: "You are a coding expert.",
|
|
523
|
+
* tools: [readFile, writeFile],
|
|
524
|
+
* },
|
|
525
|
+
* reviewer: {
|
|
526
|
+
* providerId: "anthropic",
|
|
527
|
+
* modelId: "claude-sonnet-4-20250514",
|
|
528
|
+
* systemPrompt: "You are a code reviewer.",
|
|
529
|
+
* tools: [readFile],
|
|
530
|
+
* }
|
|
531
|
+
* })
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
export function createAgentTeam(
|
|
535
|
+
configs: Record<string, TeamMemberConfig>,
|
|
536
|
+
onTeamEvent?: (event: TeamEvent) => void,
|
|
537
|
+
): AgentTeam {
|
|
538
|
+
return new AgentTeam(configs, onTeamEvent);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// =============================================================================
|
|
542
|
+
// Specialized Teams
|
|
543
|
+
// =============================================================================
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Create a simple two-agent team with a worker and reviewer
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```typescript
|
|
550
|
+
* const team = createWorkerReviewerTeam({
|
|
551
|
+
* worker: {
|
|
552
|
+
* providerId: "anthropic",
|
|
553
|
+
* modelId: "claude-sonnet-4-20250514",
|
|
554
|
+
* systemPrompt: "You are a coding expert.",
|
|
555
|
+
* tools: [...],
|
|
556
|
+
* },
|
|
557
|
+
* reviewer: {
|
|
558
|
+
* providerId: "anthropic",
|
|
559
|
+
* modelId: "claude-sonnet-4-20250514",
|
|
560
|
+
* systemPrompt: "You review code for issues.",
|
|
561
|
+
* tools: [...],
|
|
562
|
+
* }
|
|
563
|
+
* })
|
|
564
|
+
*
|
|
565
|
+
* const result = await team.doAndReview("Implement feature X")
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
export function createWorkerReviewerTeam(configs: {
|
|
569
|
+
worker: TeamMemberConfig;
|
|
570
|
+
reviewer: TeamMemberConfig;
|
|
571
|
+
}): AgentTeam & {
|
|
572
|
+
doAndReview: (
|
|
573
|
+
message: string,
|
|
574
|
+
) => Promise<{ workerResult: AgentResult; reviewResult: AgentResult }>;
|
|
575
|
+
} {
|
|
576
|
+
const team = createAgentTeam({
|
|
577
|
+
worker: configs.worker,
|
|
578
|
+
reviewer: configs.reviewer,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Add specialized method
|
|
582
|
+
const enhanced = team as AgentTeam & {
|
|
583
|
+
doAndReview: (
|
|
584
|
+
message: string,
|
|
585
|
+
) => Promise<{ workerResult: AgentResult; reviewResult: AgentResult }>;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
enhanced.doAndReview = async (message: string) => {
|
|
589
|
+
const workerResult = await team.routeTo("worker", message);
|
|
590
|
+
const reviewResult = await team.routeTo(
|
|
591
|
+
"reviewer",
|
|
592
|
+
`Please review this work:\n\n${workerResult.text}`,
|
|
593
|
+
);
|
|
594
|
+
return { workerResult, reviewResult };
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
return enhanced;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// =============================================================================
|
|
601
|
+
// Agent Teams Runtime (lead + teammate collaboration)
|
|
602
|
+
// =============================================================================
|
|
603
|
+
|
|
604
|
+
export type TeamTaskStatus =
|
|
605
|
+
| "pending"
|
|
606
|
+
| "in_progress"
|
|
607
|
+
| "blocked"
|
|
608
|
+
| "completed";
|
|
609
|
+
|
|
610
|
+
export interface TeamTask {
|
|
611
|
+
id: string;
|
|
612
|
+
title: string;
|
|
613
|
+
description: string;
|
|
614
|
+
status: TeamTaskStatus;
|
|
615
|
+
createdAt: Date;
|
|
616
|
+
updatedAt: Date;
|
|
617
|
+
createdBy: string;
|
|
618
|
+
assignee?: string;
|
|
619
|
+
dependsOn: string[];
|
|
620
|
+
summary?: string;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export type MissionLogKind =
|
|
624
|
+
| "progress"
|
|
625
|
+
| "handoff"
|
|
626
|
+
| "blocked"
|
|
627
|
+
| "decision"
|
|
628
|
+
| "done"
|
|
629
|
+
| "error";
|
|
630
|
+
|
|
631
|
+
export interface MissionLogEntry {
|
|
632
|
+
id: string;
|
|
633
|
+
ts: Date;
|
|
634
|
+
teamId: string;
|
|
635
|
+
agentId: string;
|
|
636
|
+
taskId?: string;
|
|
637
|
+
kind: MissionLogKind;
|
|
638
|
+
summary: string;
|
|
639
|
+
evidence?: string[];
|
|
640
|
+
nextAction?: string;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export interface TeamMailboxMessage {
|
|
644
|
+
id: string;
|
|
645
|
+
teamId: string;
|
|
646
|
+
fromAgentId: string;
|
|
647
|
+
toAgentId: string;
|
|
648
|
+
subject: string;
|
|
649
|
+
body: string;
|
|
650
|
+
taskId?: string;
|
|
651
|
+
sentAt: Date;
|
|
652
|
+
readAt?: Date;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export interface TeamMemberSnapshot {
|
|
656
|
+
agentId: string;
|
|
657
|
+
role: "lead" | "teammate";
|
|
658
|
+
description?: string;
|
|
659
|
+
status: "idle" | "running" | "stopped";
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
interface TeamMemberState extends TeamMemberSnapshot {
|
|
663
|
+
agent?: Agent;
|
|
664
|
+
runningCount: number;
|
|
665
|
+
lastMissionStep: number;
|
|
666
|
+
lastMissionAt: number;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export interface TeamRuntimeSnapshot {
|
|
670
|
+
teamId: string;
|
|
671
|
+
teamName: string;
|
|
672
|
+
members: TeamMemberSnapshot[];
|
|
673
|
+
taskCounts: Record<TeamTaskStatus, number>;
|
|
674
|
+
unreadMessages: number;
|
|
675
|
+
missionLogEntries: number;
|
|
676
|
+
activeRuns: number;
|
|
677
|
+
queuedRuns: number;
|
|
678
|
+
outcomeCounts: Record<TeamOutcomeStatus, number>;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export interface TeamRuntimeState {
|
|
682
|
+
teamId: string;
|
|
683
|
+
teamName: string;
|
|
684
|
+
members: TeamMemberSnapshot[];
|
|
685
|
+
tasks: TeamTask[];
|
|
686
|
+
mailbox: TeamMailboxMessage[];
|
|
687
|
+
missionLog: MissionLogEntry[];
|
|
688
|
+
runs: TeamRunRecord[];
|
|
689
|
+
outcomes: TeamOutcome[];
|
|
690
|
+
outcomeFragments: TeamOutcomeFragment[];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export interface AgentTeamsRuntimeOptions {
|
|
694
|
+
teamName: string;
|
|
695
|
+
leadAgentId?: string;
|
|
696
|
+
missionLogIntervalSteps?: number;
|
|
697
|
+
missionLogIntervalMs?: number;
|
|
698
|
+
maxConcurrentRuns?: number;
|
|
699
|
+
onTeamEvent?: (event: TeamEvent) => void;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export interface SpawnTeammateOptions {
|
|
703
|
+
agentId: string;
|
|
704
|
+
config: TeamMemberConfig;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export interface RouteToTeammateOptions {
|
|
708
|
+
taskId?: string;
|
|
709
|
+
fromAgentId?: string;
|
|
710
|
+
continueConversation?: boolean;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export type TeamRunStatus =
|
|
714
|
+
| "queued"
|
|
715
|
+
| "running"
|
|
716
|
+
| "completed"
|
|
717
|
+
| "failed"
|
|
718
|
+
| "cancelled"
|
|
719
|
+
| "interrupted";
|
|
720
|
+
|
|
721
|
+
export interface TeamRunRecord {
|
|
722
|
+
id: string;
|
|
723
|
+
agentId: string;
|
|
724
|
+
taskId?: string;
|
|
725
|
+
status: TeamRunStatus;
|
|
726
|
+
message: string;
|
|
727
|
+
priority: number;
|
|
728
|
+
retryCount: number;
|
|
729
|
+
maxRetries: number;
|
|
730
|
+
nextAttemptAt?: Date;
|
|
731
|
+
continueConversation?: boolean;
|
|
732
|
+
startedAt: Date;
|
|
733
|
+
endedAt?: Date;
|
|
734
|
+
leaseOwner?: string;
|
|
735
|
+
heartbeatAt?: Date;
|
|
736
|
+
result?: AgentResult;
|
|
737
|
+
error?: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export type TeamOutcomeStatus = "draft" | "in_review" | "finalized";
|
|
741
|
+
|
|
742
|
+
export interface TeamOutcome {
|
|
743
|
+
id: string;
|
|
744
|
+
teamId: string;
|
|
745
|
+
title: string;
|
|
746
|
+
status: TeamOutcomeStatus;
|
|
747
|
+
requiredSections: string[];
|
|
748
|
+
createdBy: string;
|
|
749
|
+
createdAt: Date;
|
|
750
|
+
finalizedAt?: Date;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export type TeamOutcomeFragmentStatus = "draft" | "reviewed" | "rejected";
|
|
754
|
+
|
|
755
|
+
export interface TeamOutcomeFragment {
|
|
756
|
+
id: string;
|
|
757
|
+
teamId: string;
|
|
758
|
+
outcomeId: string;
|
|
759
|
+
section: string;
|
|
760
|
+
sourceAgentId: string;
|
|
761
|
+
sourceRunId?: string;
|
|
762
|
+
content: string;
|
|
763
|
+
status: TeamOutcomeFragmentStatus;
|
|
764
|
+
reviewedBy?: string;
|
|
765
|
+
reviewedAt?: Date;
|
|
766
|
+
createdAt: Date;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export interface AppendMissionLogInput {
|
|
770
|
+
agentId: string;
|
|
771
|
+
taskId?: string;
|
|
772
|
+
kind: MissionLogKind;
|
|
773
|
+
summary: string;
|
|
774
|
+
evidence?: string[];
|
|
775
|
+
nextAction?: string;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export interface CreateTeamTaskInput {
|
|
779
|
+
title: string;
|
|
780
|
+
description: string;
|
|
781
|
+
createdBy: string;
|
|
782
|
+
dependsOn?: string[];
|
|
783
|
+
assignee?: string;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export interface CreateTeamOutcomeInput {
|
|
787
|
+
title: string;
|
|
788
|
+
requiredSections: string[];
|
|
789
|
+
createdBy: string;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export interface AttachTeamOutcomeFragmentInput {
|
|
793
|
+
outcomeId: string;
|
|
794
|
+
section: string;
|
|
795
|
+
sourceAgentId: string;
|
|
796
|
+
sourceRunId?: string;
|
|
797
|
+
content: string;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export interface ReviewTeamOutcomeFragmentInput {
|
|
801
|
+
fragmentId: string;
|
|
802
|
+
reviewedBy: string;
|
|
803
|
+
approved: boolean;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export class AgentTeamsRuntime {
|
|
807
|
+
private readonly teamId: string;
|
|
808
|
+
private readonly teamName: string;
|
|
809
|
+
private readonly onTeamEvent?: (event: TeamEvent) => void;
|
|
810
|
+
private readonly members: Map<string, TeamMemberState> = new Map();
|
|
811
|
+
private readonly tasks: Map<string, TeamTask> = new Map();
|
|
812
|
+
private readonly missionLog: MissionLogEntry[] = [];
|
|
813
|
+
private readonly mailbox: TeamMailboxMessage[] = [];
|
|
814
|
+
private missionStepCounter = 0;
|
|
815
|
+
private taskCounter = 0;
|
|
816
|
+
private messageCounter = 0;
|
|
817
|
+
private missionCounter = 0;
|
|
818
|
+
private runCounter = 0;
|
|
819
|
+
private outcomeCounter = 0;
|
|
820
|
+
private outcomeFragmentCounter = 0;
|
|
821
|
+
private readonly runs: Map<string, TeamRunRecord> = new Map();
|
|
822
|
+
private readonly runQueue: string[] = [];
|
|
823
|
+
private readonly outcomes: Map<string, TeamOutcome> = new Map();
|
|
824
|
+
private readonly outcomeFragments: Map<string, TeamOutcomeFragment> =
|
|
825
|
+
new Map();
|
|
826
|
+
private readonly missionLogIntervalSteps: number;
|
|
827
|
+
private readonly missionLogIntervalMs: number;
|
|
828
|
+
private readonly maxConcurrentRuns: number;
|
|
829
|
+
|
|
830
|
+
constructor(options: AgentTeamsRuntimeOptions) {
|
|
831
|
+
this.teamName = options.teamName;
|
|
832
|
+
this.teamId = `team_${sanitizeId(options.teamName)}_${Date.now().toString(36)}`;
|
|
833
|
+
this.onTeamEvent = options.onTeamEvent;
|
|
834
|
+
this.missionLogIntervalSteps = Math.max(
|
|
835
|
+
1,
|
|
836
|
+
options.missionLogIntervalSteps ?? 3,
|
|
837
|
+
);
|
|
838
|
+
this.missionLogIntervalMs = Math.max(
|
|
839
|
+
1000,
|
|
840
|
+
options.missionLogIntervalMs ?? 120000,
|
|
841
|
+
);
|
|
842
|
+
this.maxConcurrentRuns = Math.max(1, options.maxConcurrentRuns ?? 2);
|
|
843
|
+
const leadAgentId = options.leadAgentId ?? "lead";
|
|
844
|
+
this.members.set(leadAgentId, {
|
|
845
|
+
agentId: leadAgentId,
|
|
846
|
+
role: "lead",
|
|
847
|
+
status: "idle",
|
|
848
|
+
runningCount: 0,
|
|
849
|
+
lastMissionStep: 0,
|
|
850
|
+
lastMissionAt: Date.now(),
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
getTeamId(): string {
|
|
855
|
+
return this.teamId;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
getTeamName(): string {
|
|
859
|
+
return this.teamName;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
getMemberRole(agentId: string): "lead" | "teammate" | undefined {
|
|
863
|
+
return this.members.get(agentId)?.role;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
getMemberIds(): string[] {
|
|
867
|
+
return Array.from(this.members.keys());
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
getTeammateIds(): string[] {
|
|
871
|
+
return Array.from(this.members.values())
|
|
872
|
+
.filter((member) => member.role === "teammate")
|
|
873
|
+
.map((member) => member.agentId);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
getTask(taskId: string): TeamTask | undefined {
|
|
877
|
+
return this.tasks.get(taskId);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
listTasks(): TeamTask[] {
|
|
881
|
+
return Array.from(this.tasks.values());
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
listMissionLog(limit?: number): MissionLogEntry[] {
|
|
885
|
+
if (!limit || limit <= 0) {
|
|
886
|
+
return [...this.missionLog];
|
|
887
|
+
}
|
|
888
|
+
return this.missionLog.slice(Math.max(0, this.missionLog.length - limit));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
listMailbox(
|
|
892
|
+
agentId: string,
|
|
893
|
+
options?: { unreadOnly?: boolean; markRead?: boolean; limit?: number },
|
|
894
|
+
): TeamMailboxMessage[] {
|
|
895
|
+
const unreadOnly = options?.unreadOnly ?? true;
|
|
896
|
+
const markRead = options?.markRead ?? true;
|
|
897
|
+
const limit = options?.limit;
|
|
898
|
+
const messages = this.mailbox.filter(
|
|
899
|
+
(message) =>
|
|
900
|
+
message.toAgentId === agentId && (!unreadOnly || !message.readAt),
|
|
901
|
+
);
|
|
902
|
+
const selected =
|
|
903
|
+
typeof limit === "number" && limit > 0
|
|
904
|
+
? messages.slice(Math.max(0, messages.length - limit))
|
|
905
|
+
: messages;
|
|
906
|
+
if (markRead) {
|
|
907
|
+
const now = new Date();
|
|
908
|
+
for (const message of selected) {
|
|
909
|
+
if (!message.readAt) {
|
|
910
|
+
message.readAt = now;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return selected.map((message) => ({ ...message }));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
getSnapshot(): TeamRuntimeSnapshot {
|
|
918
|
+
const taskCounts: Record<TeamTaskStatus, number> = {
|
|
919
|
+
pending: 0,
|
|
920
|
+
in_progress: 0,
|
|
921
|
+
blocked: 0,
|
|
922
|
+
completed: 0,
|
|
923
|
+
};
|
|
924
|
+
for (const task of this.tasks.values()) {
|
|
925
|
+
taskCounts[task.status]++;
|
|
926
|
+
}
|
|
927
|
+
const outcomeCounts: Record<TeamOutcomeStatus, number> = {
|
|
928
|
+
draft: 0,
|
|
929
|
+
in_review: 0,
|
|
930
|
+
finalized: 0,
|
|
931
|
+
};
|
|
932
|
+
for (const outcome of this.outcomes.values()) {
|
|
933
|
+
outcomeCounts[outcome.status]++;
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
teamId: this.teamId,
|
|
937
|
+
teamName: this.teamName,
|
|
938
|
+
members: Array.from(this.members.values()).map((member) => ({
|
|
939
|
+
agentId: member.agentId,
|
|
940
|
+
role: member.role,
|
|
941
|
+
description: member.description,
|
|
942
|
+
status: member.status,
|
|
943
|
+
})),
|
|
944
|
+
taskCounts,
|
|
945
|
+
unreadMessages: this.mailbox.filter((message) => !message.readAt).length,
|
|
946
|
+
missionLogEntries: this.missionLog.length,
|
|
947
|
+
activeRuns: Array.from(this.runs.values()).filter(
|
|
948
|
+
(run) => run.status === "running",
|
|
949
|
+
).length,
|
|
950
|
+
queuedRuns: Array.from(this.runs.values()).filter(
|
|
951
|
+
(run) => run.status === "queued",
|
|
952
|
+
).length,
|
|
953
|
+
outcomeCounts,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
exportState(): TeamRuntimeState {
|
|
958
|
+
return {
|
|
959
|
+
teamId: this.teamId,
|
|
960
|
+
teamName: this.teamName,
|
|
961
|
+
members: Array.from(this.members.values()).map((member) => ({
|
|
962
|
+
agentId: member.agentId,
|
|
963
|
+
role: member.role,
|
|
964
|
+
description: member.description,
|
|
965
|
+
status: member.status,
|
|
966
|
+
})),
|
|
967
|
+
tasks: Array.from(this.tasks.values()).map((task) => ({ ...task })),
|
|
968
|
+
mailbox: this.mailbox.map((message) => ({ ...message })),
|
|
969
|
+
missionLog: this.missionLog.map((entry) => ({ ...entry })),
|
|
970
|
+
runs: Array.from(this.runs.values()).map((run) => ({ ...run })),
|
|
971
|
+
outcomes: Array.from(this.outcomes.values()).map((outcome) => ({
|
|
972
|
+
...outcome,
|
|
973
|
+
})),
|
|
974
|
+
outcomeFragments: Array.from(this.outcomeFragments.values()).map(
|
|
975
|
+
(fragment) => ({ ...fragment }),
|
|
976
|
+
),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
hydrateState(state: TeamRuntimeState): void {
|
|
981
|
+
this.tasks.clear();
|
|
982
|
+
for (const task of state.tasks) {
|
|
983
|
+
this.tasks.set(task.id, { ...task });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
this.mailbox.length = 0;
|
|
987
|
+
this.mailbox.push(...state.mailbox.map((message) => ({ ...message })));
|
|
988
|
+
|
|
989
|
+
this.missionLog.length = 0;
|
|
990
|
+
this.missionLog.push(...state.missionLog.map((entry) => ({ ...entry })));
|
|
991
|
+
|
|
992
|
+
this.runs.clear();
|
|
993
|
+
for (const run of state.runs ?? []) {
|
|
994
|
+
this.runs.set(run.id, { ...run });
|
|
995
|
+
}
|
|
996
|
+
this.runQueue.length = 0;
|
|
997
|
+
this.runQueue.push(
|
|
998
|
+
...Array.from(this.runs.values())
|
|
999
|
+
.filter((run) => run.status === "queued")
|
|
1000
|
+
.map((run) => run.id),
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
this.outcomes.clear();
|
|
1004
|
+
for (const outcome of state.outcomes ?? []) {
|
|
1005
|
+
this.outcomes.set(outcome.id, { ...outcome });
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
this.outcomeFragments.clear();
|
|
1009
|
+
for (const fragment of state.outcomeFragments ?? []) {
|
|
1010
|
+
this.outcomeFragments.set(fragment.id, { ...fragment });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Keep lead member from current runtime, restore teammate placeholders.
|
|
1014
|
+
const leadMembers = Array.from(this.members.values()).filter(
|
|
1015
|
+
(member) => member.role === "lead",
|
|
1016
|
+
);
|
|
1017
|
+
this.members.clear();
|
|
1018
|
+
for (const lead of leadMembers) {
|
|
1019
|
+
this.members.set(lead.agentId, {
|
|
1020
|
+
...lead,
|
|
1021
|
+
status: "idle",
|
|
1022
|
+
runningCount: 0,
|
|
1023
|
+
lastMissionStep: this.missionStepCounter,
|
|
1024
|
+
lastMissionAt: Date.now(),
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
for (const member of state.members) {
|
|
1028
|
+
if (member.role !== "teammate") {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
this.members.set(member.agentId, {
|
|
1032
|
+
agentId: member.agentId,
|
|
1033
|
+
role: "teammate",
|
|
1034
|
+
description: member.description,
|
|
1035
|
+
status: "stopped",
|
|
1036
|
+
agent: undefined,
|
|
1037
|
+
runningCount: 0,
|
|
1038
|
+
lastMissionStep: this.missionStepCounter,
|
|
1039
|
+
lastMissionAt: Date.now(),
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
this.taskCounter = Math.max(
|
|
1044
|
+
this.taskCounter,
|
|
1045
|
+
maxCounter(
|
|
1046
|
+
state.tasks.map((task) => task.id),
|
|
1047
|
+
"task_",
|
|
1048
|
+
),
|
|
1049
|
+
);
|
|
1050
|
+
this.messageCounter = Math.max(
|
|
1051
|
+
this.messageCounter,
|
|
1052
|
+
maxCounter(
|
|
1053
|
+
state.mailbox.map((message) => message.id),
|
|
1054
|
+
"msg_",
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
this.missionCounter = Math.max(
|
|
1058
|
+
this.missionCounter,
|
|
1059
|
+
maxCounter(
|
|
1060
|
+
state.missionLog.map((entry) => entry.id),
|
|
1061
|
+
"log_",
|
|
1062
|
+
),
|
|
1063
|
+
);
|
|
1064
|
+
this.runCounter = Math.max(
|
|
1065
|
+
this.runCounter,
|
|
1066
|
+
maxCounter(
|
|
1067
|
+
(state.runs ?? []).map((run) => run.id),
|
|
1068
|
+
"run_",
|
|
1069
|
+
),
|
|
1070
|
+
);
|
|
1071
|
+
this.outcomeCounter = Math.max(
|
|
1072
|
+
this.outcomeCounter,
|
|
1073
|
+
maxCounter(
|
|
1074
|
+
(state.outcomes ?? []).map((outcome) => outcome.id),
|
|
1075
|
+
"out_",
|
|
1076
|
+
),
|
|
1077
|
+
);
|
|
1078
|
+
this.outcomeFragmentCounter = Math.max(
|
|
1079
|
+
this.outcomeFragmentCounter,
|
|
1080
|
+
maxCounter(
|
|
1081
|
+
(state.outcomeFragments ?? []).map((fragment) => fragment.id),
|
|
1082
|
+
"frag_",
|
|
1083
|
+
),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
isTeammateActive(agentId: string): boolean {
|
|
1088
|
+
const member = this.members.get(agentId);
|
|
1089
|
+
return !!member && member.role === "teammate" && !!member.agent;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
spawnTeammate({ agentId, config }: SpawnTeammateOptions): TeamMemberSnapshot {
|
|
1093
|
+
const existing = this.members.get(agentId);
|
|
1094
|
+
if (existing && existing.role !== "teammate") {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`Team member "${agentId}" already exists and is not a teammate`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
if (existing && existing.runningCount > 0) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`Teammate "${agentId}" is currently running and cannot be respawned`,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const wrappedConfig: TeamMemberConfig = {
|
|
1106
|
+
...config,
|
|
1107
|
+
onEvent: (event: AgentEvent) => {
|
|
1108
|
+
config.onEvent?.(event);
|
|
1109
|
+
this.emitEvent({ type: TeamMessageType.AgentEvent, agentId, event });
|
|
1110
|
+
this.trackMeaningfulEvent(agentId, event);
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
const agent = createAgent(wrappedConfig);
|
|
1115
|
+
const teammate: TeamMemberState = {
|
|
1116
|
+
agentId,
|
|
1117
|
+
role: "teammate",
|
|
1118
|
+
description: config.role,
|
|
1119
|
+
status: "idle",
|
|
1120
|
+
agent,
|
|
1121
|
+
runningCount: 0,
|
|
1122
|
+
lastMissionStep: 0,
|
|
1123
|
+
lastMissionAt: Date.now(),
|
|
1124
|
+
};
|
|
1125
|
+
this.members.set(agentId, teammate);
|
|
1126
|
+
this.emitEvent({
|
|
1127
|
+
type: TeamMessageType.TeammateSpawned,
|
|
1128
|
+
agentId,
|
|
1129
|
+
role: config.role,
|
|
1130
|
+
teammate: {
|
|
1131
|
+
rolePrompt: config.systemPrompt,
|
|
1132
|
+
modelId: config.modelId,
|
|
1133
|
+
maxIterations: config.maxIterations,
|
|
1134
|
+
},
|
|
1135
|
+
});
|
|
1136
|
+
return {
|
|
1137
|
+
agentId: teammate.agentId,
|
|
1138
|
+
role: teammate.role,
|
|
1139
|
+
description: teammate.description,
|
|
1140
|
+
status: teammate.status,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
shutdownTeammate(agentId: string, reason?: string): void {
|
|
1145
|
+
const member = this.members.get(agentId);
|
|
1146
|
+
if (!member || member.role !== "teammate") {
|
|
1147
|
+
throw new Error(`Teammate "${agentId}" was not found`);
|
|
1148
|
+
}
|
|
1149
|
+
member.agent?.abort();
|
|
1150
|
+
member.status = "stopped";
|
|
1151
|
+
this.emitEvent({ type: TeamMessageType.TeammateShutdown, agentId, reason });
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Update connection overrides (e.g. refreshed API key) on all active
|
|
1156
|
+
* teammate agents so they stay in sync with the lead agent's credentials.
|
|
1157
|
+
*/
|
|
1158
|
+
updateTeammateConnections(
|
|
1159
|
+
overrides: Partial<Pick<AgentConfig, "apiKey" | "baseUrl" | "headers">>,
|
|
1160
|
+
): void {
|
|
1161
|
+
for (const member of this.members.values()) {
|
|
1162
|
+
if (member.role !== "teammate" || !member.agent) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
member.agent.updateConnection(overrides);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
createTask(input: CreateTeamTaskInput): TeamTask {
|
|
1170
|
+
const taskId = `task_${String(++this.taskCounter).padStart(4, "0")}`;
|
|
1171
|
+
const now = new Date();
|
|
1172
|
+
const task: TeamTask = {
|
|
1173
|
+
id: taskId,
|
|
1174
|
+
title: input.title,
|
|
1175
|
+
description: input.description,
|
|
1176
|
+
status: input.assignee ? "in_progress" : "pending",
|
|
1177
|
+
createdAt: now,
|
|
1178
|
+
updatedAt: now,
|
|
1179
|
+
createdBy: input.createdBy,
|
|
1180
|
+
assignee: input.assignee,
|
|
1181
|
+
dependsOn: input.dependsOn ?? [],
|
|
1182
|
+
};
|
|
1183
|
+
this.tasks.set(taskId, task);
|
|
1184
|
+
this.emitEvent({
|
|
1185
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
1186
|
+
task: { ...task },
|
|
1187
|
+
});
|
|
1188
|
+
return { ...task };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
claimTask(taskId: string, agentId: string): TeamTask {
|
|
1192
|
+
const task = this.requireTask(taskId);
|
|
1193
|
+
this.assertDependenciesResolved(task);
|
|
1194
|
+
task.status = "in_progress";
|
|
1195
|
+
task.assignee = agentId;
|
|
1196
|
+
task.updatedAt = new Date();
|
|
1197
|
+
this.emitEvent({
|
|
1198
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
1199
|
+
task: { ...task },
|
|
1200
|
+
});
|
|
1201
|
+
this.appendMissionLog({
|
|
1202
|
+
agentId,
|
|
1203
|
+
taskId,
|
|
1204
|
+
kind: "progress",
|
|
1205
|
+
summary: `Claimed task "${task.title}"`,
|
|
1206
|
+
});
|
|
1207
|
+
return { ...task };
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
blockTask(taskId: string, agentId: string, reason: string): TeamTask {
|
|
1211
|
+
const task = this.requireTask(taskId);
|
|
1212
|
+
task.status = "blocked";
|
|
1213
|
+
task.updatedAt = new Date();
|
|
1214
|
+
task.summary = reason;
|
|
1215
|
+
this.emitEvent({
|
|
1216
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
1217
|
+
task: { ...task },
|
|
1218
|
+
});
|
|
1219
|
+
this.appendMissionLog({
|
|
1220
|
+
agentId,
|
|
1221
|
+
taskId,
|
|
1222
|
+
kind: "blocked",
|
|
1223
|
+
summary: reason,
|
|
1224
|
+
});
|
|
1225
|
+
return { ...task };
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
completeTask(taskId: string, agentId: string, summary: string): TeamTask {
|
|
1229
|
+
const task = this.requireTask(taskId);
|
|
1230
|
+
task.status = "completed";
|
|
1231
|
+
task.updatedAt = new Date();
|
|
1232
|
+
task.summary = summary;
|
|
1233
|
+
if (!task.assignee) {
|
|
1234
|
+
task.assignee = agentId;
|
|
1235
|
+
}
|
|
1236
|
+
this.emitEvent({
|
|
1237
|
+
type: TeamMessageType.TeamTaskUpdated,
|
|
1238
|
+
task: { ...task },
|
|
1239
|
+
});
|
|
1240
|
+
this.appendMissionLog({
|
|
1241
|
+
agentId,
|
|
1242
|
+
taskId,
|
|
1243
|
+
kind: "done",
|
|
1244
|
+
summary,
|
|
1245
|
+
});
|
|
1246
|
+
return { ...task };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async routeToTeammate(
|
|
1250
|
+
agentId: string,
|
|
1251
|
+
message: string,
|
|
1252
|
+
options?: RouteToTeammateOptions,
|
|
1253
|
+
): Promise<AgentResult> {
|
|
1254
|
+
const member = this.members.get(agentId);
|
|
1255
|
+
if (!member || member.role !== "teammate" || !member.agent) {
|
|
1256
|
+
throw new Error(`Teammate "${agentId}" was not found`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
member.runningCount++;
|
|
1260
|
+
member.status = "running";
|
|
1261
|
+
this.emitEvent({ type: TeamMessageType.TaskStart, agentId, message });
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
const result = options?.continueConversation
|
|
1265
|
+
? await member.agent.continue(message)
|
|
1266
|
+
: await member.agent.run(message);
|
|
1267
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, result });
|
|
1268
|
+
this.recordProgressStep(
|
|
1269
|
+
agentId,
|
|
1270
|
+
`Completed a delegated run (${result.iterations} iterations)`,
|
|
1271
|
+
options?.taskId,
|
|
1272
|
+
true,
|
|
1273
|
+
);
|
|
1274
|
+
return result;
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1277
|
+
this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
|
|
1278
|
+
this.appendMissionLog({
|
|
1279
|
+
agentId,
|
|
1280
|
+
taskId: options?.taskId,
|
|
1281
|
+
kind: "error",
|
|
1282
|
+
summary: err.message,
|
|
1283
|
+
});
|
|
1284
|
+
throw err;
|
|
1285
|
+
} finally {
|
|
1286
|
+
member.runningCount--;
|
|
1287
|
+
if (
|
|
1288
|
+
member.runningCount <= 0 &&
|
|
1289
|
+
this.members.get(agentId)?.status !== "stopped"
|
|
1290
|
+
) {
|
|
1291
|
+
member.status = "idle";
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
startTeammateRun(
|
|
1297
|
+
agentId: string,
|
|
1298
|
+
message: string,
|
|
1299
|
+
options?: RouteToTeammateOptions & {
|
|
1300
|
+
priority?: number;
|
|
1301
|
+
maxRetries?: number;
|
|
1302
|
+
leaseOwner?: string;
|
|
1303
|
+
},
|
|
1304
|
+
): TeamRunRecord {
|
|
1305
|
+
const runId = `run_${String(++this.runCounter).padStart(5, "0")}`;
|
|
1306
|
+
const record: TeamRunRecord = {
|
|
1307
|
+
id: runId,
|
|
1308
|
+
agentId,
|
|
1309
|
+
taskId: options?.taskId,
|
|
1310
|
+
status: "queued",
|
|
1311
|
+
message,
|
|
1312
|
+
priority: options?.priority ?? 0,
|
|
1313
|
+
retryCount: 0,
|
|
1314
|
+
maxRetries: Math.max(0, options?.maxRetries ?? 0),
|
|
1315
|
+
continueConversation: options?.continueConversation,
|
|
1316
|
+
startedAt: new Date(0),
|
|
1317
|
+
leaseOwner: options?.leaseOwner,
|
|
1318
|
+
heartbeatAt: undefined,
|
|
1319
|
+
};
|
|
1320
|
+
this.runs.set(runId, record);
|
|
1321
|
+
this.runQueue.push(runId);
|
|
1322
|
+
this.emitEvent({ type: TeamMessageType.RunQueued, run: { ...record } });
|
|
1323
|
+
this.dispatchQueuedRuns();
|
|
1324
|
+
return { ...record };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private dispatchQueuedRuns(): void {
|
|
1328
|
+
while (
|
|
1329
|
+
this.countActiveRuns() < this.maxConcurrentRuns &&
|
|
1330
|
+
this.runQueue.length > 0
|
|
1331
|
+
) {
|
|
1332
|
+
const nextRunIndex = this.selectNextQueuedRunIndex();
|
|
1333
|
+
const [runId] = this.runQueue.splice(nextRunIndex, 1);
|
|
1334
|
+
const run = runId ? this.runs.get(runId) : undefined;
|
|
1335
|
+
if (!run || run.status !== "queued") {
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
void this.executeQueuedRun(run);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private selectNextQueuedRunIndex(): number {
|
|
1343
|
+
let selectedIndex = 0;
|
|
1344
|
+
let bestPriority = Number.NEGATIVE_INFINITY;
|
|
1345
|
+
for (let index = 0; index < this.runQueue.length; index++) {
|
|
1346
|
+
const run = this.runs.get(this.runQueue[index]);
|
|
1347
|
+
if (!run || run.status !== "queued") {
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (run.priority > bestPriority) {
|
|
1351
|
+
bestPriority = run.priority;
|
|
1352
|
+
selectedIndex = index;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return selectedIndex;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
private countActiveRuns(): number {
|
|
1359
|
+
let count = 0;
|
|
1360
|
+
for (const run of this.runs.values()) {
|
|
1361
|
+
if (run.status === "running") {
|
|
1362
|
+
count++;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return count;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
private async executeQueuedRun(run: TeamRunRecord): Promise<void> {
|
|
1369
|
+
run.status = "running";
|
|
1370
|
+
run.startedAt = new Date();
|
|
1371
|
+
run.heartbeatAt = new Date();
|
|
1372
|
+
this.emitEvent({ type: TeamMessageType.RunStarted, run: { ...run } });
|
|
1373
|
+
|
|
1374
|
+
const heartbeatTimer = setInterval(() => {
|
|
1375
|
+
if (run.status !== "running") {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
run.heartbeatAt = new Date();
|
|
1379
|
+
this.emitEvent({
|
|
1380
|
+
type: TeamMessageType.RunProgress,
|
|
1381
|
+
run: { ...run },
|
|
1382
|
+
message: "heartbeat",
|
|
1383
|
+
});
|
|
1384
|
+
}, 2000);
|
|
1385
|
+
|
|
1386
|
+
try {
|
|
1387
|
+
const result = await this.routeToTeammate(run.agentId, run.message, {
|
|
1388
|
+
taskId: run.taskId,
|
|
1389
|
+
continueConversation: run.continueConversation,
|
|
1390
|
+
});
|
|
1391
|
+
run.status = "completed";
|
|
1392
|
+
run.result = result;
|
|
1393
|
+
run.endedAt = new Date();
|
|
1394
|
+
this.emitEvent({ type: TeamMessageType.RunCompleted, run: { ...run } });
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
const message =
|
|
1397
|
+
error instanceof Error
|
|
1398
|
+
? error.message
|
|
1399
|
+
: String(error ?? "Unknown error");
|
|
1400
|
+
run.error = message;
|
|
1401
|
+
run.endedAt = new Date();
|
|
1402
|
+
if (run.retryCount < run.maxRetries) {
|
|
1403
|
+
run.retryCount++;
|
|
1404
|
+
run.status = "queued";
|
|
1405
|
+
run.nextAttemptAt = new Date(
|
|
1406
|
+
Date.now() + Math.min(30000, 1000 * 2 ** run.retryCount),
|
|
1407
|
+
);
|
|
1408
|
+
this.runQueue.push(run.id);
|
|
1409
|
+
this.emitEvent({
|
|
1410
|
+
type: TeamMessageType.RunProgress,
|
|
1411
|
+
run: { ...run },
|
|
1412
|
+
message: `retry_scheduled_${run.retryCount}`,
|
|
1413
|
+
});
|
|
1414
|
+
} else {
|
|
1415
|
+
run.status = "failed";
|
|
1416
|
+
this.emitEvent({ type: TeamMessageType.RunFailed, run: { ...run } });
|
|
1417
|
+
}
|
|
1418
|
+
} finally {
|
|
1419
|
+
clearInterval(heartbeatTimer);
|
|
1420
|
+
this.dispatchQueuedRuns();
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
listRuns(options?: {
|
|
1425
|
+
status?: TeamRunStatus;
|
|
1426
|
+
agentId?: string;
|
|
1427
|
+
includeCompleted?: boolean;
|
|
1428
|
+
}): TeamRunRecord[] {
|
|
1429
|
+
const includeCompleted = options?.includeCompleted ?? true;
|
|
1430
|
+
return Array.from(this.runs.values())
|
|
1431
|
+
.filter((run) => {
|
|
1432
|
+
if (!includeCompleted && !["running", "queued"].includes(run.status)) {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
if (options?.status && run.status !== options.status) {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
if (options?.agentId && run.agentId !== options.agentId) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
return true;
|
|
1442
|
+
})
|
|
1443
|
+
.map((run) => ({ ...run }));
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
getRun(runId: string): TeamRunRecord | undefined {
|
|
1447
|
+
const run = this.runs.get(runId);
|
|
1448
|
+
return run ? { ...run } : undefined;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async awaitRun(runId: string, pollIntervalMs = 250): Promise<TeamRunRecord> {
|
|
1452
|
+
const run = this.runs.get(runId);
|
|
1453
|
+
if (!run) {
|
|
1454
|
+
throw new Error(`Run "${runId}" was not found`);
|
|
1455
|
+
}
|
|
1456
|
+
while (run.status === "running") {
|
|
1457
|
+
await sleep(pollIntervalMs);
|
|
1458
|
+
}
|
|
1459
|
+
return { ...run };
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async awaitAllRuns(pollIntervalMs = 250): Promise<TeamRunRecord[]> {
|
|
1463
|
+
while (
|
|
1464
|
+
Array.from(this.runs.values()).some((run) =>
|
|
1465
|
+
["queued", "running"].includes(run.status),
|
|
1466
|
+
)
|
|
1467
|
+
) {
|
|
1468
|
+
await sleep(pollIntervalMs);
|
|
1469
|
+
}
|
|
1470
|
+
return this.listRuns();
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
cancelRun(runId: string, reason?: string): TeamRunRecord {
|
|
1474
|
+
const run = this.runs.get(runId);
|
|
1475
|
+
if (!run) {
|
|
1476
|
+
throw new Error(`Run "${runId}" was not found`);
|
|
1477
|
+
}
|
|
1478
|
+
if (run.status === "completed" || run.status === "failed") {
|
|
1479
|
+
return { ...run };
|
|
1480
|
+
}
|
|
1481
|
+
run.status = "cancelled";
|
|
1482
|
+
run.error = reason;
|
|
1483
|
+
run.endedAt = new Date();
|
|
1484
|
+
const queueIndex = this.runQueue.indexOf(runId);
|
|
1485
|
+
if (queueIndex >= 0) {
|
|
1486
|
+
this.runQueue.splice(queueIndex, 1);
|
|
1487
|
+
}
|
|
1488
|
+
this.emitEvent({
|
|
1489
|
+
type: TeamMessageType.RunCancelled,
|
|
1490
|
+
run: { ...run },
|
|
1491
|
+
reason,
|
|
1492
|
+
});
|
|
1493
|
+
return { ...run };
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
markStaleRunsInterrupted(reason = "runtime_recovered"): TeamRunRecord[] {
|
|
1497
|
+
const interrupted: TeamRunRecord[] = [];
|
|
1498
|
+
for (const run of this.runs.values()) {
|
|
1499
|
+
if (!["queued", "running"].includes(run.status)) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
run.status = "interrupted";
|
|
1503
|
+
run.error = reason;
|
|
1504
|
+
run.endedAt = new Date();
|
|
1505
|
+
interrupted.push({ ...run });
|
|
1506
|
+
this.emitEvent({
|
|
1507
|
+
type: TeamMessageType.RunInterrupted,
|
|
1508
|
+
run: { ...run },
|
|
1509
|
+
reason,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
this.runQueue.length = 0;
|
|
1513
|
+
return interrupted;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
sendMessage(
|
|
1517
|
+
fromAgentId: string,
|
|
1518
|
+
toAgentId: string,
|
|
1519
|
+
subject: string,
|
|
1520
|
+
body: string,
|
|
1521
|
+
taskId?: string,
|
|
1522
|
+
): TeamMailboxMessage {
|
|
1523
|
+
if (!this.members.has(fromAgentId)) {
|
|
1524
|
+
throw new Error(`Unknown sender "${fromAgentId}"`);
|
|
1525
|
+
}
|
|
1526
|
+
if (!this.members.has(toAgentId)) {
|
|
1527
|
+
throw new Error(`Unknown recipient "${toAgentId}"`);
|
|
1528
|
+
}
|
|
1529
|
+
const message: TeamMailboxMessage = {
|
|
1530
|
+
id: `msg_${String(++this.messageCounter).padStart(5, "0")}`,
|
|
1531
|
+
teamId: this.teamId,
|
|
1532
|
+
fromAgentId,
|
|
1533
|
+
toAgentId,
|
|
1534
|
+
subject,
|
|
1535
|
+
body,
|
|
1536
|
+
taskId,
|
|
1537
|
+
sentAt: new Date(),
|
|
1538
|
+
};
|
|
1539
|
+
this.mailbox.push(message);
|
|
1540
|
+
this.emitEvent({
|
|
1541
|
+
type: TeamMessageType.TeamMessage,
|
|
1542
|
+
message: { ...message },
|
|
1543
|
+
});
|
|
1544
|
+
return { ...message };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
broadcast(
|
|
1548
|
+
fromAgentId: string,
|
|
1549
|
+
subject: string,
|
|
1550
|
+
body: string,
|
|
1551
|
+
options?: { includeLead?: boolean; taskId?: string },
|
|
1552
|
+
): TeamMailboxMessage[] {
|
|
1553
|
+
const includeLead = options?.includeLead ?? false;
|
|
1554
|
+
const messages: TeamMailboxMessage[] = [];
|
|
1555
|
+
for (const member of this.members.values()) {
|
|
1556
|
+
if (member.agentId === fromAgentId) {
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (!includeLead && member.role === "lead") {
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
messages.push(
|
|
1563
|
+
this.sendMessage(
|
|
1564
|
+
fromAgentId,
|
|
1565
|
+
member.agentId,
|
|
1566
|
+
subject,
|
|
1567
|
+
body,
|
|
1568
|
+
options?.taskId,
|
|
1569
|
+
),
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
return messages;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
appendMissionLog(input: AppendMissionLogInput): MissionLogEntry {
|
|
1576
|
+
if (!this.members.has(input.agentId)) {
|
|
1577
|
+
throw new Error(`Unknown team member "${input.agentId}"`);
|
|
1578
|
+
}
|
|
1579
|
+
const entry: MissionLogEntry = {
|
|
1580
|
+
id: `log_${String(++this.missionCounter).padStart(6, "0")}`,
|
|
1581
|
+
ts: new Date(),
|
|
1582
|
+
teamId: this.teamId,
|
|
1583
|
+
agentId: input.agentId,
|
|
1584
|
+
taskId: input.taskId,
|
|
1585
|
+
kind: input.kind,
|
|
1586
|
+
summary: input.summary,
|
|
1587
|
+
evidence: input.evidence,
|
|
1588
|
+
nextAction: input.nextAction,
|
|
1589
|
+
};
|
|
1590
|
+
this.missionLog.push(entry);
|
|
1591
|
+
const member = this.members.get(input.agentId);
|
|
1592
|
+
if (member) {
|
|
1593
|
+
member.lastMissionAt = Date.now();
|
|
1594
|
+
member.lastMissionStep = this.missionStepCounter;
|
|
1595
|
+
}
|
|
1596
|
+
this.emitEvent({
|
|
1597
|
+
type: TeamMessageType.TeamMissionLog,
|
|
1598
|
+
entry: { ...entry },
|
|
1599
|
+
});
|
|
1600
|
+
return { ...entry };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
createOutcome(input: CreateTeamOutcomeInput): TeamOutcome {
|
|
1604
|
+
const outcome: TeamOutcome = {
|
|
1605
|
+
id: `out_${String(++this.outcomeCounter).padStart(4, "0")}`,
|
|
1606
|
+
teamId: this.teamId,
|
|
1607
|
+
title: input.title,
|
|
1608
|
+
status: "draft",
|
|
1609
|
+
requiredSections: [...new Set(input.requiredSections)],
|
|
1610
|
+
createdBy: input.createdBy,
|
|
1611
|
+
createdAt: new Date(),
|
|
1612
|
+
};
|
|
1613
|
+
this.outcomes.set(outcome.id, outcome);
|
|
1614
|
+
this.emitEvent({
|
|
1615
|
+
type: TeamMessageType.OutcomeCreated,
|
|
1616
|
+
outcome: { ...outcome },
|
|
1617
|
+
});
|
|
1618
|
+
return { ...outcome };
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
listOutcomes(): TeamOutcome[] {
|
|
1622
|
+
return Array.from(this.outcomes.values()).map((outcome) => ({
|
|
1623
|
+
...outcome,
|
|
1624
|
+
}));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
attachOutcomeFragment(
|
|
1628
|
+
input: AttachTeamOutcomeFragmentInput,
|
|
1629
|
+
): TeamOutcomeFragment {
|
|
1630
|
+
const outcome = this.outcomes.get(input.outcomeId);
|
|
1631
|
+
if (!outcome) {
|
|
1632
|
+
throw new Error(`Outcome "${input.outcomeId}" was not found`);
|
|
1633
|
+
}
|
|
1634
|
+
if (!outcome.requiredSections.includes(input.section)) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`Section "${input.section}" is not part of outcome "${input.outcomeId}"`,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
const fragment: TeamOutcomeFragment = {
|
|
1640
|
+
id: `frag_${String(++this.outcomeFragmentCounter).padStart(5, "0")}`,
|
|
1641
|
+
teamId: this.teamId,
|
|
1642
|
+
outcomeId: input.outcomeId,
|
|
1643
|
+
section: input.section,
|
|
1644
|
+
sourceAgentId: input.sourceAgentId,
|
|
1645
|
+
sourceRunId: input.sourceRunId,
|
|
1646
|
+
content: input.content,
|
|
1647
|
+
status: "draft",
|
|
1648
|
+
createdAt: new Date(),
|
|
1649
|
+
};
|
|
1650
|
+
this.outcomeFragments.set(fragment.id, fragment);
|
|
1651
|
+
if (outcome.status === "draft") {
|
|
1652
|
+
outcome.status = "in_review";
|
|
1653
|
+
}
|
|
1654
|
+
this.emitEvent({
|
|
1655
|
+
type: TeamMessageType.OutcomeFragmentAttached,
|
|
1656
|
+
fragment: { ...fragment },
|
|
1657
|
+
});
|
|
1658
|
+
return { ...fragment };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
reviewOutcomeFragment(
|
|
1662
|
+
input: ReviewTeamOutcomeFragmentInput,
|
|
1663
|
+
): TeamOutcomeFragment {
|
|
1664
|
+
const fragment = this.outcomeFragments.get(input.fragmentId);
|
|
1665
|
+
if (!fragment) {
|
|
1666
|
+
throw new Error(`Fragment "${input.fragmentId}" was not found`);
|
|
1667
|
+
}
|
|
1668
|
+
fragment.status = input.approved ? "reviewed" : "rejected";
|
|
1669
|
+
fragment.reviewedBy = input.reviewedBy;
|
|
1670
|
+
fragment.reviewedAt = new Date();
|
|
1671
|
+
this.emitEvent({
|
|
1672
|
+
type: TeamMessageType.OutcomeFragmentReviewed,
|
|
1673
|
+
fragment: { ...fragment },
|
|
1674
|
+
});
|
|
1675
|
+
return { ...fragment };
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
listOutcomeFragments(outcomeId: string): TeamOutcomeFragment[] {
|
|
1679
|
+
return Array.from(this.outcomeFragments.values())
|
|
1680
|
+
.filter((fragment) => fragment.outcomeId === outcomeId)
|
|
1681
|
+
.map((fragment) => ({ ...fragment }));
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
finalizeOutcome(outcomeId: string): TeamOutcome {
|
|
1685
|
+
const outcome = this.outcomes.get(outcomeId);
|
|
1686
|
+
if (!outcome) {
|
|
1687
|
+
throw new Error(`Outcome "${outcomeId}" was not found`);
|
|
1688
|
+
}
|
|
1689
|
+
const fragments = this.listOutcomeFragments(outcomeId);
|
|
1690
|
+
for (const section of outcome.requiredSections) {
|
|
1691
|
+
const approvedForSection = fragments.some(
|
|
1692
|
+
(fragment) =>
|
|
1693
|
+
fragment.section === section && fragment.status === "reviewed",
|
|
1694
|
+
);
|
|
1695
|
+
if (!approvedForSection) {
|
|
1696
|
+
throw new Error(
|
|
1697
|
+
`Outcome "${outcomeId}" cannot be finalized. Section "${section}" is missing a reviewed fragment.`,
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
outcome.status = "finalized";
|
|
1702
|
+
outcome.finalizedAt = new Date();
|
|
1703
|
+
this.emitEvent({
|
|
1704
|
+
type: TeamMessageType.OutcomeFinalized,
|
|
1705
|
+
outcome: { ...outcome },
|
|
1706
|
+
});
|
|
1707
|
+
return { ...outcome };
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
cleanup(): void {
|
|
1711
|
+
for (const member of this.members.values()) {
|
|
1712
|
+
if (member.role === "teammate" && member.runningCount > 0) {
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
`Cannot cleanup team while teammate "${member.agentId}" is still running`,
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (
|
|
1719
|
+
Array.from(this.runs.values()).some((run) =>
|
|
1720
|
+
["queued", "running"].includes(run.status),
|
|
1721
|
+
)
|
|
1722
|
+
) {
|
|
1723
|
+
throw new Error(
|
|
1724
|
+
"Cannot cleanup team while async teammate runs are still active",
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
for (const member of this.members.values()) {
|
|
1729
|
+
if (member.role === "teammate") {
|
|
1730
|
+
member.agent?.abort();
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
this.tasks.clear();
|
|
1735
|
+
this.mailbox.length = 0;
|
|
1736
|
+
this.missionLog.length = 0;
|
|
1737
|
+
this.runs.clear();
|
|
1738
|
+
this.runQueue.length = 0;
|
|
1739
|
+
this.outcomes.clear();
|
|
1740
|
+
this.outcomeFragments.clear();
|
|
1741
|
+
|
|
1742
|
+
for (const [memberId, member] of this.members.entries()) {
|
|
1743
|
+
if (member.role === "teammate") {
|
|
1744
|
+
this.members.delete(memberId);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private requireTask(taskId: string): TeamTask {
|
|
1750
|
+
const task = this.tasks.get(taskId);
|
|
1751
|
+
if (!task) {
|
|
1752
|
+
throw new Error(`Task "${taskId}" was not found`);
|
|
1753
|
+
}
|
|
1754
|
+
return task;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
private assertDependenciesResolved(task: TeamTask): void {
|
|
1758
|
+
for (const dependencyId of task.dependsOn) {
|
|
1759
|
+
const dependency = this.tasks.get(dependencyId);
|
|
1760
|
+
if (!dependency || dependency.status !== "completed") {
|
|
1761
|
+
throw new Error(`Task "${task.id}" is blocked by "${dependencyId}"`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
private trackMeaningfulEvent(agentId: string, event: AgentEvent): void {
|
|
1767
|
+
if (event.type === "iteration_end" && event.hadToolCalls) {
|
|
1768
|
+
this.recordProgressStep(
|
|
1769
|
+
agentId,
|
|
1770
|
+
`Completed iteration ${event.iteration} with ${event.toolCallCount} tool call(s)`,
|
|
1771
|
+
);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (
|
|
1776
|
+
event.type === "content_end" &&
|
|
1777
|
+
event.contentType === "tool" &&
|
|
1778
|
+
!event.error
|
|
1779
|
+
) {
|
|
1780
|
+
this.recordProgressStep(
|
|
1781
|
+
agentId,
|
|
1782
|
+
`Finished tool "${event.toolName ?? "unknown"}"`,
|
|
1783
|
+
);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (event.type === "done") {
|
|
1788
|
+
this.appendMissionLog({
|
|
1789
|
+
agentId,
|
|
1790
|
+
kind: "done",
|
|
1791
|
+
summary: `Run completed after ${event.iterations} iteration(s)`,
|
|
1792
|
+
});
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (event.type === "error") {
|
|
1797
|
+
this.appendMissionLog({
|
|
1798
|
+
agentId,
|
|
1799
|
+
kind: "error",
|
|
1800
|
+
summary: event.error.message,
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
private recordProgressStep(
|
|
1806
|
+
agentId: string,
|
|
1807
|
+
summary: string,
|
|
1808
|
+
taskId?: string,
|
|
1809
|
+
force = false,
|
|
1810
|
+
): void {
|
|
1811
|
+
this.missionStepCounter++;
|
|
1812
|
+
const member = this.members.get(agentId);
|
|
1813
|
+
if (!member) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const stepsSinceLast = this.missionStepCounter - member.lastMissionStep;
|
|
1817
|
+
const elapsedMs = Date.now() - member.lastMissionAt;
|
|
1818
|
+
if (
|
|
1819
|
+
!force &&
|
|
1820
|
+
stepsSinceLast < this.missionLogIntervalSteps &&
|
|
1821
|
+
elapsedMs < this.missionLogIntervalMs
|
|
1822
|
+
) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
this.appendMissionLog({
|
|
1826
|
+
agentId,
|
|
1827
|
+
taskId,
|
|
1828
|
+
kind: "progress",
|
|
1829
|
+
summary,
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
private emitEvent(event: TeamEvent): void {
|
|
1834
|
+
try {
|
|
1835
|
+
this.onTeamEvent?.(event);
|
|
1836
|
+
} catch {
|
|
1837
|
+
// Ignore callback errors to avoid disrupting execution.
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function sanitizeId(value: string): string {
|
|
1843
|
+
return value
|
|
1844
|
+
.toLowerCase()
|
|
1845
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
1846
|
+
.replace(/^_+|_+$/g, "")
|
|
1847
|
+
.slice(0, 24);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function sleep(ms: number): Promise<void> {
|
|
1851
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function maxCounter(ids: string[], prefix: string): number {
|
|
1855
|
+
let max = 0;
|
|
1856
|
+
for (const id of ids) {
|
|
1857
|
+
if (!id.startsWith(prefix)) {
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
const value = Number.parseInt(id.slice(prefix.length), 10);
|
|
1861
|
+
if (Number.isFinite(value)) {
|
|
1862
|
+
max = Math.max(max, value);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return max;
|
|
1866
|
+
}
|