pi-extended-teams 2.2.9 → 2.3.0
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 +182 -2
- package/extensions/agents/read-agent-report.ts +11 -2
- package/extensions/agents/read-agent-session-lifecycle.ts +25 -10
- package/extensions/agents/read-agent.ts +387 -65
- package/extensions/agents/write-agent.ts +3 -0
- package/extensions/events/register-events.ts +22 -10
- package/extensions/index.ts +132 -38
- package/extensions/internal/pi-check-operations.ts +24 -0
- package/extensions/internal/session-files.ts +10 -2
- package/extensions/runtime/teammate-interrupt.ts +11 -5
- package/extensions/runtime/types.ts +4 -0
- package/extensions/team/lifecycle.ts +26 -0
- package/extensions/team/session-cost.ts +155 -0
- package/extensions/tools/agent-communication-tools.ts +24 -8
- package/extensions/tools/agent-status-tool.ts +50 -7
- package/extensions/tools/coordination-tools.ts +78 -8
- package/extensions/tools/task-runtime-tools.ts +23 -4
- package/extensions/tools/team-tools.ts +287 -40
- package/extensions/ui/activity-colors.ts +59 -0
- package/extensions/ui/agent-follow-view.ts +22 -12
- package/extensions/ui/agent-navigation.ts +35 -1
- package/extensions/ui/checkpoints-command.ts +28 -0
- package/extensions/ui/extensions-command.ts +5 -1
- package/extensions/ui/onboarding-command.ts +218 -0
- package/extensions/ui/status-widget.ts +22 -14
- package/package.json +1 -1
- package/skills/teams.md +16 -0
- package/src/orchestration/index.ts +19 -1
- package/src/orchestration/status-projection.ts +2 -3
- package/src/orchestration/types.ts +9 -1
- package/src/results/check-journal.ts +149 -0
- package/src/results/check-policy.ts +86 -0
- package/src/results/check-runner.ts +110 -0
- package/src/results/checkpoint-assignment.ts +60 -0
- package/src/results/checkpoint-policy.ts +20 -0
- package/src/results/checkpoint-report.ts +109 -0
- package/src/results/checkpoint-retention.ts +15 -0
- package/src/results/completion-group-delivery.ts +66 -0
- package/src/results/completion-group-recovery.ts +61 -0
- package/src/results/completion-group-wake.ts +62 -0
- package/src/results/completion-group.ts +290 -0
- package/src/results/continuation-recipient.ts +37 -0
- package/src/results/durable-json.ts +29 -0
- package/src/results/repair-policy.ts +24 -0
- package/src/results/report-result.ts +77 -0
- package/src/results/source-identity.ts +115 -0
- package/src/results/specialist-checkpoint.ts +227 -0
- package/src/results/verification-controller.ts +308 -0
- package/src/utils/messaging.ts +47 -10
- package/src/utils/models.ts +13 -0
- package/src/utils/paths.ts +4 -0
- package/src/utils/report-events.ts +95 -10
- package/src/utils/teams.ts +3 -1
- package/src/utils/write-queue.ts +3 -0
|
@@ -12,12 +12,23 @@ import * as messaging from "../../src/utils/messaging";
|
|
|
12
12
|
import * as writeQueue from "../../src/utils/write-queue";
|
|
13
13
|
import { ACCEPTED_FAVORITE_MODEL_SLOTS, FAVORITE_MODEL_SLOTS, canonicalPersistedModelSlot, isFavoriteModelSlot, loadSettings, normalizeFavoriteModelSlot, requireFavoriteModelLevel, resolveModel, roleForFavoriteModelSlot, type AgentRole, type CanonicalFavoriteModelSlot } from "../../src/utils/settings";
|
|
14
14
|
import type { Member } from "../../src/utils/models";
|
|
15
|
+
import { CheckPolicySchema, normalizeCheckPolicy } from "../../src/results/check-policy";
|
|
16
|
+
import { RepairPolicySchema, normalizeRepairPolicy } from "../../src/results/repair-policy";
|
|
17
|
+
import { CheckpointPolicySchema } from "../../src/results/checkpoint-policy";
|
|
18
|
+
import { createCheckpointAssignment, captureCheckpointAssignment, continuationPrompt } from "../../src/results/checkpoint-assignment";
|
|
19
|
+
import { assertUnusedContinuationRecipient, freshContinuationName } from "../../src/results/continuation-recipient";
|
|
20
|
+
import { readCheckpoint } from "../../src/results/specialist-checkpoint";
|
|
21
|
+
import { CompletionGroup, CompletionGroupPolicySchema, completionGroupIdentity, normalizeCompletionGroupPolicy, type CompletionGroupBinding } from "../../src/results/completion-group";
|
|
22
|
+
import { listStoredTeamReportEvents } from "../../src/utils/report-events";
|
|
23
|
+
import { enqueueCompletionGroupDeliveries } from "../../src/results/completion-group-delivery";
|
|
24
|
+
import { shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
|
|
15
25
|
|
|
16
26
|
import type { RunningReadAgent } from "../runtime/types";
|
|
17
27
|
import type { ReadAgentTeardownResult } from "../agents/read-agent-session-lifecycle";
|
|
18
28
|
import type { ShutdownTeammateOptions } from "../team/lifecycle";
|
|
19
29
|
import {
|
|
20
30
|
onLifecycleTombstoneCleared,
|
|
31
|
+
listLifecycleTombstones,
|
|
21
32
|
readLifecycleTombstone,
|
|
22
33
|
withLifecycleTombstoneLock,
|
|
23
34
|
type LifecycleTombstoneLock,
|
|
@@ -55,6 +66,7 @@ export interface TeamToolsOptions {
|
|
|
55
66
|
startWriteAgent(teamName: string, member: Member, prompt: string): Promise<string>;
|
|
56
67
|
shutdownTeammate(teamName: string, member: Member, options?: ShutdownTeammateOptions): Promise<ReadAgentTeardownResult>;
|
|
57
68
|
adoptTeamAsLead(teamName: string, ctx?: any): void;
|
|
69
|
+
onCompletionGroupUse?(groupId: string): void;
|
|
58
70
|
buildRoster(teamName: string): Promise<any>;
|
|
59
71
|
isTeammate: boolean;
|
|
60
72
|
agentName: string;
|
|
@@ -71,13 +83,21 @@ export interface NestedReadAgentToolBinding {
|
|
|
71
83
|
outerCtx: any;
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
export interface NestedChildSnapshot {
|
|
87
|
+
running: number;
|
|
88
|
+
queued: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
export interface TeamToolsRuntime {
|
|
75
92
|
createNestedReadAgentTools(binding: NestedReadAgentToolBinding): any[];
|
|
76
|
-
|
|
93
|
+
nestedChildSnapshot(binding: NestedReadAgentToolBinding): NestedChildSnapshot;
|
|
94
|
+
cancelQueuedAgent(teamName: string, agentName: string): boolean | Promise<boolean>;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
interface SpawnTeammateOptions {
|
|
80
98
|
once?: boolean;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
completionGroup?: CompletionGroupBinding;
|
|
81
101
|
nestedParent?: {
|
|
82
102
|
teamName: string;
|
|
83
103
|
name: string;
|
|
@@ -97,15 +117,21 @@ interface QueuedReadSpawn {
|
|
|
97
117
|
ctx: any;
|
|
98
118
|
requestedAt: number;
|
|
99
119
|
nameReservationId?: string;
|
|
120
|
+
signal?: AbortSignal;
|
|
100
121
|
pendingChildAcceptance?: PendingChildAcceptance;
|
|
101
122
|
admissionError?: string;
|
|
102
123
|
quarantineError?: string;
|
|
103
124
|
launchCommitted?: boolean;
|
|
125
|
+
groupSettlement?: Promise<void>;
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamToolsRuntime {
|
|
107
129
|
if (options.isTeammate) {
|
|
108
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
createNestedReadAgentTools: () => [],
|
|
132
|
+
nestedChildSnapshot: () => ({ running: 0, queued: 0 }),
|
|
133
|
+
cancelQueuedAgent: () => false,
|
|
134
|
+
};
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
function emitOrchestrationResponse(requestId: string | undefined, type: string, payload: Record<string, any>): void {
|
|
@@ -297,6 +323,20 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
297
323
|
return queuedReadSpawnsByTeam.get(teamName) ?? [];
|
|
298
324
|
}
|
|
299
325
|
|
|
326
|
+
function nestedChildSnapshot(binding: NestedReadAgentToolBinding): NestedChildSnapshot {
|
|
327
|
+
const parent = {
|
|
328
|
+
teamName: binding.teamName,
|
|
329
|
+
parentName: binding.parent.name,
|
|
330
|
+
parentRunId: binding.parentRunId,
|
|
331
|
+
};
|
|
332
|
+
const queued = readQueue(binding.teamName).filter((child) => {
|
|
333
|
+
const childParent = pendingParentForMember(binding.teamName, child.member);
|
|
334
|
+
return childParent?.parentName === parent.parentName && childParent.parentRunId === parent.parentRunId;
|
|
335
|
+
}).length;
|
|
336
|
+
const pending = pendingChildController.pendingCount(parent);
|
|
337
|
+
return { running: Math.max(0, pending - queued), queued };
|
|
338
|
+
}
|
|
339
|
+
|
|
300
340
|
async function listQueuedAgentStatuses(teamName: string): Promise<QueuedAgentStatus[]> {
|
|
301
341
|
const readers = [...readQueue(teamName), ...(failedAdmissionsByTeam.get(teamName) ?? [])].map((queued, index) => ({
|
|
302
342
|
name: queued.member.name,
|
|
@@ -342,17 +382,34 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
342
382
|
queued: readQueue(activeTeamName).length,
|
|
343
383
|
});
|
|
344
384
|
});
|
|
385
|
+
const checkpointAdmissions = new Map<string, { controller: AbortController; settled: Promise<void> }>();
|
|
386
|
+
function cancelCheckpointAdmission(teamName: string, name: string): Promise<void> | undefined {
|
|
387
|
+
const pending = checkpointAdmissions.get(options.readAgentKey(teamName, name));
|
|
388
|
+
pending?.controller.abort(new Error("Checkpoint preparation cancelled."));
|
|
389
|
+
return pending?.settled;
|
|
390
|
+
}
|
|
345
391
|
let lifecycleProbeCleanedUp = false;
|
|
346
392
|
let pendingChildCancelUnsubscribe = (): void => {};
|
|
347
|
-
pi.on?.("session_shutdown", () => {
|
|
393
|
+
pi.on?.("session_shutdown", async () => {
|
|
348
394
|
if (lifecycleProbeCleanedUp) return;
|
|
349
395
|
lifecycleProbeCleanedUp = true;
|
|
396
|
+
const settlements: Promise<void>[] = [];
|
|
397
|
+
for (const pending of checkpointAdmissions.values()) {
|
|
398
|
+
pending.controller.abort(new Error("Checkpoint preparation cancelled: session is closing."));
|
|
399
|
+
settlements.push(pending.settled);
|
|
400
|
+
}
|
|
350
401
|
for (const [teamName, queue] of queuedReadSpawnsByTeam) {
|
|
351
|
-
for (const queued of queue)
|
|
402
|
+
for (const queued of queue) {
|
|
403
|
+
const removed = removeQueuedReadSpawnById(teamName, queued.id);
|
|
404
|
+
if (removed?.groupSettlement) settlements.push(removed.groupSettlement);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
try { await Promise.all(settlements); }
|
|
408
|
+
finally {
|
|
409
|
+
if (typeof lifecycleProbeUnsubscribe === "function") lifecycleProbeUnsubscribe();
|
|
410
|
+
pendingChildCancelUnsubscribe();
|
|
411
|
+
lifecycleFenceUnsubscribe();
|
|
352
412
|
}
|
|
353
|
-
if (typeof lifecycleProbeUnsubscribe === "function") lifecycleProbeUnsubscribe();
|
|
354
|
-
pendingChildCancelUnsubscribe();
|
|
355
|
-
lifecycleFenceUnsubscribe();
|
|
356
413
|
});
|
|
357
414
|
|
|
358
415
|
function setReadQueue(teamName: string, queue: QueuedReadSpawn[]): void {
|
|
@@ -373,6 +430,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
373
430
|
};
|
|
374
431
|
}
|
|
375
432
|
|
|
433
|
+
function settleQueuedGroup(queued: QueuedReadSpawn): Promise<void> | undefined {
|
|
434
|
+
const binding = queued.member.completionGroup;
|
|
435
|
+
if (!binding) return;
|
|
436
|
+
return Promise.resolve().then(async () => {
|
|
437
|
+
const group = new CompletionGroup(queued.teamName, binding.groupId);
|
|
438
|
+
await group.apply({ type: queued.admissionError ? "rejected" : "cancelled", ...binding,
|
|
439
|
+
queueId: queued.id, reason: queued.admissionError || "Queued assignment cancelled." });
|
|
440
|
+
await enqueueCompletionGroupDeliveries(group);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
376
444
|
function removeQueuedReadSpawnById(
|
|
377
445
|
teamName: string,
|
|
378
446
|
id: string,
|
|
@@ -386,6 +454,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
386
454
|
if (settlePendingAcceptance && removed.pendingChildAcceptance) {
|
|
387
455
|
pendingChildController.settleAcceptance(removed.pendingChildAcceptance);
|
|
388
456
|
}
|
|
457
|
+
if (settlePendingAcceptance) {
|
|
458
|
+
cancelCheckpointAdmission(teamName, removed.member.name);
|
|
459
|
+
removed.groupSettlement = settleQueuedGroup(removed);
|
|
460
|
+
}
|
|
389
461
|
return removed;
|
|
390
462
|
}
|
|
391
463
|
|
|
@@ -397,6 +469,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
397
469
|
for (const queued of removed) {
|
|
398
470
|
releaseNestedReadName(teamName, queued.member.name, queued.nameReservationId);
|
|
399
471
|
if (queued.pendingChildAcceptance) pendingChildController.settleAcceptance(queued.pendingChildAcceptance);
|
|
472
|
+
const pending = cancelCheckpointAdmission(teamName, queued.member.name);
|
|
473
|
+
const group = settleQueuedGroup(queued);
|
|
474
|
+
queued.groupSettlement = pending ? Promise.all([pending, group]).then(() => {}) : group;
|
|
400
475
|
}
|
|
401
476
|
return removed;
|
|
402
477
|
}
|
|
@@ -561,6 +636,12 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
561
636
|
}
|
|
562
637
|
|
|
563
638
|
try {
|
|
639
|
+
if (member.completionGroup) {
|
|
640
|
+
if (!member.lifecycleRunId) throw new Error("Grouped admission requires an actual lifecycle run.");
|
|
641
|
+
await new CompletionGroup(teamName, member.completionGroup.groupId).apply({
|
|
642
|
+
type: "running", ...member.completionGroup, runId: member.lifecycleRunId,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
564
645
|
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
565
646
|
assertPending?.();
|
|
566
647
|
} catch (error) {
|
|
@@ -608,9 +689,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
608
689
|
sessionContextReference = null;
|
|
609
690
|
}
|
|
610
691
|
}
|
|
692
|
+
const continuedPrompt = continuationPrompt(member.checkpointAssignment, prompt);
|
|
611
693
|
const launchPrompt = sessionContextReference
|
|
612
|
-
? `${
|
|
613
|
-
:
|
|
694
|
+
? `${continuedPrompt}${sessionContextReference.promptSuffix}`
|
|
695
|
+
: continuedPrompt;
|
|
614
696
|
const removeReference = () => {
|
|
615
697
|
try {
|
|
616
698
|
removeSessionContextReference(sessionContextReference);
|
|
@@ -622,6 +704,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
622
704
|
try {
|
|
623
705
|
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
624
706
|
assertPending?.();
|
|
707
|
+
if (member.checkpointAssignment?.parent) readCheckpoint(member.checkpointAssignment.parent.id);
|
|
625
708
|
commitLaunch?.();
|
|
626
709
|
const launch = options.runReadAgentInProcess(teamName, member, launchPrompt, ctx, options.readAgentOptions());
|
|
627
710
|
return {
|
|
@@ -654,10 +737,21 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
654
737
|
releaseNameOnFailure = true,
|
|
655
738
|
queuedAcceptance?: PendingChildAcceptance,
|
|
656
739
|
assertPending?: () => void,
|
|
740
|
+
signal?: AbortSignal,
|
|
657
741
|
commitLaunch?: () => void,
|
|
658
742
|
): Promise<boolean> {
|
|
659
743
|
const key = options.readAgentKey(teamName, member.name);
|
|
744
|
+
if (checkpointAdmissions.has(key)) throw new Error(`Checkpoint admission for ${member.name} is already in progress.`);
|
|
660
745
|
reserveReadAdmission(teamName, key, member.role || "read");
|
|
746
|
+
const controller = member.checkpointAssignment ? new AbortController() : undefined;
|
|
747
|
+
let settleCheckpoint = () => {};
|
|
748
|
+
if (controller) checkpointAdmissions.set(key, { controller, settled: new Promise<void>(resolve => { settleCheckpoint = resolve; }) });
|
|
749
|
+
const checkpointSignal = controller ? AbortSignal.any([controller.signal, AbortSignal.timeout(30_000), ...(signal ? [signal] : [])]) : undefined;
|
|
750
|
+
const finishCheckpoint = () => {
|
|
751
|
+
if (controller && checkpointAdmissions.get(key)?.controller === controller) checkpointAdmissions.delete(key);
|
|
752
|
+
settleCheckpoint();
|
|
753
|
+
};
|
|
754
|
+
const assertAdmissionPending = () => { checkpointSignal?.throwIfAborted(); assertPending?.(); };
|
|
661
755
|
|
|
662
756
|
const releaseNameReservation = () => {
|
|
663
757
|
releaseNestedReadName(teamName, member.name, nameReservationId);
|
|
@@ -688,7 +782,15 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
688
782
|
};
|
|
689
783
|
|
|
690
784
|
try {
|
|
691
|
-
|
|
785
|
+
if (member.checkpointAssignment && checkpointSignal) {
|
|
786
|
+
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
787
|
+
assertAdmissionPending();
|
|
788
|
+
member.checkpointAssignment = await captureCheckpointAssignment(member.checkpointAssignment, member.cwd, checkpointSignal);
|
|
789
|
+
}
|
|
790
|
+
const admitted = await admitAndLaunchReadAgentMember(teamName, member, prompt, ctx, queuedAcceptance, assertAdmissionPending, () => {
|
|
791
|
+
finishCheckpoint();
|
|
792
|
+
commitLaunch?.();
|
|
793
|
+
});
|
|
692
794
|
releaseNameReservation();
|
|
693
795
|
void Promise.resolve(admitted.launch).then(
|
|
694
796
|
() => drainAfterRun(admitted.pendingChildRun),
|
|
@@ -699,7 +801,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
699
801
|
settlePendingChild(queuedAcceptance);
|
|
700
802
|
releaseReservationAndDrain();
|
|
701
803
|
throw error;
|
|
702
|
-
}
|
|
804
|
+
} finally { finishCheckpoint(); }
|
|
703
805
|
}
|
|
704
806
|
|
|
705
807
|
async function enqueueReadSpawn(
|
|
@@ -709,13 +811,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
709
811
|
params: any,
|
|
710
812
|
resolved: ReturnType<typeof resolveModel>,
|
|
711
813
|
ctx: any,
|
|
712
|
-
nameReservationId?: string
|
|
814
|
+
nameReservationId?: string,
|
|
815
|
+
signal?: AbortSignal,
|
|
713
816
|
): Promise<QueuedReadSpawn> {
|
|
714
|
-
const append = (pendingChildAcceptance?: PendingChildAcceptance): QueuedReadSpawn => {
|
|
715
|
-
const queue = readQueue(teamName);
|
|
716
|
-
if (queue.some((queued) => queued.member.name === member.name)) {
|
|
717
|
-
throw new Error(`Nested read agent ${member.name} is already active or queued; nested delegation cannot replace an existing run.`);
|
|
718
|
-
}
|
|
817
|
+
const append = async (pendingChildAcceptance?: PendingChildAcceptance): Promise<QueuedReadSpawn> => {
|
|
719
818
|
const queued: QueuedReadSpawn = {
|
|
720
819
|
id: crypto.randomUUID(),
|
|
721
820
|
teamName,
|
|
@@ -726,9 +825,21 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
726
825
|
ctx,
|
|
727
826
|
requestedAt: Date.now(),
|
|
728
827
|
nameReservationId,
|
|
828
|
+
signal,
|
|
729
829
|
pendingChildAcceptance,
|
|
730
830
|
};
|
|
731
|
-
|
|
831
|
+
if (member.completionGroup) {
|
|
832
|
+
const group = new CompletionGroup(teamName, member.completionGroup.groupId);
|
|
833
|
+
await group.apply({ type: "queued", ...member.completionGroup, queueId: queued.id });
|
|
834
|
+
if (lifecycleProbeCleanedUp) {
|
|
835
|
+
await group.apply({ type: "cancelled", ...member.completionGroup, reason: "Agent session is closing." });
|
|
836
|
+
throw new Error("Agent session is closing; admission cancelled.");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (readQueue(teamName).some(item => item.member.name === member.name)) {
|
|
840
|
+
throw new Error(`Nested read agent ${member.name} is already active or queued; nested delegation cannot replace an existing run.`);
|
|
841
|
+
}
|
|
842
|
+
setReadQueue(teamName, [...readQueue(teamName), queued]);
|
|
732
843
|
void drainQueuedReadSpawns(teamName);
|
|
733
844
|
return queued;
|
|
734
845
|
};
|
|
@@ -744,7 +855,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
744
855
|
if (!parent) throw new Error(`Nested read agent ${member.name} is missing exact parent identity.`);
|
|
745
856
|
const pendingChildAcceptance = pendingChildController.acceptChild(parent, member.name);
|
|
746
857
|
try {
|
|
747
|
-
return append(pendingChildAcceptance);
|
|
858
|
+
return await append(pendingChildAcceptance);
|
|
748
859
|
} catch (error) {
|
|
749
860
|
pendingChildController.settleAcceptance(pendingChildAcceptance);
|
|
750
861
|
throw error;
|
|
@@ -785,7 +896,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
785
896
|
if (activeAgentCount(teamName, role, true) >= capacity.maxConcurrent) continue;
|
|
786
897
|
queued.member.joinedAt = Date.now();
|
|
787
898
|
await startReadAgentMember(teamName, queued.member, queued.prompt, queued.ctx,
|
|
788
|
-
queued.nameReservationId, false, queued.pendingChildAcceptance, assertPending,
|
|
899
|
+
queued.nameReservationId, false, queued.pendingChildAcceptance, assertPending, queued.signal,
|
|
789
900
|
() => { queued.launchCommitted = true; });
|
|
790
901
|
removeQueuedReadSpawnById(teamName, queued.id, false);
|
|
791
902
|
progressed = true;
|
|
@@ -799,10 +910,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
799
910
|
}
|
|
800
911
|
queued.admissionError = error instanceof Error ? error.message : String(error);
|
|
801
912
|
failedAdmissionsByTeam.set(teamName, [...(failedAdmissionsByTeam.get(teamName) ?? []), queued].slice(-20));
|
|
802
|
-
removeQueuedReadSpawnById(teamName, queued.id);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
913
|
+
const removed = removeQueuedReadSpawnById(teamName, queued.id);
|
|
914
|
+
let groupSettlementFailed = false;
|
|
915
|
+
await removed?.groupSettlement?.catch(error => {
|
|
916
|
+
groupSettlementFailed = true;
|
|
917
|
+
queued.admissionError += ` Group settlement failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
918
|
+
});
|
|
919
|
+
if (!queued.member.completionGroup || groupSettlementFailed) {
|
|
920
|
+
await messaging.sendPlainMessage(teamName, "system", queued.member.parentAgentName || "team-lead",
|
|
921
|
+
`Queued agent ${queued.member.name} failed admission: ${queued.admissionError}`,
|
|
922
|
+
`Queued agent ${queued.member.name} failed`, "red").catch(() => {});
|
|
923
|
+
}
|
|
806
924
|
}
|
|
807
925
|
}
|
|
808
926
|
}
|
|
@@ -817,8 +935,38 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
817
935
|
return paths.sanitizeName(`session-${sessionId}`);
|
|
818
936
|
}
|
|
819
937
|
|
|
938
|
+
let admittingPublicTeam: string | undefined;
|
|
939
|
+
let publicAdmissions = 0;
|
|
940
|
+
async function withPublicScope<T>(teamName: string, action: () => Promise<T>): Promise<T> {
|
|
941
|
+
if (teamName.startsWith("prompt-build-")) return action();
|
|
942
|
+
if (publicAdmissions > 0 && admittingPublicTeam !== teamName) {
|
|
943
|
+
throw new Error(`Cannot switch to ${teamName}: ${admittingPublicTeam} has unfinished agents being admitted.`);
|
|
944
|
+
}
|
|
945
|
+
admittingPublicTeam = teamName;
|
|
946
|
+
publicAdmissions += 1;
|
|
947
|
+
try {
|
|
948
|
+
return await action();
|
|
949
|
+
} finally {
|
|
950
|
+
if (--publicAdmissions === 0) admittingPublicTeam = undefined;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function requireSettledPublicScope(nextTeamName: string): Promise<void> {
|
|
955
|
+
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
956
|
+
const current = options.getTeamName();
|
|
957
|
+
if (!current || current === nextTeamName || nextTeamName.startsWith("prompt-build-")) return;
|
|
958
|
+
const [config, fences, queued] = await Promise.all([
|
|
959
|
+
teams.readConfig(current), listLifecycleTombstones(current), listQueuedAgentStatuses(current),
|
|
960
|
+
]);
|
|
961
|
+
const unfinished = config.members.some(member => member.name !== "team-lead")
|
|
962
|
+
|| fences.length > 0 || queued.some(item => !item.failed)
|
|
963
|
+
|| Array.from(options.runningReadAgents.values()).some(agent => agent.teamName === current && agent.teardownState !== "finalized");
|
|
964
|
+
if (unfinished) throw new Error(`Cannot switch from ${current} to ${nextTeamName}: unfinished agents must remain visible. Finish or stop them first.`);
|
|
965
|
+
}
|
|
966
|
+
|
|
820
967
|
async function ensureCurrentSessionAgentGroup(ctx: any, explicitDefaultModel: string): Promise<string> {
|
|
821
968
|
const sessionName = currentSessionAgentGroupName(ctx);
|
|
969
|
+
await requireSettledPublicScope(sessionName);
|
|
822
970
|
if (teams.teamExists(sessionName)) {
|
|
823
971
|
options.adoptTeamAsLead(sessionName, ctx);
|
|
824
972
|
return sessionName;
|
|
@@ -837,10 +985,23 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
837
985
|
}
|
|
838
986
|
|
|
839
987
|
async function spawnTeammate(params: any, ctx: any, spawnOptions: SpawnTeammateOptions = {}): Promise<{ content: any[]; details: any }> {
|
|
988
|
+
const checkpointAssignment = createCheckpointAssignment(params.checkpoint, params.prompt, params.continue_from);
|
|
989
|
+
const assignedChecks = normalizeCheckPolicy(params.checks);
|
|
990
|
+
const repairPolicy = normalizeRepairPolicy(params.repair);
|
|
991
|
+
if (repairPolicy && !assignedChecks?.length) throw new Error("Repair policy requires explicitly assigned checks.");
|
|
840
992
|
const safeName = paths.sanitizeName(params.name);
|
|
841
993
|
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
842
994
|
const cwd = params.cwd || ctx.cwd;
|
|
843
995
|
const teamConfig = await teams.readConfig(safeTeamName);
|
|
996
|
+
if (checkpointAssignment?.parent) {
|
|
997
|
+
requireSpawnLevel(params, `Continuation ${safeName}`);
|
|
998
|
+
if (safeName.toLowerCase() === checkpointAssignment.parent.author.agentName.toLowerCase()
|
|
999
|
+
|| teamConfig.members.some(member => member.name.toLowerCase() === safeName.toLowerCase())
|
|
1000
|
+
|| options.runningReadAgents.has(options.readAgentKey(safeTeamName, safeName)) || findQueuedReadSpawn(safeTeamName, { name: safeName })) {
|
|
1001
|
+
throw new Error(`Continuation recipient ${safeName} is not unused; choose a new prefix and retry.`);
|
|
1002
|
+
}
|
|
1003
|
+
assertUnusedContinuationRecipient(safeTeamName, safeName);
|
|
1004
|
+
}
|
|
844
1005
|
failedAdmissionsByTeam.set(safeTeamName, (failedAdmissionsByTeam.get(safeTeamName) ?? []).filter(item => item.member.name !== safeName));
|
|
845
1006
|
let nestedNameReservationId: string | undefined;
|
|
846
1007
|
let nestedNameReservationTransferred = false;
|
|
@@ -871,7 +1032,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
871
1032
|
}
|
|
872
1033
|
|
|
873
1034
|
try {
|
|
874
|
-
if (spawnOptions.once) {
|
|
1035
|
+
if (spawnOptions.once && !checkpointAssignment?.parent) {
|
|
875
1036
|
const existingOnceMember = teamConfig.members.find(m => m.agentType === "teammate" && (m.name === safeName || memberMatchesOperation(m, params)));
|
|
876
1037
|
if (existingOnceMember) {
|
|
877
1038
|
return {
|
|
@@ -910,6 +1071,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
910
1071
|
}
|
|
911
1072
|
}
|
|
912
1073
|
|
|
1074
|
+
if (checkpointAdmissions.has(options.readAgentKey(safeTeamName, safeName)) && !findQueuedReadSpawn(safeTeamName, { name: safeName })) {
|
|
1075
|
+
throw new Error(`Checkpoint admission for ${safeName} is already in progress.`);
|
|
1076
|
+
}
|
|
913
1077
|
const existingMember = teamConfig.members.find(m => m.name === safeName && m.agentType === "teammate");
|
|
914
1078
|
if (existingMember) {
|
|
915
1079
|
const key = options.readAgentKey(safeTeamName, existingMember.name);
|
|
@@ -980,6 +1144,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
980
1144
|
color: role === "read" ? "cyan" : "blue",
|
|
981
1145
|
thinking: chosenThinking,
|
|
982
1146
|
planModeRequired: params.plan_mode_required,
|
|
1147
|
+
assignedChecks,
|
|
1148
|
+
repairPolicy,
|
|
1149
|
+
checkpointAssignment,
|
|
1150
|
+
completionGroup: spawnOptions.completionGroup,
|
|
983
1151
|
metadata: operationMetadataFromParams(params),
|
|
984
1152
|
delegationDepth: spawnOptions.nestedParent ? 1 : 0,
|
|
985
1153
|
allowNestedReadAgents: !spawnOptions.nestedParent && spawnOptions.allowNestedReadAgents === true,
|
|
@@ -991,7 +1159,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
991
1159
|
};
|
|
992
1160
|
|
|
993
1161
|
if (role === "write") await writeQueue.removeQueuedWriteSpawnsByName(safeTeamName, safeName);
|
|
994
|
-
if (!spawnOptions.nestedParent)
|
|
1162
|
+
if (!spawnOptions.nestedParent) {
|
|
1163
|
+
const removed = removeQueuedReadSpawnsByName(safeTeamName, safeName);
|
|
1164
|
+
if (removed.some(item => item.groupSettlement)) await Promise.all(removed.map(item => item.groupSettlement));
|
|
1165
|
+
}
|
|
995
1166
|
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
996
1167
|
const capacity = role === "write" ? settings.writeAgents : settings.readAgents;
|
|
997
1168
|
const activeCount = activeAgentCount(safeTeamName, role, true);
|
|
@@ -999,7 +1170,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
999
1170
|
if (!capacity.queueOverflow) {
|
|
1000
1171
|
throw new Error(`${role === "write" ? "Edit" : "Read"}-agent capacity reached (${activeCount}/${capacity.maxConcurrent}) and queueOverflow is disabled.`);
|
|
1001
1172
|
}
|
|
1002
|
-
const queued = await enqueueReadSpawn(safeTeamName, member, params.prompt, params, resolved, ctx, nestedNameReservationId);
|
|
1173
|
+
const queued = await enqueueReadSpawn(safeTeamName, member, params.prompt, params, resolved, ctx, nestedNameReservationId, spawnOptions.signal);
|
|
1003
1174
|
nestedNameReservationTransferred = !!nestedNameReservationId;
|
|
1004
1175
|
const queuePosition = readQueue(safeTeamName).findIndex(item => item.id === queued.id) + 1;
|
|
1005
1176
|
return {
|
|
@@ -1008,7 +1179,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1008
1179
|
};
|
|
1009
1180
|
}
|
|
1010
1181
|
|
|
1011
|
-
const sessionContextAvailable = await startReadAgentMember(safeTeamName, member, params.prompt, ctx, nestedNameReservationId);
|
|
1182
|
+
const sessionContextAvailable = await startReadAgentMember(safeTeamName, member, params.prompt, ctx, nestedNameReservationId, true, undefined, undefined, spawnOptions.signal);
|
|
1012
1183
|
if (role === "write") {
|
|
1013
1184
|
await writeTeamsDebugEvent(safeTeamName, "write-agent.spawn.success", {
|
|
1014
1185
|
agentName: safeName, cwd, requestedRole: role, model: chosenModel,
|
|
@@ -1033,7 +1204,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1033
1204
|
}
|
|
1034
1205
|
}
|
|
1035
1206
|
|
|
1036
|
-
|
|
1207
|
+
async function handleOrchestrationRequest(payload: any): Promise<void> {
|
|
1037
1208
|
const requestId = payload?.requestId;
|
|
1038
1209
|
const type = String(payload?.type || "");
|
|
1039
1210
|
const params = payload?.params || {};
|
|
@@ -1045,6 +1216,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1045
1216
|
if (options.isTeammate) throw new Error("Teammates cannot satisfy orchestration requests directly.");
|
|
1046
1217
|
if (!ctx) throw new Error("No active lead session context is available for orchestration request. If pi-extended-teams was registered after session_start, include the current Pi command context as payload.ctx.");
|
|
1047
1218
|
|
|
1219
|
+
if (type === "ensure_team" || type === "spawn_teammate_once") {
|
|
1220
|
+
await requireSettledPublicScope(paths.sanitizeName(params.team_name));
|
|
1221
|
+
}
|
|
1048
1222
|
if (type === "ensure_team") {
|
|
1049
1223
|
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
1050
1224
|
if (teams.teamExists(safeTeamName)) {
|
|
@@ -1078,7 +1252,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1078
1252
|
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
1079
1253
|
if (!teams.teamExists(safeTeamName)) throw new Error(`Team ${params.team_name} does not exist`);
|
|
1080
1254
|
options.adoptTeamAsLead(safeTeamName, ctx);
|
|
1081
|
-
const
|
|
1255
|
+
const prepared = params.continue_from === undefined ? params : { ...params, name: freshContinuationName(params.name || "agent") };
|
|
1256
|
+
const result = await spawnTeammate(prepared, ctx, {
|
|
1257
|
+
once: true,
|
|
1258
|
+
allowNestedReadAgents: params.allow_nested_read_agents === true,
|
|
1259
|
+
});
|
|
1260
|
+
emitOrchestrationResponse(requestId, type, { ok: true, details: result.details, content: result.content });
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (type === "spawn_agent") {
|
|
1265
|
+
const result = await spawnPublicAgent(params, ctx);
|
|
1082
1266
|
emitOrchestrationResponse(requestId, type, { ok: true, details: result.details, content: result.content });
|
|
1083
1267
|
return;
|
|
1084
1268
|
}
|
|
@@ -1087,6 +1271,11 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1087
1271
|
} catch (error) {
|
|
1088
1272
|
emitOrchestrationResponse(requestId, type, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1089
1273
|
}
|
|
1274
|
+
}
|
|
1275
|
+
pi.events?.on?.("pi-extended-teams:orchestration-request", (payload: any) => {
|
|
1276
|
+
if (payload?.type !== "ensure_team" && payload?.type !== "spawn_teammate_once") return handleOrchestrationRequest(payload);
|
|
1277
|
+
return withPublicScope(String(payload?.params?.team_name || ""), () => handleOrchestrationRequest(payload))
|
|
1278
|
+
.catch(error => emitOrchestrationResponse(payload?.requestId, payload?.type, { ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1090
1279
|
});
|
|
1091
1280
|
|
|
1092
1281
|
const levelDescription = "Required intent tier. Configured favorites take priority; an unconfigured tier inherits the current lead model and thinking. Read tiers: read-collect gathers bounded facts without owning the conclusion; read-review is the normal default for focused review, verification, and bounded synthesis; read-analyze explains behavior or root cause across connected evidence; read-critical is only for irreducible high-stakes security, architecture, concurrency, migration, or data-correctness reasoning. Write tiers: write-patch makes a narrow localized change; write-feature implements a bounded feature with a known design; write-system owns a cross-cutting integration or refactor within explicitly claimed files; write-critical is only for high-risk security, concurrency, recovery, migration, or data-integrity changes. Prefer canonical tiers; legacy reading-*/writing-* aliases remain accepted for this minor release. Do not pass role, model, or thinking directly; see README.md.";
|
|
@@ -1094,6 +1283,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1094
1283
|
name: Type.Optional(Type.String({ description: "Stable display name. Defaults to a generated agent name." })),
|
|
1095
1284
|
prompt: Type.String({ description: "The agent's assignment, relevant prior context, evidence already gathered, constraints, and report shape." }),
|
|
1096
1285
|
cwd: Type.Optional(Type.String({ description: "Working directory. Defaults to the lead session cwd." })),
|
|
1286
|
+
checks: Type.Optional(CheckPolicySchema),
|
|
1287
|
+
repair: Type.Optional(RepairPolicySchema),
|
|
1288
|
+
checkpoint: Type.Optional(CheckpointPolicySchema),
|
|
1289
|
+
continue_from: Type.Optional(Type.String({ description: "Continue this checkpoint as a fresh recipient/run with the current assignment and tier. name is a prefix, not a recipient to reuse." })),
|
|
1097
1290
|
session_context: Type.Optional(StringEnum(["none", "lazy"] as const, { description: "Optional filtered snapshot of the lead's active session branch. Use lazy only when omitted session history may materially affect the lane; the child reads it on demand rather than receiving transcript content in its prompt.", default: "none" })),
|
|
1098
1291
|
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
1099
1292
|
allow_nested_read_agents: Type.Optional(Type.Boolean({ description: "Opt in eligible depth-0 write-feature/write-critical agents to restricted read-only child spawning.", default: false })),
|
|
@@ -1112,18 +1305,23 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1112
1305
|
return `agent-${Date.now().toString(36)}${position}-${crypto.randomUUID().slice(0, 8)}`;
|
|
1113
1306
|
}
|
|
1114
1307
|
|
|
1115
|
-
|
|
1308
|
+
function spawnPublicAgent(params: any, ctx: any, signal?: AbortSignal): Promise<{ content: any[]; details: any }> {
|
|
1309
|
+
return withPublicScope(currentSessionAgentGroupName(ctx), () => spawnPublicAgentInScope(params, ctx, signal));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
async function spawnPublicAgentInScope(params: any, ctx: any, signal?: AbortSignal): Promise<{ content: any[]; details: any }> {
|
|
1116
1313
|
if (options.isTeammate) throw new Error("Only the lead session can spawn agents.");
|
|
1117
1314
|
if (!ctx) throw new Error("No active Pi session context is available for spawn_agent.");
|
|
1118
1315
|
|
|
1316
|
+
params = { ...params, checkpoint: createCheckpointAssignment(params.checkpoint, params.prompt, params.continue_from)?.policy };
|
|
1317
|
+
const name = params.continue_from === undefined ? params.name || generatedAgentName() : freshContinuationName(params.name || "agent");
|
|
1119
1318
|
const sessionDefaultModel = configuredFavoriteModelForSpawn(params, ctx);
|
|
1120
1319
|
const sessionName = await ensureCurrentSessionAgentGroup(ctx, sessionDefaultModel);
|
|
1121
|
-
const name = params.name || generatedAgentName();
|
|
1122
1320
|
const result = await spawnTeammate({
|
|
1123
1321
|
...params,
|
|
1124
1322
|
name,
|
|
1125
1323
|
team_name: sessionName,
|
|
1126
|
-
}, ctx, { allowNestedReadAgents: params.allow_nested_read_agents === true });
|
|
1324
|
+
}, ctx, { allowNestedReadAgents: params.allow_nested_read_agents === true, signal });
|
|
1127
1325
|
|
|
1128
1326
|
const outcome = result.details.queued
|
|
1129
1327
|
? `Agent ${name} queued at position ${result.details.queuePosition}.`
|
|
@@ -1253,7 +1451,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1253
1451
|
description: "Spawn one agent by intent tier only. Configured favorites take priority; an unconfigured tier inherits the current lead model and thinking. Give it the relevant goal, decisions, prior attempts, inspected evidence, constraints, and expected delta rather than a context-free task. Use session_context=lazy only as an on-demand fallback when omitted session history may materially matter; it never replaces a good mission prompt. read-review is the normal read default; use read-collect for bounded fact gathering, read-analyze for connected explanation/root cause, and read-critical only for irreducible high-stakes reasoning. For edits, choose write-patch, write-feature, write-system, or the rare high-risk write-critical by scope and risk. After spawning, do not duplicate or take over its lane; work only on unrelated work, then end the turn so the automatic report can resume you. One get_agent_status snapshot is allowed when current status is needed; never sleep, busy-wait, repeatedly read inbox/status, or treat healthy silence as failure. Wait for the actual report before synthesizing; intervene only on a reported blocker/error, actual health failure, or explicit user cancellation. model_slot selects behavior, model, and thinking; do not pass role, model, or thinking directly.",
|
|
1254
1452
|
parameters: Type.Object(publicAgentParams),
|
|
1255
1453
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
1256
|
-
return spawnPublicAgent(params, ctx);
|
|
1454
|
+
return spawnPublicAgent(params, ctx, _signal);
|
|
1257
1455
|
},
|
|
1258
1456
|
});
|
|
1259
1457
|
|
|
@@ -1264,11 +1462,15 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1264
1462
|
parameters: Type.Object({
|
|
1265
1463
|
defaults: Type.Optional(Type.Object({
|
|
1266
1464
|
cwd: Type.Optional(Type.String()),
|
|
1465
|
+
checks: Type.Optional(CheckPolicySchema),
|
|
1466
|
+
repair: Type.Optional(RepairPolicySchema),
|
|
1467
|
+
checkpoint: Type.Optional(CheckpointPolicySchema),
|
|
1267
1468
|
model_slot: Type.Optional(StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription })),
|
|
1268
1469
|
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
1269
1470
|
session_context: Type.Optional(StringEnum(["none", "lazy"] as const, { description: "Shared lazy session-reference policy.", default: "none" })),
|
|
1270
1471
|
allow_nested_read_agents: Type.Optional(Type.Boolean({ description: "Shared opt-in for eligible depth-0 write-feature/write-critical agents.", default: false })),
|
|
1271
1472
|
})),
|
|
1473
|
+
completion_group: Type.Optional(CompletionGroupPolicySchema),
|
|
1272
1474
|
agents: Type.Array(Type.Object(publicSwarmAgentParams), { description: "Agents to spawn as one batch. Each one must have model_slot directly or inherit it from defaults." }),
|
|
1273
1475
|
}),
|
|
1274
1476
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
@@ -1276,44 +1478,89 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1276
1478
|
if (!ctx) throw new Error("No active Pi session context is available for spawn_swarm_agents.");
|
|
1277
1479
|
if (!Array.isArray(params.agents) || params.agents.length === 0) throw new Error("spawn_swarm_agents requires at least one agent.");
|
|
1278
1480
|
|
|
1279
|
-
const
|
|
1481
|
+
const groupPolicy = normalizeCompletionGroupPolicy(params.completion_group);
|
|
1482
|
+
const sessionId = getPiSessionId(ctx);
|
|
1483
|
+
if (groupPolicy && (!sessionId || !_toolCallId?.trim())) throw new Error("Completion groups require a bound session and runtime submission identity.");
|
|
1484
|
+
const mergedAgents = params.agents.map((agent: any, index: number) => {
|
|
1485
|
+
const merged = mergeSwarmAgentParams(params.defaults || {}, agent);
|
|
1486
|
+
merged.checkpoint = createCheckpointAssignment(merged.checkpoint, merged.prompt, merged.continue_from)?.policy;
|
|
1487
|
+
if (merged.continue_from !== undefined) merged.name = freshContinuationName(merged.name || "agent", JSON.stringify([sessionId, _toolCallId, index, merged.continue_from]));
|
|
1488
|
+
return merged;
|
|
1489
|
+
});
|
|
1280
1490
|
const defaultModel = configuredFavoriteModelForSpawn(mergedAgents[0], ctx, "spawn_swarm_agents");
|
|
1281
1491
|
for (let index = 0; index < mergedAgents.length; index += 1) {
|
|
1282
1492
|
configuredFavoriteModelForSpawn(mergedAgents[index], ctx, `spawn_swarm_agents agent ${mergedAgents[index].name || index + 1}`);
|
|
1283
1493
|
}
|
|
1284
1494
|
const sessionName = await ensureCurrentSessionAgentGroup(ctx, defaultModel);
|
|
1495
|
+
let group: CompletionGroup | undefined;
|
|
1496
|
+
if (groupPolicy && sessionId) {
|
|
1497
|
+
const identity = { teamName: sessionName, sessionId, submissionId: `tool:${_toolCallId}` };
|
|
1498
|
+
const groupId = completionGroupIdentity(identity);
|
|
1499
|
+
options.onCompletionGroupUse?.(groupId);
|
|
1500
|
+
const config = await teams.readConfig(sessionName);
|
|
1501
|
+
const reports = await listStoredTeamReportEvents(sessionName);
|
|
1502
|
+
const inbox = await messaging.readInbox(sessionName, "team-lead", false, false);
|
|
1503
|
+
const entries = ctx.sessionManager?.getBranch?.() ?? [];
|
|
1504
|
+
const references = [...config.members.map(member => member.completionGroup),
|
|
1505
|
+
...readQueue(sessionName).map(queued => queued.member.completionGroup),
|
|
1506
|
+
...reports.map(report => report.completionGroup), ...inbox.map(message => message.metadata?.completionGroup),
|
|
1507
|
+
...entries.map((entry: any) => entry.message?.details?.completionGroup)];
|
|
1508
|
+
if (references.some(binding => binding?.groupId === groupId)) new CompletionGroup(sessionName, groupId).read();
|
|
1509
|
+
group = await CompletionGroup.create({ ...identity, policy: groupPolicy, members: mergedAgents.map((agent: any, index: number) => ({
|
|
1510
|
+
name: paths.sanitizeName(agent.name || `agent-${crypto.createHash("sha256").update(`${_toolCallId}:${index}`).digest("hex").slice(0, 12)}`),
|
|
1511
|
+
assignmentKey: crypto.createHash("sha256").update(JSON.stringify([agent, ctx.cwd])).digest("hex"),
|
|
1512
|
+
suppressed: sessionName.startsWith("prompt-build-") || shouldSuppressLeadReportInjection(agent),
|
|
1513
|
+
})) });
|
|
1514
|
+
if (group && !group.created) return {
|
|
1515
|
+
content: [{ type: "text", text: `Completion group ${group.groupId} already exists; no assignments replayed. State: ${group.journalPath}\n${AGENT_WAIT_CONTRACT}` }],
|
|
1516
|
+
details: { session: sessionName, completionGroup: { groupId: group.groupId, journalPath: group.journalPath },
|
|
1517
|
+
idempotent: true, spawned: [], failed: [], members: group.read().members },
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1285
1520
|
const spawned: any[] = [];
|
|
1286
1521
|
const failed: Array<{ name: string; error: string }> = [];
|
|
1287
1522
|
|
|
1288
1523
|
for (let index = 0; index < mergedAgents.length; index += 1) {
|
|
1289
1524
|
const merged = mergedAgents[index];
|
|
1290
|
-
const name = merged.name || generatedAgentName(index);
|
|
1525
|
+
const name = group?.read().members[index].name || merged.name || generatedAgentName(index);
|
|
1291
1526
|
try {
|
|
1292
1527
|
const result = await spawnTeammate({
|
|
1293
1528
|
...merged,
|
|
1294
1529
|
name,
|
|
1295
1530
|
team_name: sessionName,
|
|
1296
|
-
}, ctx, { allowNestedReadAgents: merged.allow_nested_read_agents === true });
|
|
1531
|
+
}, ctx, { allowNestedReadAgents: merged.allow_nested_read_agents === true, completionGroup: group?.binding(index), signal: _signal });
|
|
1297
1532
|
spawned.push({ ...result.details, name });
|
|
1298
1533
|
} catch (error) {
|
|
1299
|
-
|
|
1534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1535
|
+
failed.push({ name, error: message });
|
|
1536
|
+
if (group) await group.apply({ type: "rejected", ...group.binding(index), runId: group.read().members[index].runId, reason: message });
|
|
1300
1537
|
}
|
|
1301
1538
|
}
|
|
1539
|
+
if (group) {
|
|
1540
|
+
await group.seal();
|
|
1541
|
+
await enqueueCompletionGroupDeliveries(group);
|
|
1542
|
+
}
|
|
1302
1543
|
|
|
1303
1544
|
const lines = [`Spawned ${spawned.length}/${params.agents.length} agents in the current Pi session.`];
|
|
1304
1545
|
for (const item of spawned) lines.push(`- ${item.name}: ${item.queued ? `queued at position ${item.queuePosition}` : `${item.role}, ${item.mode || "in-process"}`}`);
|
|
1305
1546
|
for (const item of failed) lines.push(`- ${item.name}: failed — ${item.error}`);
|
|
1306
1547
|
lines.push(AGENT_WAIT_CONTRACT);
|
|
1307
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { session: sessionName, spawned, failed
|
|
1548
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { session: sessionName, spawned, failed,
|
|
1549
|
+
...(group ? { completionGroup: { groupId: group.groupId, journalPath: group.journalPath } } : {}) } };
|
|
1308
1550
|
},
|
|
1309
1551
|
});
|
|
1310
1552
|
|
|
1311
1553
|
return {
|
|
1312
1554
|
createNestedReadAgentTools,
|
|
1555
|
+
nestedChildSnapshot,
|
|
1313
1556
|
cancelQueuedAgent: (teamName, agentName) => {
|
|
1314
1557
|
// Retain the entry for admission bookkeeping, but let active teardown own cancellation after launch commits.
|
|
1315
1558
|
if (readQueue(teamName).some(queued => queued.member.name === agentName && queued.launchCommitted)) return false;
|
|
1316
|
-
|
|
1559
|
+
const pending = cancelCheckpointAdmission(teamName, agentName);
|
|
1560
|
+
const removed = removeQueuedReadSpawnsByName(teamName, agentName);
|
|
1561
|
+
return pending || removed.some(item => item.groupSettlement)
|
|
1562
|
+
? Promise.all([pending, ...removed.map(item => item.groupSettlement)]).then(() => !!pending || removed.length > 0)
|
|
1563
|
+
: removed.length > 0;
|
|
1317
1564
|
},
|
|
1318
1565
|
};
|
|
1319
1566
|
}
|