@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.
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/agent-team.d.ts +6 -0
- package/dist/agent-team.d.ts.map +1 -0
- package/dist/agent-team.js +823 -0
- package/dist/agent-team.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -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
|