@agent-link/agent 0.1.79 → 0.1.80
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/dist/claude.d.ts +13 -1
- package/dist/claude.js +32 -5
- package/dist/claude.js.map +1 -1
- package/dist/connection.js +78 -3
- package/dist/connection.js.map +1 -1
- package/dist/sdk.js +2 -0
- package/dist/sdk.js.map +1 -1
- package/dist/team.d.ts +250 -0
- package/dist/team.js +1046 -0
- package/dist/team.js.map +1 -0
- package/package.json +1 -1
package/dist/team.js
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Team module — manages team state, output stream parsing,
|
|
3
|
+
* and team lifecycle for multi-agent team mode.
|
|
4
|
+
*
|
|
5
|
+
* team.ts is an observer, not an orchestrator: it intercepts the Lead's
|
|
6
|
+
* output stream to extract UI state (agent list, task board, activity feed)
|
|
7
|
+
* while the Lead Claude process drives all planning/execution autonomously.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, renameSync, unlinkSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { CONFIG_DIR } from './config.js';
|
|
13
|
+
// ── Color palette for auto-assigning agent colors ──────────────────────
|
|
14
|
+
const AGENT_COLORS = [
|
|
15
|
+
'#EF4444', // red (Lead)
|
|
16
|
+
'#EAB308', // yellow
|
|
17
|
+
'#3B82F6', // blue
|
|
18
|
+
'#10B981', // emerald
|
|
19
|
+
'#8B5CF6', // violet
|
|
20
|
+
'#F97316', // orange
|
|
21
|
+
'#EC4899', // pink
|
|
22
|
+
'#06B6D4', // cyan
|
|
23
|
+
'#84CC16', // lime
|
|
24
|
+
'#6366F1', // indigo
|
|
25
|
+
];
|
|
26
|
+
let activeTeam = null;
|
|
27
|
+
let lastCompletedTeamId = null;
|
|
28
|
+
let sendFn = () => { };
|
|
29
|
+
let handleChatFn = null;
|
|
30
|
+
let cancelExecutionFn = null;
|
|
31
|
+
let setOutputObserverFn = null;
|
|
32
|
+
let clearOutputObserverFn = null;
|
|
33
|
+
let setCloseObserverFn = null;
|
|
34
|
+
let clearCloseObserverFn = null;
|
|
35
|
+
let agentMessageIdCounter = 0;
|
|
36
|
+
const TEAMS_DIR = join(CONFIG_DIR, 'teams');
|
|
37
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
38
|
+
export function setTeamSendFn(fn) {
|
|
39
|
+
sendFn = fn;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Inject claude.ts dependencies to avoid circular imports.
|
|
43
|
+
* Called once during agent startup from connection.ts.
|
|
44
|
+
*/
|
|
45
|
+
export function setTeamClaudeFns(fns) {
|
|
46
|
+
handleChatFn = fns.handleChat;
|
|
47
|
+
cancelExecutionFn = fns.cancelExecution;
|
|
48
|
+
setOutputObserverFn = fns.setOutputObserver;
|
|
49
|
+
clearOutputObserverFn = fns.clearOutputObserver;
|
|
50
|
+
setCloseObserverFn = fns.setCloseObserver;
|
|
51
|
+
clearCloseObserverFn = fns.clearCloseObserver;
|
|
52
|
+
}
|
|
53
|
+
export function getActiveTeam() {
|
|
54
|
+
return activeTeam;
|
|
55
|
+
}
|
|
56
|
+
export function getLastCompletedTeamId() {
|
|
57
|
+
return lastCompletedTeamId;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a new team. Returns the TeamState.
|
|
61
|
+
* Does NOT start the Lead process — the caller (team lifecycle functions) does that.
|
|
62
|
+
*/
|
|
63
|
+
export function createTeamState(config, conversationId) {
|
|
64
|
+
if (activeTeam) {
|
|
65
|
+
throw new Error('A team is already active. Dissolve it before creating a new one.');
|
|
66
|
+
}
|
|
67
|
+
const teamId = randomUUID();
|
|
68
|
+
const title = config.instruction.length > 60
|
|
69
|
+
? config.instruction.slice(0, 57) + '...'
|
|
70
|
+
: config.instruction;
|
|
71
|
+
const team = {
|
|
72
|
+
teamId,
|
|
73
|
+
title,
|
|
74
|
+
config,
|
|
75
|
+
conversationId,
|
|
76
|
+
claudeSessionId: null,
|
|
77
|
+
agents: new Map(),
|
|
78
|
+
tasks: [],
|
|
79
|
+
feed: [],
|
|
80
|
+
status: 'planning',
|
|
81
|
+
leadStatus: 'Reading the codebase and crafting a plan...',
|
|
82
|
+
summary: null,
|
|
83
|
+
totalCost: 0,
|
|
84
|
+
durationMs: 0,
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
};
|
|
87
|
+
// Register Lead as the first agent
|
|
88
|
+
team.agents.set('lead', {
|
|
89
|
+
role: { id: 'lead', name: 'Lead', color: AGENT_COLORS[0] },
|
|
90
|
+
toolUseId: null,
|
|
91
|
+
agentTaskId: null,
|
|
92
|
+
status: 'working',
|
|
93
|
+
currentTaskId: null,
|
|
94
|
+
messages: [],
|
|
95
|
+
});
|
|
96
|
+
activeTeam = team;
|
|
97
|
+
agentMessageIdCounter = 0;
|
|
98
|
+
addFeedEntry(team, 'lead', 'lead_activity', 'Lead is reading the codebase and crafting a plan...');
|
|
99
|
+
return team;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Clear the active team (used on dissolve/complete).
|
|
103
|
+
*/
|
|
104
|
+
export function clearActiveTeam() {
|
|
105
|
+
if (activeTeam) {
|
|
106
|
+
lastCompletedTeamId = activeTeam.teamId;
|
|
107
|
+
}
|
|
108
|
+
activeTeam = null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the next color for an agent (based on current count).
|
|
112
|
+
*/
|
|
113
|
+
export function getNextAgentColor(team) {
|
|
114
|
+
const idx = team.agents.size % AGENT_COLORS.length;
|
|
115
|
+
return AGENT_COLORS[idx];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Register a subagent when Lead calls the Agent tool.
|
|
119
|
+
*/
|
|
120
|
+
export function registerSubagent(team, toolUseId, input) {
|
|
121
|
+
const agentId = (input.name || `agent-${team.agents.size}`).toLowerCase().replace(/\s+/g, '-');
|
|
122
|
+
// Derive a role-based display name from the description (e.g. "Designer: review design doc" → "Designer")
|
|
123
|
+
// Fall back to input.name, then generic "Agent N"
|
|
124
|
+
let displayName = input.name || `Agent ${team.agents.size}`;
|
|
125
|
+
if (input.description) {
|
|
126
|
+
const colonIdx = input.description.indexOf(':');
|
|
127
|
+
if (colonIdx > 0 && colonIdx <= 30) {
|
|
128
|
+
// Use role prefix from description (e.g. "Designer: review design doc" → "Designer")
|
|
129
|
+
displayName = input.description.slice(0, colonIdx).trim();
|
|
130
|
+
}
|
|
131
|
+
// Otherwise keep input.name as-is — don't override with raw description
|
|
132
|
+
}
|
|
133
|
+
const color = getNextAgentColor(team);
|
|
134
|
+
const teammate = {
|
|
135
|
+
role: { id: agentId, name: displayName, color },
|
|
136
|
+
toolUseId,
|
|
137
|
+
agentTaskId: null,
|
|
138
|
+
status: 'starting',
|
|
139
|
+
currentTaskId: null,
|
|
140
|
+
messages: [],
|
|
141
|
+
};
|
|
142
|
+
team.agents.set(agentId, teammate);
|
|
143
|
+
// Auto-create a task for this agent
|
|
144
|
+
const task = {
|
|
145
|
+
id: `task-${randomUUID().slice(0, 8)}`,
|
|
146
|
+
title: displayName,
|
|
147
|
+
description: input.prompt || input.description || '',
|
|
148
|
+
status: 'pending',
|
|
149
|
+
assignee: agentId,
|
|
150
|
+
toolUseId,
|
|
151
|
+
agentTaskId: null,
|
|
152
|
+
dependencies: [],
|
|
153
|
+
createdAt: Date.now(),
|
|
154
|
+
updatedAt: Date.now(),
|
|
155
|
+
};
|
|
156
|
+
team.tasks.push(task);
|
|
157
|
+
teammate.currentTaskId = task.id;
|
|
158
|
+
// Update team status
|
|
159
|
+
if (team.status === 'planning') {
|
|
160
|
+
team.status = 'running';
|
|
161
|
+
}
|
|
162
|
+
return teammate;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Link a subagent's task_started system message to its tool_use_id.
|
|
166
|
+
*/
|
|
167
|
+
export function linkSubagentTaskId(team, toolUseId, taskId) {
|
|
168
|
+
for (const agent of team.agents.values()) {
|
|
169
|
+
if (agent.toolUseId === toolUseId) {
|
|
170
|
+
agent.agentTaskId = taskId;
|
|
171
|
+
agent.status = 'working';
|
|
172
|
+
// Also update the task
|
|
173
|
+
const task = team.tasks.find(t => t.toolUseId === toolUseId);
|
|
174
|
+
if (task) {
|
|
175
|
+
task.agentTaskId = taskId;
|
|
176
|
+
task.status = 'active';
|
|
177
|
+
task.updatedAt = Date.now();
|
|
178
|
+
}
|
|
179
|
+
return agent;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Find agent by toolUseId (parent_tool_use_id).
|
|
186
|
+
*/
|
|
187
|
+
export function findAgentByToolUseId(team, toolUseId) {
|
|
188
|
+
for (const agent of team.agents.values()) {
|
|
189
|
+
if (agent.toolUseId === toolUseId)
|
|
190
|
+
return agent;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Find agent by agentTaskId (task_id from system.task_started).
|
|
196
|
+
*/
|
|
197
|
+
export function findAgentByTaskId(team, agentTaskId) {
|
|
198
|
+
for (const agent of team.agents.values()) {
|
|
199
|
+
if (agent.agentTaskId === agentTaskId)
|
|
200
|
+
return agent;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Add a message to an agent's message list.
|
|
206
|
+
*/
|
|
207
|
+
export function addAgentMessage(agent, role, fields) {
|
|
208
|
+
const msg = {
|
|
209
|
+
id: ++agentMessageIdCounter,
|
|
210
|
+
role,
|
|
211
|
+
...fields,
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
};
|
|
214
|
+
agent.messages.push(msg);
|
|
215
|
+
return msg;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Add an entry to the team's activity feed.
|
|
219
|
+
*/
|
|
220
|
+
export function addFeedEntry(team, agentId, type, content) {
|
|
221
|
+
const entry = {
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
agentId,
|
|
224
|
+
type,
|
|
225
|
+
content,
|
|
226
|
+
};
|
|
227
|
+
team.feed.push(entry);
|
|
228
|
+
// Cap feed at 200 entries
|
|
229
|
+
if (team.feed.length > 200) {
|
|
230
|
+
team.feed = team.feed.slice(-200);
|
|
231
|
+
}
|
|
232
|
+
return entry;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Mark a task as done/failed.
|
|
236
|
+
*/
|
|
237
|
+
export function updateTaskStatus(team, taskId, status) {
|
|
238
|
+
const task = team.tasks.find(t => t.id === taskId);
|
|
239
|
+
if (task) {
|
|
240
|
+
task.status = status;
|
|
241
|
+
task.updatedAt = Date.now();
|
|
242
|
+
}
|
|
243
|
+
return task ?? null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if all subagent tasks are completed.
|
|
247
|
+
*/
|
|
248
|
+
export function allSubagentsDone(team) {
|
|
249
|
+
const subagents = [...team.agents.values()].filter(a => a.role.id !== 'lead');
|
|
250
|
+
if (subagents.length === 0)
|
|
251
|
+
return false;
|
|
252
|
+
return subagents.every(a => a.status === 'done' || a.status === 'error');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Serialize TeamState for persistence/transmission (Map → array).
|
|
256
|
+
*/
|
|
257
|
+
export function serializeTeam(team, includeMessages = false) {
|
|
258
|
+
return {
|
|
259
|
+
teamId: team.teamId,
|
|
260
|
+
title: team.title,
|
|
261
|
+
config: team.config,
|
|
262
|
+
conversationId: team.conversationId,
|
|
263
|
+
claudeSessionId: team.claudeSessionId,
|
|
264
|
+
agents: [...team.agents.entries()].map(([, agent]) => ({
|
|
265
|
+
id: agent.role.id,
|
|
266
|
+
name: agent.role.name,
|
|
267
|
+
color: agent.role.color,
|
|
268
|
+
toolUseId: agent.toolUseId,
|
|
269
|
+
agentTaskId: agent.agentTaskId,
|
|
270
|
+
status: agent.status,
|
|
271
|
+
currentTaskId: agent.currentTaskId,
|
|
272
|
+
...(includeMessages ? { messages: agent.messages } : {}),
|
|
273
|
+
})),
|
|
274
|
+
tasks: team.tasks,
|
|
275
|
+
feed: team.feed,
|
|
276
|
+
status: team.status,
|
|
277
|
+
leadStatus: team.leadStatus,
|
|
278
|
+
summary: team.summary,
|
|
279
|
+
totalCost: team.totalCost,
|
|
280
|
+
durationMs: team.durationMs,
|
|
281
|
+
createdAt: team.createdAt,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// ── Persistence ──────────────────────────────────────────────────────────
|
|
285
|
+
function ensureTeamsDir() {
|
|
286
|
+
if (!existsSync(TEAMS_DIR)) {
|
|
287
|
+
mkdirSync(TEAMS_DIR, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Deserialize a TeamStateSerialized back into a live TeamState.
|
|
292
|
+
*/
|
|
293
|
+
function deserializeTeam(data) {
|
|
294
|
+
const agents = new Map();
|
|
295
|
+
for (const a of data.agents) {
|
|
296
|
+
agents.set(a.id, {
|
|
297
|
+
role: { id: a.id, name: a.name, color: a.color },
|
|
298
|
+
toolUseId: a.toolUseId,
|
|
299
|
+
agentTaskId: a.agentTaskId,
|
|
300
|
+
status: a.status,
|
|
301
|
+
currentTaskId: a.currentTaskId,
|
|
302
|
+
messages: a.messages || [],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
teamId: data.teamId,
|
|
307
|
+
title: data.title,
|
|
308
|
+
config: data.config,
|
|
309
|
+
conversationId: data.conversationId,
|
|
310
|
+
claudeSessionId: data.claudeSessionId,
|
|
311
|
+
agents,
|
|
312
|
+
tasks: data.tasks,
|
|
313
|
+
feed: data.feed,
|
|
314
|
+
status: data.status,
|
|
315
|
+
leadStatus: data.leadStatus || '',
|
|
316
|
+
summary: data.summary,
|
|
317
|
+
totalCost: data.totalCost,
|
|
318
|
+
durationMs: data.durationMs,
|
|
319
|
+
createdAt: data.createdAt,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Persist team state to disk (atomic write: tmp → rename).
|
|
324
|
+
*/
|
|
325
|
+
export function persistTeam(team) {
|
|
326
|
+
ensureTeamsDir();
|
|
327
|
+
const serialized = serializeTeam(team, true);
|
|
328
|
+
const filePath = join(TEAMS_DIR, `${team.teamId}.json`);
|
|
329
|
+
const tmpPath = filePath + '.tmp';
|
|
330
|
+
writeFileSync(tmpPath, JSON.stringify(serialized, null, 2), 'utf-8');
|
|
331
|
+
renameSync(tmpPath, filePath);
|
|
332
|
+
}
|
|
333
|
+
// Debounce timers for persist calls per team
|
|
334
|
+
const persistTimers = new Map();
|
|
335
|
+
/**
|
|
336
|
+
* Debounced persist — coalesces rapid state changes into a single write.
|
|
337
|
+
* Flushes after 500ms of quiet.
|
|
338
|
+
*/
|
|
339
|
+
export function persistTeamDebounced(team) {
|
|
340
|
+
const existing = persistTimers.get(team.teamId);
|
|
341
|
+
if (existing)
|
|
342
|
+
clearTimeout(existing);
|
|
343
|
+
persistTimers.set(team.teamId, setTimeout(() => {
|
|
344
|
+
persistTimers.delete(team.teamId);
|
|
345
|
+
persistTeam(team);
|
|
346
|
+
}, 500));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Flush all pending debounced persists immediately.
|
|
350
|
+
*/
|
|
351
|
+
export function flushPendingPersists() {
|
|
352
|
+
for (const [teamId, timer] of persistTimers.entries()) {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
persistTimers.delete(teamId);
|
|
355
|
+
if (activeTeam?.teamId === teamId) {
|
|
356
|
+
persistTeam(activeTeam);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Load a team from disk by teamId.
|
|
362
|
+
*/
|
|
363
|
+
export function loadTeam(teamId) {
|
|
364
|
+
const filePath = join(TEAMS_DIR, `${teamId}.json`);
|
|
365
|
+
try {
|
|
366
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
367
|
+
const data = JSON.parse(raw);
|
|
368
|
+
return deserializeTeam(data);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* List all persisted teams, sorted by createdAt descending (newest first).
|
|
376
|
+
*/
|
|
377
|
+
export function listTeams() {
|
|
378
|
+
ensureTeamsDir();
|
|
379
|
+
const files = readdirSync(TEAMS_DIR).filter(f => f.endsWith('.json'));
|
|
380
|
+
const teams = [];
|
|
381
|
+
for (const file of files) {
|
|
382
|
+
try {
|
|
383
|
+
const raw = readFileSync(join(TEAMS_DIR, file), 'utf-8');
|
|
384
|
+
const data = JSON.parse(raw);
|
|
385
|
+
teams.push({
|
|
386
|
+
teamId: data.teamId,
|
|
387
|
+
title: data.title,
|
|
388
|
+
status: data.status,
|
|
389
|
+
template: data.config.template,
|
|
390
|
+
agentCount: data.agents.length,
|
|
391
|
+
taskCount: data.tasks.length,
|
|
392
|
+
totalCost: data.totalCost,
|
|
393
|
+
createdAt: data.createdAt,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// Skip corrupted files
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
teams.sort((a, b) => b.createdAt - a.createdAt);
|
|
401
|
+
return teams;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Delete a persisted team file.
|
|
405
|
+
*/
|
|
406
|
+
export function deleteTeam(teamId) {
|
|
407
|
+
const filePath = join(TEAMS_DIR, `${teamId}.json`);
|
|
408
|
+
try {
|
|
409
|
+
unlinkSync(filePath);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const TEMPLATE_AGENTS = {
|
|
417
|
+
'code-review': {
|
|
418
|
+
'security-reviewer': {
|
|
419
|
+
description: 'Security expert focused on cryptographic, auth, and injection vulnerabilities',
|
|
420
|
+
prompt: 'You are a security reviewer. Analyze code for vulnerabilities including injection attacks, authentication/authorization flaws, cryptographic issues, and data exposure risks. Provide specific file/line references and severity ratings.',
|
|
421
|
+
tools: ['Read', 'Grep', 'Glob'],
|
|
422
|
+
},
|
|
423
|
+
'quality-reviewer': {
|
|
424
|
+
description: 'Code quality expert focused on maintainability, patterns, and best practices',
|
|
425
|
+
prompt: 'You are a code quality reviewer. Analyze code structure, naming conventions, error handling, test coverage, and adherence to best practices. Identify code smells, unnecessary complexity, and improvement opportunities.',
|
|
426
|
+
tools: ['Read', 'Grep', 'Glob'],
|
|
427
|
+
},
|
|
428
|
+
'performance-reviewer': {
|
|
429
|
+
description: 'Performance expert focused on efficiency, resource usage, and scalability',
|
|
430
|
+
prompt: 'You are a performance reviewer. Identify performance bottlenecks, memory leaks, inefficient algorithms, unnecessary allocations, and scalability concerns. Suggest concrete optimizations with benchmarks where possible.',
|
|
431
|
+
tools: ['Read', 'Grep', 'Glob'],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
'full-stack': {
|
|
435
|
+
'backend-dev': {
|
|
436
|
+
description: 'Backend developer for API endpoints, database, and server-side logic',
|
|
437
|
+
prompt: 'You are a backend developer. Implement server-side features including API endpoints, data models, business logic, and integrations. Write clean, tested, production-ready code.',
|
|
438
|
+
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
439
|
+
},
|
|
440
|
+
'frontend-dev': {
|
|
441
|
+
description: 'Frontend developer for UI components, styling, and client-side logic',
|
|
442
|
+
prompt: 'You are a frontend developer. Build user interface components, handle state management, implement responsive layouts, and ensure good UX. Follow the project\'s existing patterns and framework conventions.',
|
|
443
|
+
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
444
|
+
},
|
|
445
|
+
'test-engineer': {
|
|
446
|
+
description: 'Test engineer for unit tests, integration tests, and quality assurance',
|
|
447
|
+
prompt: 'You are a test engineer. Write comprehensive tests (unit, integration, E2E) for new and existing code. Ensure edge cases are covered, mocks are appropriate, and tests are maintainable.',
|
|
448
|
+
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
'debug': {
|
|
452
|
+
'hypothesis-a': {
|
|
453
|
+
description: 'Debug investigator exploring the first hypothesis',
|
|
454
|
+
prompt: 'You are a debugging specialist. Investigate the bug by exploring one specific hypothesis. Read relevant code, trace execution paths, check logs, and report your findings with evidence.',
|
|
455
|
+
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
456
|
+
},
|
|
457
|
+
'hypothesis-b': {
|
|
458
|
+
description: 'Debug investigator exploring an alternative hypothesis',
|
|
459
|
+
prompt: 'You are a debugging specialist. Investigate the bug by exploring an alternative hypothesis different from other investigators. Read relevant code, trace execution paths, check logs, and report your findings with evidence.',
|
|
460
|
+
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
461
|
+
},
|
|
462
|
+
'hypothesis-c': {
|
|
463
|
+
description: 'Debug investigator exploring a third hypothesis',
|
|
464
|
+
prompt: 'You are a debugging specialist. Investigate the bug by exploring yet another hypothesis different from the other investigators. Think creatively about less obvious causes. Report findings with evidence.',
|
|
465
|
+
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
'custom': {
|
|
469
|
+
'worker-1': {
|
|
470
|
+
description: 'General-purpose development agent',
|
|
471
|
+
prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
|
|
472
|
+
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
473
|
+
},
|
|
474
|
+
'worker-2': {
|
|
475
|
+
description: 'General-purpose development agent',
|
|
476
|
+
prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
|
|
477
|
+
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
478
|
+
},
|
|
479
|
+
'worker-3': {
|
|
480
|
+
description: 'General-purpose development agent',
|
|
481
|
+
prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
|
|
482
|
+
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
const TEMPLATE_LEAD_INSTRUCTIONS = {
|
|
487
|
+
'code-review': `You are a team lead coordinating a code review.
|
|
488
|
+
|
|
489
|
+
Instructions:
|
|
490
|
+
1. First, analyze the codebase to understand its structure and what needs reviewing
|
|
491
|
+
2. Use the Agent tool to spawn each reviewer IN PARALLEL (multiple Agent calls simultaneously)
|
|
492
|
+
3. Give each reviewer specific, detailed instructions referencing exact files and directories to review
|
|
493
|
+
4. After all reviewers complete, synthesize their findings into a unified summary with prioritized action items
|
|
494
|
+
|
|
495
|
+
Important:
|
|
496
|
+
- When calling the Agent tool, use a descriptive role-based name (e.g., "Security Reviewer", "Quality Analyst", "Performance Auditor") instead of generic names. The name should reflect the agent's specialty.
|
|
497
|
+
- Spawn agents in parallel for efficiency. Each agent should focus on their specialty area.`,
|
|
498
|
+
'full-stack': `You are a team lead coordinating full-stack development.
|
|
499
|
+
|
|
500
|
+
Instructions:
|
|
501
|
+
1. First, analyze the codebase to understand the architecture, existing patterns, and what needs building
|
|
502
|
+
2. Break the task into backend, frontend, and test subtasks, and analyze dependencies between them
|
|
503
|
+
3. Define clear interfaces, API contracts, and data schemas before spawning any agents
|
|
504
|
+
4. Spawn independent subtasks IN PARALLEL using the Agent tool. If a subtask depends on another's output (e.g., frontend needs the API built first, tests need the implementation), wait for the dependency to complete, then spawn the dependent agent with the prior result as context
|
|
505
|
+
5. Provide each agent with specific, detailed instructions including file paths and shared contracts
|
|
506
|
+
6. After all agents complete, review their work and provide a summary of what was built
|
|
507
|
+
|
|
508
|
+
Important:
|
|
509
|
+
- When calling the Agent tool, use a descriptive role-based name (e.g., "Backend Engineer", "Frontend Engineer", "Test Engineer") instead of generic names. The name should reflect the agent's responsibility.
|
|
510
|
+
- Maximize parallelism for truly independent tasks, but respect dependencies. Do not spawn all agents simultaneously if some need others' output first.`,
|
|
511
|
+
'debug': `You are a team lead coordinating a debugging investigation.
|
|
512
|
+
|
|
513
|
+
Instructions:
|
|
514
|
+
1. First, analyze the bug report and relevant code to understand the problem space
|
|
515
|
+
2. Formulate 3 distinct hypotheses about the root cause
|
|
516
|
+
3. Use the Agent tool to assign each hypothesis to a different investigator IN PARALLEL
|
|
517
|
+
4. Give each investigator specific areas of code to examine and tests to run
|
|
518
|
+
5. After all investigators complete, compare their findings and synthesize a diagnosis with a recommended fix
|
|
519
|
+
|
|
520
|
+
Important:
|
|
521
|
+
- When calling the Agent tool, use a descriptive name that reflects the hypothesis being investigated (e.g., "Race Condition Investigator", "Memory Leak Analyst", "Config Error Detective") instead of generic names.
|
|
522
|
+
- Each investigator should explore a DIFFERENT hypothesis. Avoid overlap.`,
|
|
523
|
+
'custom': `You are a team lead coordinating a development task.
|
|
524
|
+
|
|
525
|
+
Instructions:
|
|
526
|
+
1. First, analyze the codebase and the user's request to understand what needs to be done
|
|
527
|
+
2. Break the task into subtasks and analyze dependencies between them
|
|
528
|
+
3. Spawn independent tasks IN PARALLEL using the Agent tool for efficiency
|
|
529
|
+
4. If a task depends on another's output (e.g., implementation needs a design doc, tests need the implementation), wait for the dependency to complete first, then spawn the dependent task with the prior result as context
|
|
530
|
+
5. Give each worker specific, detailed instructions
|
|
531
|
+
6. After all workers complete, review their work and provide a summary
|
|
532
|
+
|
|
533
|
+
Important:
|
|
534
|
+
- When calling the Agent tool, use a descriptive role-based name (e.g., "Designer", "Developer", "Tester", "Architect") instead of generic names like "Agent 1". The name should reflect what the agent does.
|
|
535
|
+
- Maximize parallelism for truly independent tasks, but respect dependencies. For example, if one agent writes a design doc and another implements it, spawn the doc agent first, wait for its result, then spawn the implementation agent with the doc content.`,
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* Build the agents definition JSON for the --agents CLI flag.
|
|
539
|
+
*/
|
|
540
|
+
export function buildAgentsDef(template) {
|
|
541
|
+
const key = template && TEMPLATE_AGENTS[template] ? template : 'custom';
|
|
542
|
+
return { ...TEMPLATE_AGENTS[key] };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Build the lead prompt that instructs the Lead to use Agent tool.
|
|
546
|
+
*/
|
|
547
|
+
export function buildLeadPrompt(config, agentsDef) {
|
|
548
|
+
const template = config.template || 'custom';
|
|
549
|
+
const instructions = TEMPLATE_LEAD_INSTRUCTIONS[template] || TEMPLATE_LEAD_INSTRUCTIONS['custom'];
|
|
550
|
+
const agentList = Object.entries(agentsDef)
|
|
551
|
+
.map(([id, def]) => `- ${id}: ${def.description}`)
|
|
552
|
+
.join('\n');
|
|
553
|
+
return `${instructions}
|
|
554
|
+
|
|
555
|
+
Available agents (use the Agent tool to delegate to them):
|
|
556
|
+
${agentList}
|
|
557
|
+
|
|
558
|
+
User's request: "${config.instruction}"`;
|
|
559
|
+
}
|
|
560
|
+
// ── Output stream parser (observer callback) ────────────────────────────
|
|
561
|
+
/**
|
|
562
|
+
* Output observer callback for the Lead's stdout stream.
|
|
563
|
+
* Registered via setOutputObserver() when a team is active.
|
|
564
|
+
* Returns true to suppress the message from normal web client forwarding.
|
|
565
|
+
*/
|
|
566
|
+
export function onLeadOutput(conversationId, msg) {
|
|
567
|
+
if (!activeTeam || conversationId !== activeTeam.conversationId) {
|
|
568
|
+
return false; // not our team's conversation
|
|
569
|
+
}
|
|
570
|
+
const team = activeTeam;
|
|
571
|
+
// Capture session ID from system init
|
|
572
|
+
if (msg.type === 'system' && msg.session_id) {
|
|
573
|
+
team.claudeSessionId = msg.session_id;
|
|
574
|
+
}
|
|
575
|
+
// 1. assistant message → check for Agent tool calls + track lead activity
|
|
576
|
+
if (msg.type === 'assistant' && msg.message) {
|
|
577
|
+
const message = msg.message;
|
|
578
|
+
const content = message.content;
|
|
579
|
+
if (Array.isArray(content)) {
|
|
580
|
+
// Track lead's text as activity (only for Lead's own messages, not subagent)
|
|
581
|
+
if (!msg.parent_tool_use_id) {
|
|
582
|
+
const textBlocks = content.filter(b => b.type === 'text');
|
|
583
|
+
const leadText = textBlocks.map(b => b.text || '').join('');
|
|
584
|
+
if (leadText.trim()) {
|
|
585
|
+
// Extract a short summary for lead status
|
|
586
|
+
const firstLine = leadText.trim().split('\n')[0].slice(0, 80);
|
|
587
|
+
team.leadStatus = firstLine + (leadText.trim().length > 80 ? '...' : '');
|
|
588
|
+
sendFn({ type: 'team_lead_status', teamId: team.teamId, leadStatus: team.leadStatus });
|
|
589
|
+
}
|
|
590
|
+
// Check if Lead is dispatching Agent tools
|
|
591
|
+
const agentCalls = content.filter(b => b.type === 'tool_use' && b.name === 'Agent');
|
|
592
|
+
if (agentCalls.length > 0) {
|
|
593
|
+
team.leadStatus = `Assigning work to ${agentCalls.length} agent${agentCalls.length > 1 ? 's' : ''}...`;
|
|
594
|
+
sendFn({ type: 'team_lead_status', teamId: team.teamId, leadStatus: team.leadStatus });
|
|
595
|
+
}
|
|
596
|
+
// Forward lead's own messages with team context for lead detail view
|
|
597
|
+
if (leadText) {
|
|
598
|
+
sendFn({
|
|
599
|
+
type: 'claude_output',
|
|
600
|
+
teamId: team.teamId,
|
|
601
|
+
agentRole: 'lead',
|
|
602
|
+
data: { type: 'content_block_delta', delta: leadText },
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const toolBlocks = content.filter(b => b.type === 'tool_use');
|
|
606
|
+
if (toolBlocks.length > 0) {
|
|
607
|
+
sendFn({
|
|
608
|
+
type: 'claude_output',
|
|
609
|
+
teamId: team.teamId,
|
|
610
|
+
agentRole: 'lead',
|
|
611
|
+
data: { type: 'tool_use', tools: toolBlocks },
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const block of content) {
|
|
616
|
+
if (block.type === 'tool_use' && block.name === 'Agent') {
|
|
617
|
+
const input = (block.input || {});
|
|
618
|
+
const toolUseId = block.id;
|
|
619
|
+
const agent = registerSubagent(team, toolUseId, input);
|
|
620
|
+
// Emit agent status + task update to web
|
|
621
|
+
sendFn({
|
|
622
|
+
type: 'team_agent_status',
|
|
623
|
+
teamId: team.teamId,
|
|
624
|
+
agent: {
|
|
625
|
+
id: agent.role.id,
|
|
626
|
+
name: agent.role.name,
|
|
627
|
+
color: agent.role.color,
|
|
628
|
+
status: agent.status,
|
|
629
|
+
taskId: agent.currentTaskId,
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
633
|
+
if (task) {
|
|
634
|
+
sendFn({
|
|
635
|
+
type: 'team_task_update',
|
|
636
|
+
teamId: team.teamId,
|
|
637
|
+
task,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
addFeedEntry(team, agent.role.id, 'status_change', `${agent.role.name} has joined and is getting ready`);
|
|
641
|
+
sendFn({
|
|
642
|
+
type: 'team_feed',
|
|
643
|
+
teamId: team.teamId,
|
|
644
|
+
entry: team.feed[team.feed.length - 1],
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Don't suppress Lead's assistant messages — they need to be forwarded
|
|
650
|
+
// with team context for the Lead planning view
|
|
651
|
+
if (!msg.parent_tool_use_id) {
|
|
652
|
+
return false; // let normal forwarding handle Lead's own messages
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// 2. system.task_started → subagent began executing
|
|
656
|
+
if (msg.type === 'system' && msg.subtype === 'task_started') {
|
|
657
|
+
const toolUseId = msg.tool_use_id;
|
|
658
|
+
const taskId = msg.task_id;
|
|
659
|
+
const agent = linkSubagentTaskId(team, toolUseId, taskId);
|
|
660
|
+
if (agent) {
|
|
661
|
+
addFeedEntry(team, agent.role.id, 'task_started', `${agent.role.name} started working on the task`);
|
|
662
|
+
sendFn({
|
|
663
|
+
type: 'team_agent_status',
|
|
664
|
+
teamId: team.teamId,
|
|
665
|
+
agent: {
|
|
666
|
+
id: agent.role.id,
|
|
667
|
+
name: agent.role.name,
|
|
668
|
+
color: agent.role.color,
|
|
669
|
+
status: agent.status,
|
|
670
|
+
taskId: agent.currentTaskId,
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
sendFn({
|
|
674
|
+
type: 'team_feed',
|
|
675
|
+
teamId: team.teamId,
|
|
676
|
+
entry: team.feed[team.feed.length - 1],
|
|
677
|
+
});
|
|
678
|
+
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
679
|
+
if (task) {
|
|
680
|
+
sendFn({
|
|
681
|
+
type: 'team_task_update',
|
|
682
|
+
teamId: team.teamId,
|
|
683
|
+
task,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return true; // suppress system.task_started from normal forwarding
|
|
688
|
+
}
|
|
689
|
+
// 3. Messages with parent_tool_use_id → route to specific subagent
|
|
690
|
+
if (msg.parent_tool_use_id) {
|
|
691
|
+
const agent = findAgentByToolUseId(team, msg.parent_tool_use_id);
|
|
692
|
+
if (agent) {
|
|
693
|
+
routeMessageToAgent(team, agent, msg);
|
|
694
|
+
return true; // suppress from normal forwarding
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// 4. user message with tool_use_result → check for subagent completion
|
|
698
|
+
if (msg.type === 'user' && msg.message) {
|
|
699
|
+
const message = msg.message;
|
|
700
|
+
if (Array.isArray(message.content)) {
|
|
701
|
+
for (const block of message.content) {
|
|
702
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
703
|
+
// Check if this is a result for an Agent tool call
|
|
704
|
+
const agent = findAgentByToolUseId(team, block.tool_use_id);
|
|
705
|
+
if (agent) {
|
|
706
|
+
const isError = !!block.is_error;
|
|
707
|
+
agent.status = isError ? 'error' : 'done';
|
|
708
|
+
if (agent.currentTaskId) {
|
|
709
|
+
updateTaskStatus(team, agent.currentTaskId, isError ? 'failed' : 'done');
|
|
710
|
+
}
|
|
711
|
+
addFeedEntry(team, agent.role.id, isError ? 'task_failed' : 'task_completed', isError ? `${agent.role.name} ran into an issue and stopped` : `${agent.role.name} finished the task successfully`);
|
|
712
|
+
sendFn({
|
|
713
|
+
type: 'team_agent_status',
|
|
714
|
+
teamId: team.teamId,
|
|
715
|
+
agent: {
|
|
716
|
+
id: agent.role.id,
|
|
717
|
+
name: agent.role.name,
|
|
718
|
+
color: agent.role.color,
|
|
719
|
+
status: agent.status,
|
|
720
|
+
taskId: agent.currentTaskId,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
724
|
+
if (task) {
|
|
725
|
+
sendFn({
|
|
726
|
+
type: 'team_task_update',
|
|
727
|
+
teamId: team.teamId,
|
|
728
|
+
task,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
sendFn({
|
|
732
|
+
type: 'team_feed',
|
|
733
|
+
teamId: team.teamId,
|
|
734
|
+
entry: team.feed[team.feed.length - 1],
|
|
735
|
+
});
|
|
736
|
+
// Check if all subagents are done → transition to summarizing
|
|
737
|
+
if (allSubagentsDone(team) && team.status === 'running') {
|
|
738
|
+
team.status = 'summarizing';
|
|
739
|
+
team.leadStatus = 'Reviewing results and writing summary...';
|
|
740
|
+
sendFn({ type: 'team_lead_status', teamId: team.teamId, leadStatus: team.leadStatus });
|
|
741
|
+
addFeedEntry(team, 'lead', 'lead_activity', 'Lead is reviewing everyone\'s work and writing a summary');
|
|
742
|
+
sendFn({ type: 'team_feed', teamId: team.teamId, entry: team.feed[team.feed.length - 1] });
|
|
743
|
+
}
|
|
744
|
+
return true; // suppress from normal forwarding
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// 5. result message → team completed
|
|
751
|
+
if (msg.type === 'result') {
|
|
752
|
+
team.totalCost = msg.total_cost_usd || 0;
|
|
753
|
+
team.durationMs = msg.duration_ms || 0;
|
|
754
|
+
// Extract summary from result if available
|
|
755
|
+
const resultText = msg.result || undefined;
|
|
756
|
+
completeTeam(resultText);
|
|
757
|
+
return false; // let normal forwarding handle result (turn_completed)
|
|
758
|
+
}
|
|
759
|
+
// Default: don't suppress (Lead's own messages flow through normally)
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Close observer callback for the Lead's process exit.
|
|
764
|
+
* Detects Lead crash (process exited without a result message) and dissolves the team.
|
|
765
|
+
*/
|
|
766
|
+
export function onLeadClose(conversationId, _exitCode, resultReceived) {
|
|
767
|
+
if (!activeTeam || conversationId !== activeTeam.conversationId)
|
|
768
|
+
return;
|
|
769
|
+
// If result was received, completeTeam() was already called from onLeadOutput
|
|
770
|
+
if (resultReceived)
|
|
771
|
+
return;
|
|
772
|
+
// Lead process crashed without producing a result — dissolve the team
|
|
773
|
+
console.log(`[Team] Lead process exited without result — marking team as failed`);
|
|
774
|
+
const team = activeTeam;
|
|
775
|
+
// Mark remaining agents as error
|
|
776
|
+
for (const agent of team.agents.values()) {
|
|
777
|
+
if (agent.status === 'starting' || agent.status === 'working') {
|
|
778
|
+
agent.status = 'error';
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// Mark remaining tasks as failed
|
|
782
|
+
for (const task of team.tasks) {
|
|
783
|
+
if (task.status === 'pending' || task.status === 'active') {
|
|
784
|
+
task.status = 'failed';
|
|
785
|
+
task.updatedAt = Date.now();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
team.status = 'failed';
|
|
789
|
+
team.durationMs = Date.now() - team.createdAt;
|
|
790
|
+
persistTeam(team);
|
|
791
|
+
// Remove observers
|
|
792
|
+
if (clearOutputObserverFn)
|
|
793
|
+
clearOutputObserverFn();
|
|
794
|
+
if (clearCloseObserverFn)
|
|
795
|
+
clearCloseObserverFn();
|
|
796
|
+
// Notify clients
|
|
797
|
+
sendFn({
|
|
798
|
+
type: 'team_completed',
|
|
799
|
+
teamId: team.teamId,
|
|
800
|
+
status: 'failed',
|
|
801
|
+
team: serializeTeam(team),
|
|
802
|
+
});
|
|
803
|
+
clearActiveTeam();
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Generate a human-readable description for a tool call feed entry.
|
|
807
|
+
*/
|
|
808
|
+
function describeToolCall(agentName, block) {
|
|
809
|
+
const name = block.name;
|
|
810
|
+
const input = (block.input || {});
|
|
811
|
+
const shortPath = (p) => {
|
|
812
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
813
|
+
return parts.length > 2 ? '.../' + parts.slice(-2).join('/') : p;
|
|
814
|
+
};
|
|
815
|
+
switch (name) {
|
|
816
|
+
case 'Read':
|
|
817
|
+
return `${agentName} is reading ${shortPath(String(input.file_path || input.path || ''))}`;
|
|
818
|
+
case 'Write':
|
|
819
|
+
return `${agentName} is creating ${shortPath(String(input.file_path || input.path || ''))}`;
|
|
820
|
+
case 'Edit':
|
|
821
|
+
return `${agentName} is modifying ${shortPath(String(input.file_path || input.path || ''))}`;
|
|
822
|
+
case 'Bash': {
|
|
823
|
+
const cmd = String(input.command || '').slice(0, 60);
|
|
824
|
+
return `${agentName} is running \`${cmd}${String(input.command || '').length > 60 ? '...' : ''}\``;
|
|
825
|
+
}
|
|
826
|
+
case 'Grep': {
|
|
827
|
+
const pat = String(input.pattern || '').slice(0, 40);
|
|
828
|
+
return `${agentName} is searching for "${pat}"`;
|
|
829
|
+
}
|
|
830
|
+
case 'Glob': {
|
|
831
|
+
const pat = String(input.pattern || '').slice(0, 40);
|
|
832
|
+
return `${agentName} is looking for files matching "${pat}"`;
|
|
833
|
+
}
|
|
834
|
+
case 'Agent': {
|
|
835
|
+
const desc = String(input.description || input.prompt || '').slice(0, 60);
|
|
836
|
+
return `${agentName} is delegating: ${desc}`;
|
|
837
|
+
}
|
|
838
|
+
default:
|
|
839
|
+
return `${agentName} is using ${name}`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Route a subagent message to the agent's message list and forward to web.
|
|
844
|
+
*/
|
|
845
|
+
function routeMessageToAgent(team, agent, msg) {
|
|
846
|
+
// Extract useful content from the message for agent detail view
|
|
847
|
+
if (msg.type === 'assistant' && msg.message) {
|
|
848
|
+
const message = msg.message;
|
|
849
|
+
if (Array.isArray(message.content)) {
|
|
850
|
+
// Text content
|
|
851
|
+
const textBlocks = message.content.filter(b => b.type === 'text');
|
|
852
|
+
for (const block of textBlocks) {
|
|
853
|
+
if (block.text) {
|
|
854
|
+
addAgentMessage(agent, 'assistant', { content: block.text });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// Tool use blocks
|
|
858
|
+
const toolBlocks = message.content.filter(b => b.type === 'tool_use');
|
|
859
|
+
for (const block of toolBlocks) {
|
|
860
|
+
addAgentMessage(agent, 'tool', {
|
|
861
|
+
toolName: block.name,
|
|
862
|
+
toolInput: typeof block.input === 'string' ? block.input : JSON.stringify(block.input),
|
|
863
|
+
});
|
|
864
|
+
addFeedEntry(team, agent.role.id, 'tool_call', describeToolCall(agent.role.name, block));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// Tool results (user messages within subagent context)
|
|
869
|
+
if (msg.type === 'user' && msg.message) {
|
|
870
|
+
const message = msg.message;
|
|
871
|
+
if (Array.isArray(message.content)) {
|
|
872
|
+
for (const block of message.content) {
|
|
873
|
+
if (block.type === 'tool_result') {
|
|
874
|
+
const content = typeof block.content === 'string'
|
|
875
|
+
? block.content
|
|
876
|
+
: Array.isArray(block.content)
|
|
877
|
+
? block.content.map(b => b.text || '').join('')
|
|
878
|
+
: '';
|
|
879
|
+
addAgentMessage(agent, 'tool', {
|
|
880
|
+
toolOutput: content.slice(0, 2000), // cap for memory
|
|
881
|
+
hasResult: true,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Forward to web with team + agent context — extract delta like claude.ts does
|
|
888
|
+
const data = extractOutputData(msg);
|
|
889
|
+
if (data) {
|
|
890
|
+
sendFn({
|
|
891
|
+
type: 'claude_output',
|
|
892
|
+
teamId: team.teamId,
|
|
893
|
+
agentRole: agent.role.id,
|
|
894
|
+
data,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Extract the output data payload from a raw message (similar to claude.ts processing).
|
|
900
|
+
*/
|
|
901
|
+
function extractOutputData(msg) {
|
|
902
|
+
if (msg.type === 'assistant' && msg.message) {
|
|
903
|
+
const message = msg.message;
|
|
904
|
+
if (Array.isArray(message.content)) {
|
|
905
|
+
const textBlocks = message.content.filter(b => b.type === 'text');
|
|
906
|
+
const fullText = textBlocks.map(b => b.text || '').join('');
|
|
907
|
+
const toolBlocks = message.content.filter(b => b.type === 'tool_use');
|
|
908
|
+
const result = {};
|
|
909
|
+
if (fullText) {
|
|
910
|
+
result.type = 'content_block_delta';
|
|
911
|
+
result.delta = fullText;
|
|
912
|
+
}
|
|
913
|
+
if (toolBlocks.length > 0) {
|
|
914
|
+
return { type: 'tool_use', tools: toolBlocks };
|
|
915
|
+
}
|
|
916
|
+
if (fullText)
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (msg.type === 'user') {
|
|
921
|
+
return msg;
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
// ── Team lifecycle ──────────────────────────────────────────────────────
|
|
926
|
+
/**
|
|
927
|
+
* Create and launch a team. Returns the TeamState.
|
|
928
|
+
* Spawns the Lead Claude process with --agents flag and attaches the output observer.
|
|
929
|
+
*/
|
|
930
|
+
export function createTeam(config, workDir) {
|
|
931
|
+
if (!handleChatFn || !setOutputObserverFn) {
|
|
932
|
+
throw new Error('Team claude functions not initialized. Call setTeamClaudeFns() first.');
|
|
933
|
+
}
|
|
934
|
+
const conversationId = `team-${randomUUID().slice(0, 8)}`;
|
|
935
|
+
const team = createTeamState(config, conversationId);
|
|
936
|
+
// Build agents definition for --agents flag
|
|
937
|
+
const agentsDef = buildAgentsDef(config.template);
|
|
938
|
+
const agentsJson = JSON.stringify(agentsDef);
|
|
939
|
+
// Build the lead prompt
|
|
940
|
+
const leadPrompt = buildLeadPrompt(config, agentsDef);
|
|
941
|
+
// Attach output observer before spawning
|
|
942
|
+
setOutputObserverFn(onLeadOutput);
|
|
943
|
+
// Attach close observer to detect Lead crash
|
|
944
|
+
if (setCloseObserverFn) {
|
|
945
|
+
setCloseObserverFn(onLeadClose);
|
|
946
|
+
}
|
|
947
|
+
// Spawn the Lead process with --agents flag
|
|
948
|
+
handleChatFn(conversationId, leadPrompt, workDir, {
|
|
949
|
+
extraArgs: ['--agents', agentsJson],
|
|
950
|
+
});
|
|
951
|
+
// Persist initial state
|
|
952
|
+
persistTeam(team);
|
|
953
|
+
// Notify clients
|
|
954
|
+
sendFn({
|
|
955
|
+
type: 'team_created',
|
|
956
|
+
team: serializeTeam(team),
|
|
957
|
+
});
|
|
958
|
+
return team;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Dissolve (cancel) the active team.
|
|
962
|
+
*/
|
|
963
|
+
export function dissolveTeam() {
|
|
964
|
+
if (!activeTeam)
|
|
965
|
+
return;
|
|
966
|
+
const team = activeTeam;
|
|
967
|
+
// Cancel the Lead process
|
|
968
|
+
if (cancelExecutionFn) {
|
|
969
|
+
cancelExecutionFn(team.conversationId);
|
|
970
|
+
}
|
|
971
|
+
// Mark remaining agents as error
|
|
972
|
+
for (const agent of team.agents.values()) {
|
|
973
|
+
if (agent.status === 'starting' || agent.status === 'working') {
|
|
974
|
+
agent.status = 'error';
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Mark remaining tasks as failed
|
|
978
|
+
for (const task of team.tasks) {
|
|
979
|
+
if (task.status === 'pending' || task.status === 'active') {
|
|
980
|
+
task.status = 'failed';
|
|
981
|
+
task.updatedAt = Date.now();
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
team.status = 'failed';
|
|
985
|
+
persistTeam(team);
|
|
986
|
+
// Remove output observer
|
|
987
|
+
if (clearOutputObserverFn) {
|
|
988
|
+
clearOutputObserverFn();
|
|
989
|
+
}
|
|
990
|
+
// Remove close observer
|
|
991
|
+
if (clearCloseObserverFn) {
|
|
992
|
+
clearCloseObserverFn();
|
|
993
|
+
}
|
|
994
|
+
// Notify clients
|
|
995
|
+
sendFn({
|
|
996
|
+
type: 'team_completed',
|
|
997
|
+
teamId: team.teamId,
|
|
998
|
+
status: 'failed',
|
|
999
|
+
team: serializeTeam(team),
|
|
1000
|
+
});
|
|
1001
|
+
clearActiveTeam();
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Called when the Lead's result message arrives (from onLeadOutput).
|
|
1005
|
+
* Marks the team as completed, persists state, and notifies clients.
|
|
1006
|
+
*/
|
|
1007
|
+
export function completeTeam(summary) {
|
|
1008
|
+
if (!activeTeam)
|
|
1009
|
+
return;
|
|
1010
|
+
const team = activeTeam;
|
|
1011
|
+
team.status = 'completed';
|
|
1012
|
+
if (summary) {
|
|
1013
|
+
team.summary = summary;
|
|
1014
|
+
}
|
|
1015
|
+
// Mark all agents as done (unless they already errored)
|
|
1016
|
+
for (const agent of team.agents.values()) {
|
|
1017
|
+
if (agent.status !== 'error') {
|
|
1018
|
+
agent.status = 'done';
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// Mark remaining active/pending tasks as done
|
|
1022
|
+
for (const task of team.tasks) {
|
|
1023
|
+
if (task.status === 'active' || task.status === 'pending') {
|
|
1024
|
+
task.status = 'done';
|
|
1025
|
+
task.updatedAt = Date.now();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
persistTeam(team);
|
|
1029
|
+
// Remove output observer
|
|
1030
|
+
if (clearOutputObserverFn) {
|
|
1031
|
+
clearOutputObserverFn();
|
|
1032
|
+
}
|
|
1033
|
+
// Remove close observer
|
|
1034
|
+
if (clearCloseObserverFn) {
|
|
1035
|
+
clearCloseObserverFn();
|
|
1036
|
+
}
|
|
1037
|
+
// Notify clients
|
|
1038
|
+
sendFn({
|
|
1039
|
+
type: 'team_completed',
|
|
1040
|
+
teamId: team.teamId,
|
|
1041
|
+
status: 'completed',
|
|
1042
|
+
team: serializeTeam(team),
|
|
1043
|
+
});
|
|
1044
|
+
clearActiveTeam();
|
|
1045
|
+
}
|
|
1046
|
+
//# sourceMappingURL=team.js.map
|