@nanmicoder/dsh-agent-teams 0.1.22-rc.1 → 0.1.23
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 +35 -12
- package/README_ZH.md +35 -12
- package/compatibility.json +6 -10
- package/lib/artifacts.js +342 -0
- package/lib/capabilities.js +6 -4
- package/lib/client/ActivityPanel.js +5 -2
- package/lib/client/agent-teams-card-definition.js +100 -6
- package/lib/client.js +90 -7
- package/lib/client.js.map +1 -1
- package/lib/command.js +3 -2
- package/lib/git-artifact-stamp.json +2 -2
- package/lib/harness-compat.js +70 -21
- package/lib/index.js +26 -3
- package/lib/lineage.js +198 -0
- package/lib/mailbox.js +59 -34
- package/lib/members.js +301 -53
- package/lib/profiles.js +3 -2
- package/lib/quality-gates.js +850 -65
- package/lib/scheduler.js +22 -3
- package/lib/snapshot.js +22 -1
- package/lib/state.js +279 -13
- package/lib/tool-policies.js +187 -0
- package/lib/tools.js +370 -89
- package/lib/types/artifacts.d.ts +66 -0
- package/lib/types/client/activity-monitor.d.ts +6 -0
- package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
- package/lib/types/harness-compat.d.ts +56 -14
- package/lib/types/index.d.ts +26 -1
- package/lib/types/lineage.d.ts +92 -0
- package/lib/types/members.d.ts +112 -1
- package/lib/types/profiles.d.ts +6 -0
- package/lib/types/quality-gates.d.ts +216 -10
- package/lib/types/scheduler.d.ts +2 -1
- package/lib/types/snapshot.d.ts +20 -0
- package/lib/types/state.d.ts +22 -5
- package/lib/types/tool-policies.d.ts +119 -0
- package/lib/types/tools.d.ts +12 -1
- package/lib/types/types.d.ts +95 -0
- package/lib/types/workspace-audit.d.ts +27 -0
- package/lib/workspace-audit.js +52 -0
- package/package.json +59 -52
- package/pnpm-workspace.yaml +273 -273
- package/release-notes/v0.1.22.md +18 -0
- package/release-notes/v0.1.23.md +34 -0
- package/scripts/doctor.mjs +4 -4
- package/scripts/legacy-session-source-repair.mjs +344 -0
package/lib/tools.js
CHANGED
|
@@ -12,13 +12,19 @@ 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 { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, readMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
15
|
+
import { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, readMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, isValidFindingLine, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
16
16
|
import { appendTaskEvidence } from "./quality-gates.js";
|
|
17
|
+
import { artifactDrift, bindReviewToRevision, isQualityKind, verdictScopePatterns, } from "./quality-gates.js";
|
|
18
|
+
import { auditTeamWorkspace } from "./workspace-audit.js";
|
|
19
|
+
import { readArtifactScope, snapshotWorkspacePaths, workspacePathExists } from "./artifacts.js";
|
|
20
|
+
import { restrictableToolNames } from "./harness-compat.js";
|
|
21
|
+
import { memberSpawnRelaxableDeny, optionalToolPolicy, requireToolPolicy, resolveMemberToolFilter } from "./tool-policies.js";
|
|
17
22
|
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
18
23
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
19
24
|
import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
|
|
20
25
|
import { installMailboxAdmission, isCurrentMail, mailboxContent, mailboxPrompt, readCurrentMailbox } from "./mailbox.js";
|
|
21
26
|
import { resolveTeamProfile } from "./profiles.js";
|
|
27
|
+
import { forkLineageNote, leadIdentityOfAsync, nearestLiveLead, sessionLeadsTeamAsync } from "./lineage.js";
|
|
22
28
|
export { steerCaptainReport } from "./members.js";
|
|
23
29
|
/** The caller agent, or a loud failure for non-agent callers. */
|
|
24
30
|
function requireCaptain(exec) {
|
|
@@ -39,29 +45,64 @@ function stateRootOf(workspace, config) {
|
|
|
39
45
|
function teamLockKey(stateRoot, teamId) {
|
|
40
46
|
return `team:${stateRoot}:${teamId}`;
|
|
41
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Bind a closing verdict to the revision it judged, using the snapshot taken
|
|
50
|
+
* when its attempt started (if any) to notice that the workspace changed while
|
|
51
|
+
* the gate was being formed. A verdict that ran over a moving workspace binds
|
|
52
|
+
* the drift explicitly instead of claiming the current bytes.
|
|
53
|
+
*/
|
|
54
|
+
function bindClosingVerdict(workspace, team, task) {
|
|
55
|
+
if (task.status !== 'completed')
|
|
56
|
+
return;
|
|
57
|
+
const kind = taskKindOf(task);
|
|
58
|
+
if (kind !== 'review' && kind !== 'requirements' && kind !== 'verification')
|
|
59
|
+
return;
|
|
60
|
+
if ((kind === 'review' || kind === 'requirements') && task.verdict !== 'pass')
|
|
61
|
+
return;
|
|
62
|
+
const scope = verdictScopePatterns(team, task);
|
|
63
|
+
if (scope.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
const read = readArtifactScope(workspace, scope);
|
|
66
|
+
const duringDrift = task.verdictStartSnapshot === undefined
|
|
67
|
+
? []
|
|
68
|
+
: artifactDrift(task.verdictStartSnapshot.artifacts, read.artifacts);
|
|
69
|
+
const bound = bindReviewToRevision(team, task, {
|
|
70
|
+
artifacts: read.artifacts,
|
|
71
|
+
gaps: read.gaps,
|
|
72
|
+
scope,
|
|
73
|
+
duringDrift,
|
|
74
|
+
});
|
|
75
|
+
task.reviewedTaskId = bound.reviewedTaskId;
|
|
76
|
+
task.reviewedAttempt = bound.reviewedAttempt;
|
|
77
|
+
task.reviewedArtifacts = bound.reviewedArtifacts;
|
|
78
|
+
task.reviewedGaps = bound.reviewedGaps;
|
|
79
|
+
task.reviewedScope = bound.reviewedScope;
|
|
80
|
+
task.reviewedDuringDrift = bound.reviewedDuringDrift;
|
|
81
|
+
task.verdictStartSnapshot = undefined;
|
|
82
|
+
}
|
|
42
83
|
/** Process-local lock key enforcing one active team per captain session. */
|
|
43
84
|
function captainLockKey(stateRoot, captainId) {
|
|
44
85
|
return `captain:${stateRoot}:${captainId}`;
|
|
45
86
|
}
|
|
46
87
|
/** The team this captain currently leads, or a loud failure. */
|
|
47
|
-
async function requireCaptainTeam(workspace, config, captain) {
|
|
48
|
-
const team = await findTeamByCaptain(stateRootOf(workspace, config), captain.id);
|
|
88
|
+
async function requireCaptainTeam(ctx, workspace, config, captain) {
|
|
89
|
+
const team = await findTeamByCaptain(stateRootOf(workspace, config), captain.id, await leadIdentityOfAsync(ctx, captain.session));
|
|
49
90
|
if (team === undefined) {
|
|
50
|
-
throw new Error(
|
|
91
|
+
throw new Error(`you are not leading any team yet — call agent_teams_create first${forkLineageNote(captain.session)}`);
|
|
51
92
|
}
|
|
52
93
|
return team;
|
|
53
94
|
}
|
|
54
95
|
/** The team this captain or active member currently participates in. */
|
|
55
|
-
async function requireParticipantTeam(workspace, config, caller) {
|
|
56
|
-
const team = await findTeamByParticipant(stateRootOf(workspace, config), caller.id);
|
|
96
|
+
async function requireParticipantTeam(ctx, workspace, config, caller) {
|
|
97
|
+
const team = await findTeamByParticipant(stateRootOf(workspace, config), caller.id, await leadIdentityOfAsync(ctx, caller.session));
|
|
57
98
|
if (team === undefined) {
|
|
58
|
-
throw new Error(
|
|
99
|
+
throw new Error(`you do not lead or belong to any active team yet${forkLineageNote(caller.session)}`);
|
|
59
100
|
}
|
|
60
101
|
return team;
|
|
61
102
|
}
|
|
62
103
|
/** Re-derive a caller's role from fresh state while holding the team lock. */
|
|
63
|
-
function participantIdentityOf(team, agentId) {
|
|
64
|
-
if (team
|
|
104
|
+
async function participantIdentityOf(ctx, team, agentId) {
|
|
105
|
+
if (await sessionLeadsTeamAsync(ctx, team, agentId))
|
|
65
106
|
return { kind: 'captain', name: CAPTAIN_KEY };
|
|
66
107
|
const member = team.members.find((candidate) => candidate.id === agentId && candidate.status !== 'removed');
|
|
67
108
|
return member === undefined ? undefined : { kind: 'member', name: member.name };
|
|
@@ -74,17 +115,17 @@ async function requireFreshTeam(stateRoot, teamId) {
|
|
|
74
115
|
return fresh;
|
|
75
116
|
}
|
|
76
117
|
/** Fresh state with captain authorization rechecked inside the lock. */
|
|
77
|
-
async function requireFreshCaptainTeam(stateRoot, teamId, captainId) {
|
|
118
|
+
async function requireFreshCaptainTeam(ctx, stateRoot, teamId, captainId) {
|
|
78
119
|
const fresh = await requireFreshTeam(stateRoot, teamId);
|
|
79
|
-
if (fresh
|
|
80
|
-
throw new Error(`only the captain of team "${fresh.name}" may perform this operation`);
|
|
120
|
+
if (!(await sessionLeadsTeamAsync(ctx, fresh, captainId))) {
|
|
121
|
+
throw new Error(`only the captain of team "${fresh.name}" (session ${fresh.captainSessionId}) or one of its fork descendants may perform this operation`);
|
|
81
122
|
}
|
|
82
123
|
return fresh;
|
|
83
124
|
}
|
|
84
125
|
/** Fresh state and caller identity rechecked inside the lock. */
|
|
85
|
-
async function requireFreshParticipant(stateRoot, teamId, callerId) {
|
|
126
|
+
async function requireFreshParticipant(ctx, stateRoot, teamId, callerId) {
|
|
86
127
|
const fresh = await requireFreshTeam(stateRoot, teamId);
|
|
87
|
-
const identity = participantIdentityOf(fresh, callerId);
|
|
128
|
+
const identity = await participantIdentityOf(ctx, fresh, callerId);
|
|
88
129
|
if (identity === undefined)
|
|
89
130
|
throw new Error(`you are no longer an active participant in team "${fresh.name}"`);
|
|
90
131
|
return { team: fresh, identity };
|
|
@@ -116,6 +157,48 @@ function trimmedOptional(value) {
|
|
|
116
157
|
const trimmed = value?.trim();
|
|
117
158
|
return trimmed === undefined || trimmed === '' ? undefined : trimmed;
|
|
118
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve one member's selected tool policy into the persisted policy id and
|
|
162
|
+
* the immutable effective filter (#59).
|
|
163
|
+
*
|
|
164
|
+
* An unknown id fails here — before the member is written and before any child
|
|
165
|
+
* is created — so a typo can never persist a half-configured member or leave a
|
|
166
|
+
* resumable orphan behind. A member without a policy resolves to `{}`: the
|
|
167
|
+
* previous behavior and record shape stay untouched.
|
|
168
|
+
* @param config - resolved plugin config (declared policies + depth cap).
|
|
169
|
+
* @param captain - the live captain, whose registry view names the host tools.
|
|
170
|
+
* @param policyId - the selected policy id, or undefined for no policy.
|
|
171
|
+
* @returns the member fields to persist (`toolPolicy` + `toolFilter`), or `{}`.
|
|
172
|
+
*/
|
|
173
|
+
function memberToolSelection(config, captain, policyId) {
|
|
174
|
+
if (policyId === undefined)
|
|
175
|
+
return {};
|
|
176
|
+
const policy = requireToolPolicy(config.toolPolicies, policyId);
|
|
177
|
+
return {
|
|
178
|
+
toolPolicy: policyId,
|
|
179
|
+
toolFilter: resolveMemberToolFilter(policy, {
|
|
180
|
+
maxDepth: config.memberMaxDepth,
|
|
181
|
+
knownTools: restrictableToolNames(captain),
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Deny names the lenient spawn retry may drop for one member.
|
|
187
|
+
*
|
|
188
|
+
* A member without a policy keeps the built-in #163/#164 recovery path. A
|
|
189
|
+
* policy member may only relax built-in entries the owner did not declare
|
|
190
|
+
* itself (`memberSpawnRelaxableDeny`), and a selected policy id this
|
|
191
|
+
* deployment no longer declares relaxes nothing: the member then fails loud
|
|
192
|
+
* instead of spawning under a different filter.
|
|
193
|
+
* @param config - resolved plugin config.
|
|
194
|
+
* @param member - the member being spawned.
|
|
195
|
+
* @returns the relaxable deny names, or undefined for the legacy default.
|
|
196
|
+
*/
|
|
197
|
+
function memberRelaxableDeny(config, member) {
|
|
198
|
+
if (member.toolPolicy === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
return memberSpawnRelaxableDeny(optionalToolPolicy(config.toolPolicies, member.toolPolicy), true);
|
|
201
|
+
}
|
|
119
202
|
/** Validate references and cycles before a staged graph can be saved or run. */
|
|
120
203
|
function validateStagedGraph(team, requireRunnable) {
|
|
121
204
|
const members = team.members.filter((member) => member.status !== 'removed');
|
|
@@ -183,6 +266,106 @@ function captainOpenTask(team, exceptTaskId) {
|
|
|
183
266
|
* boundary: it waits for the activation handles to release, so a child cannot
|
|
184
267
|
* keep executing after the captain-chat Stop control has reported success.
|
|
185
268
|
*/
|
|
269
|
+
/**
|
|
270
|
+
* The live Agent the host admits member operations from.
|
|
271
|
+
*
|
|
272
|
+
* Harness continuable delivery is authorized against the member's durable
|
|
273
|
+
* direct parent (`subagents.authorizeLineage`), which is the session that
|
|
274
|
+
* spawned it — the team's recorded captain. A captain acting from its own fork
|
|
275
|
+
* therefore commands members *through* that session:
|
|
276
|
+
*
|
|
277
|
+
* - while the recorded captain is resident, its Agent is used directly;
|
|
278
|
+
* - while it is cold, this resolves it through the public session controller
|
|
279
|
+
* (`workspaceRegistry.create` + `sessionController.create` with the existing
|
|
280
|
+
* session id — attach/resume only: no prompt, no model request, and the
|
|
281
|
+
* durable `captainSessionId` is never rewritten);
|
|
282
|
+
* - when the composition exposes neither service, or the resume fails, the
|
|
283
|
+
* operation fails with an explicit bounded error instead of migrating team
|
|
284
|
+
* ownership to a session the host would reject at delivery time.
|
|
285
|
+
*
|
|
286
|
+
* @param ctx - the plugin context (injects `agents`).
|
|
287
|
+
* @param team - current team state.
|
|
288
|
+
* @param caller - the captain-side session performing the operation.
|
|
289
|
+
* @returns the caller itself, or the live recorded captain session.
|
|
290
|
+
*/
|
|
291
|
+
async function memberOperationParent(ctx, team, caller) {
|
|
292
|
+
if (caller.id === team.captainSessionId)
|
|
293
|
+
return caller;
|
|
294
|
+
const resident = ctx.agents.get(team.captainSessionId);
|
|
295
|
+
if (resident !== undefined)
|
|
296
|
+
return resident;
|
|
297
|
+
const resumed = await resumeLeadAnchor(ctx, team, caller);
|
|
298
|
+
if (resumed !== undefined)
|
|
299
|
+
return resumed;
|
|
300
|
+
throw new Error(`member operations require the live captain session ${team.captainSessionId} that owns team "${team.name}"; this session is its fork and ${resumeUnavailableReason(ctx, team)}`);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Best-effort live anchor for scheduler calls: the recorded captain when
|
|
304
|
+
* resident, the resumed one when this composition can attach it, and
|
|
305
|
+
* `undefined` otherwise (the scheduler then keeps its existing
|
|
306
|
+
* registry-only behavior). Never throws, so mutating tools stay usable on
|
|
307
|
+
* compositions without a session controller.
|
|
308
|
+
* @param ctx - the plugin context (injects `agents`).
|
|
309
|
+
* @param team - the team whose recorded captain is the delivery parent.
|
|
310
|
+
* @param caller - the captain-side session performing the operation.
|
|
311
|
+
* @returns the anchor agent when one is live, else undefined.
|
|
312
|
+
*/
|
|
313
|
+
async function liveLeadAnchorAgent(ctx, team, caller) {
|
|
314
|
+
if (caller.id === team.captainSessionId)
|
|
315
|
+
return caller;
|
|
316
|
+
const resident = ctx.agents.get(team.captainSessionId);
|
|
317
|
+
if (resident !== undefined)
|
|
318
|
+
return resident;
|
|
319
|
+
return resumeLeadAnchor(ctx, team, caller);
|
|
320
|
+
}
|
|
321
|
+
/** Why the recorded captain could not be resumed, for one bounded error message. */
|
|
322
|
+
function resumeUnavailableReason(ctx, team) {
|
|
323
|
+
const services = optionalTeamServices(ctx);
|
|
324
|
+
if (services.controller === undefined || services.registry === undefined) {
|
|
325
|
+
return 'this composition exposes no session controller to resume it, so add_member, member delivery and member drain are unavailable from the fork';
|
|
326
|
+
}
|
|
327
|
+
return `resuming it through the session controller failed (its workspace is "${team.name}"'s workspace)`;
|
|
328
|
+
}
|
|
329
|
+
/** Public optional services used only to attach the recorded captain session. */
|
|
330
|
+
function optionalTeamServices(ctx) {
|
|
331
|
+
const get = ctx.get;
|
|
332
|
+
if (typeof get !== 'function')
|
|
333
|
+
return {};
|
|
334
|
+
const read = (name) => {
|
|
335
|
+
try {
|
|
336
|
+
return get.call(ctx, name);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
controller: read('sessionController'),
|
|
344
|
+
registry: read('workspaceRegistry'),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Attach the team's recorded captain session in this process without sending a
|
|
349
|
+
* prompt: the Harness controller's create-with-existing-id path resumes the
|
|
350
|
+
* session and leaves every durable field (including `captainSessionId`)
|
|
351
|
+
* untouched.
|
|
352
|
+
* @returns the resumed Agent, or undefined when this composition cannot resume it.
|
|
353
|
+
*/
|
|
354
|
+
async function resumeLeadAnchor(ctx, team, caller) {
|
|
355
|
+
const { controller, registry } = optionalTeamServices(ctx);
|
|
356
|
+
if (controller?.create === undefined || registry?.create === undefined)
|
|
357
|
+
return undefined;
|
|
358
|
+
const cwd = caller.session.header.cwd ?? process.cwd();
|
|
359
|
+
try {
|
|
360
|
+
const workspace = await registry.create(cwd, cwd);
|
|
361
|
+
await controller.create({ sessionId: team.captainSessionId, workspaceId: workspace.id, agentPreset: 'standard' });
|
|
362
|
+
return ctx.agents.get(team.captainSessionId);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
ctx.logger.warn(`agent-teams: could not resume the recorded captain session ${team.captainSessionId}: ${String(error)}`);
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
186
369
|
async function stopTeamMemberActivations(ctx, captain, members, signal) {
|
|
187
370
|
const memberIds = members.filter(member => member.id !== '').map(member => member.id);
|
|
188
371
|
if (memberIds.length === 0)
|
|
@@ -194,10 +377,11 @@ async function stopTeamMemberActivations(ctx, captain, members, signal) {
|
|
|
194
377
|
}
|
|
195
378
|
export async function haltTeamWork(input) {
|
|
196
379
|
const halted = await withTeamLock(teamLockKey(input.stateRoot, input.teamId), async () => {
|
|
197
|
-
const fresh = await requireFreshCaptainTeam(input.stateRoot, input.teamId, input.captain.id);
|
|
380
|
+
const fresh = await requireFreshCaptainTeam(input.ctx, input.stateRoot, input.teamId, input.captain.id);
|
|
198
381
|
if (fresh.halted === true) {
|
|
199
382
|
return {
|
|
200
383
|
teamName: fresh.name,
|
|
384
|
+
captainSessionId: fresh.captainSessionId,
|
|
201
385
|
cancelledTasks: fresh.tasks.filter((task) => task.status === 'cancelled').length,
|
|
202
386
|
alreadyHalted: true,
|
|
203
387
|
members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
|
|
@@ -225,6 +409,7 @@ export async function haltTeamWork(input) {
|
|
|
225
409
|
});
|
|
226
410
|
return {
|
|
227
411
|
teamName: fresh.name,
|
|
412
|
+
captainSessionId: fresh.captainSessionId,
|
|
228
413
|
cancelledTasks,
|
|
229
414
|
alreadyHalted: false,
|
|
230
415
|
members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
|
|
@@ -234,7 +419,17 @@ export async function haltTeamWork(input) {
|
|
|
234
419
|
// children. Otherwise its current model turn can observe `halted`, call
|
|
235
420
|
// resume, and race the still-running HTTP stop request.
|
|
236
421
|
input.captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
237
|
-
|
|
422
|
+
// Members are children of the recorded captain session, so the parent-bound
|
|
423
|
+
// drain must run as that session when a fork requested the stop.
|
|
424
|
+
const haltAnchor = input.captain.id === halted.captainSessionId
|
|
425
|
+
? input.captain
|
|
426
|
+
: input.ctx.agents.get(halted.captainSessionId);
|
|
427
|
+
if (haltAnchor === undefined) {
|
|
428
|
+
input.ctx.logger.warn(`agent-teams: captain session ${halted.captainSessionId} is not live; member activations of "${halted.teamName}" were not drained from this session`);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
await stopTeamMemberActivations(input.ctx, haltAnchor, halted.members, input.signal);
|
|
432
|
+
}
|
|
238
433
|
// Interrupting a child emits a trailing subagent-settled notification. That
|
|
239
434
|
// notification can start a fresh Captain turn after the first cancellation,
|
|
240
435
|
// so close the stop boundary again once every child activation has drained.
|
|
@@ -243,6 +438,7 @@ export async function haltTeamWork(input) {
|
|
|
243
438
|
input.captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
244
439
|
return {
|
|
245
440
|
teamName: halted.teamName,
|
|
441
|
+
captainSessionId: halted.captainSessionId,
|
|
246
442
|
cancelledTasks: halted.cancelledTasks,
|
|
247
443
|
alreadyHalted: halted.alreadyHalted,
|
|
248
444
|
};
|
|
@@ -293,7 +489,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
293
489
|
const recordSpawnError = async (reason) => {
|
|
294
490
|
try {
|
|
295
491
|
await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
296
|
-
const fresh = await requireFreshCaptainTeam(root, teamId, captain.id);
|
|
492
|
+
const fresh = await requireFreshCaptainTeam(ctx, root, teamId, captain.id);
|
|
297
493
|
const failed = fresh.members.find(item => item.name === memberName && item.status !== 'removed');
|
|
298
494
|
if (failed === undefined || failed.id !== '')
|
|
299
495
|
return;
|
|
@@ -309,7 +505,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
309
505
|
try {
|
|
310
506
|
return await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
311
507
|
const team = await readTeam(root, teamId);
|
|
312
|
-
if (team
|
|
508
|
+
if (team === undefined || !(await sessionLeadsTeamAsync(ctx, team, captain.id)) || team.halted === true || team.phase === 'staged')
|
|
313
509
|
return false;
|
|
314
510
|
const member = team.members.find(item => item.name === memberName && item.status !== 'removed');
|
|
315
511
|
if (member === undefined || member.stopping === true || team.tasks.some(task => task.reassigning === true && task.assignee === memberName))
|
|
@@ -321,7 +517,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
321
517
|
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
322
518
|
provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
|
|
323
519
|
}, signal);
|
|
324
|
-
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
|
|
520
|
+
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text, member.toolFilter, memberRelaxableDeny(config, member));
|
|
325
521
|
orphan = { ...member };
|
|
326
522
|
delete member.spawnError;
|
|
327
523
|
await writeTeam(root, team);
|
|
@@ -349,7 +545,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
349
545
|
const workspace = workspaceOf(captain);
|
|
350
546
|
const stateRoot = stateRootOf(workspace, config);
|
|
351
547
|
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
352
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
548
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
353
549
|
const staged = fresh.phase === 'staged';
|
|
354
550
|
if (!staged && !allowPendingEdits)
|
|
355
551
|
requireStagedTeam(fresh);
|
|
@@ -374,6 +570,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
374
570
|
member.model = selection.model;
|
|
375
571
|
member.reasoningEffort = selection.reasoningEffort;
|
|
376
572
|
member.executionPrompt = trimmedOptional(mutation.executionPrompt);
|
|
573
|
+
// The roster editor replaces route/prompt fields; the tool policy is
|
|
574
|
+
// only touched when the mutation names one, so a plan revision cannot
|
|
575
|
+
// silently widen a member's tool set.
|
|
576
|
+
if (mutation.toolPolicy !== undefined) {
|
|
577
|
+
const selected = memberToolSelection(config, captain, trimmedOptional(mutation.toolPolicy));
|
|
578
|
+
member.toolPolicy = selected.toolPolicy;
|
|
579
|
+
member.toolFilter = selected.toolFilter;
|
|
580
|
+
}
|
|
377
581
|
}
|
|
378
582
|
else if (mutation.action === 'update_task') {
|
|
379
583
|
const task = requireTask(fresh, mutation.taskId);
|
|
@@ -455,7 +659,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
455
659
|
const stateRoot = stateRootOf(workspace, config);
|
|
456
660
|
const runSignal = signal ?? new AbortController().signal;
|
|
457
661
|
const approved = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
458
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
662
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
459
663
|
requireStagedTeam(fresh);
|
|
460
664
|
// A staged removal has no child session to retain in history. Drop those
|
|
461
665
|
// placeholders before transitioning to the stricter running shape.
|
|
@@ -479,7 +683,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
479
683
|
return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
|
|
480
684
|
});
|
|
481
685
|
try {
|
|
482
|
-
await scheduler.kickTeam(workspace, teamId, captain);
|
|
686
|
+
await scheduler.kickTeam(workspace, teamId, await liveLeadAnchorAgent(ctx, { captainSessionId: captain.id, name: teamId }, captain));
|
|
483
687
|
}
|
|
484
688
|
catch (error) {
|
|
485
689
|
// Approval is already durably committed. A transient wake-up failure is
|
|
@@ -493,7 +697,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
493
697
|
const workspace = workspaceOf(captain);
|
|
494
698
|
const stateRoot = stateRootOf(workspace, config);
|
|
495
699
|
const prepared = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
496
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
700
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
497
701
|
requireStagedTeam(fresh);
|
|
498
702
|
if (fresh.planReviewState === 'awaiting_feedback') {
|
|
499
703
|
return { teamName: fresh.name, alreadyWaiting: true };
|
|
@@ -518,7 +722,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
518
722
|
// Do not leave the durable UI in a false waiting state when the live
|
|
519
723
|
// Captain disappeared between lookup and delivery.
|
|
520
724
|
await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
521
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
725
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
522
726
|
requireStagedTeam(fresh);
|
|
523
727
|
if (fresh.planReviewState === 'awaiting_feedback') {
|
|
524
728
|
fresh.planReviewState = 'awaiting_review';
|
|
@@ -533,7 +737,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
533
737
|
const workspace = workspaceOf(captain);
|
|
534
738
|
const stateRoot = stateRootOf(workspace, config);
|
|
535
739
|
const discarded = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
536
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
740
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
537
741
|
requireStagedTeam(fresh);
|
|
538
742
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/plan-discarded', {
|
|
539
743
|
teamId: fresh.id,
|
|
@@ -583,6 +787,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
583
787
|
members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
584
788
|
name: { type: 'string', required: true }, role: { type: 'string' },
|
|
585
789
|
provider: { type: 'string' }, model: { type: 'string' }, reasoning_effort: { type: 'string' },
|
|
790
|
+
tool_policy: { type: 'string', description: 'Optional owner-declared tool-policy id for this member.' },
|
|
586
791
|
} } },
|
|
587
792
|
tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
588
793
|
id: { type: 'string', required: true, description: 'Local reference used by dependencies in this plan; the result maps it to a durable task id.' },
|
|
@@ -604,11 +809,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
604
809
|
properties: {
|
|
605
810
|
team_id: { type: 'string', required: true },
|
|
606
811
|
team_name: { type: 'string', required: true },
|
|
812
|
+
captain_session_id: { type: 'string', required: true },
|
|
607
813
|
state_dir: { type: 'string', required: true },
|
|
608
814
|
phase: { type: 'string', required: true },
|
|
609
815
|
profile: { type: 'string' },
|
|
610
816
|
task_planning: { type: 'string' },
|
|
611
|
-
members: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { member_name: { type: 'string', required: true }, member_id: { type: 'string', required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true }, reasoning_effort: { type: 'string' }, status: { type: 'string', required: true } } } },
|
|
817
|
+
members: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { member_name: { type: 'string', required: true }, member_id: { type: 'string', required: true }, member_role: { type: 'string' }, provider: { type: 'string', required: true }, model: { type: 'string', required: true }, reasoning_effort: { type: 'string' }, tool_policy: { type: 'string' }, status: { type: 'string', required: true } } } },
|
|
612
818
|
tasks: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { task_id: { type: 'string', required: true }, seed_id: { type: 'string', required: true }, subject: { type: 'string', required: true }, status: { type: 'string', required: true }, kind: { type: 'string' }, assignee: { type: 'string' }, dependencies: { type: 'array', items: { type: 'string' }, required: true } } } },
|
|
613
819
|
},
|
|
614
820
|
},
|
|
@@ -619,6 +825,23 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
619
825
|
: `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`)
|
|
620
826
|
+ (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')),
|
|
621
827
|
}],
|
|
828
|
+
// The conversation card folds this durable result, and the card cannot
|
|
829
|
+
// reproduce the host's key cleaning for every name (issue #203). Publish
|
|
830
|
+
// the canonical identity instead of letting a client re-derive it.
|
|
831
|
+
presentationMeta: (_args, value) => ({
|
|
832
|
+
team_id: value.team_id,
|
|
833
|
+
team_name: value.team_name,
|
|
834
|
+
captain_session_id: value.captain_session_id,
|
|
835
|
+
phase: value.phase,
|
|
836
|
+
...value.members === undefined ? {} : {
|
|
837
|
+
members: value.members.map(member => ({
|
|
838
|
+
id: member.member_id,
|
|
839
|
+
name: member.member_name,
|
|
840
|
+
role: member.member_role ?? '',
|
|
841
|
+
status: member.status,
|
|
842
|
+
})),
|
|
843
|
+
},
|
|
844
|
+
}),
|
|
622
845
|
},
|
|
623
846
|
async execute(args, exec) {
|
|
624
847
|
const captain = requireCaptain(exec);
|
|
@@ -638,10 +861,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
638
861
|
if (profileName !== undefined && args.plan !== undefined)
|
|
639
862
|
throw new Error('choose either a configured profile or an inline plan');
|
|
640
863
|
const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
|
|
641
|
-
const current = await findTeamByParticipant(stateRoot, captain.id);
|
|
864
|
+
const current = await findTeamByParticipant(stateRoot, captain.id, await leadIdentityOfAsync(ctx, captain.session));
|
|
642
865
|
if (current !== undefined) {
|
|
643
|
-
const
|
|
644
|
-
const
|
|
866
|
+
const leads = await sessionLeadsTeamAsync(ctx, current, captain.id);
|
|
867
|
+
const relationship = leads ? 'lead' : 'belong to';
|
|
868
|
+
const guidance = leads
|
|
645
869
|
? 'Use agent_teams_status and continue the existing team. Do not delete and recreate it merely to continue work. End it only when the user explicitly wants a separate new team.'
|
|
646
870
|
: 'Continue your assigned member work and report to your captain; do not create a separate team.';
|
|
647
871
|
throw new Error(`you already ${relationship} team "${current.name}" (id ${current.id}). ${guidance}`);
|
|
@@ -684,7 +908,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
684
908
|
});
|
|
685
909
|
if (created.committed) {
|
|
686
910
|
try {
|
|
687
|
-
await scheduler.kickTeam(workspace, created.state.id, captain);
|
|
911
|
+
await scheduler.kickTeam(workspace, created.state.id, await liveLeadAnchorAgent(ctx, created.state, captain));
|
|
688
912
|
}
|
|
689
913
|
catch (error) {
|
|
690
914
|
ctx.logger.warn(`agent-teams: post-create kick failed for "${created.state.id}": ${String(error)}`);
|
|
@@ -725,6 +949,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
725
949
|
return {
|
|
726
950
|
team_id: snapshot.id,
|
|
727
951
|
team_name: snapshot.name,
|
|
952
|
+
captain_session_id: snapshot.captainSessionId,
|
|
728
953
|
state_dir: join(stateRoot, snapshot.id),
|
|
729
954
|
phase: snapshot.phase ?? 'running',
|
|
730
955
|
};
|
|
@@ -732,6 +957,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
732
957
|
return {
|
|
733
958
|
team_id: snapshot.id,
|
|
734
959
|
team_name: snapshot.name,
|
|
960
|
+
captain_session_id: snapshot.captainSessionId,
|
|
735
961
|
state_dir: join(stateRoot, snapshot.id),
|
|
736
962
|
phase: snapshot.phase ?? 'running',
|
|
737
963
|
...snapshot.profile === undefined ? {} : { profile: snapshot.profile.name },
|
|
@@ -739,9 +965,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
739
965
|
members: snapshot.members.map((member) => ({
|
|
740
966
|
member_name: member.name,
|
|
741
967
|
member_id: member.id,
|
|
968
|
+
...member.role === undefined ? {} : { member_role: member.role },
|
|
742
969
|
provider: member.provider ?? '',
|
|
743
970
|
model: member.model ?? '',
|
|
744
971
|
...member.reasoningEffort === undefined ? {} : { reasoning_effort: member.reasoningEffort },
|
|
972
|
+
...member.toolPolicy === undefined ? {} : { tool_policy: member.toolPolicy },
|
|
745
973
|
status: member.status,
|
|
746
974
|
})),
|
|
747
975
|
tasks: snapshot.tasks.map((task) => ({
|
|
@@ -784,6 +1012,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
784
1012
|
model: { type: 'string', description: 'Optional member model; defaults to the current staged route.' },
|
|
785
1013
|
reasoning_effort: { type: 'string', description: 'Optional member reasoning effort.' },
|
|
786
1014
|
execution_prompt: { type: 'string', description: 'Optional member-specific execution prompt.' },
|
|
1015
|
+
tool_policy: { type: 'string', description: 'Optional owner-declared tool-policy id for this member; an empty string clears it back to the unchanged default tool set.' },
|
|
787
1016
|
},
|
|
788
1017
|
},
|
|
789
1018
|
},
|
|
@@ -810,7 +1039,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
810
1039
|
async execute(args, exec) {
|
|
811
1040
|
const captain = requireCaptain(exec);
|
|
812
1041
|
const workspace = workspaceOf(captain);
|
|
813
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1042
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
814
1043
|
if (args.operations.length === 0)
|
|
815
1044
|
throw new Error('at least one staged plan operation is required');
|
|
816
1045
|
const mutations = args.operations.map((operation, index) => {
|
|
@@ -828,6 +1057,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
828
1057
|
model: operation.model?.trim() || member.model || '',
|
|
829
1058
|
reasoningEffort: operation.reasoning_effort ?? member.reasoningEffort,
|
|
830
1059
|
executionPrompt: operation.execution_prompt ?? member.executionPrompt,
|
|
1060
|
+
// Omitted means "keep the current policy": a route/prompt revision
|
|
1061
|
+
// must not silently change what the member may do.
|
|
1062
|
+
...operation.tool_policy === undefined ? {} : { toolPolicy: operation.tool_policy },
|
|
831
1063
|
};
|
|
832
1064
|
}
|
|
833
1065
|
if (operation.action === 'update_task') {
|
|
@@ -869,7 +1101,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
869
1101
|
});
|
|
870
1102
|
const updated = await updatePlanBatch(captain, team.id, mutations, exec.signal, true);
|
|
871
1103
|
if (updated.phase !== 'staged')
|
|
872
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1104
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
873
1105
|
return {
|
|
874
1106
|
status: updated.phase ?? 'running',
|
|
875
1107
|
team_id: updated.id,
|
|
@@ -908,7 +1140,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
908
1140
|
throw new Error('explicit user approval text is required');
|
|
909
1141
|
const captain = requireCaptain(exec);
|
|
910
1142
|
const workspace = workspaceOf(captain);
|
|
911
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1143
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
912
1144
|
const approved = await approveStagedTeam(captain, team.id, exec.signal);
|
|
913
1145
|
return { status: 'running', team_id: approved.teamId, members: approved.members, tasks: approved.tasks };
|
|
914
1146
|
},
|
|
@@ -919,10 +1151,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
919
1151
|
parameters: {
|
|
920
1152
|
name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
|
|
921
1153
|
role: { type: 'string', description: 'Role of the member (e.g. researcher, engineer, reviewer).' },
|
|
922
|
-
provider: { type: 'string', description: 'Optional LLM provider route. Use only when the user explicitly requests a different provider; requires model.' },
|
|
923
|
-
model: { type: 'string', description: 'Optional model override. Omit for the
|
|
1154
|
+
provider: { type: 'string', description: 'Optional LLM provider route. Use only when the user explicitly requests a different provider; requires model. Omit to use the configured member route (memberLlmProvider/memberModel) or the captain\'s current route.' },
|
|
1155
|
+
model: { type: 'string', description: 'Optional model override (used verbatim, including any "/" in the id). Omit for the configured memberModel default (paired with memberLlmProvider when set) or the captain\'s current model.' },
|
|
924
1156
|
reasoning_effort: { type: 'string', description: 'Optional reasoning effort override: one of the target model\'s supported effort ids, or "default" to force its default. When omitted, the captain\'s effort is inherited only for the same provider/model; a changed route uses the target default.' },
|
|
925
1157
|
executionPrompt: { type: 'string', description: 'Optional member-specific execution prompt. It remains editable while staged.' },
|
|
1158
|
+
tool_policy: { type: 'string', description: 'Optional id of a tool policy declared by the deployment owner (config toolPolicies). It restricts which global tools this member sees and may invoke: a policy allow-list also keeps the four coordination tools, and captain-only tools can never be reopened. Omit for the unchanged default tool set.' },
|
|
926
1159
|
},
|
|
927
1160
|
output: {
|
|
928
1161
|
schema: {
|
|
@@ -934,24 +1167,26 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
934
1167
|
provider: { type: 'string', required: true },
|
|
935
1168
|
model: { type: 'string', required: true },
|
|
936
1169
|
reasoning_effort: { type: 'string' },
|
|
1170
|
+
tool_policy: { type: 'string' },
|
|
937
1171
|
status: { type: 'string', required: true },
|
|
938
1172
|
phase: { type: 'string', required: true },
|
|
939
1173
|
},
|
|
940
1174
|
},
|
|
941
1175
|
render: (args, value) => [{
|
|
942
1176
|
type: 'text',
|
|
943
|
-
text: value.phase === 'staged'
|
|
1177
|
+
text: (value.phase === 'staged'
|
|
944
1178
|
? `Member "${value.member_name}" added to the staged roster (${value.provider}/${value.model}); no child was spawned.`
|
|
945
|
-
: `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})
|
|
1179
|
+
: `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}).`)
|
|
1180
|
+
+ (value.tool_policy === undefined ? '' : ` Tool policy "${value.tool_policy}" applies to this member.`),
|
|
946
1181
|
}],
|
|
947
1182
|
},
|
|
948
1183
|
async execute(args, exec) {
|
|
949
1184
|
const captain = requireCaptain(exec);
|
|
950
1185
|
const workspace = workspaceOf(captain);
|
|
951
1186
|
const stateRoot = stateRootOf(workspace, config);
|
|
952
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1187
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
953
1188
|
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
954
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1189
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
955
1190
|
const memberName = args.name.trim();
|
|
956
1191
|
if (memberName === '')
|
|
957
1192
|
throw new Error('member name must not be empty');
|
|
@@ -969,9 +1204,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
969
1204
|
provider: args.provider,
|
|
970
1205
|
model: args.model,
|
|
971
1206
|
defaultModel: config.memberModel,
|
|
1207
|
+
defaultProvider: config.memberLlmProvider,
|
|
972
1208
|
reasoningEffort: args.reasoning_effort,
|
|
973
1209
|
fallback: config.fallback,
|
|
974
1210
|
}, exec.signal);
|
|
1211
|
+
const toolSelection = memberToolSelection(config, captain, trimmedOptional(args.tool_policy));
|
|
975
1212
|
const member = {
|
|
976
1213
|
id: '',
|
|
977
1214
|
name: memberName,
|
|
@@ -981,6 +1218,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
981
1218
|
reasoningEffort: selection.reasoningEffort,
|
|
982
1219
|
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
983
1220
|
executionPrompt: trimmedOptional(args.executionPrompt),
|
|
1221
|
+
...toolSelection,
|
|
984
1222
|
joinedAt: Date.now(),
|
|
985
1223
|
status: 'idle',
|
|
986
1224
|
};
|
|
@@ -1001,11 +1239,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1001
1239
|
...selection.reasoningEffort === undefined
|
|
1002
1240
|
? {}
|
|
1003
1241
|
: { reasoning_effort: selection.reasoningEffort },
|
|
1242
|
+
...member.toolPolicy === undefined ? {} : { tool_policy: member.toolPolicy },
|
|
1004
1243
|
status: member.status,
|
|
1005
1244
|
phase: fresh.phase ?? 'running',
|
|
1006
1245
|
};
|
|
1007
1246
|
});
|
|
1008
|
-
await scheduler.kickMember(workspace, team.id, created.member_name, captain);
|
|
1247
|
+
await scheduler.kickMember(workspace, team.id, created.member_name, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1009
1248
|
const latest = (await readTeam(stateRoot, team.id))?.members.find(member => member.name === created.member_name);
|
|
1010
1249
|
return latest === undefined ? created : { ...created, member_id: latest.id, status: latest.status };
|
|
1011
1250
|
},
|
|
@@ -1035,9 +1274,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1035
1274
|
const captain = requireCaptain(exec);
|
|
1036
1275
|
const workspace = workspaceOf(captain);
|
|
1037
1276
|
const stateRoot = stateRootOf(workspace, config);
|
|
1038
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1277
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1039
1278
|
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1040
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1279
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1041
1280
|
const member = fresh.members.find(item => item.name === args.name);
|
|
1042
1281
|
if (member === undefined)
|
|
1043
1282
|
throw new Error(`no member \"${args.name}\" in team \"${fresh.name}\"`);
|
|
@@ -1060,9 +1299,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1060
1299
|
});
|
|
1061
1300
|
if (revoked.member.id !== '') {
|
|
1062
1301
|
await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
|
|
1063
|
-
await stopTeamMemberActivations(ctx, captain, [revoked.member], exec.signal);
|
|
1302
|
+
await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), [revoked.member], exec.signal);
|
|
1064
1303
|
}
|
|
1065
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1304
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1066
1305
|
return {
|
|
1067
1306
|
member_name: revoked.member.name,
|
|
1068
1307
|
status: revoked.member.status,
|
|
@@ -1123,14 +1362,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1123
1362
|
const captain = requireCaptain(exec);
|
|
1124
1363
|
const workspace = workspaceOf(captain);
|
|
1125
1364
|
const stateRoot = stateRootOf(workspace, config);
|
|
1126
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1365
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1127
1366
|
// Some models materialize optional parameters as "" instead of omitting
|
|
1128
1367
|
// them (issue #105). Normalize blank optional fields to omitted before
|
|
1129
1368
|
// validation so a blank value can neither be rejected spuriously nor be
|
|
1130
1369
|
// persisted into team.json, where it would brick the team on reload.
|
|
1131
1370
|
const input = normalizeBlankOptionalTaskFields(args);
|
|
1132
1371
|
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1133
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1372
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1134
1373
|
const gate = validateCreateTask(fresh, {
|
|
1135
1374
|
subject: input.subject,
|
|
1136
1375
|
description: input.description,
|
|
@@ -1225,7 +1464,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1225
1464
|
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
1226
1465
|
};
|
|
1227
1466
|
});
|
|
1228
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1467
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1229
1468
|
return created;
|
|
1230
1469
|
},
|
|
1231
1470
|
}));
|
|
@@ -1259,12 +1498,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1259
1498
|
const captain = requireCaptain(exec);
|
|
1260
1499
|
const workspace = workspaceOf(captain);
|
|
1261
1500
|
const stateRoot = stateRootOf(workspace, config);
|
|
1262
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1501
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1263
1502
|
const target = args.assignee.trim();
|
|
1264
1503
|
if (target === '')
|
|
1265
1504
|
throw new Error('reassignment assignee must not be empty');
|
|
1266
1505
|
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1267
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1506
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1268
1507
|
const task = requireTask(fresh, args.task_id);
|
|
1269
1508
|
if (task.status === 'completed')
|
|
1270
1509
|
throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
|
|
@@ -1312,14 +1551,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1312
1551
|
let quiescenceError;
|
|
1313
1552
|
if (revoked.previousMember !== undefined) {
|
|
1314
1553
|
try {
|
|
1315
|
-
await stopTeamMemberActivations(ctx, captain, [revoked.previousMember], exec.signal);
|
|
1554
|
+
await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), [revoked.previousMember], exec.signal);
|
|
1316
1555
|
}
|
|
1317
1556
|
catch (error) {
|
|
1318
1557
|
quiescenceError = error;
|
|
1319
1558
|
}
|
|
1320
1559
|
}
|
|
1321
1560
|
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1322
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1561
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1323
1562
|
const task = requireTask(fresh, args.task_id);
|
|
1324
1563
|
if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
|
|
1325
1564
|
throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
|
|
@@ -1350,7 +1589,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1350
1589
|
if (quiescenceError !== undefined)
|
|
1351
1590
|
throw quiescenceError;
|
|
1352
1591
|
if (target !== CAPTAIN_KEY)
|
|
1353
|
-
await scheduler.kickMember(workspace, team.id, target, captain);
|
|
1592
|
+
await scheduler.kickMember(workspace, team.id, target, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1354
1593
|
const current = await readTeam(stateRoot, team.id);
|
|
1355
1594
|
const task = current === undefined ? undefined : requireTask(current, args.task_id);
|
|
1356
1595
|
if (task === undefined)
|
|
@@ -1394,9 +1633,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1394
1633
|
const caller = requireCaptain(exec);
|
|
1395
1634
|
const workspace = workspaceOf(caller);
|
|
1396
1635
|
const stateRoot = stateRootOf(workspace, config);
|
|
1397
|
-
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1636
|
+
const team = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
1398
1637
|
return withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1399
|
-
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1638
|
+
const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
|
|
1400
1639
|
const task = requireTask(fresh, args.task_id);
|
|
1401
1640
|
if (task.reassigning === true) {
|
|
1402
1641
|
throw new Error(`task ${task.id} is being reassigned; wait for the handoff to finish`);
|
|
@@ -1558,10 +1797,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1558
1797
|
const caller = requireCaptain(exec);
|
|
1559
1798
|
const workspace = workspaceOf(caller);
|
|
1560
1799
|
const stateRoot = stateRootOf(workspace, config);
|
|
1561
|
-
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1800
|
+
const team = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
1562
1801
|
let followUpMessage;
|
|
1563
1802
|
const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1564
|
-
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1803
|
+
const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
|
|
1565
1804
|
const task = requireTask(fresh, args.task_id);
|
|
1566
1805
|
if (identity.kind === 'captain'
|
|
1567
1806
|
&& task.assignee !== undefined
|
|
@@ -1635,19 +1874,22 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1635
1874
|
task.acceptanceResults = acceptanceResults;
|
|
1636
1875
|
if (commandsRun !== undefined)
|
|
1637
1876
|
task.commandsRun = commandsRun;
|
|
1877
|
+
// A passing gate is evidence about a revision: record which one, so a
|
|
1878
|
+
// later edit cannot leave the team advertising an unreviewed revision.
|
|
1879
|
+
bindClosingVerdict(workspace, fresh, task);
|
|
1638
1880
|
task.updatedAt = Date.now();
|
|
1639
1881
|
const priorDependencies = new Map(fresh.tasks.map(item => [item.id, [...item.dependencies]]));
|
|
1640
|
-
const followUp = (task.status === 'failed' && (task
|
|
1641
|
-
? applyQualityFollowUp(fresh, task)
|
|
1882
|
+
const followUp = (task.status === 'failed' && isQualityKind(taskKindOf(task)))
|
|
1883
|
+
? applyQualityFollowUp(fresh, task, (paths) => snapshotWorkspacePaths(workspace, paths), (path) => workspacePathExists(workspace, path))
|
|
1642
1884
|
: undefined;
|
|
1643
1885
|
let followUpSummary;
|
|
1644
1886
|
if ((followUp?.created.length ?? 0) > 0) {
|
|
1645
1887
|
const rewired = fresh.tasks.filter(item => priorDependencies.has(item.id) && JSON.stringify(priorDependencies.get(item.id)) !== JSON.stringify(item.dependencies));
|
|
1646
|
-
followUpSummary = `Automatic quality follow-up for ${task.id}: ${followUp.created.map(item => `${item.id} (${item.kind}, owner=${item.assignee ?? 'unassigned'}, deps=${item.dependencies.join(',') || 'none'})`).join('; ')}.${rewired.length ? ` Updated dependencies: ${rewired.map(item => `${item.id} -> ${item.dependencies.join(',')}`).join('; ')}.` : ''} Use these tasks; do not create duplicate repair/review work
|
|
1888
|
+
followUpSummary = `Automatic quality follow-up for ${task.id}: ${followUp.created.map(item => `${item.id} (${item.kind}, owner=${item.assignee ?? 'unassigned'}, deps=${item.dependencies.join(',') || 'none'})`).join('; ')}.${rewired.length ? ` Updated dependencies: ${rewired.map(item => `${item.id} -> ${item.dependencies.join(',')}`).join('; ')}.` : ''} Use these tasks; do not create duplicate repair/review work.${followUp.excludedFindings === undefined ? '' : ` Findings on default-excluded paths were not turned into a contract and stay with the user: ${followUp.excludedFindings.join(', ')}.`}`;
|
|
1647
1889
|
followUpMessage = createMessage(CAPTAIN_KEY, CAPTAIN_KEY, followUpSummary);
|
|
1648
1890
|
}
|
|
1649
1891
|
if (followUp?.escalated === true) {
|
|
1650
|
-
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, createMessage(CAPTAIN_KEY, CAPTAIN_KEY, `Quality-gate loop escalated after ${task.id} (${task.kind ?? 'review'} verdict=${task.verdict}). Automatic repair/review stopped.`));
|
|
1892
|
+
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, createMessage(CAPTAIN_KEY, CAPTAIN_KEY, `Quality-gate loop escalated after ${task.id} (${task.kind ?? 'review'} verdict=${task.verdict}). Automatic repair/review stopped.${followUp.reason === undefined ? '' : ` Reason: ${followUp.reason}.`}${followUp.excludedFindings === undefined ? '' : ` Findings left for the user (default-excluded paths, not a member's to edit): ${followUp.excludedFindings.join(', ')}.`}`));
|
|
1651
1893
|
}
|
|
1652
1894
|
await writeTeam(stateRoot, fresh);
|
|
1653
1895
|
if (followUpMessage !== undefined)
|
|
@@ -1682,12 +1924,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1682
1924
|
};
|
|
1683
1925
|
});
|
|
1684
1926
|
if (followUpMessage !== undefined) {
|
|
1685
|
-
const captain = ctx
|
|
1927
|
+
const captain = nearestLiveLead(ctx, team);
|
|
1686
1928
|
if (captain !== undefined && steerCaptainReport(captain, CAPTAIN_KEY, followUpMessage.content, mailboxPrompt(team.id, CAPTAIN_KEY, [followUpMessage]))) {
|
|
1687
1929
|
await withTeamLock(teamLockKey(stateRoot, team.id), () => markMailboxDelivered(stateRoot, team.id, CAPTAIN_KEY, [followUpMessage.id]));
|
|
1688
1930
|
}
|
|
1689
1931
|
}
|
|
1690
|
-
await scheduler.kickTeam(workspace, team.id,
|
|
1932
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, caller));
|
|
1691
1933
|
return updated;
|
|
1692
1934
|
},
|
|
1693
1935
|
}));
|
|
@@ -1724,9 +1966,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1724
1966
|
const captain = requireCaptain(exec);
|
|
1725
1967
|
const workspace = workspaceOf(captain);
|
|
1726
1968
|
const stateRoot = stateRootOf(workspace, config);
|
|
1727
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1969
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1728
1970
|
const amended = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1729
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1971
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1730
1972
|
const task = requireTask(fresh, args.task_id);
|
|
1731
1973
|
const input = {
|
|
1732
1974
|
...args.objective === undefined ? {} : { objective: args.objective },
|
|
@@ -1801,10 +2043,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1801
2043
|
const caller = requireCaptain(exec);
|
|
1802
2044
|
const workspace = workspaceOf(caller);
|
|
1803
2045
|
const stateRoot = stateRootOf(workspace, config);
|
|
1804
|
-
const team = await requireParticipantTeam(workspace, config, caller);
|
|
2046
|
+
const team = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
1805
2047
|
const to = args.to.trim();
|
|
1806
2048
|
const prepared = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1807
|
-
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
2049
|
+
const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
|
|
1808
2050
|
const from = identity.name;
|
|
1809
2051
|
// `from` may only be the caller's own identity: impersonating another
|
|
1810
2052
|
// member (or the captain) would poison the mailbox and event records.
|
|
@@ -1862,9 +2104,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1862
2104
|
});
|
|
1863
2105
|
if (prepared.kind === 'duplicate')
|
|
1864
2106
|
return { message_id: prepared.message.id, from: prepared.from, to: prepared.message.to, delivered: 'duplicate' };
|
|
1865
|
-
// Resolve
|
|
1866
|
-
//
|
|
1867
|
-
|
|
2107
|
+
// Resolve a live lead only after releasing the state lock: the recorded
|
|
2108
|
+
// captain while resident, otherwise the nearest live fork descendant, so
|
|
2109
|
+
// a human working in a fork still receives member reports. The plugin
|
|
2110
|
+
// mailbox is already durable if no lead session is live.
|
|
2111
|
+
const captain = nearestLiveLead(ctx, prepared.fresh);
|
|
1868
2112
|
if (prepared.kind === 'captain') {
|
|
1869
2113
|
let delivered = 'mailbox';
|
|
1870
2114
|
if (captain !== undefined && prepared.identity.kind === 'member') {
|
|
@@ -1910,12 +2154,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1910
2154
|
const caller = requireCaptain(exec);
|
|
1911
2155
|
const workspace = workspaceOf(caller);
|
|
1912
2156
|
const stateRoot = stateRootOf(workspace, config);
|
|
1913
|
-
const located = await requireParticipantTeam(workspace, config, caller);
|
|
1914
|
-
if (located
|
|
1915
|
-
await scheduler.kickTeam(workspace, located.id, caller);
|
|
2157
|
+
const located = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
2158
|
+
if (await sessionLeadsTeamAsync(ctx, located, caller.id)) {
|
|
2159
|
+
await scheduler.kickTeam(workspace, located.id, await memberOperationParent(ctx, located, caller));
|
|
1916
2160
|
}
|
|
1917
|
-
const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(stateRoot, located.id, caller.id));
|
|
2161
|
+
const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(ctx, stateRoot, located.id, caller.id));
|
|
1918
2162
|
const activity = memberActivity(ctx, team.members.map((member) => member.id));
|
|
2163
|
+
const audit = auditTeamWorkspace(workspace, team);
|
|
1919
2164
|
const members = team.members
|
|
1920
2165
|
.filter((member) => member.status !== 'removed')
|
|
1921
2166
|
.map((member) => ({
|
|
@@ -1924,6 +2169,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1924
2169
|
provider: member.provider ?? '',
|
|
1925
2170
|
model: member.model ?? '',
|
|
1926
2171
|
reasoning_effort: member.reasoningEffort ?? '',
|
|
2172
|
+
tool_policy: member.toolPolicy ?? '',
|
|
1927
2173
|
status: member.status,
|
|
1928
2174
|
activity: member.id !== '' ? (activity.get(member.id) ?? 'unknown') : 'unspawned',
|
|
1929
2175
|
...member.spawnError === undefined ? {} : { spawn_error: member.spawnError },
|
|
@@ -1941,6 +2187,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1941
2187
|
...task.round === undefined ? {} : { round: task.round },
|
|
1942
2188
|
...task.verdict === undefined ? {} : { verdict: task.verdict },
|
|
1943
2189
|
...task.supplementalEvidence === undefined ? {} : { supplemental_evidence: JSON.stringify(task.supplementalEvidence) },
|
|
2190
|
+
...task.findingSourceTaskIds === undefined ? {} : { fixes: task.findingSourceTaskIds.join(',') },
|
|
1944
2191
|
findings_open: (task.findings ?? []).filter((finding) => finding.resolved !== true).length,
|
|
1945
2192
|
...task.profileSeedId === undefined ? {} : { seed_id: task.profileSeedId },
|
|
1946
2193
|
...task.output !== undefined ? { output: task.output } : {},
|
|
@@ -1976,9 +2223,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1976
2223
|
status: row.status,
|
|
1977
2224
|
...row.evidence === undefined ? {} : { evidence: row.evidence },
|
|
1978
2225
|
}));
|
|
1979
|
-
const deliveryCheck = canDeclareDelivery(team);
|
|
1980
|
-
const delivery = {
|
|
1981
|
-
|
|
2226
|
+
const deliveryCheck = canDeclareDelivery(team, audit);
|
|
2227
|
+
const delivery = {
|
|
2228
|
+
ok: deliveryCheck.ok,
|
|
2229
|
+
blockers: [...deliveryCheck.blockers],
|
|
2230
|
+
warnings: [...deliveryCheck.warnings],
|
|
2231
|
+
};
|
|
2232
|
+
const loop = describeQualityLoop(team, audit);
|
|
1982
2233
|
const result = {
|
|
1983
2234
|
team_id: team.id,
|
|
1984
2235
|
team_name: team.name,
|
|
@@ -2049,9 +2300,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2049
2300
|
const captain = requireCaptain(exec);
|
|
2050
2301
|
const workspace = workspaceOf(captain);
|
|
2051
2302
|
const stateRoot = stateRootOf(workspace, config);
|
|
2052
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
2303
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
2053
2304
|
const result = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
2054
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
2305
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
2055
2306
|
const resumed = resumeTeamState(fresh, args.reason);
|
|
2056
2307
|
if (resumed.status === 'rejected')
|
|
2057
2308
|
throw new Error(resumed.error ?? 'resume rejected');
|
|
@@ -2071,7 +2322,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2071
2322
|
};
|
|
2072
2323
|
});
|
|
2073
2324
|
if (result.status === 'resumed')
|
|
2074
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
2325
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
2075
2326
|
return result;
|
|
2076
2327
|
},
|
|
2077
2328
|
}));
|
|
@@ -2097,9 +2348,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2097
2348
|
const captain = requireCaptain(exec);
|
|
2098
2349
|
const workspace = workspaceOf(captain);
|
|
2099
2350
|
const stateRoot = stateRootOf(workspace, config);
|
|
2100
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
2351
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
2101
2352
|
const members = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
2102
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
2353
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
2103
2354
|
// Include previously removed members so deleting a pre-fix team also
|
|
2104
2355
|
// retires durable catalog entries left behind by remove_member.
|
|
2105
2356
|
const roster = fresh.members.map(member => ({ ...member }));
|
|
@@ -2117,9 +2368,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2117
2368
|
return roster;
|
|
2118
2369
|
});
|
|
2119
2370
|
await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
|
|
2120
|
-
await stopTeamMemberActivations(ctx, captain, members, exec.signal);
|
|
2371
|
+
await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), members, exec.signal);
|
|
2121
2372
|
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
2122
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
2373
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
2123
2374
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/team-deleted', {
|
|
2124
2375
|
teamId: fresh.id,
|
|
2125
2376
|
});
|
|
@@ -2140,6 +2391,7 @@ async function initializeProfileTeam(input) {
|
|
|
2140
2391
|
provider: template.provider,
|
|
2141
2392
|
model: template.model,
|
|
2142
2393
|
defaultModel: input.config.memberModel,
|
|
2394
|
+
defaultProvider: input.config.memberLlmProvider,
|
|
2143
2395
|
reasoningEffort: template.reasoningEffort,
|
|
2144
2396
|
fallback: template.fallback ?? profile.fallback ?? input.config.fallback,
|
|
2145
2397
|
}, input.exec.signal));
|
|
@@ -2166,6 +2418,9 @@ async function initializeProfileTeam(input) {
|
|
|
2166
2418
|
...input.staged ? { phase: 'staged', planReviewState: 'awaiting_review' } : {},
|
|
2167
2419
|
members: profile.members.map((template, index) => {
|
|
2168
2420
|
const selection = selections[index];
|
|
2421
|
+
// A profile member selects a policy by id; an unknown id fails here,
|
|
2422
|
+
// before the team directory and any child session exist (#59).
|
|
2423
|
+
const toolSelection = memberToolSelection(input.config, input.captain, template.toolPolicy);
|
|
2169
2424
|
return {
|
|
2170
2425
|
id: '',
|
|
2171
2426
|
name: template.name,
|
|
@@ -2175,6 +2430,7 @@ async function initializeProfileTeam(input) {
|
|
|
2175
2430
|
reasoningEffort: selection.reasoningEffort,
|
|
2176
2431
|
executionPrompt: template.executionPrompt ?? profile.executionPrompt ?? input.config.executionPrompt,
|
|
2177
2432
|
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
2433
|
+
...toolSelection,
|
|
2178
2434
|
joinedAt: now,
|
|
2179
2435
|
status: 'idle',
|
|
2180
2436
|
};
|
|
@@ -2227,7 +2483,12 @@ function parseFindings(value) {
|
|
|
2227
2483
|
// A blank optional file must be omitted, not persisted: durable-state
|
|
2228
2484
|
// validation requires non-empty optional strings (issue #105 class).
|
|
2229
2485
|
...typeof raw['file'] === 'string' && raw['file'].trim() !== '' ? { file: raw['file'] } : {},
|
|
2230
|
-
|
|
2486
|
+
// Only a contract-valid line is persisted. A model that materializes
|
|
2487
|
+
// "no line number" as -1 (or sends a non-integer) must not write a record
|
|
2488
|
+
// the durable validator rejects on the next read, which used to brick the
|
|
2489
|
+
// entire team (issue #188). The predicate is shared with
|
|
2490
|
+
// `isReviewFinding`, so the two sides cannot drift apart again.
|
|
2491
|
+
...isValidFindingLine(raw['line']) ? { line: raw['line'] } : {},
|
|
2231
2492
|
...typeof raw['resolved'] === 'boolean' ? { resolved: raw['resolved'] } : {},
|
|
2232
2493
|
};
|
|
2233
2494
|
});
|
|
@@ -2279,8 +2540,8 @@ function parseCommandResults(value) {
|
|
|
2279
2540
|
};
|
|
2280
2541
|
});
|
|
2281
2542
|
}
|
|
2282
|
-
export function applyQualityFollowUp(team, closed) {
|
|
2283
|
-
const planned = planQualityFollowUp(team, closed);
|
|
2543
|
+
export function applyQualityFollowUp(team, closed, snapshotPaths, pathExists) {
|
|
2544
|
+
const planned = planQualityFollowUp(team, closed, pathExists === undefined ? {} : { pathExists });
|
|
2284
2545
|
if (planned.escalated === true)
|
|
2285
2546
|
team.escalated = true;
|
|
2286
2547
|
const created = [];
|
|
@@ -2299,6 +2560,12 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2299
2560
|
return dependency;
|
|
2300
2561
|
return idBySubject.get(dependency) ?? dependency;
|
|
2301
2562
|
});
|
|
2563
|
+
// Record the workspace the generated work order was derived from, so a
|
|
2564
|
+
// later round that changes a cited file can mark it stale instead of
|
|
2565
|
+
// letting a member read a frozen snapshot as authoritative (issue #174).
|
|
2566
|
+
const contractSnapshot = draft.contractPaths === undefined || draft.contractPaths.length === 0 || snapshotPaths === undefined
|
|
2567
|
+
? undefined
|
|
2568
|
+
: snapshotPaths(draft.contractPaths);
|
|
2302
2569
|
const next = {
|
|
2303
2570
|
id,
|
|
2304
2571
|
subject: draft.subject ?? `${draft.kind}-round-${draft.round ?? 1}`,
|
|
@@ -2309,6 +2576,7 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2309
2576
|
createdAt: now,
|
|
2310
2577
|
updatedAt: now,
|
|
2311
2578
|
kind: draft.kind,
|
|
2579
|
+
...draft.description === undefined ? {} : { description: draft.description },
|
|
2312
2580
|
...draft.round === undefined ? {} : { round: draft.round },
|
|
2313
2581
|
...draft.objective === undefined ? {} : { objective: draft.objective },
|
|
2314
2582
|
...draft.inScope === undefined ? {} : { inScope: draft.inScope },
|
|
@@ -2317,7 +2585,9 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2317
2585
|
...draft.verify === undefined ? {} : { verify: draft.verify },
|
|
2318
2586
|
...draft.sourceTaskId === undefined ? {} : { sourceTaskId: draft.sourceTaskId },
|
|
2319
2587
|
...draft.sourceFindingIds === undefined ? {} : { sourceFindingIds: draft.sourceFindingIds },
|
|
2588
|
+
...draft.findingSourceTaskIds === undefined ? {} : { findingSourceTaskIds: draft.findingSourceTaskIds },
|
|
2320
2589
|
...draft.reviewedTaskId === undefined ? {} : { reviewedTaskId: idBySubject.get(draft.reviewedTaskId) ?? draft.reviewedTaskId },
|
|
2590
|
+
...contractSnapshot === undefined ? {} : { contractSnapshot },
|
|
2321
2591
|
};
|
|
2322
2592
|
team.tasks.push(next);
|
|
2323
2593
|
created.push(next);
|
|
@@ -2336,7 +2606,12 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2336
2606
|
task.updatedAt = now;
|
|
2337
2607
|
}
|
|
2338
2608
|
}
|
|
2339
|
-
return {
|
|
2609
|
+
return {
|
|
2610
|
+
created,
|
|
2611
|
+
escalated: planned.escalated === true,
|
|
2612
|
+
...planned.reason === undefined ? {} : { reason: planned.reason },
|
|
2613
|
+
...planned.excludedFindings === undefined ? {} : { excludedFindings: planned.excludedFindings },
|
|
2614
|
+
};
|
|
2340
2615
|
}
|
|
2341
2616
|
/** Build the `memberRuntime` config handed to member helpers. */
|
|
2342
2617
|
function memberRuntime(config) {
|
|
@@ -2368,7 +2643,8 @@ function renderStatus(value) {
|
|
|
2368
2643
|
const route = member.provider && member.model ? ` · ${member.provider}/${member.model}` : '';
|
|
2369
2644
|
const effort = member.reasoning_effort ? ` · reasoning ${member.reasoning_effort}` : '';
|
|
2370
2645
|
const failure = member.spawn_error === undefined ? '' : `\n start failed: ${member.spawn_error.slice(0, 400)}`;
|
|
2371
|
-
|
|
2646
|
+
const policy = member.tool_policy ? ` · policy ${member.tool_policy}` : '';
|
|
2647
|
+
return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}${policy}${failure}`;
|
|
2372
2648
|
}),
|
|
2373
2649
|
`Tasks (${team.tasks.length}):`,
|
|
2374
2650
|
...team.tasks.map((task) => {
|
|
@@ -2380,7 +2656,8 @@ function renderStatus(value) {
|
|
|
2380
2656
|
const kind = task.kind ? ` ${task.kind}` : '';
|
|
2381
2657
|
const round = task.round === undefined ? '' : ` r${task.round}`;
|
|
2382
2658
|
const verdict = task.verdict === undefined ? '' : ` verdict ${task.verdict}`;
|
|
2383
|
-
|
|
2659
|
+
const fixes = task.fixes === undefined ? '' : ` fixes ${task.fixes}`;
|
|
2660
|
+
return ` - ${task.id} [${task.status}]${kind}${round}${verdict}${fixes} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}${evidence}`;
|
|
2384
2661
|
}),
|
|
2385
2662
|
...team.coverage === undefined || team.coverage.length === 0 ? [] : [
|
|
2386
2663
|
'Coverage:',
|
|
@@ -2389,6 +2666,10 @@ function renderStatus(value) {
|
|
|
2389
2666
|
...team.delivery === undefined ? [] : [
|
|
2390
2667
|
`Delivery: ${team.delivery.ok ? 'ok' : `blocked (${team.delivery.blockers.join('; ')})`}`,
|
|
2391
2668
|
],
|
|
2669
|
+
...(team.delivery?.warnings ?? []).length === 0 ? [] : [
|
|
2670
|
+
'Gate warnings (delivery is not blocked, but these recorded verdicts or generated work orders no longer describe the current bytes):',
|
|
2671
|
+
...(team.delivery?.warnings ?? []).map((warning) => ` - ${warning}`),
|
|
2672
|
+
],
|
|
2392
2673
|
`Captain inbox (${team.captain_inbox.length}):`,
|
|
2393
2674
|
...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content}`),
|
|
2394
2675
|
...(team.member_inbox ?? []).map(message => ` - [${message.from}] ${message.content}`),
|