@nanmicoder/dsh-agent-teams 0.1.17 → 0.1.18
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 +5 -3
- package/README_ZH.md +6 -2
- package/lib/client/AgentTeamsCard.js +1 -1
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/harness-compat.js +22 -0
- package/lib/index.js +4 -4
- package/lib/mailbox.js +154 -0
- package/lib/members.js +90 -18
- package/lib/quality-gates.js +21 -6
- package/lib/scheduler.js +27 -15
- package/lib/snapshot.js +1 -1
- package/lib/state.js +24 -5
- package/lib/tools.js +205 -213
- package/lib/types/harness-compat.d.ts +2 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/mailbox.d.ts +7 -0
- package/lib/types/members.d.ts +6 -4
- package/lib/types/scheduler.d.ts +1 -0
- package/lib/types/state.d.ts +5 -0
- package/lib/types/types.d.ts +9 -0
- package/package.json +2 -2
- package/release-notes/v0.1.18.md +21 -0
package/lib/tools.js
CHANGED
|
@@ -12,10 +12,11 @@ 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 { acknowledgeMailbox, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam,
|
|
16
|
-
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime,
|
|
15
|
+
import { acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
16
|
+
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
17
17
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
18
|
-
import { installTeamScheduler } from "./scheduler.js";
|
|
18
|
+
import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
|
|
19
|
+
import { installMailboxAdmission, mailboxPrompt } from "./mailbox.js";
|
|
19
20
|
import { resolveTeamProfile } from "./profiles.js";
|
|
20
21
|
export { steerCaptainReport } from "./members.js";
|
|
21
22
|
/** The caller agent, or a loud failure for non-agent callers. */
|
|
@@ -158,32 +159,22 @@ function memberOpenTask(team, memberName, exceptTaskId) {
|
|
|
158
159
|
&& task.assignee === memberName
|
|
159
160
|
&& (task.status === 'claimed' || task.status === 'in_progress'));
|
|
160
161
|
}
|
|
162
|
+
function taskDetails(team, task) {
|
|
163
|
+
return [task.subject, task.description ?? '',
|
|
164
|
+
`Kind: ${task.kind ?? 'work'}`,
|
|
165
|
+
`Objective: ${task.objective ?? ''}`,
|
|
166
|
+
`In scope: ${(task.inScope ?? []).join(', ')}; Out of scope: ${(task.outOfScope ?? []).join(', ')}`,
|
|
167
|
+
`Acceptance: ${(task.acceptance ?? []).join('; ')}`,
|
|
168
|
+
`Verify: ${(task.verify ?? []).join('; ')}`,
|
|
169
|
+
`Dependency results:\n${formatDependencyOutputs(collectCompletedDependencyOutputs(team.tasks, task.id))}`,
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
161
172
|
/** Captain work is immediate, not a durable scheduler lane: allow one unfinished takeover at a time. */
|
|
162
173
|
function captainOpenTask(team, exceptTaskId) {
|
|
163
174
|
return team.tasks.find(task => task.id !== exceptTaskId
|
|
164
175
|
&& task.assignee === CAPTAIN_KEY
|
|
165
176
|
&& !TERMINAL_TASK_STATUSES.includes(task.status));
|
|
166
177
|
}
|
|
167
|
-
async function waitForMemberIdle(ctx, member, signal) {
|
|
168
|
-
if (member.id === '')
|
|
169
|
-
return;
|
|
170
|
-
const live = ctx.agents.get(member.id);
|
|
171
|
-
if (live === undefined)
|
|
172
|
-
return;
|
|
173
|
-
if (signal.aborted)
|
|
174
|
-
throw signal.reason;
|
|
175
|
-
let onAbort;
|
|
176
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
177
|
-
onAbort = () => reject(signal.reason ?? new Error('task reassignment was cancelled'));
|
|
178
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
179
|
-
});
|
|
180
|
-
try {
|
|
181
|
-
await Promise.race([live.whenIdle(), aborted]);
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
signal.removeEventListener('abort', onAbort);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
178
|
/** Stop every currently-resident member activation for one halted team.
|
|
188
179
|
*
|
|
189
180
|
* Interrupt requests only cancel the member's current model turn and retain its
|
|
@@ -192,34 +183,13 @@ async function waitForMemberIdle(ctx, member, signal) {
|
|
|
192
183
|
* keep executing after the captain-chat Stop control has reported success.
|
|
193
184
|
*/
|
|
194
185
|
async function stopTeamMemberActivations(ctx, captain, members, signal) {
|
|
195
|
-
const
|
|
196
|
-
const memberIds = activeMembers.map((member) => member.id);
|
|
186
|
+
const memberIds = members.filter(member => member.id !== '').map(member => member.id);
|
|
197
187
|
if (memberIds.length === 0)
|
|
198
188
|
return;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// hosts, where interrupt is the strongest available lifecycle operation.
|
|
204
|
-
const runtime = ctx.subagents;
|
|
205
|
-
if (runtime.drainContinuableChildren !== undefined) {
|
|
206
|
-
try {
|
|
207
|
-
await runtime.drainContinuableChildren(captain, memberIds);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
// Do not claim the browser action stopped work when the runtime could not
|
|
212
|
-
// release all selected child activations. The HTTP route surfaces this
|
|
213
|
-
// failure instead of returning a false successful stop.
|
|
214
|
-
ctx.logger.warn(`agent-teams: failed to drain halted members: ${String(error)}`);
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const fallbackSignal = signal ?? new AbortController().signal;
|
|
219
|
-
const results = await Promise.allSettled(activeMembers.map((member) => waitForMemberIdle(ctx, member, fallbackSignal)));
|
|
220
|
-
const failed = results.find((result) => result.status === 'rejected');
|
|
221
|
-
if (failed?.status === 'rejected')
|
|
222
|
-
throw failed.reason;
|
|
189
|
+
// Every supported exact host has targeted recursive drain. Unlike interrupt,
|
|
190
|
+
// it closes admission and clears queued work before awaiting descendants.
|
|
191
|
+
signal?.throwIfAborted();
|
|
192
|
+
await ctx.subagents.drainContinuableChildren(captain, memberIds);
|
|
223
193
|
}
|
|
224
194
|
export async function haltTeamWork(input) {
|
|
225
195
|
const halted = await withTeamLock(teamLockKey(input.stateRoot, input.teamId), async () => {
|
|
@@ -310,16 +280,59 @@ export function stagedPlanFeedbackContext(teamName) {
|
|
|
310
280
|
*/
|
|
311
281
|
export function registerAgentTeamsTools(ctx, config) {
|
|
312
282
|
installRetiredMemberGuard(ctx, config.stateDir);
|
|
313
|
-
|
|
283
|
+
installMemberDelegationGuard(ctx, config.stateDir, config.memberMaxDepth ?? 0);
|
|
284
|
+
installMailboxAdmission(ctx, config.stateDir);
|
|
285
|
+
const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir, executionPrompt: config.executionPrompt, dispatch: dispatchMember });
|
|
314
286
|
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
|
|
315
|
-
|
|
287
|
+
async function dispatchMember(captain, teamId, memberName, text, signal, mode, attemptId) {
|
|
288
|
+
const root = stateRootOf(workspaceOf(captain), config);
|
|
289
|
+
let orphan;
|
|
290
|
+
try {
|
|
291
|
+
return await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
292
|
+
const team = await readTeam(root, teamId);
|
|
293
|
+
if (team?.captainSessionId !== captain.id || team.halted === true || team.phase === 'staged')
|
|
294
|
+
return false;
|
|
295
|
+
const member = team.members.find(item => item.name === memberName && item.status !== 'removed');
|
|
296
|
+
if (member === undefined || member.stopping === true || team.tasks.some(task => task.reassigning === true && task.assignee === memberName))
|
|
297
|
+
return false;
|
|
298
|
+
if (attemptId !== undefined && !team.tasks.some(task => task.attemptId === attemptId && task.assignee === memberName && (task.status === 'claimed' || task.status === 'in_progress')))
|
|
299
|
+
return false;
|
|
300
|
+
if (member.id !== '')
|
|
301
|
+
return deliverToMember(ctx, captain, member.id, text, signal, mode);
|
|
302
|
+
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
303
|
+
provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
|
|
304
|
+
}, signal);
|
|
305
|
+
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
|
|
306
|
+
orphan = { ...member };
|
|
307
|
+
await writeTeam(root, team);
|
|
308
|
+
orphan = undefined;
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (orphan !== undefined) {
|
|
314
|
+
await recordRetiredMemberIds(root, [orphan.id]);
|
|
315
|
+
await stopTeamMemberActivations(ctx, captain, [orphan]);
|
|
316
|
+
}
|
|
317
|
+
ctx.logger.warn(`agent-teams: member dispatch failed for ${memberName}: ${String(error)}`);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const updatePlanBatch = async (captain, teamId, mutations, signal, allowPendingEdits = false) => {
|
|
316
322
|
if (mutations.length === 0)
|
|
317
323
|
throw new Error('at least one staged plan operation is required');
|
|
318
324
|
const workspace = workspaceOf(captain);
|
|
319
325
|
const stateRoot = stateRootOf(workspace, config);
|
|
320
326
|
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
321
327
|
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
322
|
-
|
|
328
|
+
const staged = fresh.phase === 'staged';
|
|
329
|
+
if (!staged && !allowPendingEdits)
|
|
330
|
+
requireStagedTeam(fresh);
|
|
331
|
+
if (fresh.halted === true)
|
|
332
|
+
throw new Error('team is halted; resume before editing tasks');
|
|
333
|
+
if (!staged && mutations.some(mutation => mutation.action !== 'update_task')) {
|
|
334
|
+
throw new Error('a running team only permits update_task edits to pending, never-started tasks; roster and removal edits require a staged plan');
|
|
335
|
+
}
|
|
323
336
|
for (const mutation of mutations) {
|
|
324
337
|
if (mutation.action === 'update_member') {
|
|
325
338
|
const member = requireMember(fresh, mutation.memberName);
|
|
@@ -339,9 +352,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
339
352
|
}
|
|
340
353
|
else if (mutation.action === 'update_task') {
|
|
341
354
|
const task = requireTask(fresh, mutation.taskId);
|
|
342
|
-
if (task.status !== 'pending' || (task.attempt ?? 0) !== 0) {
|
|
355
|
+
if (task.status !== 'pending' || (task.attempt ?? 0) !== 0 || task.reassigning === true) {
|
|
343
356
|
throw new Error(`task "${task.id}" has already started and cannot be edited`);
|
|
344
357
|
}
|
|
358
|
+
if (!staged && mutation.assignee === CAPTAIN_KEY)
|
|
359
|
+
throw new Error('use reassign_task for captain takeover');
|
|
345
360
|
const subject = mutation.subject.trim();
|
|
346
361
|
if (subject === '')
|
|
347
362
|
throw new Error('task subject must not be empty');
|
|
@@ -390,11 +405,25 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
390
405
|
}
|
|
391
406
|
}
|
|
392
407
|
validateStagedGraph(fresh, false);
|
|
393
|
-
|
|
408
|
+
if (!staged) {
|
|
409
|
+
for (const mutation of mutations) {
|
|
410
|
+
if (mutation.action !== 'update_task')
|
|
411
|
+
continue;
|
|
412
|
+
const task = requireTask(fresh, mutation.taskId);
|
|
413
|
+
const validation = validateCreateTask({ ...fresh, tasks: fresh.tasks.filter(item => item.id !== task.id) }, task);
|
|
414
|
+
if (!validation.ok)
|
|
415
|
+
throw new Error(validation.error ?? 'edited task violates the quality contract');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else
|
|
419
|
+
fresh.planReviewState = 'awaiting_review';
|
|
420
|
+
signal?.throwIfAborted();
|
|
394
421
|
await writeTeam(stateRoot, fresh);
|
|
395
422
|
return fresh;
|
|
396
423
|
});
|
|
397
424
|
};
|
|
425
|
+
// Browser review controls retain their staged-only contract.
|
|
426
|
+
const updateStagedPlanBatch = (captain, teamId, mutations, signal) => (updatePlanBatch(captain, teamId, mutations, signal));
|
|
398
427
|
const updateStagedPlan = async (captain, teamId, mutation, signal) => (updateStagedPlanBatch(captain, teamId, [mutation], signal));
|
|
399
428
|
const approveStagedTeam = async (captain, teamId, signal) => {
|
|
400
429
|
const workspace = workspaceOf(captain);
|
|
@@ -407,47 +436,22 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
407
436
|
// placeholders before transitioning to the stricter running shape.
|
|
408
437
|
fresh.members = fresh.members.filter((member) => member.status !== 'removed');
|
|
409
438
|
validateStagedGraph(fresh, true);
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
reasoningEffort: member.reasoningEffort,
|
|
420
|
-
fallback: member.fallback,
|
|
421
|
-
}, runSignal);
|
|
422
|
-
selections.set(member, selection);
|
|
423
|
-
member.provider = selection.provider;
|
|
424
|
-
member.model = selection.model;
|
|
425
|
-
member.reasoningEffort = selection.reasoningEffort;
|
|
426
|
-
}
|
|
427
|
-
// This is the approval commit barrier: resolve and validate the whole
|
|
428
|
-
// final roster before spawning even the first durable child.
|
|
429
|
-
await validateMemberLlmSelections(ctx, [...selections.values()], runSignal);
|
|
430
|
-
for (const [member, selection] of selections) {
|
|
431
|
-
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, runSignal);
|
|
432
|
-
spawned.push(member);
|
|
433
|
-
}
|
|
434
|
-
if (fresh.members.some((member) => member.id === '')) {
|
|
435
|
-
throw new Error('one or more staged members could not be spawned');
|
|
436
|
-
}
|
|
437
|
-
fresh.phase = 'running';
|
|
438
|
-
delete fresh.planReviewState;
|
|
439
|
-
fresh.approvedAt = Date.now();
|
|
440
|
-
await writeTeam(stateRoot, fresh);
|
|
441
|
-
return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
|
|
442
|
-
}
|
|
443
|
-
catch (error) {
|
|
444
|
-
await recordRetiredMemberIds(stateRoot, spawned.map((member) => member.id)).catch(() => undefined);
|
|
445
|
-
for (const member of spawned) {
|
|
446
|
-
if (member.id !== '')
|
|
447
|
-
interruptMember(ctx, captain, member.id);
|
|
448
|
-
}
|
|
449
|
-
throw error;
|
|
439
|
+
const selections = [];
|
|
440
|
+
for (const member of fresh.members) {
|
|
441
|
+
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
442
|
+
provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
|
|
443
|
+
}, runSignal);
|
|
444
|
+
selections.push(selection);
|
|
445
|
+
member.provider = selection.provider;
|
|
446
|
+
member.model = selection.model;
|
|
447
|
+
member.reasoningEffort = selection.reasoningEffort;
|
|
450
448
|
}
|
|
449
|
+
await validateMemberLlmSelections(ctx, selections, runSignal);
|
|
450
|
+
fresh.phase = 'running';
|
|
451
|
+
delete fresh.planReviewState;
|
|
452
|
+
fresh.approvedAt = Date.now();
|
|
453
|
+
await writeTeam(stateRoot, fresh);
|
|
454
|
+
return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
|
|
451
455
|
});
|
|
452
456
|
try {
|
|
453
457
|
await scheduler.kickTeam(workspace, teamId, captain);
|
|
@@ -547,6 +551,21 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
547
551
|
name: { type: 'string', required: true, description: 'Name for the new team (used as its stable id).' },
|
|
548
552
|
description: { type: 'string', description: 'Team purpose / the goal the team will work on.' },
|
|
549
553
|
profile: { type: 'string', description: 'Optional configured profile name.' },
|
|
554
|
+
plan: {
|
|
555
|
+
type: 'object', additionalProperties: false,
|
|
556
|
+
description: 'Optional complete ordinary-work roster and DAG in one atomic call, instead of separate add_member/create_task rounds. Mutually exclusive with profile. For quality gates use create_task with the explicit quality contract.',
|
|
557
|
+
properties: {
|
|
558
|
+
members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
559
|
+
name: { type: 'string', required: true }, role: { type: 'string' },
|
|
560
|
+
provider: { type: 'string' }, model: { type: 'string' }, reasoning_effort: { type: 'string' },
|
|
561
|
+
} } },
|
|
562
|
+
tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
563
|
+
id: { type: 'string', required: true, description: 'Local reference used by dependencies in this plan; the result maps it to a durable task id.' },
|
|
564
|
+
subject: { type: 'string', required: true }, description: { type: 'string' }, assignee: { type: 'string' },
|
|
565
|
+
dependencies: { type: 'array', items: { type: 'string' } },
|
|
566
|
+
} } },
|
|
567
|
+
},
|
|
568
|
+
},
|
|
550
569
|
approval: {
|
|
551
570
|
type: 'string',
|
|
552
571
|
enum: ['required', 'automatic'],
|
|
@@ -570,9 +589,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
570
589
|
},
|
|
571
590
|
render: (args, value) => [{
|
|
572
591
|
type: 'text',
|
|
573
|
-
text: value.phase === 'staged'
|
|
592
|
+
text: (value.phase === 'staged'
|
|
574
593
|
? `Team "${value.team_name}" plan created under ${value.state_dir}. It is staged: finish the roster and DAG, then wait for the user to edit and approve it. Do not start or approve it yourself.`
|
|
575
|
-
: `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain
|
|
594
|
+
: `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`)
|
|
595
|
+
+ (value.tasks === undefined ? '' : '\n' + value.tasks.map(task => `${task.task_id} [${task.seed_id}]: ${task.subject}; assignee=${task.assignee ?? 'unassigned'}; dependencies=${task.dependencies.join(',')}`).join('\n')),
|
|
576
596
|
}],
|
|
577
597
|
},
|
|
578
598
|
async execute(args, exec) {
|
|
@@ -590,6 +610,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
590
610
|
const profileName = args.profile !== undefined && args.profile.trim() !== ''
|
|
591
611
|
? args.profile.trim()
|
|
592
612
|
: undefined;
|
|
613
|
+
if (profileName !== undefined && args.plan !== undefined)
|
|
614
|
+
throw new Error('choose either a configured profile or an inline plan');
|
|
593
615
|
const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
|
|
594
616
|
const current = await findTeamByParticipant(stateRoot, captain.id);
|
|
595
617
|
if (current !== undefined) {
|
|
@@ -604,7 +626,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
604
626
|
if (existing !== undefined) {
|
|
605
627
|
throw new Error(`team id "${teamId}" is taken by another captain — pick a different team name`);
|
|
606
628
|
}
|
|
607
|
-
if (profileName === undefined) {
|
|
629
|
+
if (profileName === undefined && args.plan === undefined) {
|
|
608
630
|
const state = {
|
|
609
631
|
name: teamName,
|
|
610
632
|
id: teamId,
|
|
@@ -628,7 +650,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
628
650
|
stateRoot,
|
|
629
651
|
teamName,
|
|
630
652
|
teamId,
|
|
631
|
-
profileName,
|
|
653
|
+
profileName: profileName ?? 'inline-plan',
|
|
654
|
+
inlinePlan: args.plan,
|
|
632
655
|
description: args.description,
|
|
633
656
|
staged,
|
|
634
657
|
});
|
|
@@ -673,7 +696,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
673
696
|
}
|
|
674
697
|
const persisted = await readTeam(stateRoot, created.state.id).catch(() => undefined);
|
|
675
698
|
const snapshot = persisted ?? created.state;
|
|
676
|
-
if (snapshot.profile === undefined) {
|
|
699
|
+
if (snapshot.profile === undefined && args.plan === undefined) {
|
|
677
700
|
return {
|
|
678
701
|
team_id: snapshot.id,
|
|
679
702
|
team_name: snapshot.name,
|
|
@@ -686,8 +709,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
686
709
|
team_name: snapshot.name,
|
|
687
710
|
state_dir: join(stateRoot, snapshot.id),
|
|
688
711
|
phase: snapshot.phase ?? 'running',
|
|
689
|
-
profile: snapshot.profile.name,
|
|
690
|
-
task_planning: snapshot.profile
|
|
712
|
+
...snapshot.profile === undefined ? {} : { profile: snapshot.profile.name },
|
|
713
|
+
task_planning: snapshot.profile?.taskPlanning ?? 'seed',
|
|
691
714
|
members: snapshot.members.map((member) => ({
|
|
692
715
|
member_name: member.name,
|
|
693
716
|
member_id: member.id,
|
|
@@ -710,12 +733,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
710
733
|
}));
|
|
711
734
|
ctx.tools.register(defineTool({
|
|
712
735
|
name: 'agent_teams_edit_plan',
|
|
713
|
-
description: 'Atomically revise
|
|
736
|
+
description: 'Atomically revise an AgentTeams plan. While staged, edit tasks and roster without starting work. While running, only update_task is allowed, and only for pending tasks with no prior attempt: correct dependencies, assignees or descriptions before they start; newly ready work is scheduled after commit. Never edit active/finished attempts. Submit dependent edits in order. Never modify .agent-teams files directly.',
|
|
714
737
|
parameters: {
|
|
715
738
|
operations: {
|
|
716
739
|
type: 'array',
|
|
717
740
|
required: true,
|
|
718
|
-
description: 'One atomic, ordered batch
|
|
741
|
+
description: 'One atomic, ordered batch. Running teams allow only update_task for never-started pending tasks. If any operation is invalid, none of the edits are saved.',
|
|
719
742
|
items: {
|
|
720
743
|
type: 'object',
|
|
721
744
|
additionalProperties: false,
|
|
@@ -756,14 +779,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
756
779
|
},
|
|
757
780
|
render: (_args, value) => [{
|
|
758
781
|
type: 'text',
|
|
759
|
-
text:
|
|
782
|
+
text: `${value.status === 'staged' ? 'Staged plan' : 'Pending task graph'} updated atomically (${value.members} members, ${value.tasks} tasks, ${value.dependencies} dependencies). ${value.status === 'staged' ? 'No members were spawned and no tasks were scheduled.' : 'The scheduler will dispatch any newly ready work.'}\n${value.graph.join('\n')}`,
|
|
760
783
|
}],
|
|
761
784
|
},
|
|
762
785
|
async execute(args, exec) {
|
|
763
786
|
const captain = requireCaptain(exec);
|
|
764
787
|
const workspace = workspaceOf(captain);
|
|
765
788
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
766
|
-
requireStagedTeam(team);
|
|
767
789
|
if (args.operations.length === 0)
|
|
768
790
|
throw new Error('at least one staged plan operation is required');
|
|
769
791
|
const mutations = args.operations.map((operation, index) => {
|
|
@@ -820,9 +842,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
820
842
|
throw new Error(`${label} requires member_name`);
|
|
821
843
|
return { action: 'remove_member', memberName };
|
|
822
844
|
});
|
|
823
|
-
const updated = await
|
|
845
|
+
const updated = await updatePlanBatch(captain, team.id, mutations, exec.signal, true);
|
|
846
|
+
if (updated.phase !== 'staged')
|
|
847
|
+
await scheduler.kickTeam(workspace, team.id, captain);
|
|
824
848
|
return {
|
|
825
|
-
status: '
|
|
849
|
+
status: updated.phase ?? 'running',
|
|
826
850
|
team_id: updated.id,
|
|
827
851
|
members: updated.members.length,
|
|
828
852
|
tasks: updated.tasks.length,
|
|
@@ -866,7 +890,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
866
890
|
}));
|
|
867
891
|
ctx.tools.register(defineTool({
|
|
868
892
|
name: 'agent_teams_add_member',
|
|
869
|
-
description: 'Add a member to the team roster.
|
|
893
|
+
description: 'Add a member to the team roster. Planning and idle roster rows do not call a model. After approval, the member session starts with its first ready task or explicit message and remains durable for later work.',
|
|
870
894
|
parameters: {
|
|
871
895
|
name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
|
|
872
896
|
role: { type: 'string', description: 'Role of the member (e.g. researcher, engineer, reviewer).' },
|
|
@@ -893,7 +917,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
893
917
|
type: 'text',
|
|
894
918
|
text: value.phase === 'staged'
|
|
895
919
|
? `Member "${value.member_name}" added to the staged roster (${value.provider}/${value.model}); no child was spawned.`
|
|
896
|
-
: `Member "${value.member_name}" added (
|
|
920
|
+
: `Member "${value.member_name}" added (session ${value.member_id || 'starts with first ready task'}, ${value.provider}/${value.model}${value.reasoning_effort === undefined ? '' : `, reasoning ${value.reasoning_effort}`}, status ${value.status}).`,
|
|
897
921
|
}],
|
|
898
922
|
},
|
|
899
923
|
async execute(args, exec) {
|
|
@@ -935,23 +959,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
935
959
|
joinedAt: Date.now(),
|
|
936
960
|
status: 'idle',
|
|
937
961
|
};
|
|
938
|
-
|
|
939
|
-
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, exec.signal);
|
|
940
|
-
}
|
|
962
|
+
await validateMemberLlmSelections(ctx, [selection], exec.signal);
|
|
941
963
|
fresh.members.push(member);
|
|
942
|
-
|
|
943
|
-
await writeTeam(stateRoot, fresh);
|
|
944
|
-
}
|
|
945
|
-
catch (error) {
|
|
946
|
-
// The continuable child is already live, but the durable team record
|
|
947
|
-
// never saw it. Retire the orphan so it disappears from subagent
|
|
948
|
-
// listings and cannot be resumed, then surface the write failure.
|
|
949
|
-
if (member.id !== '') {
|
|
950
|
-
await recordRetiredMemberIds(stateRoot, [member.id]).catch(() => undefined);
|
|
951
|
-
interruptMember(ctx, captain, member.id);
|
|
952
|
-
}
|
|
953
|
-
throw error;
|
|
954
|
-
}
|
|
964
|
+
await writeTeam(stateRoot, fresh);
|
|
955
965
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/member-added', {
|
|
956
966
|
teamId: fresh.id,
|
|
957
967
|
memberId: member.id,
|
|
@@ -971,7 +981,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
971
981
|
};
|
|
972
982
|
});
|
|
973
983
|
await scheduler.kickMember(workspace, team.id, created.member_name, captain);
|
|
974
|
-
|
|
984
|
+
const latest = (await readTeam(stateRoot, team.id))?.members.find(member => member.name === created.member_name);
|
|
985
|
+
return latest === undefined ? created : { ...created, member_id: latest.id, status: latest.status };
|
|
975
986
|
},
|
|
976
987
|
}));
|
|
977
988
|
ctx.tools.register(defineTool({
|
|
@@ -1002,7 +1013,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1002
1013
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1003
1014
|
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1004
1015
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1005
|
-
const member =
|
|
1016
|
+
const member = fresh.members.find(item => item.name === args.name);
|
|
1017
|
+
if (member === undefined)
|
|
1018
|
+
throw new Error(`no member \"${args.name}\" in team \"${fresh.name}\"`);
|
|
1006
1019
|
const requeued = [];
|
|
1007
1020
|
for (const task of fresh.tasks) {
|
|
1008
1021
|
if (task.assignee !== member.name || task.status === 'completed')
|
|
@@ -1012,6 +1025,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1012
1025
|
requeued.push(task.id);
|
|
1013
1026
|
}
|
|
1014
1027
|
member.status = 'removed';
|
|
1028
|
+
await discardMailboxMessages(stateRoot, fresh.id, member.name, (await readUnreadMailbox(stateRoot, fresh.id, member.name)).map(message => message.id));
|
|
1015
1029
|
await writeTeam(stateRoot, fresh);
|
|
1016
1030
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/member-removed', {
|
|
1017
1031
|
teamId: fresh.id,
|
|
@@ -1021,8 +1035,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1021
1035
|
});
|
|
1022
1036
|
if (revoked.member.id !== '') {
|
|
1023
1037
|
await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
|
|
1024
|
-
|
|
1025
|
-
await waitForMemberIdle(ctx, revoked.member, exec.signal);
|
|
1038
|
+
await stopTeamMemberActivations(ctx, captain, [revoked.member], exec.signal);
|
|
1026
1039
|
}
|
|
1027
1040
|
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1028
1041
|
return {
|
|
@@ -1034,7 +1047,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1034
1047
|
}));
|
|
1035
1048
|
ctx.tools.register(defineTool({
|
|
1036
1049
|
name: 'agent_teams_create_task',
|
|
1037
|
-
description: 'Create a task in your team\'s task list. Every call must include a non-empty subject, including verification and review tasks. Tasks can depend on other tasks (dependencies): a task is only claimable once every dependency is completed. Optionally assign it to a member, who still claims it before working.',
|
|
1050
|
+
description: 'Create a task in your team\'s task list. Use kind=work (default) for research, repository audits and general tasks. kind=review is only a quality gate for an existing implementation/repair task via reviewedTaskId. Every call must include a non-empty subject, including verification and review tasks. Tasks can depend on other tasks (dependencies): a task is only claimable once every dependency is completed. Optionally assign it to a member, who still claims it before working.',
|
|
1038
1051
|
parameters: {
|
|
1039
1052
|
subject: { type: 'string', required: true, description: 'Required non-empty title for this task. Never omit it, including for verification or review tasks.' },
|
|
1040
1053
|
description: { type: 'string', description: 'What needs to be done, in detail.' },
|
|
@@ -1043,11 +1056,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1043
1056
|
items: { type: 'string' },
|
|
1044
1057
|
description: 'Task ids this task depends on (must be completed before this task can be claimed).',
|
|
1045
1058
|
},
|
|
1046
|
-
assignee: { type: 'string', description: '
|
|
1059
|
+
assignee: { type: 'string', description: 'Member name when an owner is specified. Omission puts this task in the shared pool; roles, subjects and descriptions do not assign an owner.' },
|
|
1047
1060
|
kind: {
|
|
1048
1061
|
type: 'string',
|
|
1049
1062
|
enum: ['work', 'requirements', 'implementation', 'verification', 'review', 'repair', 'integration'],
|
|
1050
|
-
description: '
|
|
1063
|
+
description: 'Explicit task kind. Omission means work with no quality gates, even if the subject says implementation/review. Quality kinds require a contract. An implementation may be planned before requirements passes when its dependency chain includes that requirements task.',
|
|
1051
1064
|
},
|
|
1052
1065
|
round: { type: 'number', description: '1-based review / requirements / repair round.' },
|
|
1053
1066
|
objective: { type: 'string', description: 'Required non-empty objective for quality kinds.' },
|
|
@@ -1057,7 +1070,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1057
1070
|
verify: { type: 'array', items: { type: 'string' }, description: 'Verification commands. Required for implementation/repair.' },
|
|
1058
1071
|
deliverables: { type: 'array', items: { type: 'string' }, description: 'Expected deliverable paths or names.' },
|
|
1059
1072
|
nonGoals: { type: 'array', items: { type: 'string' }, description: 'Explicit non-goals.' },
|
|
1060
|
-
reviewedTaskId: { type: 'string', description: '
|
|
1073
|
+
reviewedTaskId: { type: 'string', description: 'Existing AgentTeams implementation/repair task id. Required for kind=review; an external repository audit is kind=work.' },
|
|
1061
1074
|
sourceTaskId: { type: 'string', description: 'Source implementation/artifact. Required for kind=repair.' },
|
|
1062
1075
|
sourceFindingIds: { type: 'array', items: { type: 'string' }, description: 'Finding ids this repair must close.' },
|
|
1063
1076
|
coverageOf: { type: 'array', items: { type: 'string' }, description: 'User-constraint / goal items this task covers.' },
|
|
@@ -1072,12 +1085,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1072
1085
|
task_id: { type: 'string', required: true },
|
|
1073
1086
|
subject: { type: 'string', required: true },
|
|
1074
1087
|
status: { type: 'string', required: true },
|
|
1088
|
+
kind: { type: 'string', required: true },
|
|
1075
1089
|
assignee: { type: 'string' },
|
|
1076
1090
|
},
|
|
1077
1091
|
},
|
|
1078
1092
|
render: (args, value) => [{
|
|
1079
1093
|
type: 'text',
|
|
1080
|
-
text: `Task "${value.subject}" created as ${value.task_id} (status ${value.status}${value.assignee ?
|
|
1094
|
+
text: `Task "${value.subject}" created as ${value.task_id} (status ${value.status}, kind ${value.kind}, ${value.assignee ? `assigned to ${value.assignee}` : 'unassigned shared pool'}).`,
|
|
1081
1095
|
}],
|
|
1082
1096
|
},
|
|
1083
1097
|
async execute(args, exec) {
|
|
@@ -1182,6 +1196,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1182
1196
|
task_id: task.id,
|
|
1183
1197
|
subject: task.subject,
|
|
1184
1198
|
status: task.status,
|
|
1199
|
+
kind: taskKindOf(task),
|
|
1185
1200
|
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
1186
1201
|
};
|
|
1187
1202
|
});
|
|
@@ -1228,8 +1243,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1228
1243
|
const task = requireTask(fresh, args.task_id);
|
|
1229
1244
|
if (task.status === 'completed')
|
|
1230
1245
|
throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
|
|
1231
|
-
if (task.reassigning === true)
|
|
1232
|
-
|
|
1246
|
+
if (task.reassigning === true) {
|
|
1247
|
+
const previousMember = fresh.members.find(member => member.id === task.handoffFromMemberId && member.stopping === true);
|
|
1248
|
+
if (task.assignee !== target || previousMember === undefined)
|
|
1249
|
+
throw new Error(`task ${task.id} is already being reassigned`);
|
|
1250
|
+
return { previousAssignee: previousMember.name, previousMember: { ...previousMember }, handoffId: task.handoffId };
|
|
1251
|
+
}
|
|
1233
1252
|
const targetMember = target === CAPTAIN_KEY ? undefined : requireMember(fresh, target);
|
|
1234
1253
|
if (target === CAPTAIN_KEY) {
|
|
1235
1254
|
const busy = captainOpenTask(fresh, task.id);
|
|
@@ -1253,6 +1272,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1253
1272
|
? undefined
|
|
1254
1273
|
: fresh.members.find(member => member.name === task.assignee && member.status !== 'removed');
|
|
1255
1274
|
invalidateTaskAttempt(task, target, true);
|
|
1275
|
+
if (previousMember !== undefined) {
|
|
1276
|
+
previousMember.stopping = true;
|
|
1277
|
+
task.handoffFromMemberId = previousMember.id;
|
|
1278
|
+
await discardMailboxMessages(stateRoot, fresh.id, previousMember.name, (await readUnreadMailbox(stateRoot, fresh.id, previousMember.name)).map(message => message.id));
|
|
1279
|
+
}
|
|
1256
1280
|
await writeTeam(stateRoot, fresh);
|
|
1257
1281
|
return {
|
|
1258
1282
|
previousAssignee,
|
|
@@ -1262,9 +1286,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1262
1286
|
});
|
|
1263
1287
|
let quiescenceError;
|
|
1264
1288
|
if (revoked.previousMember !== undefined) {
|
|
1265
|
-
interruptMember(ctx, captain, revoked.previousMember.id);
|
|
1266
1289
|
try {
|
|
1267
|
-
await
|
|
1290
|
+
await stopTeamMemberActivations(ctx, captain, [revoked.previousMember], exec.signal);
|
|
1268
1291
|
}
|
|
1269
1292
|
catch (error) {
|
|
1270
1293
|
quiescenceError = error;
|
|
@@ -1276,7 +1299,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1276
1299
|
if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
|
|
1277
1300
|
throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
|
|
1278
1301
|
}
|
|
1279
|
-
task.reassigning =
|
|
1302
|
+
task.reassigning = quiescenceError !== undefined;
|
|
1303
|
+
if (quiescenceError === undefined) {
|
|
1304
|
+
const previous = fresh.members.find(member => member.id === revoked.previousMember?.id);
|
|
1305
|
+
if (previous !== undefined)
|
|
1306
|
+
delete previous.stopping;
|
|
1307
|
+
delete task.handoffFromMemberId;
|
|
1308
|
+
}
|
|
1280
1309
|
if (quiescenceError === undefined && target === CAPTAIN_KEY) {
|
|
1281
1310
|
beginTaskAttempt(task, CAPTAIN_KEY);
|
|
1282
1311
|
// The captain is already in the turn that requested takeover; there
|
|
@@ -1328,11 +1357,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1328
1357
|
assignee: { type: 'string', required: true },
|
|
1329
1358
|
attempt: { type: 'number', required: true },
|
|
1330
1359
|
attempt_id: { type: 'string' },
|
|
1360
|
+
task_details: { type: 'string', required: true },
|
|
1331
1361
|
},
|
|
1332
1362
|
},
|
|
1333
1363
|
render: (args, value) => [{
|
|
1334
1364
|
type: 'text',
|
|
1335
|
-
text: `Task ${value.task_id} claimed by ${value.assignee} (attempt ${value.attempt}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}, status ${value.status})
|
|
1365
|
+
text: `Task ${value.task_id} claimed by ${value.assignee} (attempt ${value.attempt}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}, status ${value.status}).\n${value.task_details}`,
|
|
1336
1366
|
}],
|
|
1337
1367
|
},
|
|
1338
1368
|
async execute(args, exec) {
|
|
@@ -1371,6 +1401,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1371
1401
|
throw new Error(`task ${task.id} is already claimed by "${task.assignee ?? 'nobody'}"`);
|
|
1372
1402
|
}
|
|
1373
1403
|
return {
|
|
1404
|
+
task_details: taskDetails(fresh, task),
|
|
1374
1405
|
task_id: task.id,
|
|
1375
1406
|
status: task.status,
|
|
1376
1407
|
assignee,
|
|
@@ -1401,6 +1432,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1401
1432
|
assignee: task.assignee,
|
|
1402
1433
|
});
|
|
1403
1434
|
return {
|
|
1435
|
+
task_details: taskDetails(fresh, task),
|
|
1404
1436
|
task_id: task.id,
|
|
1405
1437
|
status: task.status,
|
|
1406
1438
|
assignee: task.assignee ?? '',
|
|
@@ -1415,13 +1447,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1415
1447
|
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.',
|
|
1416
1448
|
parameters: {
|
|
1417
1449
|
task_id: { type: 'string', required: true, description: 'The task id to update.' },
|
|
1450
|
+
attempt_id: { type: 'string', description: 'Members must explicitly include the current attempt_id from their assignment/claim in EVERY update, including failed reviews with findings. If omitted, retry with the same current id; omission does not revoke the attempt.' },
|
|
1418
1451
|
status: {
|
|
1419
1452
|
type: 'string',
|
|
1420
1453
|
enum: ['in_progress', 'completed', 'failed', 'cancelled'],
|
|
1421
1454
|
description: 'New status (in_progress, completed, failed, cancelled).',
|
|
1422
1455
|
},
|
|
1423
1456
|
output: { type: 'string', description: 'Result summary; set when completing or failing.' },
|
|
1424
|
-
attempt_id: { type: 'string', description: 'Current execution capability returned by claim_task (required for members when present on the task).' },
|
|
1425
1457
|
verdict: {
|
|
1426
1458
|
type: 'string',
|
|
1427
1459
|
enum: ['pass', 'needs_revision', 'reject'],
|
|
@@ -1504,13 +1536,17 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1504
1536
|
const task = requireTask(fresh, args.task_id);
|
|
1505
1537
|
if (identity.kind === 'captain'
|
|
1506
1538
|
&& task.assignee !== undefined
|
|
1507
|
-
&& task.assignee !== CAPTAIN_KEY
|
|
1539
|
+
&& task.assignee !== CAPTAIN_KEY
|
|
1540
|
+
&& !(args.status === 'cancelled' && task.status === 'pending' && (task.attempt ?? 0) === 0 && task.reassigning !== true)) {
|
|
1508
1541
|
throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call agent_teams_reassign_task with assignee="captain" before takeover`);
|
|
1509
1542
|
}
|
|
1510
1543
|
if (identity.kind === 'member') {
|
|
1511
1544
|
if (task.assignee !== identity.name) {
|
|
1512
1545
|
throw new Error(`task ${task.id} is assigned to "${task.assignee ?? 'nobody'}", not you`);
|
|
1513
1546
|
}
|
|
1547
|
+
if (task.attemptId !== undefined && (args.attempt_id === undefined || args.attempt_id.trim() === '')) {
|
|
1548
|
+
throw new Error(`missing attempt_id for task ${task.id}. Retry this update with attempt_id="${task.attemptId}" from your current assignment. This is a missing parameter, not a revoked attempt; do not restart the work or request reassignment.`);
|
|
1549
|
+
}
|
|
1514
1550
|
if (task.attemptId !== undefined && args.attempt_id !== task.attemptId) {
|
|
1515
1551
|
throw new Error(`stale attempt for task ${task.id}: expected the current attempt_id; stop work and request fresh assignment`);
|
|
1516
1552
|
}
|
|
@@ -1607,7 +1643,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1607
1643
|
}));
|
|
1608
1644
|
ctx.tools.register(defineTool({
|
|
1609
1645
|
name: 'agent_teams_send_message',
|
|
1610
|
-
description: 'Send
|
|
1646
|
+
description: 'Send coordination or current-task guidance directly to the captain or a teammate. A running recipient receives it at the next model step; an idle recipient wakes. Messages are durably retained until read. Use task creation/reassignment for a new unit of work, not repeated status nudges.',
|
|
1611
1647
|
parameters: {
|
|
1612
1648
|
to: { type: 'string', required: true, description: 'Recipient: "captain" or a member name.' },
|
|
1613
1649
|
content: { type: 'string', required: true, description: 'The message text.' },
|
|
@@ -1660,7 +1696,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1660
1696
|
throw new Error(`team "${fresh.name}" is halted; call agent_teams_resume before waking a member`);
|
|
1661
1697
|
}
|
|
1662
1698
|
const recipient = requireMember(fresh, to);
|
|
1663
|
-
const
|
|
1699
|
+
const owned = memberOpenTask(fresh, recipient.name);
|
|
1700
|
+
const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now(),
|
|
1701
|
+
...owned?.attemptId === undefined ? {} : { taskId: owned.id, attemptId: owned.attemptId },
|
|
1702
|
+
};
|
|
1664
1703
|
await appendMailbox(stateRoot, fresh.id, recipient.name, message);
|
|
1665
1704
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/message-sent', {
|
|
1666
1705
|
teamId: fresh.id,
|
|
@@ -1678,10 +1717,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1678
1717
|
if (prepared.kind === 'captain') {
|
|
1679
1718
|
let delivered = 'mailbox';
|
|
1680
1719
|
if (captain !== undefined && prepared.identity.kind === 'member') {
|
|
1681
|
-
delivered = steerCaptainReport(captain, prepared.from, args.content) ? 'live' : 'mailbox';
|
|
1720
|
+
delivered = steerCaptainReport(captain, prepared.from, args.content, mailboxPrompt(prepared.fresh.id, CAPTAIN_KEY, [prepared.message])) ? 'live' : 'mailbox';
|
|
1682
1721
|
}
|
|
1683
1722
|
if (delivered === 'live') {
|
|
1684
|
-
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (
|
|
1723
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (markMailboxDelivered(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
|
|
1685
1724
|
}
|
|
1686
1725
|
else {
|
|
1687
1726
|
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (releaseMailboxDelivery(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
|
|
@@ -1689,15 +1728,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1689
1728
|
return { message_id: prepared.message.id, from: prepared.from, to: CAPTAIN_KEY, delivered };
|
|
1690
1729
|
}
|
|
1691
1730
|
let delivered = 'mailbox';
|
|
1692
|
-
if (captain !== undefined
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1695
|
-
: `Message from team member ${prepared.from}:\n\n${args.content}`;
|
|
1696
|
-
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}`;
|
|
1697
|
-
const accepted = await deliverToMember(ctx, captain, prepared.recipient.id, text, exec.signal);
|
|
1731
|
+
if (captain !== undefined) {
|
|
1732
|
+
const text = mailboxPrompt(prepared.fresh.id, prepared.recipient.name, [prepared.message]);
|
|
1733
|
+
const accepted = await dispatchMember(captain, prepared.fresh.id, prepared.recipient.name, text, exec.signal, 'steer', prepared.message.attemptId);
|
|
1698
1734
|
delivered = accepted ? 'wake' : 'mailbox';
|
|
1699
1735
|
if (accepted) {
|
|
1700
|
-
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (
|
|
1736
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (markMailboxDelivered(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
|
|
1701
1737
|
}
|
|
1702
1738
|
}
|
|
1703
1739
|
if (delivered === 'mailbox') {
|
|
@@ -1767,6 +1803,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1767
1803
|
const captainInbox = identity.kind === 'captain'
|
|
1768
1804
|
? await readUnreadMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
|
|
1769
1805
|
: [];
|
|
1806
|
+
const ownInbox = identity.kind === 'member' ? (await readUnreadMailbox(stateRoot, team.id, identity.name)).slice(0, 10) : [];
|
|
1770
1807
|
const memberInboxes = {};
|
|
1771
1808
|
const visibleMembers = identity.kind === 'captain'
|
|
1772
1809
|
? members
|
|
@@ -1813,18 +1850,19 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1813
1850
|
viewer: identity.name,
|
|
1814
1851
|
members,
|
|
1815
1852
|
tasks,
|
|
1816
|
-
captain_inbox: captainInbox.slice(
|
|
1853
|
+
captain_inbox: captainInbox.slice(0, 10).map((message) => ({
|
|
1817
1854
|
from: message.from,
|
|
1818
1855
|
content: message.content,
|
|
1819
1856
|
ts: message.ts,
|
|
1820
1857
|
})),
|
|
1858
|
+
member_inbox: ownInbox.map(message => ({ from: message.from, content: message.content, ts: message.ts })),
|
|
1821
1859
|
member_inboxes: memberInboxes,
|
|
1822
1860
|
mailbox_warnings: mailboxWarnings,
|
|
1823
1861
|
mailbox_warning_count: mailboxWarningCount,
|
|
1824
1862
|
};
|
|
1825
1863
|
const acknowledged = identity.kind === 'captain'
|
|
1826
|
-
? captainInbox.map(message => message.id)
|
|
1827
|
-
:
|
|
1864
|
+
? captainInbox.slice(0, 10).map(message => message.id)
|
|
1865
|
+
: ownInbox.map(message => message.id);
|
|
1828
1866
|
if (acknowledged.length > 0) {
|
|
1829
1867
|
await withTeamLock(teamLockKey(stateRoot, team.id), () => (acknowledgeMailbox(stateRoot, team.id, identity.kind === 'captain' ? CAPTAIN_KEY : identity.name, acknowledged)));
|
|
1830
1868
|
}
|
|
@@ -1913,6 +1951,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1913
1951
|
// retires durable catalog entries left behind by remove_member.
|
|
1914
1952
|
const roster = fresh.members.map(member => ({ ...member }));
|
|
1915
1953
|
for (const member of fresh.members) {
|
|
1954
|
+
await discardMailboxMessages(stateRoot, fresh.id, member.name, (await readUnreadMailbox(stateRoot, fresh.id, member.name)).map(message => message.id));
|
|
1916
1955
|
if (member.status === 'removed')
|
|
1917
1956
|
continue;
|
|
1918
1957
|
member.status = 'removed';
|
|
@@ -1925,17 +1964,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1925
1964
|
return roster;
|
|
1926
1965
|
});
|
|
1927
1966
|
await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
|
|
1928
|
-
|
|
1929
|
-
if (member.id === '')
|
|
1930
|
-
continue;
|
|
1931
|
-
interruptMember(ctx, captain, member.id);
|
|
1932
|
-
}
|
|
1933
|
-
const quiescence = await Promise.allSettled(members.map(member => waitForMemberIdle(ctx, member, exec.signal)));
|
|
1934
|
-
for (const result of quiescence) {
|
|
1935
|
-
if (result.status === 'rejected') {
|
|
1936
|
-
ctx.logger.warn(`agent-teams: member did not quiesce cleanly before team archive: ${String(result.reason)}`);
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1967
|
+
await stopTeamMemberActivations(ctx, captain, members, exec.signal);
|
|
1939
1968
|
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1940
1969
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1941
1970
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/team-deleted', {
|
|
@@ -1951,7 +1980,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1951
1980
|
return runtime;
|
|
1952
1981
|
}
|
|
1953
1982
|
async function initializeProfileTeam(input) {
|
|
1954
|
-
const profile = resolveTeamProfile(input.config.profiles, input.profileName, input.config.maxMembers);
|
|
1983
|
+
const profile = resolveTeamProfile(input.inlinePlan === undefined ? input.config.profiles : { [input.profileName]: input.inlinePlan }, input.profileName, input.config.maxMembers);
|
|
1955
1984
|
const selections = [];
|
|
1956
1985
|
for (const template of profile.members) {
|
|
1957
1986
|
selections.push(await resolveMemberLlmSelection(input.ctx, input.captain, {
|
|
@@ -2011,50 +2040,12 @@ async function initializeProfileTeam(input) {
|
|
|
2011
2040
|
})),
|
|
2012
2041
|
taskSeq: profile.tasks.length,
|
|
2013
2042
|
};
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
for (const member of draft.members) {
|
|
2021
|
-
const selection = selections[spawned.length];
|
|
2022
|
-
await spawnMember(input.ctx, memberRuntime(input.config), input.memberSelections, selection, input.captain, draft, member, input.config.stateDir, input.exec.signal);
|
|
2023
|
-
spawned.push(member);
|
|
2024
|
-
}
|
|
2025
|
-
if (draft.members.some((member) => member.id === '')) {
|
|
2026
|
-
throw new Error(`failed to initialize profile "${profile.name}": a spawned member is missing its child id`);
|
|
2027
|
-
}
|
|
2028
|
-
await createTeamDir(input.stateRoot, draft);
|
|
2029
|
-
return { committed: true, state: draft };
|
|
2030
|
-
}
|
|
2031
|
-
catch (error) {
|
|
2032
|
-
const cleanupErrors = [];
|
|
2033
|
-
try {
|
|
2034
|
-
await removeTeamDir(input.stateRoot, draft.id);
|
|
2035
|
-
}
|
|
2036
|
-
catch (cleanupError) {
|
|
2037
|
-
cleanupErrors.push(cleanupError);
|
|
2038
|
-
}
|
|
2039
|
-
try {
|
|
2040
|
-
await recordRetiredMemberIds(input.stateRoot, spawned.map((member) => member.id));
|
|
2041
|
-
}
|
|
2042
|
-
catch (cleanupError) {
|
|
2043
|
-
cleanupErrors.push(cleanupError);
|
|
2044
|
-
}
|
|
2045
|
-
for (const member of spawned) {
|
|
2046
|
-
try {
|
|
2047
|
-
interruptMember(input.ctx, input.captain, member.id);
|
|
2048
|
-
}
|
|
2049
|
-
catch (cleanupError) {
|
|
2050
|
-
cleanupErrors.push(cleanupError);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
if (cleanupErrors.length > 0) {
|
|
2054
|
-
throw new AggregateError([error, ...cleanupErrors], `failed to initialize profile "${profile.name}"`);
|
|
2055
|
-
}
|
|
2056
|
-
throw error;
|
|
2057
|
-
}
|
|
2043
|
+
// Roster creation is durable planning only. The scheduler starts each
|
|
2044
|
+
// member with its first actual task once its dependencies are satisfied.
|
|
2045
|
+
if (input.inlinePlan !== undefined)
|
|
2046
|
+
delete draft.profile;
|
|
2047
|
+
await createTeamDir(input.stateRoot, draft);
|
|
2048
|
+
return { committed: true, state: draft };
|
|
2058
2049
|
}
|
|
2059
2050
|
function parseFindings(value) {
|
|
2060
2051
|
if (value === undefined)
|
|
@@ -2244,7 +2235,8 @@ function renderStatus(value) {
|
|
|
2244
2235
|
`Delivery: ${team.delivery.ok ? 'ok' : `blocked (${team.delivery.blockers.join('; ')})`}`,
|
|
2245
2236
|
],
|
|
2246
2237
|
`Captain inbox (${team.captain_inbox.length}):`,
|
|
2247
|
-
...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content
|
|
2238
|
+
...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content}`),
|
|
2239
|
+
...(team.member_inbox ?? []).map(message => ` - [${message.from}] ${message.content}`),
|
|
2248
2240
|
];
|
|
2249
2241
|
for (const [name, inbox] of Object.entries(team.member_inboxes)) {
|
|
2250
2242
|
lines.push(`Member inbox ${name} (${inbox.count}): latest — ${inbox.latest.slice(0, 120)}`);
|