@nanmicoder/dsh-agent-teams 0.1.5 → 0.1.6
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/README.md +6 -5
- package/README_ZH.md +6 -5
- package/lib/client/ActivityPanel.js +93 -38
- package/lib/client/activity-model.js +63 -0
- package/lib/client.js +478 -367
- package/lib/client.js.map +1 -1
- package/lib/index.js +6 -4
- package/lib/members.js +73 -10
- package/lib/scheduler.js +212 -0
- package/lib/snapshot.js +32 -19
- package/lib/state.js +174 -3
- package/lib/tools.js +287 -29
- package/lib/types/client/ActivityPanel.d.ts +1 -0
- package/lib/types/client/activity-model.d.ts +34 -0
- package/lib/types/event-types.d.ts +2 -0
- package/lib/types/members.d.ts +17 -4
- package/lib/types/scheduler.d.ts +23 -0
- package/lib/types/snapshot.d.ts +10 -2
- package/lib/types/state.d.ts +24 -0
- package/lib/types/types.d.ts +15 -1
- package/package.json +2 -2
package/lib/tools.js
CHANGED
|
@@ -12,8 +12,10 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
|
12
12
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { appendTeamEvent, captainSessionOf } from "./events.js";
|
|
15
|
-
import { appendMailbox, archiveTeamDir, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant,
|
|
16
|
-
import { deliverToMember, installMemberSelectionRuntime, interruptMember, memberActivity, resolveMemberLlmSelection, spawnMember, } from "./members.js";
|
|
15
|
+
import { acknowledgeMailbox, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, } from "./state.js";
|
|
16
|
+
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, interruptMember, memberActivity, resolveMemberLlmSelection, spawnMember, } from "./members.js";
|
|
17
|
+
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
18
|
+
import { installTeamScheduler } from "./scheduler.js";
|
|
17
19
|
/** The caller agent, or a loud failure for non-agent callers. */
|
|
18
20
|
function requireCaptain(exec) {
|
|
19
21
|
if (!exec.agent) {
|
|
@@ -99,6 +101,31 @@ function requireTask(team, taskId) {
|
|
|
99
101
|
}
|
|
100
102
|
return task;
|
|
101
103
|
}
|
|
104
|
+
function memberOpenTask(team, memberName, exceptTaskId) {
|
|
105
|
+
return team.tasks.find(task => task.id !== exceptTaskId
|
|
106
|
+
&& task.assignee === memberName
|
|
107
|
+
&& (task.status === 'claimed' || task.status === 'in_progress'));
|
|
108
|
+
}
|
|
109
|
+
async function waitForMemberIdle(ctx, member, signal) {
|
|
110
|
+
if (member.id === '')
|
|
111
|
+
return;
|
|
112
|
+
const live = ctx.agents.get(member.id);
|
|
113
|
+
if (live === undefined)
|
|
114
|
+
return;
|
|
115
|
+
if (signal.aborted)
|
|
116
|
+
throw signal.reason;
|
|
117
|
+
let onAbort;
|
|
118
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
119
|
+
onAbort = () => reject(signal.reason ?? new Error('task reassignment was cancelled'));
|
|
120
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
await Promise.race([live.whenIdle(), aborted]);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
signal.removeEventListener('abort', onAbort);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
102
129
|
/**
|
|
103
130
|
* Deliver a durable member report at the captain's nearest model boundary.
|
|
104
131
|
*
|
|
@@ -126,7 +153,9 @@ export function steerCaptainReport(captain, from, content) {
|
|
|
126
153
|
* @param config - resolved tool config.
|
|
127
154
|
*/
|
|
128
155
|
export function registerAgentTeamsTools(ctx, config) {
|
|
156
|
+
installRetiredMemberGuard(ctx, config.stateDir);
|
|
129
157
|
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir);
|
|
158
|
+
const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir });
|
|
130
159
|
ctx.tools.register(defineTool({
|
|
131
160
|
name: 'agent_teams_create',
|
|
132
161
|
description: 'Create a new AgentTeams team: you (the calling agent) become the captain. A captain leads one team at a time; create tasks and members afterwards with agent_teams_add_member and agent_teams_create_task.',
|
|
@@ -222,7 +251,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
222
251
|
const workspace = workspaceOf(captain);
|
|
223
252
|
const stateRoot = stateRootOf(workspace, config);
|
|
224
253
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
225
|
-
|
|
254
|
+
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
226
255
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
227
256
|
const memberName = args.name.trim();
|
|
228
257
|
if (memberName === '')
|
|
@@ -272,11 +301,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
272
301
|
status: member.status,
|
|
273
302
|
};
|
|
274
303
|
});
|
|
304
|
+
await scheduler.kickMember(workspace, team.id, created.member_name, captain);
|
|
305
|
+
return created;
|
|
275
306
|
},
|
|
276
307
|
}));
|
|
277
308
|
ctx.tools.register(defineTool({
|
|
278
309
|
name: 'agent_teams_remove_member',
|
|
279
|
-
description: 'Remove a member
|
|
310
|
+
description: 'Remove a member safely: revoke its current attempts, return all unfinished owned tasks to the shared pending pool, interrupt its live turn, and mark it removed.',
|
|
280
311
|
parameters: {
|
|
281
312
|
name: { type: 'string', required: true, description: 'Name of the member to remove.' },
|
|
282
313
|
},
|
|
@@ -287,11 +318,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
287
318
|
properties: {
|
|
288
319
|
member_name: { type: 'string', required: true },
|
|
289
320
|
status: { type: 'string', required: true },
|
|
321
|
+
requeued_tasks: { type: 'array', items: { type: 'string' }, required: true },
|
|
290
322
|
},
|
|
291
323
|
},
|
|
292
324
|
render: (args, value) => [{
|
|
293
325
|
type: 'text',
|
|
294
|
-
text: `Member "${value.member_name}" removed (status ${value.status}).`,
|
|
326
|
+
text: `Member "${value.member_name}" removed (status ${value.status}); requeued tasks: ${value.requeued_tasks.join(', ') || 'none'}.`,
|
|
295
327
|
}],
|
|
296
328
|
},
|
|
297
329
|
async execute(args, exec) {
|
|
@@ -299,19 +331,36 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
299
331
|
const workspace = workspaceOf(captain);
|
|
300
332
|
const stateRoot = stateRootOf(workspace, config);
|
|
301
333
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
302
|
-
|
|
334
|
+
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
303
335
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
304
336
|
const member = requireMember(fresh, args.name);
|
|
305
|
-
|
|
306
|
-
|
|
337
|
+
const requeued = [];
|
|
338
|
+
for (const task of fresh.tasks) {
|
|
339
|
+
if (task.assignee !== member.name || task.status === 'completed')
|
|
340
|
+
continue;
|
|
341
|
+
invalidateTaskAttempt(task);
|
|
342
|
+
task.reassigning = false;
|
|
343
|
+
requeued.push(task.id);
|
|
344
|
+
}
|
|
307
345
|
member.status = 'removed';
|
|
308
346
|
await writeTeam(stateRoot, fresh);
|
|
309
347
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/member-removed', {
|
|
310
348
|
teamId: fresh.id,
|
|
311
349
|
memberId: member.id,
|
|
312
350
|
});
|
|
313
|
-
return {
|
|
351
|
+
return { member: { ...member }, requeued };
|
|
314
352
|
});
|
|
353
|
+
if (revoked.member.id !== '') {
|
|
354
|
+
await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
|
|
355
|
+
interruptMember(ctx, captain, revoked.member.id);
|
|
356
|
+
await waitForMemberIdle(ctx, revoked.member, exec.signal);
|
|
357
|
+
}
|
|
358
|
+
await scheduler.kickTeam(workspace, team.id, captain);
|
|
359
|
+
return {
|
|
360
|
+
member_name: revoked.member.name,
|
|
361
|
+
status: revoked.member.status,
|
|
362
|
+
requeued_tasks: revoked.requeued,
|
|
363
|
+
};
|
|
315
364
|
},
|
|
316
365
|
}));
|
|
317
366
|
ctx.tools.register(defineTool({
|
|
@@ -348,7 +397,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
348
397
|
const workspace = workspaceOf(captain);
|
|
349
398
|
const stateRoot = stateRootOf(workspace, config);
|
|
350
399
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
351
|
-
|
|
400
|
+
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
352
401
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
353
402
|
const dependencies = args.dependencies ?? [];
|
|
354
403
|
for (const dependency of dependencies) {
|
|
@@ -365,6 +414,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
365
414
|
status: 'pending',
|
|
366
415
|
assignee: args.assignee,
|
|
367
416
|
dependencies,
|
|
417
|
+
attempt: 0,
|
|
368
418
|
createdAt: Date.now(),
|
|
369
419
|
updatedAt: Date.now(),
|
|
370
420
|
};
|
|
@@ -385,11 +435,120 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
385
435
|
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
386
436
|
};
|
|
387
437
|
});
|
|
438
|
+
await scheduler.kickTeam(workspace, team.id, captain);
|
|
439
|
+
return created;
|
|
440
|
+
},
|
|
441
|
+
}));
|
|
442
|
+
ctx.tools.register(defineTool({
|
|
443
|
+
name: 'agent_teams_reassign_task',
|
|
444
|
+
description: 'Atomically retry, reassign, or let the captain take over any unfinished/failed task. The old attempt is revoked before its member is interrupted, so late updates cannot overwrite the new owner. Use assignee="captain" for captain takeover.',
|
|
445
|
+
parameters: {
|
|
446
|
+
task_id: { type: 'string', required: true, description: 'Task to retry/reassign.' },
|
|
447
|
+
assignee: { type: 'string', required: true, description: 'Active member name, or "captain" for captain takeover.' },
|
|
448
|
+
reason: { type: 'string', description: 'Why the task is being retried or reassigned.' },
|
|
449
|
+
},
|
|
450
|
+
output: {
|
|
451
|
+
schema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
additionalProperties: false,
|
|
454
|
+
properties: {
|
|
455
|
+
task_id: { type: 'string', required: true },
|
|
456
|
+
previous_assignee: { type: 'string', required: true },
|
|
457
|
+
assignee: { type: 'string', required: true },
|
|
458
|
+
status: { type: 'string', required: true },
|
|
459
|
+
attempt: { type: 'number', required: true },
|
|
460
|
+
attempt_id: { type: 'string' },
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
render: (_args, value) => [{
|
|
464
|
+
type: 'text',
|
|
465
|
+
text: `Task ${value.task_id} reassigned ${value.previous_assignee || 'unassigned'} → ${value.assignee} (attempt ${value.attempt}, status ${value.status}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}).`,
|
|
466
|
+
}],
|
|
467
|
+
},
|
|
468
|
+
async execute(args, exec) {
|
|
469
|
+
const captain = requireCaptain(exec);
|
|
470
|
+
const workspace = workspaceOf(captain);
|
|
471
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
472
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
473
|
+
const target = args.assignee.trim();
|
|
474
|
+
if (target === '')
|
|
475
|
+
throw new Error('reassignment assignee must not be empty');
|
|
476
|
+
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
477
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
478
|
+
const task = requireTask(fresh, args.task_id);
|
|
479
|
+
if (task.status === 'completed')
|
|
480
|
+
throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
|
|
481
|
+
if (task.reassigning === true)
|
|
482
|
+
throw new Error(`task ${task.id} is already being reassigned`);
|
|
483
|
+
const targetMember = target === CAPTAIN_KEY ? undefined : requireMember(fresh, target);
|
|
484
|
+
if (targetMember !== undefined) {
|
|
485
|
+
const busy = memberOpenTask(fresh, targetMember.name, task.id);
|
|
486
|
+
if (busy !== undefined) {
|
|
487
|
+
throw new Error(`member "${targetMember.name}" is busy with ${busy.id}; finish or reassign it first`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const previousAssignee = task.assignee ?? '';
|
|
491
|
+
const previousMember = (task.status !== 'claimed' && task.status !== 'in_progress')
|
|
492
|
+
|| task.assignee === undefined || task.assignee === CAPTAIN_KEY
|
|
493
|
+
? undefined
|
|
494
|
+
: fresh.members.find(member => member.name === task.assignee && member.status !== 'removed');
|
|
495
|
+
invalidateTaskAttempt(task, target, true);
|
|
496
|
+
await writeTeam(stateRoot, fresh);
|
|
497
|
+
return {
|
|
498
|
+
previousAssignee,
|
|
499
|
+
previousMember: previousMember === undefined ? undefined : { ...previousMember },
|
|
500
|
+
handoffId: task.handoffId,
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
let quiescenceError;
|
|
504
|
+
if (revoked.previousMember !== undefined) {
|
|
505
|
+
interruptMember(ctx, captain, revoked.previousMember.id);
|
|
506
|
+
try {
|
|
507
|
+
await waitForMemberIdle(ctx, revoked.previousMember, exec.signal);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
quiescenceError = error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
514
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
515
|
+
const task = requireTask(fresh, args.task_id);
|
|
516
|
+
if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
|
|
517
|
+
throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
|
|
518
|
+
}
|
|
519
|
+
task.reassigning = false;
|
|
520
|
+
if (quiescenceError === undefined && target === CAPTAIN_KEY)
|
|
521
|
+
beginTaskAttempt(task, CAPTAIN_KEY);
|
|
522
|
+
await writeTeam(stateRoot, fresh);
|
|
523
|
+
appendTeamEvent(ctx, captain.session, 'agent-teams/task-updated', {
|
|
524
|
+
teamId: fresh.id,
|
|
525
|
+
taskId: task.id,
|
|
526
|
+
status: task.status,
|
|
527
|
+
assignee: task.assignee,
|
|
528
|
+
...args.reason === undefined ? {} : { output: `Reassigned: ${args.reason}` },
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
if (quiescenceError !== undefined)
|
|
532
|
+
throw quiescenceError;
|
|
533
|
+
if (target !== CAPTAIN_KEY)
|
|
534
|
+
await scheduler.kickMember(workspace, team.id, target, captain);
|
|
535
|
+
const current = await readTeam(stateRoot, team.id);
|
|
536
|
+
const task = current === undefined ? undefined : requireTask(current, args.task_id);
|
|
537
|
+
if (task === undefined)
|
|
538
|
+
throw new Error(`team "${team.name}" ended during reassignment`);
|
|
539
|
+
return {
|
|
540
|
+
task_id: task.id,
|
|
541
|
+
previous_assignee: revoked.previousAssignee,
|
|
542
|
+
assignee: task.assignee ?? '',
|
|
543
|
+
status: task.status,
|
|
544
|
+
attempt: task.attempt ?? 0,
|
|
545
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
546
|
+
};
|
|
388
547
|
},
|
|
389
548
|
}));
|
|
390
549
|
ctx.tools.register(defineTool({
|
|
391
550
|
name: 'agent_teams_claim_task',
|
|
392
|
-
description: 'Claim
|
|
551
|
+
description: 'Claim one ready task for a member (or yourself). A member cannot own a second unfinished task. The returned attempt_id is required for that member\'s updates and becomes stale after retry/reassignment.',
|
|
393
552
|
parameters: {
|
|
394
553
|
task_id: { type: 'string', required: true, description: 'The task id to claim.' },
|
|
395
554
|
assignee: { type: 'string', description: 'Member to claim for (captain only; defaults to the task\'s assignee).' },
|
|
@@ -402,11 +561,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
402
561
|
task_id: { type: 'string', required: true },
|
|
403
562
|
status: { type: 'string', required: true },
|
|
404
563
|
assignee: { type: 'string', required: true },
|
|
564
|
+
attempt: { type: 'number', required: true },
|
|
565
|
+
attempt_id: { type: 'string' },
|
|
405
566
|
},
|
|
406
567
|
},
|
|
407
568
|
render: (args, value) => [{
|
|
408
569
|
type: 'text',
|
|
409
|
-
text: `Task ${value.task_id} claimed by ${value.assignee} (status ${value.status}).`,
|
|
570
|
+
text: `Task ${value.task_id} claimed by ${value.assignee} (attempt ${value.attempt}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}, status ${value.status}).`,
|
|
410
571
|
}],
|
|
411
572
|
},
|
|
412
573
|
async execute(args, exec) {
|
|
@@ -417,6 +578,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
417
578
|
return withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
418
579
|
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
419
580
|
const task = requireTask(fresh, args.task_id);
|
|
581
|
+
if (task.reassigning === true) {
|
|
582
|
+
throw new Error(`task ${task.id} is being reassigned; wait for the handoff to finish`);
|
|
583
|
+
}
|
|
420
584
|
let assignee = task.assignee;
|
|
421
585
|
if (identity.kind === 'captain') {
|
|
422
586
|
if (args.assignee !== undefined) {
|
|
@@ -439,7 +603,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
439
603
|
if (assignee === undefined || task.assignee !== assignee) {
|
|
440
604
|
throw new Error(`task ${task.id} is already claimed by "${task.assignee ?? 'nobody'}"`);
|
|
441
605
|
}
|
|
442
|
-
return {
|
|
606
|
+
return {
|
|
607
|
+
task_id: task.id,
|
|
608
|
+
status: task.status,
|
|
609
|
+
assignee,
|
|
610
|
+
attempt: task.attempt ?? 0,
|
|
611
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
612
|
+
};
|
|
443
613
|
}
|
|
444
614
|
const pending = unsatisfiedDependencies(fresh.tasks, task.dependencies);
|
|
445
615
|
if (pending.length > 0) {
|
|
@@ -451,9 +621,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
451
621
|
if (assignee === undefined) {
|
|
452
622
|
throw new Error('claiming an unassigned task needs an assignee (claim on behalf of a member)');
|
|
453
623
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
624
|
+
const busy = memberOpenTask(fresh, assignee, task.id);
|
|
625
|
+
if (busy !== undefined) {
|
|
626
|
+
throw new Error(`member "${assignee}" is busy with ${busy.id}; finish or reassign it first`);
|
|
627
|
+
}
|
|
628
|
+
const attemptId = beginTaskAttempt(task, assignee);
|
|
457
629
|
await writeTeam(stateRoot, fresh);
|
|
458
630
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/task-updated', {
|
|
459
631
|
teamId: fresh.id,
|
|
@@ -461,13 +633,19 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
461
633
|
status: task.status,
|
|
462
634
|
assignee: task.assignee,
|
|
463
635
|
});
|
|
464
|
-
return {
|
|
636
|
+
return {
|
|
637
|
+
task_id: task.id,
|
|
638
|
+
status: task.status,
|
|
639
|
+
assignee: task.assignee ?? '',
|
|
640
|
+
attempt: task.attempt ?? 0,
|
|
641
|
+
attempt_id: attemptId,
|
|
642
|
+
};
|
|
465
643
|
});
|
|
466
644
|
},
|
|
467
645
|
}));
|
|
468
646
|
ctx.tools.register(defineTool({
|
|
469
647
|
name: 'agent_teams_update_task',
|
|
470
|
-
description: 'Update a task
|
|
648
|
+
description: 'Update a task status/output. Members must supply the current attempt_id returned by claim_task; stale attempts are rejected after takeover/reassignment. Terminal results are immutable. A captain must use reassign_task(assignee="captain") before updating member-owned work.',
|
|
471
649
|
parameters: {
|
|
472
650
|
task_id: { type: 'string', required: true, description: 'The task id to update.' },
|
|
473
651
|
status: {
|
|
@@ -476,6 +654,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
476
654
|
description: 'New status (in_progress, completed, failed, cancelled).',
|
|
477
655
|
},
|
|
478
656
|
output: { type: 'string', description: 'Result summary; set when completing or failing.' },
|
|
657
|
+
attempt_id: { type: 'string', description: 'Current execution capability returned by claim_task (required for members when present on the task).' },
|
|
479
658
|
},
|
|
480
659
|
output: {
|
|
481
660
|
schema: {
|
|
@@ -485,11 +664,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
485
664
|
task_id: { type: 'string', required: true },
|
|
486
665
|
status: { type: 'string', required: true },
|
|
487
666
|
output: { type: 'string' },
|
|
667
|
+
attempt: { type: 'number', required: true },
|
|
668
|
+
attempt_id: { type: 'string' },
|
|
488
669
|
},
|
|
489
670
|
},
|
|
490
671
|
render: (args, value) => [{
|
|
491
672
|
type: 'text',
|
|
492
|
-
text: `Task ${value.task_id} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}`,
|
|
673
|
+
text: `Task ${value.task_id} attempt ${value.attempt} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}`,
|
|
493
674
|
}],
|
|
494
675
|
},
|
|
495
676
|
async execute(args, exec) {
|
|
@@ -497,13 +678,35 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
497
678
|
const workspace = workspaceOf(caller);
|
|
498
679
|
const stateRoot = stateRootOf(workspace, config);
|
|
499
680
|
const team = await requireParticipantTeam(workspace, config, caller);
|
|
500
|
-
|
|
681
|
+
const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
501
682
|
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
502
683
|
const task = requireTask(fresh, args.task_id);
|
|
684
|
+
if (identity.kind === 'captain'
|
|
685
|
+
&& task.assignee !== undefined
|
|
686
|
+
&& task.assignee !== CAPTAIN_KEY) {
|
|
687
|
+
throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call agent_teams_reassign_task with assignee="captain" before takeover`);
|
|
688
|
+
}
|
|
503
689
|
if (identity.kind === 'member') {
|
|
504
690
|
if (task.assignee !== identity.name) {
|
|
505
691
|
throw new Error(`task ${task.id} is assigned to "${task.assignee ?? 'nobody'}", not you`);
|
|
506
692
|
}
|
|
693
|
+
if (task.attemptId !== undefined && args.attempt_id !== task.attemptId) {
|
|
694
|
+
throw new Error(`stale attempt for task ${task.id}: expected the current attempt_id; stop work and request fresh assignment`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (TERMINAL_TASK_STATUSES.includes(task.status)) {
|
|
698
|
+
const sameStatus = args.status === undefined || args.status === task.status;
|
|
699
|
+
const sameOutput = args.output === undefined || args.output === task.output;
|
|
700
|
+
if (!sameStatus || !sameOutput) {
|
|
701
|
+
throw new Error(`terminal task ${task.id} is immutable; use agent_teams_reassign_task to retry failed/cancelled work`);
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
task_id: task.id,
|
|
705
|
+
status: task.status,
|
|
706
|
+
attempt: task.attempt ?? 0,
|
|
707
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
708
|
+
...task.output !== undefined ? { output: task.output } : {},
|
|
709
|
+
};
|
|
507
710
|
}
|
|
508
711
|
if (args.status !== undefined) {
|
|
509
712
|
const transition = transitionError(task.status, args.status);
|
|
@@ -525,9 +728,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
525
728
|
return {
|
|
526
729
|
task_id: task.id,
|
|
527
730
|
status: task.status,
|
|
731
|
+
attempt: task.attempt ?? 0,
|
|
732
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
528
733
|
...task.output !== undefined ? { output: task.output } : {},
|
|
529
734
|
};
|
|
530
735
|
});
|
|
736
|
+
await scheduler.kickTeam(workspace, team.id, team.captainSessionId === caller.id ? caller : undefined);
|
|
737
|
+
return updated;
|
|
531
738
|
},
|
|
532
739
|
}));
|
|
533
740
|
ctx.tools.register(defineTool({
|
|
@@ -569,7 +776,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
569
776
|
throw new Error(`agent_teams_send_message: "from" must be your own identity ("${from}"), not "${args.from}"`);
|
|
570
777
|
}
|
|
571
778
|
if (to === CAPTAIN_KEY) {
|
|
572
|
-
const message = createMessage(from, CAPTAIN_KEY, args.content);
|
|
779
|
+
const message = { ...createMessage(from, CAPTAIN_KEY, args.content), deliveryClaimedAt: Date.now() };
|
|
573
780
|
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, message);
|
|
574
781
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/message-sent', {
|
|
575
782
|
teamId: fresh.id,
|
|
@@ -582,7 +789,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
582
789
|
return { kind: 'captain', fresh, identity, message, from };
|
|
583
790
|
}
|
|
584
791
|
const recipient = requireMember(fresh, to);
|
|
585
|
-
const message = createMessage(from, recipient.name, args.content);
|
|
792
|
+
const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now() };
|
|
586
793
|
await appendMailbox(stateRoot, fresh.id, recipient.name, message);
|
|
587
794
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/message-sent', {
|
|
588
795
|
teamId: fresh.id,
|
|
@@ -602,6 +809,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
602
809
|
if (captain !== undefined && prepared.identity.kind === 'member') {
|
|
603
810
|
delivered = steerCaptainReport(captain, prepared.from, args.content) ? 'live' : 'mailbox';
|
|
604
811
|
}
|
|
812
|
+
if (delivered === 'live') {
|
|
813
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (acknowledgeMailbox(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (releaseMailboxDelivery(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
|
|
817
|
+
}
|
|
605
818
|
return { message_id: prepared.message.id, from: prepared.from, to: CAPTAIN_KEY, delivered };
|
|
606
819
|
}
|
|
607
820
|
let delivered = 'mailbox';
|
|
@@ -612,6 +825,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
612
825
|
const text = `AgentTeams state policy: inspect ${config.stateDir}/${prepared.fresh.id}/ read-only; never edit team.json or inbox files directly. Use agent_teams_* tools for team state.\n\n${senderText}`;
|
|
613
826
|
const accepted = await deliverToMember(ctx, captain, prepared.recipient.id, text, exec.signal);
|
|
614
827
|
delivered = accepted ? 'wake' : 'mailbox';
|
|
828
|
+
if (accepted) {
|
|
829
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (acknowledgeMailbox(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (delivered === 'mailbox') {
|
|
833
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (releaseMailboxDelivery(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
|
|
615
834
|
}
|
|
616
835
|
return {
|
|
617
836
|
message_id: prepared.message.id,
|
|
@@ -634,6 +853,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
634
853
|
const workspace = workspaceOf(caller);
|
|
635
854
|
const stateRoot = stateRootOf(workspace, config);
|
|
636
855
|
const located = await requireParticipantTeam(workspace, config, caller);
|
|
856
|
+
if (located.captainSessionId === caller.id) {
|
|
857
|
+
await scheduler.kickTeam(workspace, located.id, caller);
|
|
858
|
+
}
|
|
637
859
|
const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(stateRoot, located.id, caller.id));
|
|
638
860
|
const activity = await memberActivity(ctx, team.captainSessionId);
|
|
639
861
|
const members = team.members
|
|
@@ -653,6 +875,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
653
875
|
status: task.status,
|
|
654
876
|
assignee: task.assignee ?? '',
|
|
655
877
|
dependencies: task.dependencies,
|
|
878
|
+
attempt: task.attempt ?? 0,
|
|
879
|
+
attempt_id: task.attemptId ?? '',
|
|
880
|
+
reassigning: task.reassigning === true,
|
|
656
881
|
...task.output !== undefined ? { output: task.output } : {},
|
|
657
882
|
}));
|
|
658
883
|
const mailboxWarnings = [];
|
|
@@ -664,14 +889,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
664
889
|
}
|
|
665
890
|
};
|
|
666
891
|
const captainInbox = identity.kind === 'captain'
|
|
667
|
-
? await
|
|
892
|
+
? await readUnreadMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
|
|
668
893
|
: [];
|
|
669
894
|
const memberInboxes = {};
|
|
670
895
|
const visibleMembers = identity.kind === 'captain'
|
|
671
896
|
? members
|
|
672
897
|
: members.filter((member) => member.name === identity.name);
|
|
673
898
|
for (const member of visibleMembers) {
|
|
674
|
-
const messages = await
|
|
899
|
+
const messages = await readUnreadMailbox(stateRoot, team.id, member.name, reportMalformed(member.name));
|
|
675
900
|
if (messages.length > 0) {
|
|
676
901
|
memberInboxes[member.name] = {
|
|
677
902
|
count: messages.length,
|
|
@@ -679,7 +904,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
679
904
|
};
|
|
680
905
|
}
|
|
681
906
|
}
|
|
682
|
-
|
|
907
|
+
const result = {
|
|
683
908
|
team_id: team.id,
|
|
684
909
|
team_name: team.name,
|
|
685
910
|
description: team.description ?? '',
|
|
@@ -695,6 +920,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
695
920
|
mailbox_warnings: mailboxWarnings,
|
|
696
921
|
mailbox_warning_count: mailboxWarningCount,
|
|
697
922
|
};
|
|
923
|
+
const acknowledged = identity.kind === 'captain'
|
|
924
|
+
? captainInbox.map(message => message.id)
|
|
925
|
+
: await readUnreadMailbox(stateRoot, team.id, identity.name).then(messages => messages.map(message => message.id));
|
|
926
|
+
if (acknowledged.length > 0) {
|
|
927
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), () => (acknowledgeMailbox(stateRoot, team.id, identity.kind === 'captain' ? CAPTAIN_KEY : identity.name, acknowledged)));
|
|
928
|
+
}
|
|
929
|
+
return result;
|
|
698
930
|
},
|
|
699
931
|
}));
|
|
700
932
|
ctx.tools.register(defineTool({
|
|
@@ -720,12 +952,37 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
720
952
|
const workspace = workspaceOf(captain);
|
|
721
953
|
const stateRoot = stateRootOf(workspace, config);
|
|
722
954
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
723
|
-
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
955
|
+
const members = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
724
956
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
957
|
+
// Include previously removed members so deleting a pre-fix team also
|
|
958
|
+
// retires durable catalog entries left behind by remove_member.
|
|
959
|
+
const roster = fresh.members.map(member => ({ ...member }));
|
|
725
960
|
for (const member of fresh.members) {
|
|
726
|
-
if (member.status
|
|
727
|
-
|
|
961
|
+
if (member.status === 'removed')
|
|
962
|
+
continue;
|
|
963
|
+
member.status = 'removed';
|
|
964
|
+
for (const task of fresh.tasks) {
|
|
965
|
+
if (task.assignee === member.name && task.status !== 'completed')
|
|
966
|
+
invalidateTaskAttempt(task);
|
|
967
|
+
}
|
|
728
968
|
}
|
|
969
|
+
await writeTeam(stateRoot, fresh);
|
|
970
|
+
return roster;
|
|
971
|
+
});
|
|
972
|
+
await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
|
|
973
|
+
for (const member of members) {
|
|
974
|
+
if (member.id === '')
|
|
975
|
+
continue;
|
|
976
|
+
interruptMember(ctx, captain, member.id);
|
|
977
|
+
}
|
|
978
|
+
const quiescence = await Promise.allSettled(members.map(member => waitForMemberIdle(ctx, member, exec.signal)));
|
|
979
|
+
for (const result of quiescence) {
|
|
980
|
+
if (result.status === 'rejected') {
|
|
981
|
+
ctx.logger.warn(`agent-teams: member did not quiesce cleanly before team archive: ${String(result.reason)}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
985
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
729
986
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/team-deleted', {
|
|
730
987
|
teamId: fresh.id,
|
|
731
988
|
});
|
|
@@ -760,7 +1017,8 @@ function renderStatus(value) {
|
|
|
760
1017
|
...team.tasks.map((task) => {
|
|
761
1018
|
const deps = task.dependencies.length > 0 ? ` (deps: ${task.dependencies.join(',')})` : '';
|
|
762
1019
|
const output = task.output !== undefined ? `\n output: ${task.output.slice(0, 300)}` : '';
|
|
763
|
-
|
|
1020
|
+
const handoff = task.reassigning ? ' (reassigning)' : '';
|
|
1021
|
+
return ` - ${task.id} [${task.status}] attempt ${task.attempt}${handoff} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}`;
|
|
764
1022
|
}),
|
|
765
1023
|
`Captain inbox (${team.captain_inbox.length}):`,
|
|
766
1024
|
...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content.slice(0, 200)}`),
|
|
@@ -21,6 +21,7 @@ export interface ActivityMember {
|
|
|
21
21
|
readonly id: string;
|
|
22
22
|
readonly name: string;
|
|
23
23
|
readonly role: string;
|
|
24
|
+
readonly status?: 'idle' | 'working' | 'removed';
|
|
24
25
|
readonly activity: 'working' | 'idle' | 'unknown';
|
|
25
26
|
readonly progress: number;
|
|
26
27
|
readonly done: number;
|
|
@@ -10,6 +10,32 @@ export interface RelationshipStage<T extends RelationshipTask> {
|
|
|
10
10
|
readonly depth: number;
|
|
11
11
|
readonly tasks: readonly T[];
|
|
12
12
|
}
|
|
13
|
+
/** Geometry used by the compact task DAG in the activity panel. */
|
|
14
|
+
export interface CompactDagNode<T extends RelationshipTask> {
|
|
15
|
+
readonly task: T;
|
|
16
|
+
readonly x: number;
|
|
17
|
+
readonly y: number;
|
|
18
|
+
}
|
|
19
|
+
/** One dependency edge routed between two compact DAG nodes. */
|
|
20
|
+
export interface CompactDagEdge {
|
|
21
|
+
readonly from: string;
|
|
22
|
+
readonly to: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
}
|
|
25
|
+
/** Complete, scrollable compact DAG projection. */
|
|
26
|
+
export interface CompactDagLayout<T extends RelationshipTask> {
|
|
27
|
+
readonly width: number;
|
|
28
|
+
readonly height: number;
|
|
29
|
+
readonly nodes: readonly CompactDagNode<T>[];
|
|
30
|
+
readonly edges: readonly CompactDagEdge[];
|
|
31
|
+
}
|
|
32
|
+
/** Reference-panel geometry: narrow nodes with enough room for curved edges. */
|
|
33
|
+
export declare const COMPACT_DAG_NODE_WIDTH = 92;
|
|
34
|
+
export declare const COMPACT_DAG_NODE_HEIGHT = 30;
|
|
35
|
+
export declare const COMPACT_DAG_COLUMN_GAP = 26;
|
|
36
|
+
export declare const COMPACT_DAG_ROW_GAP = 8;
|
|
37
|
+
/** Use a fill-width grid when the task graph has no real dependency edges. */
|
|
38
|
+
export declare function usesParallelTaskGrid<T extends RelationshipTask>(tasks: readonly T[]): boolean;
|
|
13
39
|
/**
|
|
14
40
|
* Whether an expanded activity panel still belongs to the current session.
|
|
15
41
|
*
|
|
@@ -29,6 +55,14 @@ export declare function activityPanelExpandedForSession(open: boolean, owner: st
|
|
|
29
55
|
export declare function dependencyFocusTaskId(pinnedTaskId: string | null, keyboardTaskId: string | null, hoverTaskId: string | null): string | null;
|
|
30
56
|
/** Group tasks by their precomputed dependency depth. */
|
|
31
57
|
export declare function taskStages<T extends RelationshipTask>(tasks: readonly T[]): readonly RelationshipStage<T>[];
|
|
58
|
+
/**
|
|
59
|
+
* Lay tasks out as the reference panel's compact left-to-right DAG.
|
|
60
|
+
*
|
|
61
|
+
* Columns are dependency-depth stages. Rows are stable task-id order within
|
|
62
|
+
* each stage. Edges use cubic curves so fan-in remains readable without
|
|
63
|
+
* turning every task into a large card.
|
|
64
|
+
*/
|
|
65
|
+
export declare function compactDagLayout<T extends RelationshipTask>(tasks: readonly T[]): CompactDagLayout<T>;
|
|
32
66
|
/**
|
|
33
67
|
* Return the complete upstream/downstream chain around one task.
|
|
34
68
|
*
|
|
@@ -44,6 +44,8 @@ export interface AgentTeamsTaskUpdatedData {
|
|
|
44
44
|
readonly status: string;
|
|
45
45
|
readonly assignee?: string;
|
|
46
46
|
readonly output?: string;
|
|
47
|
+
readonly attempt?: number;
|
|
48
|
+
readonly attemptId?: string;
|
|
47
49
|
}
|
|
48
50
|
/** Closes one team record: the team was deleted. */
|
|
49
51
|
export interface AgentTeamsTeamDeletedData {
|