@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.
Files changed (90) hide show
  1. package/README.md +145 -0
  2. package/dist/agent-input.d.ts +2 -0
  3. package/dist/agent.d.ts +56 -0
  4. package/dist/extensions.d.ts +21 -0
  5. package/dist/hooks/engine.d.ts +42 -0
  6. package/dist/hooks/index.d.ts +2 -0
  7. package/dist/hooks/lifecycle.d.ts +5 -0
  8. package/dist/hooks/node.d.ts +2 -0
  9. package/dist/hooks/subprocess-runner.d.ts +16 -0
  10. package/dist/hooks/subprocess.d.ts +268 -0
  11. package/dist/index.browser.d.ts +1 -0
  12. package/dist/index.browser.js +49 -0
  13. package/dist/index.d.ts +15 -0
  14. package/dist/index.js +49 -0
  15. package/dist/index.node.d.ts +5 -0
  16. package/dist/index.node.js +49 -0
  17. package/dist/mcp/index.d.ts +4 -0
  18. package/dist/mcp/policies.d.ts +14 -0
  19. package/dist/mcp/tools.d.ts +9 -0
  20. package/dist/mcp/types.d.ts +35 -0
  21. package/dist/message-builder.d.ts +31 -0
  22. package/dist/prompts/cline.d.ts +1 -0
  23. package/dist/prompts/index.d.ts +1 -0
  24. package/dist/runtime/agent-runtime-bus.d.ts +13 -0
  25. package/dist/runtime/conversation-store.d.ts +16 -0
  26. package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
  27. package/dist/runtime/tool-orchestrator.d.ts +39 -0
  28. package/dist/runtime/turn-processor.d.ts +21 -0
  29. package/dist/teams/index.d.ts +3 -0
  30. package/dist/teams/multi-agent.d.ts +566 -0
  31. package/dist/teams/spawn-agent-tool.d.ts +85 -0
  32. package/dist/teams/team-tools.d.ts +51 -0
  33. package/dist/tools/ask-question.d.ts +12 -0
  34. package/dist/tools/create.d.ts +59 -0
  35. package/dist/tools/execution.d.ts +61 -0
  36. package/dist/tools/formatting.d.ts +20 -0
  37. package/dist/tools/index.d.ts +11 -0
  38. package/dist/tools/registry.d.ts +26 -0
  39. package/dist/tools/validation.d.ts +27 -0
  40. package/dist/types.d.ts +826 -0
  41. package/package.json +54 -0
  42. package/src/agent-input.ts +116 -0
  43. package/src/agent.test.ts +931 -0
  44. package/src/agent.ts +1050 -0
  45. package/src/example.test.ts +564 -0
  46. package/src/extensions.ts +337 -0
  47. package/src/hooks/engine.test.ts +163 -0
  48. package/src/hooks/engine.ts +537 -0
  49. package/src/hooks/index.ts +6 -0
  50. package/src/hooks/lifecycle.ts +239 -0
  51. package/src/hooks/node.ts +18 -0
  52. package/src/hooks/subprocess-runner.ts +140 -0
  53. package/src/hooks/subprocess.test.ts +180 -0
  54. package/src/hooks/subprocess.ts +620 -0
  55. package/src/index.browser.ts +1 -0
  56. package/src/index.node.ts +21 -0
  57. package/src/index.ts +133 -0
  58. package/src/mcp/index.ts +17 -0
  59. package/src/mcp/policies.test.ts +51 -0
  60. package/src/mcp/policies.ts +53 -0
  61. package/src/mcp/tools.test.ts +76 -0
  62. package/src/mcp/tools.ts +60 -0
  63. package/src/mcp/types.ts +41 -0
  64. package/src/message-builder.test.ts +175 -0
  65. package/src/message-builder.ts +429 -0
  66. package/src/prompts/cline.ts +49 -0
  67. package/src/prompts/index.ts +1 -0
  68. package/src/runtime/agent-runtime-bus.ts +53 -0
  69. package/src/runtime/conversation-store.ts +61 -0
  70. package/src/runtime/lifecycle-orchestrator.ts +90 -0
  71. package/src/runtime/tool-orchestrator.ts +177 -0
  72. package/src/runtime/turn-processor.ts +250 -0
  73. package/src/streaming.test.ts +197 -0
  74. package/src/streaming.ts +307 -0
  75. package/src/teams/index.ts +63 -0
  76. package/src/teams/multi-agent.lifecycle.test.ts +48 -0
  77. package/src/teams/multi-agent.ts +1866 -0
  78. package/src/teams/spawn-agent-tool.test.ts +172 -0
  79. package/src/teams/spawn-agent-tool.ts +223 -0
  80. package/src/teams/team-tools.test.ts +448 -0
  81. package/src/teams/team-tools.ts +929 -0
  82. package/src/tools/ask-question.ts +78 -0
  83. package/src/tools/create.ts +104 -0
  84. package/src/tools/execution.ts +311 -0
  85. package/src/tools/formatting.ts +73 -0
  86. package/src/tools/index.ts +45 -0
  87. package/src/tools/registry.ts +52 -0
  88. package/src/tools/tools.test.ts +292 -0
  89. package/src/tools/validation.ts +73 -0
  90. 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
+ }