@itaylor/agentic-team 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,823 @@
1
+ // Agent team coordinator - manages a team of agents working toward a goal
2
+ import { runAgentSession } from "@itaylor/agentic-loop";
3
+ import { z } from "zod";
4
+ /**
5
+ * Default logger implementation
6
+ */
7
+ const defaultLogger = {
8
+ error: (message, ...args) => console.error(message, ...args),
9
+ info: (message, ...args) => console.info(message, ...args),
10
+ trace: (message, ...args) => console.debug(message, ...args),
11
+ };
12
+ /**
13
+ * Generate a task ID
14
+ */
15
+ function generateTaskId(existingTasks) {
16
+ const maxId = existingTasks.reduce((max, task) => {
17
+ const num = parseInt(task.id.replace("T-", ""), 10);
18
+ return num > max ? num : max;
19
+ }, 0);
20
+ return `T-${String(maxId + 1).padStart(4, "0")}`;
21
+ }
22
+ /**
23
+ * Generate a message ID
24
+ */
25
+ function generateMessageId(existingMessages) {
26
+ const maxId = existingMessages.reduce((max, msg) => {
27
+ const num = parseInt(msg.id.replace("M-", ""), 10);
28
+ return num > max ? num : max;
29
+ }, 0);
30
+ return `M-${String(maxId + 1).padStart(4, "0")}`;
31
+ }
32
+ /**
33
+ * Create an agent team coordinator
34
+ */
35
+ export function createAgentTeam(config) {
36
+ const logger = config.logger || defaultLogger;
37
+ // Initialize state
38
+ const state = config.resumeFrom || {
39
+ tasks: [],
40
+ messages: [],
41
+ agentStates: new Map(),
42
+ goalComplete: false,
43
+ };
44
+ // Initialize agent states if not resuming
45
+ if (!config.resumeFrom) {
46
+ // Manager state
47
+ state.agentStates.set(config.manager.id, {
48
+ id: config.manager.id,
49
+ role: "manager",
50
+ status: "idle",
51
+ conversationHistory: [],
52
+ });
53
+ // Team member states
54
+ for (const member of config.team) {
55
+ state.agentStates.set(member.id, {
56
+ id: member.id,
57
+ role: member.role,
58
+ status: "idle",
59
+ conversationHistory: [],
60
+ });
61
+ }
62
+ }
63
+ /**
64
+ * Create coordination tools for an agent
65
+ */
66
+ function createCoordinationTools(agentId) {
67
+ const isManager = agentId === config.manager.id;
68
+ const tools = {
69
+ ask: {
70
+ description: "Ask another team member or external entity a question. Your session will pause until they reply.",
71
+ inputSchema: z.object({
72
+ to: z
73
+ .string()
74
+ .describe("Who to ask (agent ID like 'Bailey#1' or 'BigBoss' for external)"),
75
+ question: z.string().describe("Your question"),
76
+ }),
77
+ execute: async (args) => {
78
+ const messageId = config.generateMessageId
79
+ ? config.generateMessageId(state.messages)
80
+ : generateMessageId(state.messages);
81
+ const message = {
82
+ id: messageId,
83
+ from: agentId,
84
+ to: args.to,
85
+ type: "ask",
86
+ content: args.question,
87
+ status: "pending",
88
+ createdAt: new Date().toISOString(),
89
+ };
90
+ state.messages.push(message);
91
+ await config.callbacks?.onMessageSent?.(message);
92
+ logger.info(`Agent ${agentId} asked ${args.to}: ${args.question.substring(0, 50)}...`);
93
+ // Return suspension signal
94
+ return {
95
+ __suspend__: true,
96
+ reason: "waiting_for_reply",
97
+ data: { messageId, to: args.to },
98
+ };
99
+ },
100
+ },
101
+ tell: {
102
+ description: "Send a message to another team member or reply to a question.",
103
+ inputSchema: z.object({
104
+ to: z.string().describe("Who to send the message to"),
105
+ message: z.string().describe("Your message"),
106
+ inReplyTo: z
107
+ .string()
108
+ .optional()
109
+ .describe("Message ID if replying to a question"),
110
+ }),
111
+ execute: async (args) => {
112
+ const messageId = config.generateMessageId
113
+ ? config.generateMessageId(state.messages)
114
+ : generateMessageId(state.messages);
115
+ const message = {
116
+ id: messageId,
117
+ from: agentId,
118
+ to: args.to,
119
+ type: "tell",
120
+ content: args.message,
121
+ status: "delivered",
122
+ createdAt: new Date().toISOString(),
123
+ inReplyTo: args.inReplyTo,
124
+ };
125
+ state.messages.push(message);
126
+ // Find the ask this is replying to - either explicitly via inReplyTo,
127
+ // or auto-detect if the recipient has a pending ask from the sender
128
+ let originalAsk;
129
+ if (args.inReplyTo) {
130
+ originalAsk = state.messages.find((m) => m.id === args.inReplyTo && m.type === "ask");
131
+ }
132
+ else {
133
+ // Auto-detect: if the recipient is blocked waiting for a reply from this sender,
134
+ // treat this tell as the reply (LLMs often omit inReplyTo)
135
+ const recipientState = state.agentStates.get(args.to);
136
+ if (recipientState &&
137
+ recipientState.status === "blocked" &&
138
+ recipientState.blockedOn) {
139
+ const pendingAsk = state.messages.find((m) => m.id === recipientState.blockedOn &&
140
+ m.type === "ask" &&
141
+ m.from === args.to &&
142
+ m.to === agentId);
143
+ if (pendingAsk) {
144
+ originalAsk = pendingAsk;
145
+ message.inReplyTo = pendingAsk.id;
146
+ logger.info(`Auto-matched tell from ${agentId} to ${args.to} as reply to ${pendingAsk.id}`);
147
+ }
148
+ }
149
+ }
150
+ if (originalAsk) {
151
+ originalAsk.status = "delivered";
152
+ await config.callbacks?.onMessageDelivered?.(originalAsk);
153
+ // Unblock the agent that sent the original ask
154
+ const askerState = state.agentStates.get(originalAsk.from);
155
+ if (askerState && askerState.blockedOn === originalAsk.id) {
156
+ askerState.status = askerState.currentTask ? "working" : "idle";
157
+ askerState.blockedOn = undefined;
158
+ // Add the reply to their conversation history so they see it when resumed
159
+ askerState.conversationHistory.push({
160
+ role: "user",
161
+ content: `Reply to your question from ${agentId}:\n\n${args.message}`,
162
+ });
163
+ await config.callbacks?.onAgentUnblocked?.(originalAsk.from);
164
+ logger.info(`Agent ${originalAsk.from} unblocked by reply from ${agentId} to ${originalAsk.id}`);
165
+ }
166
+ }
167
+ await config.callbacks?.onMessageSent?.(message);
168
+ logger.info(`Agent ${agentId} told ${args.to}: ${args.message.substring(0, 50)}...`);
169
+ return {
170
+ success: true,
171
+ messageId,
172
+ };
173
+ },
174
+ },
175
+ get_task_brief: {
176
+ description: "Get the detailed brief for your current task",
177
+ inputSchema: z.object({}),
178
+ execute: async () => {
179
+ const agentState = state.agentStates.get(agentId);
180
+ if (!agentState?.currentTask) {
181
+ return { error: "You have no current task assigned" };
182
+ }
183
+ const task = state.tasks.find((t) => t.id === agentState.currentTask);
184
+ if (!task) {
185
+ return { error: "Task not found" };
186
+ }
187
+ return {
188
+ taskId: task.id,
189
+ title: task.title,
190
+ brief: task.brief,
191
+ status: task.status,
192
+ };
193
+ },
194
+ },
195
+ check_team_status: {
196
+ description: "Check the status of all team members and their tasks",
197
+ inputSchema: z.object({}),
198
+ execute: async () => {
199
+ const teamStatus = Array.from(state.agentStates.values()).map((agent) => {
200
+ const agentTasks = state.tasks.filter((t) => t.assignee === agent.id);
201
+ const activeTasks = agentTasks.filter((t) => t.status === "active");
202
+ const queuedTasks = agentTasks.filter((t) => t.status === "queued");
203
+ const completedTasks = agentTasks.filter((t) => t.status === "completed");
204
+ return {
205
+ agentId: agent.id,
206
+ role: agent.role,
207
+ status: agent.status,
208
+ currentTask: agent.currentTask,
209
+ blockedOn: agent.blockedOn,
210
+ activeTasks: activeTasks.length,
211
+ queuedTasks: queuedTasks.length,
212
+ completedTasks: completedTasks.length,
213
+ };
214
+ });
215
+ return {
216
+ teamSize: state.agentStates.size,
217
+ agents: teamStatus,
218
+ totalTasks: state.tasks.length,
219
+ activeTasks: state.tasks.filter((t) => t.status === "active")
220
+ .length,
221
+ queuedTasks: state.tasks.filter((t) => t.status === "queued")
222
+ .length,
223
+ completedTasks: state.tasks.filter((t) => t.status === "completed")
224
+ .length,
225
+ };
226
+ },
227
+ },
228
+ };
229
+ // Manager-only tools
230
+ if (isManager) {
231
+ tools.assign_task = {
232
+ description: "Assign a task to a team member. They will start working on it automatically if available.",
233
+ inputSchema: z.object({
234
+ assignee: z
235
+ .string()
236
+ .describe("Team member to assign to (e.g., 'Bailey#1')"),
237
+ title: z.string().describe("Short task title"),
238
+ brief: z
239
+ .string()
240
+ .describe("Detailed task description with objectives and deliverables"),
241
+ }),
242
+ execute: async (args) => {
243
+ // Check if assignee exists
244
+ const assigneeState = state.agentStates.get(args.assignee);
245
+ if (!assigneeState) {
246
+ return {
247
+ error: `Unknown team member: ${args.assignee}`,
248
+ };
249
+ }
250
+ // Create task
251
+ const taskId = config.generateTaskId
252
+ ? config.generateTaskId(state.tasks)
253
+ : generateTaskId(state.tasks);
254
+ const task = {
255
+ id: taskId,
256
+ title: args.title,
257
+ brief: args.brief,
258
+ assignee: args.assignee,
259
+ createdBy: agentId,
260
+ status: "queued",
261
+ createdAt: new Date().toISOString(),
262
+ };
263
+ // If assignee is idle, make this task active
264
+ if (!assigneeState.currentTask) {
265
+ task.status = "active";
266
+ assigneeState.currentTask = taskId;
267
+ assigneeState.status = "working";
268
+ }
269
+ state.tasks.push(task);
270
+ await config.callbacks?.onTaskCreated?.(task);
271
+ if (task.status === "active") {
272
+ await config.callbacks?.onTaskActivated?.(task);
273
+ }
274
+ logger.info(`Task ${taskId} assigned to ${args.assignee}: ${args.title}`);
275
+ return {
276
+ success: true,
277
+ taskId,
278
+ status: task.status,
279
+ message: task.status === "active"
280
+ ? `Task ${taskId} assigned and activated`
281
+ : `Task ${taskId} queued (agent is busy)`,
282
+ };
283
+ },
284
+ };
285
+ tools.wait_for_task_completions = {
286
+ description: "Wait for assigned tasks to be completed. Call this after you've assigned all tasks and are waiting for your team to finish their work. Your session will pause until task completions arrive.",
287
+ inputSchema: z.object({}),
288
+ execute: async () => {
289
+ // Check for incomplete tasks
290
+ const incompleteTasks = state.tasks.filter((t) => t.status === "active" || t.status === "queued");
291
+ if (incompleteTasks.length === 0) {
292
+ return {
293
+ allComplete: true,
294
+ message: "All tasks are complete. You can now call task_complete to finish your work.",
295
+ };
296
+ }
297
+ // Suspend to wait for completions
298
+ logger.info(`Manager waiting for ${incompleteTasks.length} incomplete tasks`);
299
+ return {
300
+ __suspend__: true,
301
+ reason: "waiting_for_task_completions",
302
+ data: {
303
+ incompleteTasks: incompleteTasks.map((t) => t.id),
304
+ count: incompleteTasks.length,
305
+ },
306
+ };
307
+ },
308
+ };
309
+ }
310
+ return tools;
311
+ }
312
+ /**
313
+ * Build the default system prompt for an agent if none is provided
314
+ */
315
+ function buildDefaultSystemPrompt(agentId) {
316
+ if (agentId === config.manager.id) {
317
+ return "You are a project manager coordinating a team of AI agents.";
318
+ }
319
+ const member = config.team.find((m) => m.id === agentId);
320
+ return `You are a ${member?.role || "team member"} on a collaborative AI team.`;
321
+ }
322
+ /**
323
+ * Build initial message for an agent based on their state
324
+ */
325
+ function buildInitialMessage(agentId) {
326
+ const agentState = state.agentStates.get(agentId);
327
+ if (!agentState) {
328
+ return undefined;
329
+ }
330
+ const isManager = agentId === config.manager.id;
331
+ const parts = [];
332
+ // Manager first-time startup: include goal + standard lifecycle instructions
333
+ if (isManager && agentState.conversationHistory.length === 0) {
334
+ const teamList = config.team.length > 0
335
+ ? config.team.map((m) => `- ${m.id} (${m.role})`).join("\n")
336
+ : "(no team members — you may complete this goal yourself)";
337
+ parts.push(`# Goal\n${config.goal}`);
338
+ parts.push(`\n# Your Role as Manager
339
+ You coordinate the team to achieve this goal by breaking it down into tasks and delegating to team members.
340
+
341
+ ## Workflow
342
+ 1. Break the goal down into specific, actionable tasks
343
+ 2. Assign tasks to team members using \`assign_task\`
344
+ 3. After assigning initial work, call \`wait_for_task_completions\` to pause until those tasks complete
345
+ 4. Review results and assign follow-up tasks if needed
346
+ 5. When the goal is fully achieved, call \`task_complete\` with a comprehensive summary
347
+
348
+ ## Your Team
349
+ ${teamList}
350
+
351
+ Begin by analyzing the goal and assigning tasks to your team.`);
352
+ return parts.join("\n");
353
+ }
354
+ // Worker first-time startup: include task brief + standard worker instructions
355
+ if (!isManager && agentState.conversationHistory.length === 0) {
356
+ const task = agentState.currentTask
357
+ ? state.tasks.find((t) => t.id === agentState.currentTask)
358
+ : undefined;
359
+ if (task) {
360
+ parts.push(`# Your Assignment\nYou are ${agentId}, a ${agentState.role}.`);
361
+ parts.push(`\n## Task: ${task.title}\nTask ID: ${task.id}\n\n${task.brief}`);
362
+ parts.push(`\nWhen you complete your task, call \`task_complete\` with a summary of what you accomplished.\nIf you need clarification or help, use the \`ask\` tool.`);
363
+ }
364
+ // Include any unread messages (e.g. task notification)
365
+ const unreadMessages = state.messages.filter((m) => m.to === agentId && m.status === "pending");
366
+ if (unreadMessages.length > 0) {
367
+ parts.push(`\n# Pending Messages`);
368
+ for (const msg of unreadMessages) {
369
+ parts.push(`\nFrom ${msg.from}:`);
370
+ if (msg.type === "ask") {
371
+ parts.push(`\n**Question:** ${msg.content}\n`);
372
+ }
373
+ else {
374
+ parts.push(`\n${msg.content}\n`);
375
+ }
376
+ }
377
+ }
378
+ return parts.length > 0 ? parts.join("\n") : undefined;
379
+ }
380
+ // Re-activation: include pending messages only (agent already has context)
381
+ const unreadMessages = state.messages.filter((m) => m.to === agentId && m.status === "pending");
382
+ if (unreadMessages.length > 0) {
383
+ parts.push(`# Pending Messages`);
384
+ for (const msg of unreadMessages) {
385
+ parts.push(`\nFrom ${msg.from}:`);
386
+ if (msg.type === "ask") {
387
+ parts.push(`\n**Question:** ${msg.content}\n`);
388
+ }
389
+ else {
390
+ parts.push(`\n${msg.content}\n`);
391
+ }
392
+ }
393
+ }
394
+ return parts.length > 0 ? parts.join("\n") : undefined;
395
+ }
396
+ /**
397
+ * Handle task completion
398
+ */
399
+ async function handleTaskCompletion(agentId, finalOutput) {
400
+ const agentState = state.agentStates.get(agentId);
401
+ if (!agentState) {
402
+ throw new Error(`Agent ${agentId} not found`);
403
+ }
404
+ // Special case: manager completing their task = goal complete
405
+ if (agentId === config.manager.id) {
406
+ state.goalComplete = true;
407
+ state.goalSummary = finalOutput;
408
+ await config.callbacks?.onGoalComplete?.(finalOutput);
409
+ logger.info(`Goal completed by manager: ${finalOutput}`);
410
+ return;
411
+ }
412
+ // Regular task completion
413
+ const task = state.tasks.find((t) => t.id === agentState.currentTask);
414
+ if (!task) {
415
+ // Agent had no current task (e.g., was run to respond to messages).
416
+ // Treat as a graceful session end rather than an error.
417
+ logger.info(`Agent ${agentId} called task_complete with no active task (likely finished responding to messages)`);
418
+ agentState.status = "idle";
419
+ return;
420
+ }
421
+ // Update task
422
+ task.status = "completed";
423
+ task.completedAt = new Date().toISOString();
424
+ task.completionSummary = finalOutput;
425
+ await config.callbacks?.onTaskCompleted?.(task);
426
+ logger.info(`Task ${task.id} completed by ${agentId}`);
427
+ // Clear agent's current task
428
+ agentState.currentTask = undefined;
429
+ agentState.status = "idle";
430
+ // Notify task creator
431
+ const notificationId = config.generateMessageId
432
+ ? config.generateMessageId(state.messages)
433
+ : generateMessageId(state.messages);
434
+ const notification = {
435
+ id: notificationId,
436
+ from: agentId,
437
+ to: task.createdBy,
438
+ type: "tell",
439
+ content: `Task ${task.id} "${task.title}" completed:\n\n${task.completionSummary}`,
440
+ status: "pending",
441
+ createdAt: new Date().toISOString(),
442
+ };
443
+ state.messages.push(notification);
444
+ await config.callbacks?.onMessageSent?.(notification);
445
+ // Check for queued tasks for this agent
446
+ const queuedTask = state.tasks.find((t) => t.assignee === agentId && t.status === "queued");
447
+ if (queuedTask) {
448
+ queuedTask.status = "active";
449
+ agentState.currentTask = queuedTask.id;
450
+ agentState.status = "working";
451
+ // Inject new task context into the agent's conversation so they know about it
452
+ agentState.conversationHistory.push({
453
+ role: "user",
454
+ content: `You have a new task assignment.\n\n# New Task: ${queuedTask.title}\nTask ID: ${queuedTask.id}\n\n${queuedTask.brief}\n\nPlease work on this task and call task_complete when done.`,
455
+ });
456
+ await config.callbacks?.onTaskActivated?.(queuedTask);
457
+ logger.info(`Task ${queuedTask.id} activated for ${agentId}`);
458
+ }
459
+ }
460
+ /**
461
+ * Handle agent suspension
462
+ */
463
+ async function handleAgentSuspension(agentId, messageId) {
464
+ const agentState = state.agentStates.get(agentId);
465
+ if (!agentState) {
466
+ throw new Error(`Agent ${agentId} not found`);
467
+ }
468
+ agentState.status = "blocked";
469
+ agentState.blockedOn = messageId;
470
+ await config.callbacks?.onAgentBlocked?.(agentId, messageId);
471
+ logger.info(`Agent ${agentId} blocked waiting for message ${messageId}`);
472
+ }
473
+ // Internal helper functions for run loop
474
+ let shouldStop = false;
475
+ let stopResolver = undefined;
476
+ const activeAgentSessions = new Set(); // Track all active agent sessions
477
+ function getBlockedAgents() {
478
+ const blocked = [];
479
+ for (const [agentId, agentState] of state.agentStates) {
480
+ if (agentState.status === "blocked" && agentState.blockedOn) {
481
+ blocked.push({
482
+ agentId,
483
+ messageId: agentState.blockedOn,
484
+ });
485
+ }
486
+ }
487
+ return blocked;
488
+ }
489
+ function getNextWork() {
490
+ const workItems = [];
491
+ for (const [agentId, agentState] of state.agentStates) {
492
+ if (agentState.status === "blocked" || !agentState.currentTask) {
493
+ continue;
494
+ }
495
+ const task = state.tasks.find((t) => t.id === agentState.currentTask);
496
+ if (task && task.status === "active") {
497
+ workItems.push({
498
+ agentId,
499
+ taskId: task.id,
500
+ task,
501
+ });
502
+ }
503
+ }
504
+ return workItems;
505
+ }
506
+ /**
507
+ * Get agents that are idle but have pending messages they need to respond to.
508
+ */
509
+ function getAgentsWithPendingMessages() {
510
+ const agents = [];
511
+ for (const [agentId, agentState] of state.agentStates) {
512
+ if (agentState.status === "blocked")
513
+ continue;
514
+ const hasPending = state.messages.some((m) => m.to === agentId && m.status === "pending");
515
+ if (hasPending) {
516
+ agents.push(agentId);
517
+ }
518
+ }
519
+ return agents;
520
+ }
521
+ // The AgentTeam interface implementation
522
+ const team = {
523
+ teamId: config.teamId,
524
+ async run() {
525
+ shouldStop = false;
526
+ const maxIterations = 100;
527
+ let iterations = 0;
528
+ logger.info(`Starting autonomous team run for goal: ${config.goal}`);
529
+ // Start with the manager
530
+ logger.info("Running manager to assign work...");
531
+ await runAgent(config.manager.id);
532
+ // Main loop
533
+ while (iterations < maxIterations && !state.goalComplete && !shouldStop) {
534
+ iterations++;
535
+ logger.info(`\n=== Iteration ${iterations} ===`);
536
+ // Check for blocked agents (waiting for external input)
537
+ const blocked = getBlockedAgents();
538
+ if (blocked.length > 0) {
539
+ const externalBlocked = blocked.filter((b) => {
540
+ const msg = state.messages.find((m) => m.id === b.messageId);
541
+ return (msg && (msg.to === "BigBoss" || !state.agentStates.has(msg.to)));
542
+ });
543
+ if (externalBlocked.length > 0) {
544
+ logger.info(`Agents blocked on external input: ${externalBlocked.map((b) => b.agentId).join(", ")}`);
545
+ return {
546
+ complete: false,
547
+ blockedAgents: externalBlocked,
548
+ iterations,
549
+ };
550
+ }
551
+ }
552
+ // Get work items for team members
553
+ const workItems = getNextWork();
554
+ // If no work items but there are internally blocked agents,
555
+ // check if any idle agents have pending messages they need to respond to
556
+ if (workItems.length === 0 && blocked.length > 0) {
557
+ const agentsWithMessages = getAgentsWithPendingMessages();
558
+ if (agentsWithMessages.length > 0) {
559
+ for (const responderId of agentsWithMessages) {
560
+ if (shouldStop)
561
+ break;
562
+ logger.info(`Running ${responderId} to respond to pending messages...`);
563
+ await runAgent(responderId);
564
+ }
565
+ continue; // Re-evaluate loop state after running responders
566
+ }
567
+ else {
568
+ // All blocked agents are internal, no one can respond - stuck
569
+ logger.info("No work and agents internally blocked with no responders - ending run");
570
+ break;
571
+ }
572
+ }
573
+ if (workItems.length === 0 && blocked.length === 0) {
574
+ // No work and no blocked agents - check if manager needs to run
575
+ const managerState = state.agentStates.get(config.manager.id);
576
+ if (managerState && managerState.status !== "blocked") {
577
+ logger.info("No work items, running manager to check status...");
578
+ const managerResult = await runAgent(config.manager.id);
579
+ if (managerResult.completionReason === "task_complete") {
580
+ // Manager completed the goal
581
+ break;
582
+ }
583
+ // If manager still found no work, we might be stuck
584
+ const workItemsAfter = getNextWork();
585
+ if (workItemsAfter.length === 0 &&
586
+ getBlockedAgents().length === 0) {
587
+ logger.info("No work and no blocked agents - ending run");
588
+ break;
589
+ }
590
+ }
591
+ else {
592
+ logger.info("No work and manager is blocked - ending run");
593
+ break;
594
+ }
595
+ }
596
+ // Run all agents with work
597
+ for (const work of workItems) {
598
+ if (shouldStop)
599
+ break;
600
+ logger.info(`Running ${work.agentId} on task ${work.taskId}...`);
601
+ await runAgent(work.agentId);
602
+ // Check if goal completed
603
+ if (state.goalComplete) {
604
+ break;
605
+ }
606
+ }
607
+ // After agents complete tasks, run manager to process notifications
608
+ if (!state.goalComplete && !shouldStop) {
609
+ const managerState = state.agentStates.get(config.manager.id);
610
+ if (managerState && managerState.status !== "blocked") {
611
+ logger.info("Running manager to process completions...");
612
+ await runAgent(config.manager.id);
613
+ }
614
+ }
615
+ }
616
+ if (shouldStop) {
617
+ logger.info("Team run stopped by user");
618
+ if (stopResolver) {
619
+ const resolver = stopResolver;
620
+ stopResolver = undefined;
621
+ resolver(state);
622
+ }
623
+ }
624
+ else if (iterations >= maxIterations) {
625
+ logger.info(`Reached max iterations (${maxIterations})`);
626
+ }
627
+ logger.info(`Team run complete. Goal complete: ${state.goalComplete}, Iterations: ${iterations}`);
628
+ return {
629
+ complete: state.goalComplete,
630
+ blockedAgents: getBlockedAgents(),
631
+ iterations,
632
+ };
633
+ },
634
+ async stop() {
635
+ logger.info("Stop requested");
636
+ shouldStop = true;
637
+ await Promise.all([...activeAgentSessions].map((session) => session.stop()));
638
+ return state;
639
+ },
640
+ deliverMessageReply(messageId, replyContent) {
641
+ // Find the original message
642
+ const originalMessage = state.messages.find((m) => m.id === messageId);
643
+ if (!originalMessage || originalMessage.type !== "ask") {
644
+ logger.error(`Cannot deliver reply - message ${messageId} not found or not an ask`);
645
+ return;
646
+ }
647
+ // Create reply message
648
+ const replyId = config.generateMessageId
649
+ ? config.generateMessageId(state.messages)
650
+ : generateMessageId(state.messages);
651
+ const reply = {
652
+ id: replyId,
653
+ from: originalMessage.to,
654
+ to: originalMessage.from,
655
+ type: "tell",
656
+ content: replyContent,
657
+ status: "delivered",
658
+ inReplyTo: messageId,
659
+ createdAt: new Date().toISOString(),
660
+ };
661
+ state.messages.push(reply);
662
+ originalMessage.status = "delivered";
663
+ // Find the blocked agent and unblock them
664
+ const blockedAgentId = originalMessage.from;
665
+ const agentState = state.agentStates.get(blockedAgentId);
666
+ if (agentState && agentState.blockedOn === messageId) {
667
+ agentState.status = agentState.currentTask ? "working" : "idle";
668
+ agentState.blockedOn = undefined;
669
+ // Add the reply to their conversation history
670
+ agentState.conversationHistory.push({
671
+ role: "user",
672
+ content: `Reply to your question from ${reply.from}:\n\n${replyContent}`,
673
+ });
674
+ config.callbacks?.onAgentUnblocked?.(blockedAgentId);
675
+ config.callbacks?.onMessageSent?.(reply);
676
+ config.callbacks?.onMessageDelivered?.(originalMessage);
677
+ logger.info(`Agent ${blockedAgentId} unblocked with reply to ${messageId}`);
678
+ }
679
+ },
680
+ };
681
+ // Internal function to run an agent
682
+ async function runAgent(agentId) {
683
+ const agentState = state.agentStates.get(agentId);
684
+ if (!agentState) {
685
+ throw new Error(`Agent ${agentId} not found`);
686
+ }
687
+ // Find agent config
688
+ const isManager = agentId === config.manager.id;
689
+ const agentConfig = isManager
690
+ ? config.manager
691
+ : config.team.find((m) => m.id === agentId);
692
+ if (!agentConfig) {
693
+ throw new Error(`Agent configuration not found for ${agentId}`);
694
+ }
695
+ // Build coordination tools
696
+ const coordinationTools = createCoordinationTools(agentId);
697
+ // Merge with domain tools
698
+ const allTools = {
699
+ ...coordinationTools,
700
+ ...(agentConfig.tools || {}),
701
+ };
702
+ // For agents with existing history, inject any pending messages into their conversation
703
+ // (buildInitialMessage handles the fresh-start case including pending messages)
704
+ if (agentState.conversationHistory.length > 0) {
705
+ const pendingMessages = state.messages.filter((m) => m.to === agentId && m.status === "pending");
706
+ if (pendingMessages.length > 0) {
707
+ const parts = ["# Pending Messages"];
708
+ for (const msg of pendingMessages) {
709
+ if (msg.type === "ask") {
710
+ parts.push(`\nFrom ${msg.from} (message ${msg.id}):\n**Question:** ${msg.content}\nPlease reply using the tell tool with to="${msg.from}" and inReplyTo="${msg.id}".`);
711
+ }
712
+ else {
713
+ parts.push(`\nFrom ${msg.from}:\n${msg.content}`);
714
+ }
715
+ msg.status = "delivered";
716
+ config.callbacks?.onMessageDelivered?.(msg);
717
+ }
718
+ // For the manager, add task status context so it knows what action to take next
719
+ if (agentId === config.manager.id) {
720
+ const incompleteTasks = state.tasks.filter((t) => t.status === "active" || t.status === "queued");
721
+ const completedTasks = state.tasks.filter((t) => t.status === "completed");
722
+ if (incompleteTasks.length > 0) {
723
+ parts.push(`\n## Current Task Status`);
724
+ parts.push(`Completed: ${completedTasks.length}, Remaining: ${incompleteTasks.length}`);
725
+ parts.push(`Remaining: ${incompleteTasks.map((t) => `${t.id} "${t.title}" (${t.status})`).join(", ")}`);
726
+ parts.push(`\nYou MUST call wait_for_task_completions now to continue waiting for the remaining tasks.`);
727
+ }
728
+ else if (completedTasks.length > 0) {
729
+ parts.push(`\n## All Tasks Complete`);
730
+ parts.push(`All ${completedTasks.length} tasks are finished.`);
731
+ parts.push(`\nReview the results above and call task_complete with a summary to finalize the goal.`);
732
+ }
733
+ }
734
+ agentState.conversationHistory.push({
735
+ role: "user",
736
+ content: parts.join("\n"),
737
+ });
738
+ }
739
+ }
740
+ // Build initial message for fresh sessions (includes goal/task context + standard instructions)
741
+ const initialMessage = agentState.conversationHistory.length === 0
742
+ ? buildInitialMessage(agentId)
743
+ : undefined;
744
+ logger.info(`Running agent ${agentId}...`);
745
+ try {
746
+ // Get any extra session callbacks from the factory (e.g. transcript management)
747
+ const extraCallbacks = config.sessionCallbacksFactory
748
+ ? await config.sessionCallbacksFactory(agentId)
749
+ : {};
750
+ // Run the agent session (returns AgentSession which is awaitable)
751
+ const session = runAgentSession(config.modelConfig, {
752
+ sessionId: agentId,
753
+ systemPrompt: agentConfig.systemPrompt || buildDefaultSystemPrompt(agentId),
754
+ tools: allTools,
755
+ messages: agentState.conversationHistory,
756
+ initialMessage,
757
+ maxTurns: config.maxTurnsPerSession,
758
+ tokenLimit: config.tokenLimit,
759
+ logger,
760
+ callbacks: {
761
+ // Pass through all extra callbacks (transcript updates, etc.)
762
+ ...extraCallbacks,
763
+ // Internal callbacks that also call through to extra callbacks
764
+ onSuspend: async (sessionId, info) => {
765
+ const messageId = info.data?.messageId;
766
+ if (messageId) {
767
+ await handleAgentSuspension(agentId, messageId);
768
+ }
769
+ await extraCallbacks.onSuspend?.(sessionId, info);
770
+ },
771
+ onMessagesUpdate: async (sessionId, messages) => {
772
+ // Update agent's conversation history
773
+ agentState.conversationHistory = messages;
774
+ await extraCallbacks.onMessagesUpdate?.(sessionId, messages);
775
+ },
776
+ },
777
+ });
778
+ // Track this session
779
+ activeAgentSessions.add(session);
780
+ const result = await session;
781
+ // Remove from active sessions
782
+ activeAgentSessions.delete(session);
783
+ // Update conversation history
784
+ agentState.conversationHistory = result.messages;
785
+ // Handle different completion reasons
786
+ if (result.completionReason === "suspended") {
787
+ return {
788
+ agentId,
789
+ suspended: true,
790
+ suspendInfo: result.suspendInfo,
791
+ finalOutput: result.finalOutput,
792
+ completionReason: "suspended",
793
+ };
794
+ }
795
+ if (result.completionReason === "task_complete") {
796
+ await handleTaskCompletion(agentId, result.finalOutput);
797
+ return {
798
+ agentId,
799
+ completed: true,
800
+ finalOutput: result.finalOutput,
801
+ completionReason: "task_complete",
802
+ };
803
+ }
804
+ // Other completion reasons (max_turns, error)
805
+ return {
806
+ agentId,
807
+ finalOutput: result.finalOutput,
808
+ completionReason: result.completionReason,
809
+ };
810
+ }
811
+ catch (error) {
812
+ logger.error(`Error running agent ${agentId}:`, error);
813
+ return {
814
+ agentId,
815
+ finalOutput: "",
816
+ completionReason: "error",
817
+ error: error,
818
+ };
819
+ }
820
+ }
821
+ return team;
822
+ }
823
+ //# sourceMappingURL=agent-team.js.map