@agent-link/agent 0.1.84 → 0.1.85
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 +18 -1
- package/dist/claude.js +118 -104
- package/dist/claude.js.map +1 -1
- package/dist/connection.js +4 -103
- package/dist/connection.js.map +1 -1
- package/dist/directory-handlers.d.ts +18 -0
- package/dist/directory-handlers.js +103 -0
- package/dist/directory-handlers.js.map +1 -0
- package/dist/team-naming.d.ts +35 -0
- package/dist/team-naming.js +119 -0
- package/dist/team-naming.js.map +1 -0
- package/dist/team-persistence.d.ts +39 -0
- package/dist/team-persistence.js +185 -0
- package/dist/team-persistence.js.map +1 -0
- package/dist/team-templates.d.ts +12 -0
- package/dist/team-templates.js +149 -0
- package/dist/team-templates.js.map +1 -0
- package/dist/team-types.d.ts +112 -0
- package/dist/team-types.js +3 -0
- package/dist/team-types.js.map +1 -0
- package/dist/team.d.ts +7 -156
- package/dist/team.js +66 -557
- package/dist/team.js.map +1 -1
- package/package.json +1 -1
package/dist/team.js
CHANGED
|
@@ -7,22 +7,13 @@
|
|
|
7
7
|
* while the Lead Claude process drives all planning/execution autonomously.
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'crypto';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
];
|
|
10
|
+
export { buildAgentsDef, buildLeadPrompt } from './team-templates.js';
|
|
11
|
+
export { getNextAgentColor, classifyRole, pickCharacter, deriveAgentDisplayName, deriveTaskTitle } from './team-naming.js';
|
|
12
|
+
export { serializeTeam, deserializeTeam, persistTeam, persistTeamDebounced, loadTeam, listTeams, deleteTeam, renameTeam, } from './team-persistence.js';
|
|
13
|
+
import { getNextAgentColor, deriveAgentDisplayName, deriveTaskTitle } from './team-naming.js';
|
|
14
|
+
import { buildAgentsDef, buildLeadPrompt } from './team-templates.js';
|
|
15
|
+
import { serializeTeam, persistTeam, flushPendingPersists as _flushPendingPersists, } from './team-persistence.js';
|
|
16
|
+
// ── Module state ───────────────────────────────────────────────────────
|
|
26
17
|
let activeTeam = null;
|
|
27
18
|
let sendFn = () => { };
|
|
28
19
|
let handleChatFn = null;
|
|
@@ -32,7 +23,6 @@ let clearOutputObserverFn = null;
|
|
|
32
23
|
let setCloseObserverFn = null;
|
|
33
24
|
let clearCloseObserverFn = null;
|
|
34
25
|
let agentMessageIdCounter = 0;
|
|
35
|
-
const TEAMS_DIR = join(CONFIG_DIR, 'teams');
|
|
36
26
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
37
27
|
export function setTeamSendFn(fn) {
|
|
38
28
|
sendFn = fn;
|
|
@@ -82,7 +72,7 @@ export function createTeamState(config, conversationId) {
|
|
|
82
72
|
};
|
|
83
73
|
// Register Lead as the first agent
|
|
84
74
|
team.agents.set('lead', {
|
|
85
|
-
role: { id: 'lead', name: 'Lead', color:
|
|
75
|
+
role: { id: 'lead', name: 'Lead', color: getNextAgentColor(team) },
|
|
86
76
|
toolUseId: null,
|
|
87
77
|
agentTaskId: null,
|
|
88
78
|
status: 'working',
|
|
@@ -100,108 +90,6 @@ export function createTeamState(config, conversationId) {
|
|
|
100
90
|
export function clearActiveTeam() {
|
|
101
91
|
activeTeam = null;
|
|
102
92
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Get the next color for an agent (based on current count).
|
|
105
|
-
*/
|
|
106
|
-
export function getNextAgentColor(team) {
|
|
107
|
-
const idx = team.agents.size % AGENT_COLORS.length;
|
|
108
|
-
return AGENT_COLORS[idx];
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Fictional character pools grouped by role archetype.
|
|
112
|
-
* Each subagent gets a character name that fits its role.
|
|
113
|
-
*/
|
|
114
|
-
const CHARACTER_POOLS = {
|
|
115
|
-
builder: ['Tony Stark', 'Neo', 'Hiro Hamada', 'Rocket', 'Q'],
|
|
116
|
-
designer: ['Elsa', 'Remy', 'Edna Mode', 'Violet', 'WALL-E'],
|
|
117
|
-
tester: ['Sherlock', 'L', 'Conan', 'Poirot', 'Columbo'],
|
|
118
|
-
writer: ['Hermione', 'Gandalf', 'Dumbledore', 'Yoda', 'Jarvis'],
|
|
119
|
-
reviewer: ['Spock', 'Baymax', 'Alfred', 'Morpheus', 'Obi-Wan'],
|
|
120
|
-
debugger: ['MacGyver', 'Strange', 'Lelouch', 'House', 'Lupin'],
|
|
121
|
-
analyst: ['Data', 'Cortana', 'Oracle', 'Vision', 'Friday'],
|
|
122
|
-
ops: ['Scotty', 'R2-D2', 'BB-8', 'C-3PO', 'HAL'],
|
|
123
|
-
general: ['Aragorn', 'Leia', 'Zoro', 'Totoro', 'Pikachu'],
|
|
124
|
-
};
|
|
125
|
-
/** Track which characters have been used in this team to avoid duplicates. */
|
|
126
|
-
function pickCharacter(team, category) {
|
|
127
|
-
const pool = CHARACTER_POOLS[category] || CHARACTER_POOLS.general;
|
|
128
|
-
const usedNames = new Set([...team.agents.values()].map(a => a.role.name));
|
|
129
|
-
// Find an unused character from the pool
|
|
130
|
-
for (const name of pool) {
|
|
131
|
-
if (!usedNames.has(name))
|
|
132
|
-
return name;
|
|
133
|
-
}
|
|
134
|
-
// Fallback: try other pools
|
|
135
|
-
for (const names of Object.values(CHARACTER_POOLS)) {
|
|
136
|
-
for (const name of names) {
|
|
137
|
-
if (!usedNames.has(name))
|
|
138
|
-
return name;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return `Agent ${team.agents.size}`;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Classify a subagent's role from its tool input into a character category.
|
|
145
|
-
*/
|
|
146
|
-
function classifyRole(input) {
|
|
147
|
-
const text = [input.name, input.description, input.prompt].filter(Boolean).join(' ').toLowerCase();
|
|
148
|
-
if (/\b(test|testing|qa|verify|validation|spec)\b/.test(text))
|
|
149
|
-
return 'tester';
|
|
150
|
-
if (/\b(review|audit|check|inspect|security|lint)\b/.test(text))
|
|
151
|
-
return 'reviewer';
|
|
152
|
-
if (/\b(debug|fix|bug|patch|troubleshoot|diagnose)\b/.test(text))
|
|
153
|
-
return 'debugger';
|
|
154
|
-
if (/\b(design|ui|ux|layout|style|css|visual|mockup)\b/.test(text))
|
|
155
|
-
return 'designer';
|
|
156
|
-
if (/\b(writ|doc|readme|comment|markdown|copy)\b/.test(text))
|
|
157
|
-
return 'writer';
|
|
158
|
-
if (/\b(analy|research|investigat|explor|study|benchmark)\b/.test(text))
|
|
159
|
-
return 'analyst';
|
|
160
|
-
if (/\b(deploy|ci|cd|devops|infra|docker|k8s|config|setup|install|pipeline)\b/.test(text))
|
|
161
|
-
return 'ops';
|
|
162
|
-
if (/\b(build|implement|creat|develop|code|program|engineer|construct|make|add)\b/.test(text))
|
|
163
|
-
return 'builder';
|
|
164
|
-
return 'general';
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Derive a fictional character name for a subagent based on its role.
|
|
168
|
-
*/
|
|
169
|
-
function deriveAgentDisplayName(team, input) {
|
|
170
|
-
const category = classifyRole(input);
|
|
171
|
-
return pickCharacter(team, category);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Derive a human-readable task title from the Agent tool input.
|
|
175
|
-
* Used on the Kanban board to describe what the agent is working on.
|
|
176
|
-
*/
|
|
177
|
-
function deriveTaskTitle(input) {
|
|
178
|
-
// Short description → use directly
|
|
179
|
-
if (input.description && input.description.length <= 80) {
|
|
180
|
-
return input.description;
|
|
181
|
-
}
|
|
182
|
-
// Colon-prefixed description → use the full thing if ≤ 80, otherwise prefix
|
|
183
|
-
if (input.description) {
|
|
184
|
-
if (input.description.length <= 80)
|
|
185
|
-
return input.description;
|
|
186
|
-
const colonIdx = input.description.indexOf(':');
|
|
187
|
-
if (colonIdx > 0 && colonIdx <= 40) {
|
|
188
|
-
return input.description.slice(0, colonIdx).trim();
|
|
189
|
-
}
|
|
190
|
-
return input.description.slice(0, 77) + '...';
|
|
191
|
-
}
|
|
192
|
-
// Descriptive input.name (not a generic ID)
|
|
193
|
-
if (input.name && !/^(worker|agent|hypothesis)-\d+$/i.test(input.name)) {
|
|
194
|
-
return input.name;
|
|
195
|
-
}
|
|
196
|
-
// Extract from prompt
|
|
197
|
-
if (input.prompt) {
|
|
198
|
-
const first = input.prompt.split('\n')[0].trim();
|
|
199
|
-
if (first.length <= 80)
|
|
200
|
-
return first;
|
|
201
|
-
return first.slice(0, 77) + '...';
|
|
202
|
-
}
|
|
203
|
-
return input.name || 'Task';
|
|
204
|
-
}
|
|
205
93
|
/**
|
|
206
94
|
* Register a subagent when Lead calls the Agent tool.
|
|
207
95
|
*/
|
|
@@ -331,325 +219,73 @@ export function allSubagentsDone(team) {
|
|
|
331
219
|
return subagents.every(a => a.status === 'done' || a.status === 'error');
|
|
332
220
|
}
|
|
333
221
|
/**
|
|
334
|
-
*
|
|
222
|
+
* Wrapper for flushPendingPersists that passes activeTeam.
|
|
223
|
+
*/
|
|
224
|
+
export function flushPendingPersists() {
|
|
225
|
+
_flushPendingPersists(activeTeam);
|
|
226
|
+
}
|
|
227
|
+
// ── Shared helpers (deduplicated) ────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Emit agent status + task update + feed entry to web client.
|
|
230
|
+
* Consolidates the 3-message broadcast pattern used in multiple places.
|
|
335
231
|
*/
|
|
336
|
-
|
|
337
|
-
|
|
232
|
+
function emitAgentUpdate(team, agent, feedType, feedMessage) {
|
|
233
|
+
sendFn({
|
|
234
|
+
type: 'team_agent_status',
|
|
338
235
|
teamId: team.teamId,
|
|
339
|
-
|
|
340
|
-
config: team.config,
|
|
341
|
-
conversationId: team.conversationId,
|
|
342
|
-
claudeSessionId: team.claudeSessionId,
|
|
343
|
-
agents: [...team.agents.entries()].map(([, agent]) => ({
|
|
236
|
+
agent: {
|
|
344
237
|
id: agent.role.id,
|
|
345
238
|
name: agent.role.name,
|
|
346
239
|
color: agent.role.color,
|
|
347
|
-
toolUseId: agent.toolUseId,
|
|
348
|
-
agentTaskId: agent.agentTaskId,
|
|
349
240
|
status: agent.status,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
durationMs: team.durationMs,
|
|
360
|
-
createdAt: team.createdAt,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
// ── Persistence ──────────────────────────────────────────────────────────
|
|
364
|
-
function ensureTeamsDir() {
|
|
365
|
-
if (!existsSync(TEAMS_DIR)) {
|
|
366
|
-
mkdirSync(TEAMS_DIR, { recursive: true });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Deserialize a TeamStateSerialized back into a live TeamState.
|
|
371
|
-
*/
|
|
372
|
-
function deserializeTeam(data) {
|
|
373
|
-
const agents = new Map();
|
|
374
|
-
for (const a of data.agents) {
|
|
375
|
-
agents.set(a.id, {
|
|
376
|
-
role: { id: a.id, name: a.name, color: a.color },
|
|
377
|
-
toolUseId: a.toolUseId,
|
|
378
|
-
agentTaskId: a.agentTaskId,
|
|
379
|
-
status: a.status,
|
|
380
|
-
currentTaskId: a.currentTaskId,
|
|
381
|
-
messages: a.messages || [],
|
|
241
|
+
taskId: agent.currentTaskId,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
245
|
+
if (task) {
|
|
246
|
+
sendFn({
|
|
247
|
+
type: 'team_task_update',
|
|
248
|
+
teamId: team.teamId,
|
|
249
|
+
task,
|
|
382
250
|
});
|
|
383
251
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
agents,
|
|
391
|
-
tasks: data.tasks,
|
|
392
|
-
feed: data.feed,
|
|
393
|
-
status: data.status,
|
|
394
|
-
leadStatus: data.leadStatus || '',
|
|
395
|
-
summary: data.summary,
|
|
396
|
-
totalCost: data.totalCost,
|
|
397
|
-
durationMs: data.durationMs,
|
|
398
|
-
createdAt: data.createdAt,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Persist team state to disk (atomic write: tmp → rename).
|
|
403
|
-
*/
|
|
404
|
-
export function persistTeam(team) {
|
|
405
|
-
ensureTeamsDir();
|
|
406
|
-
const serialized = serializeTeam(team, true);
|
|
407
|
-
const filePath = join(TEAMS_DIR, `${team.teamId}.json`);
|
|
408
|
-
const tmpPath = filePath + '.tmp';
|
|
409
|
-
writeFileSync(tmpPath, JSON.stringify(serialized, null, 2), 'utf-8');
|
|
410
|
-
renameSync(tmpPath, filePath);
|
|
411
|
-
}
|
|
412
|
-
// Debounce timers for persist calls per team
|
|
413
|
-
const persistTimers = new Map();
|
|
414
|
-
/**
|
|
415
|
-
* Debounced persist — coalesces rapid state changes into a single write.
|
|
416
|
-
* Flushes after 500ms of quiet.
|
|
417
|
-
*/
|
|
418
|
-
export function persistTeamDebounced(team) {
|
|
419
|
-
const existing = persistTimers.get(team.teamId);
|
|
420
|
-
if (existing)
|
|
421
|
-
clearTimeout(existing);
|
|
422
|
-
persistTimers.set(team.teamId, setTimeout(() => {
|
|
423
|
-
persistTimers.delete(team.teamId);
|
|
424
|
-
persistTeam(team);
|
|
425
|
-
}, 500));
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Flush all pending debounced persists immediately.
|
|
429
|
-
*/
|
|
430
|
-
export function flushPendingPersists() {
|
|
431
|
-
for (const [teamId, timer] of persistTimers.entries()) {
|
|
432
|
-
clearTimeout(timer);
|
|
433
|
-
persistTimers.delete(teamId);
|
|
434
|
-
if (activeTeam?.teamId === teamId) {
|
|
435
|
-
persistTeam(activeTeam);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Load a team from disk by teamId.
|
|
441
|
-
*/
|
|
442
|
-
export function loadTeam(teamId) {
|
|
443
|
-
const filePath = join(TEAMS_DIR, `${teamId}.json`);
|
|
444
|
-
try {
|
|
445
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
446
|
-
const data = JSON.parse(raw);
|
|
447
|
-
return deserializeTeam(data);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
252
|
+
addFeedEntry(team, agent.role.id, feedType, feedMessage);
|
|
253
|
+
sendFn({
|
|
254
|
+
type: 'team_feed',
|
|
255
|
+
teamId: team.teamId,
|
|
256
|
+
entry: team.feed[team.feed.length - 1],
|
|
257
|
+
});
|
|
452
258
|
}
|
|
453
259
|
/**
|
|
454
|
-
*
|
|
260
|
+
* Mark all active agents and tasks to a final status.
|
|
261
|
+
* Used by both dissolveTeam and completeTeam.
|
|
455
262
|
*/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const raw = readFileSync(join(TEAMS_DIR, file), 'utf-8');
|
|
463
|
-
const data = JSON.parse(raw);
|
|
464
|
-
teams.push({
|
|
465
|
-
teamId: data.teamId,
|
|
466
|
-
title: data.title,
|
|
467
|
-
status: data.status,
|
|
468
|
-
template: data.config.template,
|
|
469
|
-
agentCount: data.agents.length,
|
|
470
|
-
taskCount: data.tasks.length,
|
|
471
|
-
totalCost: data.totalCost,
|
|
472
|
-
createdAt: data.createdAt,
|
|
473
|
-
});
|
|
263
|
+
function finalizeAgentsAndTasks(team, agentStatus, taskStatus) {
|
|
264
|
+
for (const agent of team.agents.values()) {
|
|
265
|
+
if (agentStatus === 'done') {
|
|
266
|
+
if (agent.status !== 'error')
|
|
267
|
+
agent.status = 'done';
|
|
474
268
|
}
|
|
475
|
-
|
|
476
|
-
|
|
269
|
+
else {
|
|
270
|
+
if (agent.status === 'starting' || agent.status === 'working')
|
|
271
|
+
agent.status = 'error';
|
|
477
272
|
}
|
|
478
273
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
*/
|
|
485
|
-
export function deleteTeam(teamId) {
|
|
486
|
-
const filePath = join(TEAMS_DIR, `${teamId}.json`);
|
|
487
|
-
try {
|
|
488
|
-
unlinkSync(filePath);
|
|
489
|
-
return true;
|
|
490
|
-
}
|
|
491
|
-
catch {
|
|
492
|
-
return false;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
export function renameTeam(teamId, newTitle) {
|
|
496
|
-
const filePath = join(TEAMS_DIR, `${teamId}.json`);
|
|
497
|
-
try {
|
|
498
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
499
|
-
const data = JSON.parse(raw);
|
|
500
|
-
data.title = newTitle;
|
|
501
|
-
const tmpPath = filePath + '.tmp';
|
|
502
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
503
|
-
renameSync(tmpPath, filePath);
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
return false;
|
|
274
|
+
for (const task of team.tasks) {
|
|
275
|
+
if (task.status === 'pending' || task.status === 'active') {
|
|
276
|
+
task.status = taskStatus;
|
|
277
|
+
task.updatedAt = Date.now();
|
|
278
|
+
}
|
|
508
279
|
}
|
|
509
280
|
}
|
|
510
|
-
const TEMPLATE_AGENTS = {
|
|
511
|
-
'code-review': {
|
|
512
|
-
'security-reviewer': {
|
|
513
|
-
description: 'Security expert focused on cryptographic, auth, and injection vulnerabilities',
|
|
514
|
-
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.',
|
|
515
|
-
tools: ['Read', 'Grep', 'Glob'],
|
|
516
|
-
},
|
|
517
|
-
'quality-reviewer': {
|
|
518
|
-
description: 'Code quality expert focused on maintainability, patterns, and best practices',
|
|
519
|
-
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.',
|
|
520
|
-
tools: ['Read', 'Grep', 'Glob'],
|
|
521
|
-
},
|
|
522
|
-
'performance-reviewer': {
|
|
523
|
-
description: 'Performance expert focused on efficiency, resource usage, and scalability',
|
|
524
|
-
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.',
|
|
525
|
-
tools: ['Read', 'Grep', 'Glob'],
|
|
526
|
-
},
|
|
527
|
-
},
|
|
528
|
-
'full-stack': {
|
|
529
|
-
'backend-dev': {
|
|
530
|
-
description: 'Backend developer for API endpoints, database, and server-side logic',
|
|
531
|
-
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.',
|
|
532
|
-
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
533
|
-
},
|
|
534
|
-
'frontend-dev': {
|
|
535
|
-
description: 'Frontend developer for UI components, styling, and client-side logic',
|
|
536
|
-
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.',
|
|
537
|
-
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
538
|
-
},
|
|
539
|
-
'test-engineer': {
|
|
540
|
-
description: 'Test engineer for unit tests, integration tests, and quality assurance',
|
|
541
|
-
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.',
|
|
542
|
-
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
543
|
-
},
|
|
544
|
-
},
|
|
545
|
-
'debug': {
|
|
546
|
-
'hypothesis-a': {
|
|
547
|
-
description: 'Debug investigator exploring the first hypothesis',
|
|
548
|
-
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.',
|
|
549
|
-
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
550
|
-
},
|
|
551
|
-
'hypothesis-b': {
|
|
552
|
-
description: 'Debug investigator exploring an alternative hypothesis',
|
|
553
|
-
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.',
|
|
554
|
-
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
555
|
-
},
|
|
556
|
-
'hypothesis-c': {
|
|
557
|
-
description: 'Debug investigator exploring a third hypothesis',
|
|
558
|
-
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.',
|
|
559
|
-
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
'custom': {
|
|
563
|
-
'worker-1': {
|
|
564
|
-
description: 'General-purpose development agent',
|
|
565
|
-
prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
|
|
566
|
-
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
567
|
-
},
|
|
568
|
-
'worker-2': {
|
|
569
|
-
description: 'General-purpose development agent',
|
|
570
|
-
prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
|
|
571
|
-
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
572
|
-
},
|
|
573
|
-
'worker-3': {
|
|
574
|
-
description: 'General-purpose development agent',
|
|
575
|
-
prompt: 'You are a skilled software engineer. Complete the assigned task thoroughly and report your results.',
|
|
576
|
-
tools: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
577
|
-
},
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
const TEMPLATE_LEAD_INSTRUCTIONS = {
|
|
581
|
-
'code-review': `You are a team lead coordinating a code review.
|
|
582
|
-
|
|
583
|
-
Instructions:
|
|
584
|
-
1. First, analyze the codebase to understand its structure and what needs reviewing
|
|
585
|
-
2. Use the Agent tool to spawn each reviewer IN PARALLEL (multiple Agent calls simultaneously)
|
|
586
|
-
3. Give each reviewer specific, detailed instructions referencing exact files and directories to review
|
|
587
|
-
4. After all reviewers complete, synthesize their findings into a unified summary with prioritized action items
|
|
588
|
-
|
|
589
|
-
Important:
|
|
590
|
-
- 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.
|
|
591
|
-
- Spawn agents in parallel for efficiency. Each agent should focus on their specialty area.`,
|
|
592
|
-
'full-stack': `You are a team lead coordinating full-stack development.
|
|
593
|
-
|
|
594
|
-
Instructions:
|
|
595
|
-
1. First, analyze the codebase to understand the architecture, existing patterns, and what needs building
|
|
596
|
-
2. Break the task into backend, frontend, and test subtasks, and analyze dependencies between them
|
|
597
|
-
3. Define clear interfaces, API contracts, and data schemas before spawning any agents
|
|
598
|
-
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
|
|
599
|
-
5. Provide each agent with specific, detailed instructions including file paths and shared contracts
|
|
600
|
-
6. After all agents complete, review their work and provide a summary of what was built
|
|
601
|
-
|
|
602
|
-
Important:
|
|
603
|
-
- 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.
|
|
604
|
-
- Maximize parallelism for truly independent tasks, but respect dependencies. Do not spawn all agents simultaneously if some need others' output first.`,
|
|
605
|
-
'debug': `You are a team lead coordinating a debugging investigation.
|
|
606
|
-
|
|
607
|
-
Instructions:
|
|
608
|
-
1. First, analyze the bug report and relevant code to understand the problem space
|
|
609
|
-
2. Formulate 3 distinct hypotheses about the root cause
|
|
610
|
-
3. Use the Agent tool to assign each hypothesis to a different investigator IN PARALLEL
|
|
611
|
-
4. Give each investigator specific areas of code to examine and tests to run
|
|
612
|
-
5. After all investigators complete, compare their findings and synthesize a diagnosis with a recommended fix
|
|
613
|
-
|
|
614
|
-
Important:
|
|
615
|
-
- 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.
|
|
616
|
-
- Each investigator should explore a DIFFERENT hypothesis. Avoid overlap.`,
|
|
617
|
-
'custom': `You are a team lead coordinating a development task.
|
|
618
|
-
|
|
619
|
-
Instructions:
|
|
620
|
-
1. First, analyze the codebase and the user's request to understand what needs to be done
|
|
621
|
-
2. Break the task into subtasks and analyze dependencies between them
|
|
622
|
-
3. Spawn independent tasks IN PARALLEL using the Agent tool for efficiency
|
|
623
|
-
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
|
|
624
|
-
5. Give each worker specific, detailed instructions
|
|
625
|
-
6. After all workers complete, review their work and provide a summary
|
|
626
|
-
|
|
627
|
-
Important:
|
|
628
|
-
- 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.
|
|
629
|
-
- 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.`,
|
|
630
|
-
};
|
|
631
281
|
/**
|
|
632
|
-
*
|
|
282
|
+
* Remove output and close observers.
|
|
633
283
|
*/
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
* Build the lead prompt that instructs the Lead to use Agent tool.
|
|
640
|
-
*/
|
|
641
|
-
export function buildLeadPrompt(config, agentsDef) {
|
|
642
|
-
const template = config.template || 'custom';
|
|
643
|
-
const instructions = TEMPLATE_LEAD_INSTRUCTIONS[template] || TEMPLATE_LEAD_INSTRUCTIONS['custom'];
|
|
644
|
-
const agentList = Object.entries(agentsDef)
|
|
645
|
-
.map(([id, def]) => `- ${id}: ${def.description}`)
|
|
646
|
-
.join('\n');
|
|
647
|
-
return `${instructions}
|
|
648
|
-
|
|
649
|
-
Available agents (use the Agent tool to delegate to them):
|
|
650
|
-
${agentList}
|
|
651
|
-
|
|
652
|
-
User's request: "${config.instruction}"`;
|
|
284
|
+
function cleanupObservers() {
|
|
285
|
+
if (clearOutputObserverFn)
|
|
286
|
+
clearOutputObserverFn();
|
|
287
|
+
if (clearCloseObserverFn)
|
|
288
|
+
clearCloseObserverFn();
|
|
653
289
|
}
|
|
654
290
|
// ── Output stream parser (observer callback) ────────────────────────────
|
|
655
291
|
/**
|
|
@@ -711,32 +347,7 @@ export function onLeadOutput(conversationId, msg) {
|
|
|
711
347
|
const input = (block.input || {});
|
|
712
348
|
const toolUseId = block.id;
|
|
713
349
|
const agent = registerSubagent(team, toolUseId, input);
|
|
714
|
-
|
|
715
|
-
sendFn({
|
|
716
|
-
type: 'team_agent_status',
|
|
717
|
-
teamId: team.teamId,
|
|
718
|
-
agent: {
|
|
719
|
-
id: agent.role.id,
|
|
720
|
-
name: agent.role.name,
|
|
721
|
-
color: agent.role.color,
|
|
722
|
-
status: agent.status,
|
|
723
|
-
taskId: agent.currentTaskId,
|
|
724
|
-
},
|
|
725
|
-
});
|
|
726
|
-
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
727
|
-
if (task) {
|
|
728
|
-
sendFn({
|
|
729
|
-
type: 'team_task_update',
|
|
730
|
-
teamId: team.teamId,
|
|
731
|
-
task,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
addFeedEntry(team, agent.role.id, 'status_change', `${agent.role.name} has joined and is getting ready`);
|
|
735
|
-
sendFn({
|
|
736
|
-
type: 'team_feed',
|
|
737
|
-
teamId: team.teamId,
|
|
738
|
-
entry: team.feed[team.feed.length - 1],
|
|
739
|
-
});
|
|
350
|
+
emitAgentUpdate(team, agent, 'status_change', `${agent.role.name} has joined and is getting ready`);
|
|
740
351
|
}
|
|
741
352
|
}
|
|
742
353
|
}
|
|
@@ -752,31 +363,7 @@ export function onLeadOutput(conversationId, msg) {
|
|
|
752
363
|
const taskId = msg.task_id;
|
|
753
364
|
const agent = linkSubagentTaskId(team, toolUseId, taskId);
|
|
754
365
|
if (agent) {
|
|
755
|
-
|
|
756
|
-
sendFn({
|
|
757
|
-
type: 'team_agent_status',
|
|
758
|
-
teamId: team.teamId,
|
|
759
|
-
agent: {
|
|
760
|
-
id: agent.role.id,
|
|
761
|
-
name: agent.role.name,
|
|
762
|
-
color: agent.role.color,
|
|
763
|
-
status: agent.status,
|
|
764
|
-
taskId: agent.currentTaskId,
|
|
765
|
-
},
|
|
766
|
-
});
|
|
767
|
-
sendFn({
|
|
768
|
-
type: 'team_feed',
|
|
769
|
-
teamId: team.teamId,
|
|
770
|
-
entry: team.feed[team.feed.length - 1],
|
|
771
|
-
});
|
|
772
|
-
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
773
|
-
if (task) {
|
|
774
|
-
sendFn({
|
|
775
|
-
type: 'team_task_update',
|
|
776
|
-
teamId: team.teamId,
|
|
777
|
-
task,
|
|
778
|
-
});
|
|
779
|
-
}
|
|
366
|
+
emitAgentUpdate(team, agent, 'task_started', `${agent.role.name} started working on the task`);
|
|
780
367
|
}
|
|
781
368
|
return true; // suppress system.task_started from normal forwarding
|
|
782
369
|
}
|
|
@@ -802,31 +389,7 @@ export function onLeadOutput(conversationId, msg) {
|
|
|
802
389
|
if (agent.currentTaskId) {
|
|
803
390
|
updateTaskStatus(team, agent.currentTaskId, isError ? 'failed' : 'done');
|
|
804
391
|
}
|
|
805
|
-
|
|
806
|
-
sendFn({
|
|
807
|
-
type: 'team_agent_status',
|
|
808
|
-
teamId: team.teamId,
|
|
809
|
-
agent: {
|
|
810
|
-
id: agent.role.id,
|
|
811
|
-
name: agent.role.name,
|
|
812
|
-
color: agent.role.color,
|
|
813
|
-
status: agent.status,
|
|
814
|
-
taskId: agent.currentTaskId,
|
|
815
|
-
},
|
|
816
|
-
});
|
|
817
|
-
const task = team.tasks.find(t => t.assignee === agent.role.id);
|
|
818
|
-
if (task) {
|
|
819
|
-
sendFn({
|
|
820
|
-
type: 'team_task_update',
|
|
821
|
-
teamId: team.teamId,
|
|
822
|
-
task,
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
sendFn({
|
|
826
|
-
type: 'team_feed',
|
|
827
|
-
teamId: team.teamId,
|
|
828
|
-
entry: team.feed[team.feed.length - 1],
|
|
829
|
-
});
|
|
392
|
+
emitAgentUpdate(team, agent, isError ? 'task_failed' : 'task_completed', isError ? `${agent.role.name} ran into an issue and stopped` : `${agent.role.name} finished the task successfully`);
|
|
830
393
|
// Check if all subagents are done → transition to summarizing
|
|
831
394
|
if (allSubagentsDone(team) && team.status === 'running') {
|
|
832
395
|
team.status = 'summarizing';
|
|
@@ -866,27 +429,11 @@ export function onLeadClose(conversationId, _exitCode, resultReceived) {
|
|
|
866
429
|
// Lead process crashed without producing a result — dissolve the team
|
|
867
430
|
console.log(`[Team] Lead process exited without result — marking team as failed`);
|
|
868
431
|
const team = activeTeam;
|
|
869
|
-
|
|
870
|
-
for (const agent of team.agents.values()) {
|
|
871
|
-
if (agent.status === 'starting' || agent.status === 'working') {
|
|
872
|
-
agent.status = 'error';
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
// Mark remaining tasks as failed
|
|
876
|
-
for (const task of team.tasks) {
|
|
877
|
-
if (task.status === 'pending' || task.status === 'active') {
|
|
878
|
-
task.status = 'failed';
|
|
879
|
-
task.updatedAt = Date.now();
|
|
880
|
-
}
|
|
881
|
-
}
|
|
432
|
+
finalizeAgentsAndTasks(team, 'error', 'failed');
|
|
882
433
|
team.status = 'failed';
|
|
883
434
|
team.durationMs = Date.now() - team.createdAt;
|
|
884
435
|
persistTeam(team);
|
|
885
|
-
|
|
886
|
-
if (clearOutputObserverFn)
|
|
887
|
-
clearOutputObserverFn();
|
|
888
|
-
if (clearCloseObserverFn)
|
|
889
|
-
clearCloseObserverFn();
|
|
436
|
+
cleanupObservers();
|
|
890
437
|
// Notify clients
|
|
891
438
|
sendFn({
|
|
892
439
|
type: 'team_completed',
|
|
@@ -1062,29 +609,10 @@ export function dissolveTeam() {
|
|
|
1062
609
|
if (cancelExecutionFn) {
|
|
1063
610
|
cancelExecutionFn(team.conversationId);
|
|
1064
611
|
}
|
|
1065
|
-
|
|
1066
|
-
for (const agent of team.agents.values()) {
|
|
1067
|
-
if (agent.status === 'starting' || agent.status === 'working') {
|
|
1068
|
-
agent.status = 'error';
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
// Mark remaining tasks as failed
|
|
1072
|
-
for (const task of team.tasks) {
|
|
1073
|
-
if (task.status === 'pending' || task.status === 'active') {
|
|
1074
|
-
task.status = 'failed';
|
|
1075
|
-
task.updatedAt = Date.now();
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
612
|
+
finalizeAgentsAndTasks(team, 'error', 'failed');
|
|
1078
613
|
team.status = 'failed';
|
|
1079
614
|
persistTeam(team);
|
|
1080
|
-
|
|
1081
|
-
if (clearOutputObserverFn) {
|
|
1082
|
-
clearOutputObserverFn();
|
|
1083
|
-
}
|
|
1084
|
-
// Remove close observer
|
|
1085
|
-
if (clearCloseObserverFn) {
|
|
1086
|
-
clearCloseObserverFn();
|
|
1087
|
-
}
|
|
615
|
+
cleanupObservers();
|
|
1088
616
|
// Notify clients
|
|
1089
617
|
sendFn({
|
|
1090
618
|
type: 'team_completed',
|
|
@@ -1106,28 +634,9 @@ export function completeTeam(summary) {
|
|
|
1106
634
|
if (summary) {
|
|
1107
635
|
team.summary = summary;
|
|
1108
636
|
}
|
|
1109
|
-
|
|
1110
|
-
for (const agent of team.agents.values()) {
|
|
1111
|
-
if (agent.status !== 'error') {
|
|
1112
|
-
agent.status = 'done';
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
// Mark remaining active/pending tasks as done
|
|
1116
|
-
for (const task of team.tasks) {
|
|
1117
|
-
if (task.status === 'active' || task.status === 'pending') {
|
|
1118
|
-
task.status = 'done';
|
|
1119
|
-
task.updatedAt = Date.now();
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
637
|
+
finalizeAgentsAndTasks(team, 'done', 'done');
|
|
1122
638
|
persistTeam(team);
|
|
1123
|
-
|
|
1124
|
-
if (clearOutputObserverFn) {
|
|
1125
|
-
clearOutputObserverFn();
|
|
1126
|
-
}
|
|
1127
|
-
// Remove close observer
|
|
1128
|
-
if (clearCloseObserverFn) {
|
|
1129
|
-
clearCloseObserverFn();
|
|
1130
|
-
}
|
|
639
|
+
cleanupObservers();
|
|
1131
640
|
// Notify clients
|
|
1132
641
|
sendFn({
|
|
1133
642
|
type: 'team_completed',
|