@nanmicoder/dsh-agent-teams 0.1.22 → 0.1.24-rc.1
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 -13
- package/README_ZH.md +35 -13
- package/compatibility.json +6 -2
- 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 +139 -24
- package/lib/index.js +26 -3
- package/lib/lineage.js +198 -0
- package/lib/mailbox.js +59 -34
- package/lib/members.js +355 -82
- 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 +282 -13
- package/lib/tool-policies.js +187 -0
- package/lib/tools.js +382 -109
- 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 +78 -15
- package/lib/types/index.d.ts +26 -1
- package/lib/types/lineage.d.ts +92 -0
- package/lib/types/members.d.ts +113 -2
- 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 +100 -0
- package/lib/types/workspace-audit.d.ts +27 -0
- package/lib/workspace-audit.js +52 -0
- package/package.json +61 -54
- package/pnpm-workspace.yaml +304 -273
- package/release-notes/v0.1.23.md +34 -0
- package/release-notes/v0.1.24-rc.1.md +13 -0
- package/scripts/legacy-session-source-repair.mjs +344 -0
package/lib/tools.js
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
* The `agent_teams_*` model-facing tools.
|
|
3
|
-
*
|
|
4
|
-
* The captain (the agent that created the team) orchestrates: members are
|
|
5
|
-
* continuable subagents it spawns and wakes. Members share the same tools and
|
|
6
|
-
* drive their own task state, mirroring the Claude Code AgentTeams flow:
|
|
7
|
-
* create team → add members → create tasks with dependencies → claim/assign →
|
|
8
|
-
* work → report → status → delete.
|
|
9
|
-
* @module dsh-agent-teams/tools
|
|
10
|
-
*/
|
|
1
|
+
import { drainMemberChildren } from "./harness-compat.js";
|
|
11
2
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
12
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
4
|
import { join } from 'node:path';
|
|
14
5
|
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";
|
|
6
|
+
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
7
|
import { appendTaskEvidence } from "./quality-gates.js";
|
|
8
|
+
import { artifactDrift, bindReviewToRevision, isQualityKind, verdictScopePatterns, } from "./quality-gates.js";
|
|
9
|
+
import { auditTeamWorkspace } from "./workspace-audit.js";
|
|
10
|
+
import { readArtifactScope, snapshotWorkspacePaths, workspacePathExists } from "./artifacts.js";
|
|
11
|
+
import { restrictableToolNames } from "./harness-compat.js";
|
|
12
|
+
import { memberSpawnRelaxableDeny, optionalToolPolicy, requireToolPolicy, resolveMemberToolFilter } from "./tool-policies.js";
|
|
17
13
|
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
18
14
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
19
15
|
import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
|
|
20
16
|
import { installMailboxAdmission, isCurrentMail, mailboxContent, mailboxPrompt, readCurrentMailbox } from "./mailbox.js";
|
|
21
17
|
import { resolveTeamProfile } from "./profiles.js";
|
|
18
|
+
import { forkLineageNote, leadIdentityOfAsync, nearestLiveLead, sessionLeadsTeamAsync } from "./lineage.js";
|
|
22
19
|
export { steerCaptainReport } from "./members.js";
|
|
23
20
|
/** The caller agent, or a loud failure for non-agent callers. */
|
|
24
21
|
function requireCaptain(exec) {
|
|
@@ -39,29 +36,64 @@ function stateRootOf(workspace, config) {
|
|
|
39
36
|
function teamLockKey(stateRoot, teamId) {
|
|
40
37
|
return `team:${stateRoot}:${teamId}`;
|
|
41
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Bind a closing verdict to the revision it judged, using the snapshot taken
|
|
41
|
+
* when its attempt started (if any) to notice that the workspace changed while
|
|
42
|
+
* the gate was being formed. A verdict that ran over a moving workspace binds
|
|
43
|
+
* the drift explicitly instead of claiming the current bytes.
|
|
44
|
+
*/
|
|
45
|
+
function bindClosingVerdict(workspace, team, task) {
|
|
46
|
+
if (task.status !== 'completed')
|
|
47
|
+
return;
|
|
48
|
+
const kind = taskKindOf(task);
|
|
49
|
+
if (kind !== 'review' && kind !== 'requirements' && kind !== 'verification')
|
|
50
|
+
return;
|
|
51
|
+
if ((kind === 'review' || kind === 'requirements') && task.verdict !== 'pass')
|
|
52
|
+
return;
|
|
53
|
+
const scope = verdictScopePatterns(team, task);
|
|
54
|
+
if (scope.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const read = readArtifactScope(workspace, scope);
|
|
57
|
+
const duringDrift = task.verdictStartSnapshot === undefined
|
|
58
|
+
? []
|
|
59
|
+
: artifactDrift(task.verdictStartSnapshot.artifacts, read.artifacts);
|
|
60
|
+
const bound = bindReviewToRevision(team, task, {
|
|
61
|
+
artifacts: read.artifacts,
|
|
62
|
+
gaps: read.gaps,
|
|
63
|
+
scope,
|
|
64
|
+
duringDrift,
|
|
65
|
+
});
|
|
66
|
+
task.reviewedTaskId = bound.reviewedTaskId;
|
|
67
|
+
task.reviewedAttempt = bound.reviewedAttempt;
|
|
68
|
+
task.reviewedArtifacts = bound.reviewedArtifacts;
|
|
69
|
+
task.reviewedGaps = bound.reviewedGaps;
|
|
70
|
+
task.reviewedScope = bound.reviewedScope;
|
|
71
|
+
task.reviewedDuringDrift = bound.reviewedDuringDrift;
|
|
72
|
+
task.verdictStartSnapshot = undefined;
|
|
73
|
+
}
|
|
42
74
|
/** Process-local lock key enforcing one active team per captain session. */
|
|
43
75
|
function captainLockKey(stateRoot, captainId) {
|
|
44
76
|
return `captain:${stateRoot}:${captainId}`;
|
|
45
77
|
}
|
|
46
78
|
/** 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);
|
|
79
|
+
async function requireCaptainTeam(ctx, workspace, config, captain) {
|
|
80
|
+
const team = await findTeamByCaptain(stateRootOf(workspace, config), captain.id, await leadIdentityOfAsync(ctx, captain.session));
|
|
49
81
|
if (team === undefined) {
|
|
50
|
-
throw new Error(
|
|
82
|
+
throw new Error(`you are not leading any team yet — call agent_teams_create first${forkLineageNote(captain.session)}`);
|
|
51
83
|
}
|
|
52
84
|
return team;
|
|
53
85
|
}
|
|
54
86
|
/** 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);
|
|
87
|
+
async function requireParticipantTeam(ctx, workspace, config, caller) {
|
|
88
|
+
const team = await findTeamByParticipant(stateRootOf(workspace, config), caller.id, await leadIdentityOfAsync(ctx, caller.session));
|
|
57
89
|
if (team === undefined) {
|
|
58
|
-
throw new Error(
|
|
90
|
+
throw new Error(`you do not lead or belong to any active team yet${forkLineageNote(caller.session)}`);
|
|
59
91
|
}
|
|
60
92
|
return team;
|
|
61
93
|
}
|
|
62
94
|
/** Re-derive a caller's role from fresh state while holding the team lock. */
|
|
63
|
-
function participantIdentityOf(team, agentId) {
|
|
64
|
-
if (team
|
|
95
|
+
async function participantIdentityOf(ctx, team, agentId) {
|
|
96
|
+
if (await sessionLeadsTeamAsync(ctx, team, agentId))
|
|
65
97
|
return { kind: 'captain', name: CAPTAIN_KEY };
|
|
66
98
|
const member = team.members.find((candidate) => candidate.id === agentId && candidate.status !== 'removed');
|
|
67
99
|
return member === undefined ? undefined : { kind: 'member', name: member.name };
|
|
@@ -74,17 +106,17 @@ async function requireFreshTeam(stateRoot, teamId) {
|
|
|
74
106
|
return fresh;
|
|
75
107
|
}
|
|
76
108
|
/** Fresh state with captain authorization rechecked inside the lock. */
|
|
77
|
-
async function requireFreshCaptainTeam(stateRoot, teamId, captainId) {
|
|
109
|
+
async function requireFreshCaptainTeam(ctx, stateRoot, teamId, captainId) {
|
|
78
110
|
const fresh = await requireFreshTeam(stateRoot, teamId);
|
|
79
|
-
if (fresh
|
|
80
|
-
throw new Error(`only the captain of team "${fresh.name}" may perform this operation`);
|
|
111
|
+
if (!(await sessionLeadsTeamAsync(ctx, fresh, captainId))) {
|
|
112
|
+
throw new Error(`only the captain of team "${fresh.name}" (session ${fresh.captainSessionId}) or one of its fork descendants may perform this operation`);
|
|
81
113
|
}
|
|
82
114
|
return fresh;
|
|
83
115
|
}
|
|
84
116
|
/** Fresh state and caller identity rechecked inside the lock. */
|
|
85
|
-
async function requireFreshParticipant(stateRoot, teamId, callerId) {
|
|
117
|
+
async function requireFreshParticipant(ctx, stateRoot, teamId, callerId) {
|
|
86
118
|
const fresh = await requireFreshTeam(stateRoot, teamId);
|
|
87
|
-
const identity = participantIdentityOf(fresh, callerId);
|
|
119
|
+
const identity = await participantIdentityOf(ctx, fresh, callerId);
|
|
88
120
|
if (identity === undefined)
|
|
89
121
|
throw new Error(`you are no longer an active participant in team "${fresh.name}"`);
|
|
90
122
|
return { team: fresh, identity };
|
|
@@ -116,6 +148,48 @@ function trimmedOptional(value) {
|
|
|
116
148
|
const trimmed = value?.trim();
|
|
117
149
|
return trimmed === undefined || trimmed === '' ? undefined : trimmed;
|
|
118
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve one member's selected tool policy into the persisted policy id and
|
|
153
|
+
* the immutable effective filter (#59).
|
|
154
|
+
*
|
|
155
|
+
* An unknown id fails here — before the member is written and before any child
|
|
156
|
+
* is created — so a typo can never persist a half-configured member or leave a
|
|
157
|
+
* resumable orphan behind. A member without a policy resolves to `{}`: the
|
|
158
|
+
* previous behavior and record shape stay untouched.
|
|
159
|
+
* @param config - resolved plugin config (declared policies + depth cap).
|
|
160
|
+
* @param captain - the live captain, whose registry view names the host tools.
|
|
161
|
+
* @param policyId - the selected policy id, or undefined for no policy.
|
|
162
|
+
* @returns the member fields to persist (`toolPolicy` + `toolFilter`), or `{}`.
|
|
163
|
+
*/
|
|
164
|
+
function memberToolSelection(config, captain, policyId) {
|
|
165
|
+
if (policyId === undefined)
|
|
166
|
+
return {};
|
|
167
|
+
const policy = requireToolPolicy(config.toolPolicies, policyId);
|
|
168
|
+
return {
|
|
169
|
+
toolPolicy: policyId,
|
|
170
|
+
toolFilter: resolveMemberToolFilter(policy, {
|
|
171
|
+
maxDepth: config.memberMaxDepth,
|
|
172
|
+
knownTools: restrictableToolNames(captain),
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Deny names the lenient spawn retry may drop for one member.
|
|
178
|
+
*
|
|
179
|
+
* A member without a policy keeps the built-in #163/#164 recovery path. A
|
|
180
|
+
* policy member may only relax built-in entries the owner did not declare
|
|
181
|
+
* itself (`memberSpawnRelaxableDeny`), and a selected policy id this
|
|
182
|
+
* deployment no longer declares relaxes nothing: the member then fails loud
|
|
183
|
+
* instead of spawning under a different filter.
|
|
184
|
+
* @param config - resolved plugin config.
|
|
185
|
+
* @param member - the member being spawned.
|
|
186
|
+
* @returns the relaxable deny names, or undefined for the legacy default.
|
|
187
|
+
*/
|
|
188
|
+
function memberRelaxableDeny(config, member) {
|
|
189
|
+
if (member.toolPolicy === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
return memberSpawnRelaxableDeny(optionalToolPolicy(config.toolPolicies, member.toolPolicy), true);
|
|
192
|
+
}
|
|
119
193
|
/** Validate references and cycles before a staged graph can be saved or run. */
|
|
120
194
|
function validateStagedGraph(team, requireRunnable) {
|
|
121
195
|
const members = team.members.filter((member) => member.status !== 'removed');
|
|
@@ -183,6 +257,106 @@ function captainOpenTask(team, exceptTaskId) {
|
|
|
183
257
|
* boundary: it waits for the activation handles to release, so a child cannot
|
|
184
258
|
* keep executing after the captain-chat Stop control has reported success.
|
|
185
259
|
*/
|
|
260
|
+
/**
|
|
261
|
+
* The live Agent the host admits member operations from.
|
|
262
|
+
*
|
|
263
|
+
* Harness continuable delivery is authorized against the member's durable
|
|
264
|
+
* direct parent (`subagents.authorizeLineage`), which is the session that
|
|
265
|
+
* spawned it — the team's recorded captain. A captain acting from its own fork
|
|
266
|
+
* therefore commands members *through* that session:
|
|
267
|
+
*
|
|
268
|
+
* - while the recorded captain is resident, its Agent is used directly;
|
|
269
|
+
* - while it is cold, this resolves it through the public session controller
|
|
270
|
+
* (`workspaceRegistry.create` + `sessionController.create` with the existing
|
|
271
|
+
* session id — attach/resume only: no prompt, no model request, and the
|
|
272
|
+
* durable `captainSessionId` is never rewritten);
|
|
273
|
+
* - when the composition exposes neither service, or the resume fails, the
|
|
274
|
+
* operation fails with an explicit bounded error instead of migrating team
|
|
275
|
+
* ownership to a session the host would reject at delivery time.
|
|
276
|
+
*
|
|
277
|
+
* @param ctx - the plugin context (injects `agents`).
|
|
278
|
+
* @param team - current team state.
|
|
279
|
+
* @param caller - the captain-side session performing the operation.
|
|
280
|
+
* @returns the caller itself, or the live recorded captain session.
|
|
281
|
+
*/
|
|
282
|
+
async function memberOperationParent(ctx, team, caller) {
|
|
283
|
+
if (caller.id === team.captainSessionId)
|
|
284
|
+
return caller;
|
|
285
|
+
const resident = ctx.agents.get(team.captainSessionId);
|
|
286
|
+
if (resident !== undefined)
|
|
287
|
+
return resident;
|
|
288
|
+
const resumed = await resumeLeadAnchor(ctx, team, caller);
|
|
289
|
+
if (resumed !== undefined)
|
|
290
|
+
return resumed;
|
|
291
|
+
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)}`);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Best-effort live anchor for scheduler calls: the recorded captain when
|
|
295
|
+
* resident, the resumed one when this composition can attach it, and
|
|
296
|
+
* `undefined` otherwise (the scheduler then keeps its existing
|
|
297
|
+
* registry-only behavior). Never throws, so mutating tools stay usable on
|
|
298
|
+
* compositions without a session controller.
|
|
299
|
+
* @param ctx - the plugin context (injects `agents`).
|
|
300
|
+
* @param team - the team whose recorded captain is the delivery parent.
|
|
301
|
+
* @param caller - the captain-side session performing the operation.
|
|
302
|
+
* @returns the anchor agent when one is live, else undefined.
|
|
303
|
+
*/
|
|
304
|
+
async function liveLeadAnchorAgent(ctx, team, caller) {
|
|
305
|
+
if (caller.id === team.captainSessionId)
|
|
306
|
+
return caller;
|
|
307
|
+
const resident = ctx.agents.get(team.captainSessionId);
|
|
308
|
+
if (resident !== undefined)
|
|
309
|
+
return resident;
|
|
310
|
+
return resumeLeadAnchor(ctx, team, caller);
|
|
311
|
+
}
|
|
312
|
+
/** Why the recorded captain could not be resumed, for one bounded error message. */
|
|
313
|
+
function resumeUnavailableReason(ctx, team) {
|
|
314
|
+
const services = optionalTeamServices(ctx);
|
|
315
|
+
if (services.controller === undefined || services.registry === undefined) {
|
|
316
|
+
return 'this composition exposes no session controller to resume it, so add_member, member delivery and member drain are unavailable from the fork';
|
|
317
|
+
}
|
|
318
|
+
return `resuming it through the session controller failed (its workspace is "${team.name}"'s workspace)`;
|
|
319
|
+
}
|
|
320
|
+
/** Public optional services used only to attach the recorded captain session. */
|
|
321
|
+
function optionalTeamServices(ctx) {
|
|
322
|
+
const get = ctx.get;
|
|
323
|
+
if (typeof get !== 'function')
|
|
324
|
+
return {};
|
|
325
|
+
const read = (name) => {
|
|
326
|
+
try {
|
|
327
|
+
return get.call(ctx, name);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
controller: read('sessionController'),
|
|
335
|
+
registry: read('workspaceRegistry'),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Attach the team's recorded captain session in this process without sending a
|
|
340
|
+
* prompt: the Harness controller's create-with-existing-id path resumes the
|
|
341
|
+
* session and leaves every durable field (including `captainSessionId`)
|
|
342
|
+
* untouched.
|
|
343
|
+
* @returns the resumed Agent, or undefined when this composition cannot resume it.
|
|
344
|
+
*/
|
|
345
|
+
async function resumeLeadAnchor(ctx, team, caller) {
|
|
346
|
+
const { controller, registry } = optionalTeamServices(ctx);
|
|
347
|
+
if (controller?.create === undefined || registry?.create === undefined)
|
|
348
|
+
return undefined;
|
|
349
|
+
const cwd = caller.session.header.cwd ?? process.cwd();
|
|
350
|
+
try {
|
|
351
|
+
const workspace = await registry.create(cwd, cwd);
|
|
352
|
+
await controller.create({ sessionId: team.captainSessionId, workspaceId: workspace.id, agentPreset: 'standard' });
|
|
353
|
+
return ctx.agents.get(team.captainSessionId);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
ctx.logger.warn(`agent-teams: could not resume the recorded captain session ${team.captainSessionId}: ${String(error)}`);
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
186
360
|
async function stopTeamMemberActivations(ctx, captain, members, signal) {
|
|
187
361
|
const memberIds = members.filter(member => member.id !== '').map(member => member.id);
|
|
188
362
|
if (memberIds.length === 0)
|
|
@@ -190,14 +364,15 @@ async function stopTeamMemberActivations(ctx, captain, members, signal) {
|
|
|
190
364
|
// Every supported exact host has targeted recursive drain. Unlike interrupt,
|
|
191
365
|
// it closes admission and clears queued work before awaiting descendants.
|
|
192
366
|
signal?.throwIfAborted();
|
|
193
|
-
await ctx.subagents
|
|
367
|
+
await drainMemberChildren(ctx.subagents, captain, memberIds);
|
|
194
368
|
}
|
|
195
369
|
export async function haltTeamWork(input) {
|
|
196
370
|
const halted = await withTeamLock(teamLockKey(input.stateRoot, input.teamId), async () => {
|
|
197
|
-
const fresh = await requireFreshCaptainTeam(input.stateRoot, input.teamId, input.captain.id);
|
|
371
|
+
const fresh = await requireFreshCaptainTeam(input.ctx, input.stateRoot, input.teamId, input.captain.id);
|
|
198
372
|
if (fresh.halted === true) {
|
|
199
373
|
return {
|
|
200
374
|
teamName: fresh.name,
|
|
375
|
+
captainSessionId: fresh.captainSessionId,
|
|
201
376
|
cancelledTasks: fresh.tasks.filter((task) => task.status === 'cancelled').length,
|
|
202
377
|
alreadyHalted: true,
|
|
203
378
|
members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
|
|
@@ -225,6 +400,7 @@ export async function haltTeamWork(input) {
|
|
|
225
400
|
});
|
|
226
401
|
return {
|
|
227
402
|
teamName: fresh.name,
|
|
403
|
+
captainSessionId: fresh.captainSessionId,
|
|
228
404
|
cancelledTasks,
|
|
229
405
|
alreadyHalted: false,
|
|
230
406
|
members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
|
|
@@ -234,7 +410,17 @@ export async function haltTeamWork(input) {
|
|
|
234
410
|
// children. Otherwise its current model turn can observe `halted`, call
|
|
235
411
|
// resume, and race the still-running HTTP stop request.
|
|
236
412
|
input.captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
237
|
-
|
|
413
|
+
// Members are children of the recorded captain session, so the parent-bound
|
|
414
|
+
// drain must run as that session when a fork requested the stop.
|
|
415
|
+
const haltAnchor = input.captain.id === halted.captainSessionId
|
|
416
|
+
? input.captain
|
|
417
|
+
: input.ctx.agents.get(halted.captainSessionId);
|
|
418
|
+
if (haltAnchor === undefined) {
|
|
419
|
+
input.ctx.logger.warn(`agent-teams: captain session ${halted.captainSessionId} is not live; member activations of "${halted.teamName}" were not drained from this session`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
await stopTeamMemberActivations(input.ctx, haltAnchor, halted.members, input.signal);
|
|
423
|
+
}
|
|
238
424
|
// Interrupting a child emits a trailing subagent-settled notification. That
|
|
239
425
|
// notification can start a fresh Captain turn after the first cancellation,
|
|
240
426
|
// so close the stop boundary again once every child activation has drained.
|
|
@@ -243,6 +429,7 @@ export async function haltTeamWork(input) {
|
|
|
243
429
|
input.captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
244
430
|
return {
|
|
245
431
|
teamName: halted.teamName,
|
|
432
|
+
captainSessionId: halted.captainSessionId,
|
|
246
433
|
cancelledTasks: halted.cancelledTasks,
|
|
247
434
|
alreadyHalted: halted.alreadyHalted,
|
|
248
435
|
};
|
|
@@ -284,7 +471,6 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
284
471
|
installMemberDelegationGuard(ctx, config.stateDir, config.memberMaxDepth ?? 0);
|
|
285
472
|
installMailboxAdmission(ctx, config.stateDir);
|
|
286
473
|
const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir, executionPrompt: config.executionPrompt, dispatch: dispatchMember });
|
|
287
|
-
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
|
|
288
474
|
async function dispatchMember(captain, teamId, memberName, text, signal, mode, attemptId) {
|
|
289
475
|
const root = stateRootOf(workspaceOf(captain), config);
|
|
290
476
|
// Record why a member never started. The scheduler treats a failed dispatch as
|
|
@@ -293,7 +479,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
293
479
|
const recordSpawnError = async (reason) => {
|
|
294
480
|
try {
|
|
295
481
|
await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
296
|
-
const fresh = await requireFreshCaptainTeam(root, teamId, captain.id);
|
|
482
|
+
const fresh = await requireFreshCaptainTeam(ctx, root, teamId, captain.id);
|
|
297
483
|
const failed = fresh.members.find(item => item.name === memberName && item.status !== 'removed');
|
|
298
484
|
if (failed === undefined || failed.id !== '')
|
|
299
485
|
return;
|
|
@@ -309,7 +495,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
309
495
|
try {
|
|
310
496
|
return await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
311
497
|
const team = await readTeam(root, teamId);
|
|
312
|
-
if (team
|
|
498
|
+
if (team === undefined || !(await sessionLeadsTeamAsync(ctx, team, captain.id)) || team.halted === true || team.phase === 'staged')
|
|
313
499
|
return false;
|
|
314
500
|
const member = team.members.find(item => item.name === memberName && item.status !== 'removed');
|
|
315
501
|
if (member === undefined || member.stopping === true || team.tasks.some(task => task.reassigning === true && task.assignee === memberName))
|
|
@@ -321,7 +507,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
321
507
|
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
322
508
|
provider: member.provider, model: member.model, reasoningEffort: member.reasoningEffort, fallback: member.fallback,
|
|
323
509
|
}, signal);
|
|
324
|
-
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
|
|
510
|
+
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text, member.toolFilter, memberRelaxableDeny(config, member));
|
|
325
511
|
orphan = { ...member };
|
|
326
512
|
delete member.spawnError;
|
|
327
513
|
await writeTeam(root, team);
|
|
@@ -349,7 +535,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
349
535
|
const workspace = workspaceOf(captain);
|
|
350
536
|
const stateRoot = stateRootOf(workspace, config);
|
|
351
537
|
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
352
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
538
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
353
539
|
const staged = fresh.phase === 'staged';
|
|
354
540
|
if (!staged && !allowPendingEdits)
|
|
355
541
|
requireStagedTeam(fresh);
|
|
@@ -374,6 +560,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
374
560
|
member.model = selection.model;
|
|
375
561
|
member.reasoningEffort = selection.reasoningEffort;
|
|
376
562
|
member.executionPrompt = trimmedOptional(mutation.executionPrompt);
|
|
563
|
+
// The roster editor replaces route/prompt fields; the tool policy is
|
|
564
|
+
// only touched when the mutation names one, so a plan revision cannot
|
|
565
|
+
// silently widen a member's tool set.
|
|
566
|
+
if (mutation.toolPolicy !== undefined) {
|
|
567
|
+
const selected = memberToolSelection(config, captain, trimmedOptional(mutation.toolPolicy));
|
|
568
|
+
member.toolPolicy = selected.toolPolicy;
|
|
569
|
+
member.toolFilter = selected.toolFilter;
|
|
570
|
+
}
|
|
377
571
|
}
|
|
378
572
|
else if (mutation.action === 'update_task') {
|
|
379
573
|
const task = requireTask(fresh, mutation.taskId);
|
|
@@ -455,7 +649,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
455
649
|
const stateRoot = stateRootOf(workspace, config);
|
|
456
650
|
const runSignal = signal ?? new AbortController().signal;
|
|
457
651
|
const approved = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
458
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
652
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
459
653
|
requireStagedTeam(fresh);
|
|
460
654
|
// A staged removal has no child session to retain in history. Drop those
|
|
461
655
|
// placeholders before transitioning to the stricter running shape.
|
|
@@ -479,7 +673,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
479
673
|
return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
|
|
480
674
|
});
|
|
481
675
|
try {
|
|
482
|
-
await scheduler.kickTeam(workspace, teamId, captain);
|
|
676
|
+
await scheduler.kickTeam(workspace, teamId, await liveLeadAnchorAgent(ctx, { captainSessionId: captain.id, name: teamId }, captain));
|
|
483
677
|
}
|
|
484
678
|
catch (error) {
|
|
485
679
|
// Approval is already durably committed. A transient wake-up failure is
|
|
@@ -493,7 +687,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
493
687
|
const workspace = workspaceOf(captain);
|
|
494
688
|
const stateRoot = stateRootOf(workspace, config);
|
|
495
689
|
const prepared = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
496
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
690
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
497
691
|
requireStagedTeam(fresh);
|
|
498
692
|
if (fresh.planReviewState === 'awaiting_feedback') {
|
|
499
693
|
return { teamName: fresh.name, alreadyWaiting: true };
|
|
@@ -518,7 +712,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
518
712
|
// Do not leave the durable UI in a false waiting state when the live
|
|
519
713
|
// Captain disappeared between lookup and delivery.
|
|
520
714
|
await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
521
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
715
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
522
716
|
requireStagedTeam(fresh);
|
|
523
717
|
if (fresh.planReviewState === 'awaiting_feedback') {
|
|
524
718
|
fresh.planReviewState = 'awaiting_review';
|
|
@@ -533,7 +727,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
533
727
|
const workspace = workspaceOf(captain);
|
|
534
728
|
const stateRoot = stateRootOf(workspace, config);
|
|
535
729
|
const discarded = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
536
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
730
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, teamId, captain.id);
|
|
537
731
|
requireStagedTeam(fresh);
|
|
538
732
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/plan-discarded', {
|
|
539
733
|
teamId: fresh.id,
|
|
@@ -561,14 +755,6 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
561
755
|
captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
562
756
|
return { teamId: discarded.teamId };
|
|
563
757
|
};
|
|
564
|
-
const runtime = {
|
|
565
|
-
isPendingMember: memberSelections.isPendingMember,
|
|
566
|
-
updateStagedPlan,
|
|
567
|
-
updateStagedPlanBatch,
|
|
568
|
-
approveStagedTeam,
|
|
569
|
-
continueStagedPlanning,
|
|
570
|
-
discardStagedTeam,
|
|
571
|
-
};
|
|
572
758
|
ctx.tools.register(defineTool({
|
|
573
759
|
name: 'agent_teams_create',
|
|
574
760
|
description: 'Create a team. Use approval=required for a two-phase plan: members and tasks remain unspawned/unclaimed until the user reviews the Web plan and explicitly approves it. Optional profiles expand their configured roster; seed profiles also expand template tasks, while captain profiles leave the graph for the Captain to design. approval=automatic preserves the legacy immediate-execution path.',
|
|
@@ -583,6 +769,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
583
769
|
members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
584
770
|
name: { type: 'string', required: true }, role: { type: 'string' },
|
|
585
771
|
provider: { type: 'string' }, model: { type: 'string' }, reasoning_effort: { type: 'string' },
|
|
772
|
+
tool_policy: { type: 'string', description: 'Optional owner-declared tool-policy id for this member.' },
|
|
586
773
|
} } },
|
|
587
774
|
tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
588
775
|
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 +791,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
604
791
|
properties: {
|
|
605
792
|
team_id: { type: 'string', required: true },
|
|
606
793
|
team_name: { type: 'string', required: true },
|
|
794
|
+
captain_session_id: { type: 'string', required: true },
|
|
607
795
|
state_dir: { type: 'string', required: true },
|
|
608
796
|
phase: { type: 'string', required: true },
|
|
609
797
|
profile: { type: 'string' },
|
|
610
798
|
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 } } } },
|
|
799
|
+
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
800
|
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
801
|
},
|
|
614
802
|
},
|
|
@@ -619,6 +807,23 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
619
807
|
: `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`)
|
|
620
808
|
+ (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
809
|
}],
|
|
810
|
+
// The conversation card folds this durable result, and the card cannot
|
|
811
|
+
// reproduce the host's key cleaning for every name (issue #203). Publish
|
|
812
|
+
// the canonical identity instead of letting a client re-derive it.
|
|
813
|
+
presentationMeta: (_args, value) => ({
|
|
814
|
+
team_id: value.team_id,
|
|
815
|
+
team_name: value.team_name,
|
|
816
|
+
captain_session_id: value.captain_session_id,
|
|
817
|
+
phase: value.phase,
|
|
818
|
+
...value.members === undefined ? {} : {
|
|
819
|
+
members: value.members.map(member => ({
|
|
820
|
+
id: member.member_id,
|
|
821
|
+
name: member.member_name,
|
|
822
|
+
role: member.member_role ?? '',
|
|
823
|
+
status: member.status,
|
|
824
|
+
})),
|
|
825
|
+
},
|
|
826
|
+
}),
|
|
622
827
|
},
|
|
623
828
|
async execute(args, exec) {
|
|
624
829
|
const captain = requireCaptain(exec);
|
|
@@ -638,10 +843,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
638
843
|
if (profileName !== undefined && args.plan !== undefined)
|
|
639
844
|
throw new Error('choose either a configured profile or an inline plan');
|
|
640
845
|
const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
|
|
641
|
-
const current = await findTeamByParticipant(stateRoot, captain.id);
|
|
846
|
+
const current = await findTeamByParticipant(stateRoot, captain.id, await leadIdentityOfAsync(ctx, captain.session));
|
|
642
847
|
if (current !== undefined) {
|
|
643
|
-
const
|
|
644
|
-
const
|
|
848
|
+
const leads = await sessionLeadsTeamAsync(ctx, current, captain.id);
|
|
849
|
+
const relationship = leads ? 'lead' : 'belong to';
|
|
850
|
+
const guidance = leads
|
|
645
851
|
? '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
852
|
: 'Continue your assigned member work and report to your captain; do not create a separate team.';
|
|
647
853
|
throw new Error(`you already ${relationship} team "${current.name}" (id ${current.id}). ${guidance}`);
|
|
@@ -684,7 +890,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
684
890
|
});
|
|
685
891
|
if (created.committed) {
|
|
686
892
|
try {
|
|
687
|
-
await scheduler.kickTeam(workspace, created.state.id, captain);
|
|
893
|
+
await scheduler.kickTeam(workspace, created.state.id, await liveLeadAnchorAgent(ctx, created.state, captain));
|
|
688
894
|
}
|
|
689
895
|
catch (error) {
|
|
690
896
|
ctx.logger.warn(`agent-teams: post-create kick failed for "${created.state.id}": ${String(error)}`);
|
|
@@ -725,6 +931,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
725
931
|
return {
|
|
726
932
|
team_id: snapshot.id,
|
|
727
933
|
team_name: snapshot.name,
|
|
934
|
+
captain_session_id: snapshot.captainSessionId,
|
|
728
935
|
state_dir: join(stateRoot, snapshot.id),
|
|
729
936
|
phase: snapshot.phase ?? 'running',
|
|
730
937
|
};
|
|
@@ -732,6 +939,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
732
939
|
return {
|
|
733
940
|
team_id: snapshot.id,
|
|
734
941
|
team_name: snapshot.name,
|
|
942
|
+
captain_session_id: snapshot.captainSessionId,
|
|
735
943
|
state_dir: join(stateRoot, snapshot.id),
|
|
736
944
|
phase: snapshot.phase ?? 'running',
|
|
737
945
|
...snapshot.profile === undefined ? {} : { profile: snapshot.profile.name },
|
|
@@ -739,9 +947,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
739
947
|
members: snapshot.members.map((member) => ({
|
|
740
948
|
member_name: member.name,
|
|
741
949
|
member_id: member.id,
|
|
950
|
+
...member.role === undefined ? {} : { member_role: member.role },
|
|
742
951
|
provider: member.provider ?? '',
|
|
743
952
|
model: member.model ?? '',
|
|
744
953
|
...member.reasoningEffort === undefined ? {} : { reasoning_effort: member.reasoningEffort },
|
|
954
|
+
...member.toolPolicy === undefined ? {} : { tool_policy: member.toolPolicy },
|
|
745
955
|
status: member.status,
|
|
746
956
|
})),
|
|
747
957
|
tasks: snapshot.tasks.map((task) => ({
|
|
@@ -784,6 +994,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
784
994
|
model: { type: 'string', description: 'Optional member model; defaults to the current staged route.' },
|
|
785
995
|
reasoning_effort: { type: 'string', description: 'Optional member reasoning effort.' },
|
|
786
996
|
execution_prompt: { type: 'string', description: 'Optional member-specific execution prompt.' },
|
|
997
|
+
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
998
|
},
|
|
788
999
|
},
|
|
789
1000
|
},
|
|
@@ -810,7 +1021,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
810
1021
|
async execute(args, exec) {
|
|
811
1022
|
const captain = requireCaptain(exec);
|
|
812
1023
|
const workspace = workspaceOf(captain);
|
|
813
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1024
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
814
1025
|
if (args.operations.length === 0)
|
|
815
1026
|
throw new Error('at least one staged plan operation is required');
|
|
816
1027
|
const mutations = args.operations.map((operation, index) => {
|
|
@@ -828,6 +1039,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
828
1039
|
model: operation.model?.trim() || member.model || '',
|
|
829
1040
|
reasoningEffort: operation.reasoning_effort ?? member.reasoningEffort,
|
|
830
1041
|
executionPrompt: operation.execution_prompt ?? member.executionPrompt,
|
|
1042
|
+
// Omitted means "keep the current policy": a route/prompt revision
|
|
1043
|
+
// must not silently change what the member may do.
|
|
1044
|
+
...operation.tool_policy === undefined ? {} : { toolPolicy: operation.tool_policy },
|
|
831
1045
|
};
|
|
832
1046
|
}
|
|
833
1047
|
if (operation.action === 'update_task') {
|
|
@@ -869,7 +1083,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
869
1083
|
});
|
|
870
1084
|
const updated = await updatePlanBatch(captain, team.id, mutations, exec.signal, true);
|
|
871
1085
|
if (updated.phase !== 'staged')
|
|
872
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1086
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
873
1087
|
return {
|
|
874
1088
|
status: updated.phase ?? 'running',
|
|
875
1089
|
team_id: updated.id,
|
|
@@ -908,7 +1122,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
908
1122
|
throw new Error('explicit user approval text is required');
|
|
909
1123
|
const captain = requireCaptain(exec);
|
|
910
1124
|
const workspace = workspaceOf(captain);
|
|
911
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1125
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
912
1126
|
const approved = await approveStagedTeam(captain, team.id, exec.signal);
|
|
913
1127
|
return { status: 'running', team_id: approved.teamId, members: approved.members, tasks: approved.tasks };
|
|
914
1128
|
},
|
|
@@ -919,10 +1133,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
919
1133
|
parameters: {
|
|
920
1134
|
name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
|
|
921
1135
|
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
|
|
1136
|
+
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.' },
|
|
1137
|
+
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
1138
|
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
1139
|
executionPrompt: { type: 'string', description: 'Optional member-specific execution prompt. It remains editable while staged.' },
|
|
1140
|
+
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
1141
|
},
|
|
927
1142
|
output: {
|
|
928
1143
|
schema: {
|
|
@@ -934,24 +1149,26 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
934
1149
|
provider: { type: 'string', required: true },
|
|
935
1150
|
model: { type: 'string', required: true },
|
|
936
1151
|
reasoning_effort: { type: 'string' },
|
|
1152
|
+
tool_policy: { type: 'string' },
|
|
937
1153
|
status: { type: 'string', required: true },
|
|
938
1154
|
phase: { type: 'string', required: true },
|
|
939
1155
|
},
|
|
940
1156
|
},
|
|
941
1157
|
render: (args, value) => [{
|
|
942
1158
|
type: 'text',
|
|
943
|
-
text: value.phase === 'staged'
|
|
1159
|
+
text: (value.phase === 'staged'
|
|
944
1160
|
? `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})
|
|
1161
|
+
: `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}).`)
|
|
1162
|
+
+ (value.tool_policy === undefined ? '' : ` Tool policy "${value.tool_policy}" applies to this member.`),
|
|
946
1163
|
}],
|
|
947
1164
|
},
|
|
948
1165
|
async execute(args, exec) {
|
|
949
1166
|
const captain = requireCaptain(exec);
|
|
950
1167
|
const workspace = workspaceOf(captain);
|
|
951
1168
|
const stateRoot = stateRootOf(workspace, config);
|
|
952
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1169
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
953
1170
|
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
954
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1171
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
955
1172
|
const memberName = args.name.trim();
|
|
956
1173
|
if (memberName === '')
|
|
957
1174
|
throw new Error('member name must not be empty');
|
|
@@ -969,9 +1186,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
969
1186
|
provider: args.provider,
|
|
970
1187
|
model: args.model,
|
|
971
1188
|
defaultModel: config.memberModel,
|
|
1189
|
+
defaultProvider: config.memberLlmProvider,
|
|
972
1190
|
reasoningEffort: args.reasoning_effort,
|
|
973
1191
|
fallback: config.fallback,
|
|
974
1192
|
}, exec.signal);
|
|
1193
|
+
const toolSelection = memberToolSelection(config, captain, trimmedOptional(args.tool_policy));
|
|
975
1194
|
const member = {
|
|
976
1195
|
id: '',
|
|
977
1196
|
name: memberName,
|
|
@@ -981,6 +1200,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
981
1200
|
reasoningEffort: selection.reasoningEffort,
|
|
982
1201
|
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
983
1202
|
executionPrompt: trimmedOptional(args.executionPrompt),
|
|
1203
|
+
...toolSelection,
|
|
984
1204
|
joinedAt: Date.now(),
|
|
985
1205
|
status: 'idle',
|
|
986
1206
|
};
|
|
@@ -1001,11 +1221,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1001
1221
|
...selection.reasoningEffort === undefined
|
|
1002
1222
|
? {}
|
|
1003
1223
|
: { reasoning_effort: selection.reasoningEffort },
|
|
1224
|
+
...member.toolPolicy === undefined ? {} : { tool_policy: member.toolPolicy },
|
|
1004
1225
|
status: member.status,
|
|
1005
1226
|
phase: fresh.phase ?? 'running',
|
|
1006
1227
|
};
|
|
1007
1228
|
});
|
|
1008
|
-
await scheduler.kickMember(workspace, team.id, created.member_name, captain);
|
|
1229
|
+
await scheduler.kickMember(workspace, team.id, created.member_name, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1009
1230
|
const latest = (await readTeam(stateRoot, team.id))?.members.find(member => member.name === created.member_name);
|
|
1010
1231
|
return latest === undefined ? created : { ...created, member_id: latest.id, status: latest.status };
|
|
1011
1232
|
},
|
|
@@ -1035,9 +1256,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1035
1256
|
const captain = requireCaptain(exec);
|
|
1036
1257
|
const workspace = workspaceOf(captain);
|
|
1037
1258
|
const stateRoot = stateRootOf(workspace, config);
|
|
1038
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1259
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1039
1260
|
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1040
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1261
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1041
1262
|
const member = fresh.members.find(item => item.name === args.name);
|
|
1042
1263
|
if (member === undefined)
|
|
1043
1264
|
throw new Error(`no member \"${args.name}\" in team \"${fresh.name}\"`);
|
|
@@ -1060,9 +1281,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1060
1281
|
});
|
|
1061
1282
|
if (revoked.member.id !== '') {
|
|
1062
1283
|
await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
|
|
1063
|
-
await stopTeamMemberActivations(ctx, captain, [revoked.member], exec.signal);
|
|
1284
|
+
await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), [revoked.member], exec.signal);
|
|
1064
1285
|
}
|
|
1065
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1286
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1066
1287
|
return {
|
|
1067
1288
|
member_name: revoked.member.name,
|
|
1068
1289
|
status: revoked.member.status,
|
|
@@ -1123,14 +1344,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1123
1344
|
const captain = requireCaptain(exec);
|
|
1124
1345
|
const workspace = workspaceOf(captain);
|
|
1125
1346
|
const stateRoot = stateRootOf(workspace, config);
|
|
1126
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1347
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1127
1348
|
// Some models materialize optional parameters as "" instead of omitting
|
|
1128
1349
|
// them (issue #105). Normalize blank optional fields to omitted before
|
|
1129
1350
|
// validation so a blank value can neither be rejected spuriously nor be
|
|
1130
1351
|
// persisted into team.json, where it would brick the team on reload.
|
|
1131
1352
|
const input = normalizeBlankOptionalTaskFields(args);
|
|
1132
1353
|
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1133
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1354
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1134
1355
|
const gate = validateCreateTask(fresh, {
|
|
1135
1356
|
subject: input.subject,
|
|
1136
1357
|
description: input.description,
|
|
@@ -1225,7 +1446,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1225
1446
|
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
1226
1447
|
};
|
|
1227
1448
|
});
|
|
1228
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1449
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1229
1450
|
return created;
|
|
1230
1451
|
},
|
|
1231
1452
|
}));
|
|
@@ -1259,12 +1480,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1259
1480
|
const captain = requireCaptain(exec);
|
|
1260
1481
|
const workspace = workspaceOf(captain);
|
|
1261
1482
|
const stateRoot = stateRootOf(workspace, config);
|
|
1262
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1483
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1263
1484
|
const target = args.assignee.trim();
|
|
1264
1485
|
if (target === '')
|
|
1265
1486
|
throw new Error('reassignment assignee must not be empty');
|
|
1266
1487
|
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1267
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1488
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1268
1489
|
const task = requireTask(fresh, args.task_id);
|
|
1269
1490
|
if (task.status === 'completed')
|
|
1270
1491
|
throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
|
|
@@ -1312,14 +1533,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1312
1533
|
let quiescenceError;
|
|
1313
1534
|
if (revoked.previousMember !== undefined) {
|
|
1314
1535
|
try {
|
|
1315
|
-
await stopTeamMemberActivations(ctx, captain, [revoked.previousMember], exec.signal);
|
|
1536
|
+
await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), [revoked.previousMember], exec.signal);
|
|
1316
1537
|
}
|
|
1317
1538
|
catch (error) {
|
|
1318
1539
|
quiescenceError = error;
|
|
1319
1540
|
}
|
|
1320
1541
|
}
|
|
1321
1542
|
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1322
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1543
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1323
1544
|
const task = requireTask(fresh, args.task_id);
|
|
1324
1545
|
if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
|
|
1325
1546
|
throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
|
|
@@ -1350,7 +1571,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1350
1571
|
if (quiescenceError !== undefined)
|
|
1351
1572
|
throw quiescenceError;
|
|
1352
1573
|
if (target !== CAPTAIN_KEY)
|
|
1353
|
-
await scheduler.kickMember(workspace, team.id, target, captain);
|
|
1574
|
+
await scheduler.kickMember(workspace, team.id, target, await liveLeadAnchorAgent(ctx, team, captain));
|
|
1354
1575
|
const current = await readTeam(stateRoot, team.id);
|
|
1355
1576
|
const task = current === undefined ? undefined : requireTask(current, args.task_id);
|
|
1356
1577
|
if (task === undefined)
|
|
@@ -1394,9 +1615,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1394
1615
|
const caller = requireCaptain(exec);
|
|
1395
1616
|
const workspace = workspaceOf(caller);
|
|
1396
1617
|
const stateRoot = stateRootOf(workspace, config);
|
|
1397
|
-
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1618
|
+
const team = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
1398
1619
|
return withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1399
|
-
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1620
|
+
const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
|
|
1400
1621
|
const task = requireTask(fresh, args.task_id);
|
|
1401
1622
|
if (task.reassigning === true) {
|
|
1402
1623
|
throw new Error(`task ${task.id} is being reassigned; wait for the handoff to finish`);
|
|
@@ -1558,10 +1779,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1558
1779
|
const caller = requireCaptain(exec);
|
|
1559
1780
|
const workspace = workspaceOf(caller);
|
|
1560
1781
|
const stateRoot = stateRootOf(workspace, config);
|
|
1561
|
-
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1782
|
+
const team = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
1562
1783
|
let followUpMessage;
|
|
1563
1784
|
const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1564
|
-
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1785
|
+
const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
|
|
1565
1786
|
const task = requireTask(fresh, args.task_id);
|
|
1566
1787
|
if (identity.kind === 'captain'
|
|
1567
1788
|
&& task.assignee !== undefined
|
|
@@ -1635,19 +1856,22 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1635
1856
|
task.acceptanceResults = acceptanceResults;
|
|
1636
1857
|
if (commandsRun !== undefined)
|
|
1637
1858
|
task.commandsRun = commandsRun;
|
|
1859
|
+
// A passing gate is evidence about a revision: record which one, so a
|
|
1860
|
+
// later edit cannot leave the team advertising an unreviewed revision.
|
|
1861
|
+
bindClosingVerdict(workspace, fresh, task);
|
|
1638
1862
|
task.updatedAt = Date.now();
|
|
1639
1863
|
const priorDependencies = new Map(fresh.tasks.map(item => [item.id, [...item.dependencies]]));
|
|
1640
|
-
const followUp = (task.status === 'failed' && (task
|
|
1641
|
-
? applyQualityFollowUp(fresh, task)
|
|
1864
|
+
const followUp = (task.status === 'failed' && isQualityKind(taskKindOf(task)))
|
|
1865
|
+
? applyQualityFollowUp(fresh, task, (paths) => snapshotWorkspacePaths(workspace, paths), (path) => workspacePathExists(workspace, path))
|
|
1642
1866
|
: undefined;
|
|
1643
1867
|
let followUpSummary;
|
|
1644
1868
|
if ((followUp?.created.length ?? 0) > 0) {
|
|
1645
1869
|
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
|
|
1870
|
+
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
1871
|
followUpMessage = createMessage(CAPTAIN_KEY, CAPTAIN_KEY, followUpSummary);
|
|
1648
1872
|
}
|
|
1649
1873
|
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.`));
|
|
1874
|
+
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
1875
|
}
|
|
1652
1876
|
await writeTeam(stateRoot, fresh);
|
|
1653
1877
|
if (followUpMessage !== undefined)
|
|
@@ -1682,12 +1906,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1682
1906
|
};
|
|
1683
1907
|
});
|
|
1684
1908
|
if (followUpMessage !== undefined) {
|
|
1685
|
-
const captain = ctx
|
|
1909
|
+
const captain = nearestLiveLead(ctx, team);
|
|
1686
1910
|
if (captain !== undefined && steerCaptainReport(captain, CAPTAIN_KEY, followUpMessage.content, mailboxPrompt(team.id, CAPTAIN_KEY, [followUpMessage]))) {
|
|
1687
1911
|
await withTeamLock(teamLockKey(stateRoot, team.id), () => markMailboxDelivered(stateRoot, team.id, CAPTAIN_KEY, [followUpMessage.id]));
|
|
1688
1912
|
}
|
|
1689
1913
|
}
|
|
1690
|
-
await scheduler.kickTeam(workspace, team.id,
|
|
1914
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, caller));
|
|
1691
1915
|
return updated;
|
|
1692
1916
|
},
|
|
1693
1917
|
}));
|
|
@@ -1724,9 +1948,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1724
1948
|
const captain = requireCaptain(exec);
|
|
1725
1949
|
const workspace = workspaceOf(captain);
|
|
1726
1950
|
const stateRoot = stateRootOf(workspace, config);
|
|
1727
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1951
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
1728
1952
|
const amended = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1729
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1953
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
1730
1954
|
const task = requireTask(fresh, args.task_id);
|
|
1731
1955
|
const input = {
|
|
1732
1956
|
...args.objective === undefined ? {} : { objective: args.objective },
|
|
@@ -1801,10 +2025,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1801
2025
|
const caller = requireCaptain(exec);
|
|
1802
2026
|
const workspace = workspaceOf(caller);
|
|
1803
2027
|
const stateRoot = stateRootOf(workspace, config);
|
|
1804
|
-
const team = await requireParticipantTeam(workspace, config, caller);
|
|
2028
|
+
const team = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
1805
2029
|
const to = args.to.trim();
|
|
1806
2030
|
const prepared = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1807
|
-
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
2031
|
+
const { team: fresh, identity } = await requireFreshParticipant(ctx, stateRoot, team.id, caller.id);
|
|
1808
2032
|
const from = identity.name;
|
|
1809
2033
|
// `from` may only be the caller's own identity: impersonating another
|
|
1810
2034
|
// member (or the captain) would poison the mailbox and event records.
|
|
@@ -1862,9 +2086,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1862
2086
|
});
|
|
1863
2087
|
if (prepared.kind === 'duplicate')
|
|
1864
2088
|
return { message_id: prepared.message.id, from: prepared.from, to: prepared.message.to, delivered: 'duplicate' };
|
|
1865
|
-
// Resolve
|
|
1866
|
-
//
|
|
1867
|
-
|
|
2089
|
+
// Resolve a live lead only after releasing the state lock: the recorded
|
|
2090
|
+
// captain while resident, otherwise the nearest live fork descendant, so
|
|
2091
|
+
// a human working in a fork still receives member reports. The plugin
|
|
2092
|
+
// mailbox is already durable if no lead session is live.
|
|
2093
|
+
const captain = nearestLiveLead(ctx, prepared.fresh);
|
|
1868
2094
|
if (prepared.kind === 'captain') {
|
|
1869
2095
|
let delivered = 'mailbox';
|
|
1870
2096
|
if (captain !== undefined && prepared.identity.kind === 'member') {
|
|
@@ -1910,12 +2136,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1910
2136
|
const caller = requireCaptain(exec);
|
|
1911
2137
|
const workspace = workspaceOf(caller);
|
|
1912
2138
|
const stateRoot = stateRootOf(workspace, config);
|
|
1913
|
-
const located = await requireParticipantTeam(workspace, config, caller);
|
|
1914
|
-
if (located
|
|
1915
|
-
await scheduler.kickTeam(workspace, located.id, caller);
|
|
2139
|
+
const located = await requireParticipantTeam(ctx, workspace, config, caller);
|
|
2140
|
+
if (await sessionLeadsTeamAsync(ctx, located, caller.id)) {
|
|
2141
|
+
await scheduler.kickTeam(workspace, located.id, await memberOperationParent(ctx, located, caller));
|
|
1916
2142
|
}
|
|
1917
|
-
const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(stateRoot, located.id, caller.id));
|
|
2143
|
+
const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(ctx, stateRoot, located.id, caller.id));
|
|
1918
2144
|
const activity = memberActivity(ctx, team.members.map((member) => member.id));
|
|
2145
|
+
const audit = auditTeamWorkspace(workspace, team);
|
|
1919
2146
|
const members = team.members
|
|
1920
2147
|
.filter((member) => member.status !== 'removed')
|
|
1921
2148
|
.map((member) => ({
|
|
@@ -1924,6 +2151,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1924
2151
|
provider: member.provider ?? '',
|
|
1925
2152
|
model: member.model ?? '',
|
|
1926
2153
|
reasoning_effort: member.reasoningEffort ?? '',
|
|
2154
|
+
tool_policy: member.toolPolicy ?? '',
|
|
1927
2155
|
status: member.status,
|
|
1928
2156
|
activity: member.id !== '' ? (activity.get(member.id) ?? 'unknown') : 'unspawned',
|
|
1929
2157
|
...member.spawnError === undefined ? {} : { spawn_error: member.spawnError },
|
|
@@ -1941,6 +2169,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1941
2169
|
...task.round === undefined ? {} : { round: task.round },
|
|
1942
2170
|
...task.verdict === undefined ? {} : { verdict: task.verdict },
|
|
1943
2171
|
...task.supplementalEvidence === undefined ? {} : { supplemental_evidence: JSON.stringify(task.supplementalEvidence) },
|
|
2172
|
+
...task.findingSourceTaskIds === undefined ? {} : { fixes: task.findingSourceTaskIds.join(',') },
|
|
1944
2173
|
findings_open: (task.findings ?? []).filter((finding) => finding.resolved !== true).length,
|
|
1945
2174
|
...task.profileSeedId === undefined ? {} : { seed_id: task.profileSeedId },
|
|
1946
2175
|
...task.output !== undefined ? { output: task.output } : {},
|
|
@@ -1976,9 +2205,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1976
2205
|
status: row.status,
|
|
1977
2206
|
...row.evidence === undefined ? {} : { evidence: row.evidence },
|
|
1978
2207
|
}));
|
|
1979
|
-
const deliveryCheck = canDeclareDelivery(team);
|
|
1980
|
-
const delivery = {
|
|
1981
|
-
|
|
2208
|
+
const deliveryCheck = canDeclareDelivery(team, audit);
|
|
2209
|
+
const delivery = {
|
|
2210
|
+
ok: deliveryCheck.ok,
|
|
2211
|
+
blockers: [...deliveryCheck.blockers],
|
|
2212
|
+
warnings: [...deliveryCheck.warnings],
|
|
2213
|
+
};
|
|
2214
|
+
const loop = describeQualityLoop(team, audit);
|
|
1982
2215
|
const result = {
|
|
1983
2216
|
team_id: team.id,
|
|
1984
2217
|
team_name: team.name,
|
|
@@ -2049,9 +2282,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2049
2282
|
const captain = requireCaptain(exec);
|
|
2050
2283
|
const workspace = workspaceOf(captain);
|
|
2051
2284
|
const stateRoot = stateRootOf(workspace, config);
|
|
2052
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
2285
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
2053
2286
|
const result = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
2054
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
2287
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
2055
2288
|
const resumed = resumeTeamState(fresh, args.reason);
|
|
2056
2289
|
if (resumed.status === 'rejected')
|
|
2057
2290
|
throw new Error(resumed.error ?? 'resume rejected');
|
|
@@ -2071,7 +2304,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2071
2304
|
};
|
|
2072
2305
|
});
|
|
2073
2306
|
if (result.status === 'resumed')
|
|
2074
|
-
await scheduler.kickTeam(workspace, team.id, captain);
|
|
2307
|
+
await scheduler.kickTeam(workspace, team.id, await liveLeadAnchorAgent(ctx, team, captain));
|
|
2075
2308
|
return result;
|
|
2076
2309
|
},
|
|
2077
2310
|
}));
|
|
@@ -2097,9 +2330,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2097
2330
|
const captain = requireCaptain(exec);
|
|
2098
2331
|
const workspace = workspaceOf(captain);
|
|
2099
2332
|
const stateRoot = stateRootOf(workspace, config);
|
|
2100
|
-
const team = await requireCaptainTeam(workspace, config, captain);
|
|
2333
|
+
const team = await requireCaptainTeam(ctx, workspace, config, captain);
|
|
2101
2334
|
const members = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
2102
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
2335
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
2103
2336
|
// Include previously removed members so deleting a pre-fix team also
|
|
2104
2337
|
// retires durable catalog entries left behind by remove_member.
|
|
2105
2338
|
const roster = fresh.members.map(member => ({ ...member }));
|
|
@@ -2117,9 +2350,9 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2117
2350
|
return roster;
|
|
2118
2351
|
});
|
|
2119
2352
|
await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
|
|
2120
|
-
await stopTeamMemberActivations(ctx, captain, members, exec.signal);
|
|
2353
|
+
await stopTeamMemberActivations(ctx, await memberOperationParent(ctx, team, captain), members, exec.signal);
|
|
2121
2354
|
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
2122
|
-
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
2355
|
+
const fresh = await requireFreshCaptainTeam(ctx, stateRoot, team.id, captain.id);
|
|
2123
2356
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/team-deleted', {
|
|
2124
2357
|
teamId: fresh.id,
|
|
2125
2358
|
});
|
|
@@ -2130,6 +2363,16 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
2130
2363
|
return { deleted: true, team_name: team.name };
|
|
2131
2364
|
},
|
|
2132
2365
|
}));
|
|
2366
|
+
// Live-member hydration validates saved filters against registered tools.
|
|
2367
|
+
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
|
|
2368
|
+
const runtime = {
|
|
2369
|
+
isPendingMember: memberSelections.isPendingMember,
|
|
2370
|
+
updateStagedPlan,
|
|
2371
|
+
updateStagedPlanBatch,
|
|
2372
|
+
approveStagedTeam,
|
|
2373
|
+
continueStagedPlanning,
|
|
2374
|
+
discardStagedTeam,
|
|
2375
|
+
};
|
|
2133
2376
|
return runtime;
|
|
2134
2377
|
}
|
|
2135
2378
|
async function initializeProfileTeam(input) {
|
|
@@ -2140,6 +2383,7 @@ async function initializeProfileTeam(input) {
|
|
|
2140
2383
|
provider: template.provider,
|
|
2141
2384
|
model: template.model,
|
|
2142
2385
|
defaultModel: input.config.memberModel,
|
|
2386
|
+
defaultProvider: input.config.memberLlmProvider,
|
|
2143
2387
|
reasoningEffort: template.reasoningEffort,
|
|
2144
2388
|
fallback: template.fallback ?? profile.fallback ?? input.config.fallback,
|
|
2145
2389
|
}, input.exec.signal));
|
|
@@ -2166,6 +2410,9 @@ async function initializeProfileTeam(input) {
|
|
|
2166
2410
|
...input.staged ? { phase: 'staged', planReviewState: 'awaiting_review' } : {},
|
|
2167
2411
|
members: profile.members.map((template, index) => {
|
|
2168
2412
|
const selection = selections[index];
|
|
2413
|
+
// A profile member selects a policy by id; an unknown id fails here,
|
|
2414
|
+
// before the team directory and any child session exist (#59).
|
|
2415
|
+
const toolSelection = memberToolSelection(input.config, input.captain, template.toolPolicy);
|
|
2169
2416
|
return {
|
|
2170
2417
|
id: '',
|
|
2171
2418
|
name: template.name,
|
|
@@ -2175,6 +2422,7 @@ async function initializeProfileTeam(input) {
|
|
|
2175
2422
|
reasoningEffort: selection.reasoningEffort,
|
|
2176
2423
|
executionPrompt: template.executionPrompt ?? profile.executionPrompt ?? input.config.executionPrompt,
|
|
2177
2424
|
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
2425
|
+
...toolSelection,
|
|
2178
2426
|
joinedAt: now,
|
|
2179
2427
|
status: 'idle',
|
|
2180
2428
|
};
|
|
@@ -2227,7 +2475,12 @@ function parseFindings(value) {
|
|
|
2227
2475
|
// A blank optional file must be omitted, not persisted: durable-state
|
|
2228
2476
|
// validation requires non-empty optional strings (issue #105 class).
|
|
2229
2477
|
...typeof raw['file'] === 'string' && raw['file'].trim() !== '' ? { file: raw['file'] } : {},
|
|
2230
|
-
|
|
2478
|
+
// Only a contract-valid line is persisted. A model that materializes
|
|
2479
|
+
// "no line number" as -1 (or sends a non-integer) must not write a record
|
|
2480
|
+
// the durable validator rejects on the next read, which used to brick the
|
|
2481
|
+
// entire team (issue #188). The predicate is shared with
|
|
2482
|
+
// `isReviewFinding`, so the two sides cannot drift apart again.
|
|
2483
|
+
...isValidFindingLine(raw['line']) ? { line: raw['line'] } : {},
|
|
2231
2484
|
...typeof raw['resolved'] === 'boolean' ? { resolved: raw['resolved'] } : {},
|
|
2232
2485
|
};
|
|
2233
2486
|
});
|
|
@@ -2279,8 +2532,8 @@ function parseCommandResults(value) {
|
|
|
2279
2532
|
};
|
|
2280
2533
|
});
|
|
2281
2534
|
}
|
|
2282
|
-
export function applyQualityFollowUp(team, closed) {
|
|
2283
|
-
const planned = planQualityFollowUp(team, closed);
|
|
2535
|
+
export function applyQualityFollowUp(team, closed, snapshotPaths, pathExists) {
|
|
2536
|
+
const planned = planQualityFollowUp(team, closed, pathExists === undefined ? {} : { pathExists });
|
|
2284
2537
|
if (planned.escalated === true)
|
|
2285
2538
|
team.escalated = true;
|
|
2286
2539
|
const created = [];
|
|
@@ -2299,6 +2552,12 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2299
2552
|
return dependency;
|
|
2300
2553
|
return idBySubject.get(dependency) ?? dependency;
|
|
2301
2554
|
});
|
|
2555
|
+
// Record the workspace the generated work order was derived from, so a
|
|
2556
|
+
// later round that changes a cited file can mark it stale instead of
|
|
2557
|
+
// letting a member read a frozen snapshot as authoritative (issue #174).
|
|
2558
|
+
const contractSnapshot = draft.contractPaths === undefined || draft.contractPaths.length === 0 || snapshotPaths === undefined
|
|
2559
|
+
? undefined
|
|
2560
|
+
: snapshotPaths(draft.contractPaths);
|
|
2302
2561
|
const next = {
|
|
2303
2562
|
id,
|
|
2304
2563
|
subject: draft.subject ?? `${draft.kind}-round-${draft.round ?? 1}`,
|
|
@@ -2309,6 +2568,7 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2309
2568
|
createdAt: now,
|
|
2310
2569
|
updatedAt: now,
|
|
2311
2570
|
kind: draft.kind,
|
|
2571
|
+
...draft.description === undefined ? {} : { description: draft.description },
|
|
2312
2572
|
...draft.round === undefined ? {} : { round: draft.round },
|
|
2313
2573
|
...draft.objective === undefined ? {} : { objective: draft.objective },
|
|
2314
2574
|
...draft.inScope === undefined ? {} : { inScope: draft.inScope },
|
|
@@ -2317,7 +2577,9 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2317
2577
|
...draft.verify === undefined ? {} : { verify: draft.verify },
|
|
2318
2578
|
...draft.sourceTaskId === undefined ? {} : { sourceTaskId: draft.sourceTaskId },
|
|
2319
2579
|
...draft.sourceFindingIds === undefined ? {} : { sourceFindingIds: draft.sourceFindingIds },
|
|
2580
|
+
...draft.findingSourceTaskIds === undefined ? {} : { findingSourceTaskIds: draft.findingSourceTaskIds },
|
|
2320
2581
|
...draft.reviewedTaskId === undefined ? {} : { reviewedTaskId: idBySubject.get(draft.reviewedTaskId) ?? draft.reviewedTaskId },
|
|
2582
|
+
...contractSnapshot === undefined ? {} : { contractSnapshot },
|
|
2321
2583
|
};
|
|
2322
2584
|
team.tasks.push(next);
|
|
2323
2585
|
created.push(next);
|
|
@@ -2336,7 +2598,12 @@ export function applyQualityFollowUp(team, closed) {
|
|
|
2336
2598
|
task.updatedAt = now;
|
|
2337
2599
|
}
|
|
2338
2600
|
}
|
|
2339
|
-
return {
|
|
2601
|
+
return {
|
|
2602
|
+
created,
|
|
2603
|
+
escalated: planned.escalated === true,
|
|
2604
|
+
...planned.reason === undefined ? {} : { reason: planned.reason },
|
|
2605
|
+
...planned.excludedFindings === undefined ? {} : { excludedFindings: planned.excludedFindings },
|
|
2606
|
+
};
|
|
2340
2607
|
}
|
|
2341
2608
|
/** Build the `memberRuntime` config handed to member helpers. */
|
|
2342
2609
|
function memberRuntime(config) {
|
|
@@ -2368,7 +2635,8 @@ function renderStatus(value) {
|
|
|
2368
2635
|
const route = member.provider && member.model ? ` · ${member.provider}/${member.model}` : '';
|
|
2369
2636
|
const effort = member.reasoning_effort ? ` · reasoning ${member.reasoning_effort}` : '';
|
|
2370
2637
|
const failure = member.spawn_error === undefined ? '' : `\n start failed: ${member.spawn_error.slice(0, 400)}`;
|
|
2371
|
-
|
|
2638
|
+
const policy = member.tool_policy ? ` · policy ${member.tool_policy}` : '';
|
|
2639
|
+
return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}${policy}${failure}`;
|
|
2372
2640
|
}),
|
|
2373
2641
|
`Tasks (${team.tasks.length}):`,
|
|
2374
2642
|
...team.tasks.map((task) => {
|
|
@@ -2380,7 +2648,8 @@ function renderStatus(value) {
|
|
|
2380
2648
|
const kind = task.kind ? ` ${task.kind}` : '';
|
|
2381
2649
|
const round = task.round === undefined ? '' : ` r${task.round}`;
|
|
2382
2650
|
const verdict = task.verdict === undefined ? '' : ` verdict ${task.verdict}`;
|
|
2383
|
-
|
|
2651
|
+
const fixes = task.fixes === undefined ? '' : ` fixes ${task.fixes}`;
|
|
2652
|
+
return ` - ${task.id} [${task.status}]${kind}${round}${verdict}${fixes} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}${evidence}`;
|
|
2384
2653
|
}),
|
|
2385
2654
|
...team.coverage === undefined || team.coverage.length === 0 ? [] : [
|
|
2386
2655
|
'Coverage:',
|
|
@@ -2389,6 +2658,10 @@ function renderStatus(value) {
|
|
|
2389
2658
|
...team.delivery === undefined ? [] : [
|
|
2390
2659
|
`Delivery: ${team.delivery.ok ? 'ok' : `blocked (${team.delivery.blockers.join('; ')})`}`,
|
|
2391
2660
|
],
|
|
2661
|
+
...(team.delivery?.warnings ?? []).length === 0 ? [] : [
|
|
2662
|
+
'Gate warnings (delivery is not blocked, but these recorded verdicts or generated work orders no longer describe the current bytes):',
|
|
2663
|
+
...(team.delivery?.warnings ?? []).map((warning) => ` - ${warning}`),
|
|
2664
|
+
],
|
|
2392
2665
|
`Captain inbox (${team.captain_inbox.length}):`,
|
|
2393
2666
|
...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content}`),
|
|
2394
2667
|
...(team.member_inbox ?? []).map(message => ` - [${message.from}] ${message.content}`),
|