pi-extended-teams 2.2.10 → 2.3.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 +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 +37 -21
- package/extensions/index.ts +257 -60
- package/extensions/internal/pi-check-operations.ts +24 -0
- package/extensions/internal/session-files.ts +10 -2
- package/extensions/runtime/lead-attachment.ts +91 -0
- package/extensions/runtime/reload-owner.ts +107 -0
- package/extensions/runtime/team-host.ts +124 -0
- 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 +225 -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/public-team-tools.ts +51 -0
- package/extensions/tools/task-runtime-tools.ts +23 -4
- package/extensions/tools/team-tools.ts +365 -107
- package/extensions/ui/activity-colors.ts +50 -0
- package/extensions/ui/agent-follow-view.ts +15 -11
- package/extensions/ui/agent-navigation.ts +35 -1
- package/extensions/ui/checkpoints-command.ts +28 -0
- 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/settings.ts +24 -0
- package/src/utils/teams.ts +3 -1
- package/src/utils/write-queue.ts +3 -0
|
@@ -10,14 +10,24 @@ import * as teams from "../../src/utils/teams";
|
|
|
10
10
|
import * as runtime from "../../src/utils/runtime";
|
|
11
11
|
import * as messaging from "../../src/utils/messaging";
|
|
12
12
|
import * as writeQueue from "../../src/utils/write-queue";
|
|
13
|
-
import {
|
|
13
|
+
import { 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 { normalizeCheckPolicy } from "../../src/results/check-policy";
|
|
16
|
+
import { normalizeRepairPolicy } from "../../src/results/repair-policy";
|
|
17
|
+
import { createCheckpointAssignment, captureCheckpointAssignment, continuationPrompt } from "../../src/results/checkpoint-assignment";
|
|
18
|
+
import { assertUnusedContinuationRecipient, freshContinuationName } from "../../src/results/continuation-recipient";
|
|
19
|
+
import { readCheckpoint } from "../../src/results/specialist-checkpoint";
|
|
20
|
+
import { CompletionGroup, completionGroupIdentity, normalizeCompletionGroupPolicy, type CompletionGroupBinding } from "../../src/results/completion-group";
|
|
21
|
+
import { listStoredTeamReportEvents } from "../../src/utils/report-events";
|
|
22
|
+
import { enqueueCompletionGroupDeliveries } from "../../src/results/completion-group-delivery";
|
|
23
|
+
import { shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
|
|
15
24
|
|
|
16
25
|
import type { RunningReadAgent } from "../runtime/types";
|
|
17
26
|
import type { ReadAgentTeardownResult } from "../agents/read-agent-session-lifecycle";
|
|
18
27
|
import type { ShutdownTeammateOptions } from "../team/lifecycle";
|
|
19
28
|
import {
|
|
20
29
|
onLifecycleTombstoneCleared,
|
|
30
|
+
listLifecycleTombstones,
|
|
21
31
|
readLifecycleTombstone,
|
|
22
32
|
withLifecycleTombstoneLock,
|
|
23
33
|
type LifecycleTombstoneLock,
|
|
@@ -37,6 +47,8 @@ import {
|
|
|
37
47
|
type QueuedAgentStatus,
|
|
38
48
|
} from "./agent-status-tool";
|
|
39
49
|
|
|
50
|
+
import { publicTeamToolMetadata } from "./public-team-tools";
|
|
51
|
+
|
|
40
52
|
export const CHILD_AGENT_LIFECYCLE_PROBE = "pi-extended-teams:child-agent-lifecycle-probe";
|
|
41
53
|
|
|
42
54
|
interface ChildAgentLifecycleProbe {
|
|
@@ -55,6 +67,7 @@ export interface TeamToolsOptions {
|
|
|
55
67
|
startWriteAgent(teamName: string, member: Member, prompt: string): Promise<string>;
|
|
56
68
|
shutdownTeammate(teamName: string, member: Member, options?: ShutdownTeammateOptions): Promise<ReadAgentTeardownResult>;
|
|
57
69
|
adoptTeamAsLead(teamName: string, ctx?: any): void;
|
|
70
|
+
onCompletionGroupUse?(groupId: string): void;
|
|
58
71
|
buildRoster(teamName: string): Promise<any>;
|
|
59
72
|
isTeammate: boolean;
|
|
60
73
|
agentName: string;
|
|
@@ -62,6 +75,8 @@ export interface TeamToolsOptions {
|
|
|
62
75
|
getSessionCtx?(): any;
|
|
63
76
|
setSessionCtx?(ctx: any): void;
|
|
64
77
|
pendingChildController?: PendingChildController;
|
|
78
|
+
isHostAttached?(): boolean;
|
|
79
|
+
waitForHost?(): Promise<void>;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
export interface NestedReadAgentToolBinding {
|
|
@@ -71,13 +86,24 @@ export interface NestedReadAgentToolBinding {
|
|
|
71
86
|
outerCtx: any;
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
export interface NestedChildSnapshot {
|
|
90
|
+
running: number;
|
|
91
|
+
queued: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
export interface TeamToolsRuntime {
|
|
75
95
|
createNestedReadAgentTools(binding: NestedReadAgentToolBinding): any[];
|
|
76
|
-
|
|
96
|
+
nestedChildSnapshot(binding: NestedReadAgentToolBinding): NestedChildSnapshot;
|
|
97
|
+
cancelQueuedAgent(teamName: string, agentName: string): boolean | Promise<boolean>;
|
|
98
|
+
hasPendingWork(): boolean;
|
|
99
|
+
closeAdmission(): void;
|
|
100
|
+
resumeQueues(): void;
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
interface SpawnTeammateOptions {
|
|
80
104
|
once?: boolean;
|
|
105
|
+
signal?: AbortSignal;
|
|
106
|
+
completionGroup?: CompletionGroupBinding;
|
|
81
107
|
nestedParent?: {
|
|
82
108
|
teamName: string;
|
|
83
109
|
name: string;
|
|
@@ -97,15 +123,24 @@ interface QueuedReadSpawn {
|
|
|
97
123
|
ctx: any;
|
|
98
124
|
requestedAt: number;
|
|
99
125
|
nameReservationId?: string;
|
|
126
|
+
signal?: AbortSignal;
|
|
100
127
|
pendingChildAcceptance?: PendingChildAcceptance;
|
|
101
128
|
admissionError?: string;
|
|
102
129
|
quarantineError?: string;
|
|
103
130
|
launchCommitted?: boolean;
|
|
131
|
+
groupSettlement?: Promise<void>;
|
|
104
132
|
}
|
|
105
133
|
|
|
106
134
|
export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamToolsRuntime {
|
|
107
135
|
if (options.isTeammate) {
|
|
108
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
createNestedReadAgentTools: () => [],
|
|
138
|
+
nestedChildSnapshot: () => ({ running: 0, queued: 0 }),
|
|
139
|
+
cancelQueuedAgent: () => false,
|
|
140
|
+
hasPendingWork: () => false,
|
|
141
|
+
closeAdmission: () => {},
|
|
142
|
+
resumeQueues: () => {},
|
|
143
|
+
};
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
function emitOrchestrationResponse(requestId: string | undefined, type: string, payload: Record<string, any>): void {
|
|
@@ -297,6 +332,20 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
297
332
|
return queuedReadSpawnsByTeam.get(teamName) ?? [];
|
|
298
333
|
}
|
|
299
334
|
|
|
335
|
+
function nestedChildSnapshot(binding: NestedReadAgentToolBinding): NestedChildSnapshot {
|
|
336
|
+
const parent = {
|
|
337
|
+
teamName: binding.teamName,
|
|
338
|
+
parentName: binding.parent.name,
|
|
339
|
+
parentRunId: binding.parentRunId,
|
|
340
|
+
};
|
|
341
|
+
const queued = readQueue(binding.teamName).filter((child) => {
|
|
342
|
+
const childParent = pendingParentForMember(binding.teamName, child.member);
|
|
343
|
+
return childParent?.parentName === parent.parentName && childParent.parentRunId === parent.parentRunId;
|
|
344
|
+
}).length;
|
|
345
|
+
const pending = pendingChildController.pendingCount(parent);
|
|
346
|
+
return { running: Math.max(0, pending - queued), queued };
|
|
347
|
+
}
|
|
348
|
+
|
|
300
349
|
async function listQueuedAgentStatuses(teamName: string): Promise<QueuedAgentStatus[]> {
|
|
301
350
|
const readers = [...readQueue(teamName), ...(failedAdmissionsByTeam.get(teamName) ?? [])].map((queued, index) => ({
|
|
302
351
|
name: queued.member.name,
|
|
@@ -342,17 +391,62 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
342
391
|
queued: readQueue(activeTeamName).length,
|
|
343
392
|
});
|
|
344
393
|
});
|
|
394
|
+
const checkpointAdmissions = new Map<string, { controller: AbortController; settled: Promise<void> }>();
|
|
395
|
+
function cancelCheckpointAdmission(teamName: string, name: string): Promise<void> | undefined {
|
|
396
|
+
const pending = checkpointAdmissions.get(options.readAgentKey(teamName, name));
|
|
397
|
+
pending?.controller.abort(new Error("Checkpoint preparation cancelled."));
|
|
398
|
+
return pending?.settled;
|
|
399
|
+
}
|
|
345
400
|
let lifecycleProbeCleanedUp = false;
|
|
401
|
+
let shutdownStarted = false;
|
|
402
|
+
const closeAdmission = () => { lifecycleProbeCleanedUp = true; };
|
|
403
|
+
const admissions = new Set<Promise<void>>();
|
|
404
|
+
async function trackAdmission<T>(action: () => Promise<T>): Promise<T> {
|
|
405
|
+
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
406
|
+
let settle!: () => void;
|
|
407
|
+
const settled = new Promise<void>(resolve => {
|
|
408
|
+
settle = resolve;
|
|
409
|
+
});
|
|
410
|
+
admissions.add(settled);
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
return await action();
|
|
414
|
+
} finally {
|
|
415
|
+
admissions.delete(settled);
|
|
416
|
+
settle();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
346
419
|
let pendingChildCancelUnsubscribe = (): void => {};
|
|
347
|
-
pi.on?.("session_shutdown", () => {
|
|
348
|
-
if (
|
|
349
|
-
|
|
420
|
+
pi.on?.("session_shutdown", async () => {
|
|
421
|
+
if (shutdownStarted) return;
|
|
422
|
+
shutdownStarted = true;
|
|
423
|
+
closeAdmission();
|
|
424
|
+
const settlements: Promise<void>[] = [];
|
|
425
|
+
for (const pending of checkpointAdmissions.values()) {
|
|
426
|
+
pending.controller.abort(new Error("Checkpoint preparation cancelled: session is closing."));
|
|
427
|
+
settlements.push(pending.settled);
|
|
428
|
+
}
|
|
350
429
|
for (const [teamName, queue] of queuedReadSpawnsByTeam) {
|
|
351
|
-
for (const queued of queue)
|
|
430
|
+
for (const queued of queue) {
|
|
431
|
+
const removed = removeQueuedReadSpawnById(teamName, queued.id);
|
|
432
|
+
if (removed?.groupSettlement) settlements.push(removed.groupSettlement);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
436
|
+
const admissionSettlement = Promise.race([
|
|
437
|
+
Promise.all([...admissions]),
|
|
438
|
+
new Promise<never>((_resolve, reject) => {
|
|
439
|
+
deadline = setTimeout(() => reject(new Error("Agent admissions are still settling during shutdown.")), 2_500);
|
|
440
|
+
deadline.unref?.();
|
|
441
|
+
}),
|
|
442
|
+
]);
|
|
443
|
+
try { await Promise.all([...settlements, admissionSettlement]); }
|
|
444
|
+
finally {
|
|
445
|
+
clearTimeout(deadline);
|
|
446
|
+
if (typeof lifecycleProbeUnsubscribe === "function") lifecycleProbeUnsubscribe();
|
|
447
|
+
pendingChildCancelUnsubscribe();
|
|
448
|
+
lifecycleFenceUnsubscribe();
|
|
352
449
|
}
|
|
353
|
-
if (typeof lifecycleProbeUnsubscribe === "function") lifecycleProbeUnsubscribe();
|
|
354
|
-
pendingChildCancelUnsubscribe();
|
|
355
|
-
lifecycleFenceUnsubscribe();
|
|
356
450
|
});
|
|
357
451
|
|
|
358
452
|
function setReadQueue(teamName: string, queue: QueuedReadSpawn[]): void {
|
|
@@ -373,6 +467,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
373
467
|
};
|
|
374
468
|
}
|
|
375
469
|
|
|
470
|
+
function settleQueuedGroup(queued: QueuedReadSpawn): Promise<void> | undefined {
|
|
471
|
+
const binding = queued.member.completionGroup;
|
|
472
|
+
if (!binding) return;
|
|
473
|
+
return Promise.resolve().then(async () => {
|
|
474
|
+
const group = new CompletionGroup(queued.teamName, binding.groupId);
|
|
475
|
+
await group.apply({ type: queued.admissionError ? "rejected" : "cancelled", ...binding,
|
|
476
|
+
queueId: queued.id, reason: queued.admissionError || "Queued assignment cancelled." });
|
|
477
|
+
await enqueueCompletionGroupDeliveries(group);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
376
481
|
function removeQueuedReadSpawnById(
|
|
377
482
|
teamName: string,
|
|
378
483
|
id: string,
|
|
@@ -386,6 +491,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
386
491
|
if (settlePendingAcceptance && removed.pendingChildAcceptance) {
|
|
387
492
|
pendingChildController.settleAcceptance(removed.pendingChildAcceptance);
|
|
388
493
|
}
|
|
494
|
+
if (settlePendingAcceptance) {
|
|
495
|
+
cancelCheckpointAdmission(teamName, removed.member.name);
|
|
496
|
+
removed.groupSettlement = settleQueuedGroup(removed);
|
|
497
|
+
}
|
|
389
498
|
return removed;
|
|
390
499
|
}
|
|
391
500
|
|
|
@@ -397,6 +506,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
397
506
|
for (const queued of removed) {
|
|
398
507
|
releaseNestedReadName(teamName, queued.member.name, queued.nameReservationId);
|
|
399
508
|
if (queued.pendingChildAcceptance) pendingChildController.settleAcceptance(queued.pendingChildAcceptance);
|
|
509
|
+
const pending = cancelCheckpointAdmission(teamName, queued.member.name);
|
|
510
|
+
const group = settleQueuedGroup(queued);
|
|
511
|
+
queued.groupSettlement = pending ? Promise.all([pending, group]).then(() => {}) : group;
|
|
400
512
|
}
|
|
401
513
|
return removed;
|
|
402
514
|
}
|
|
@@ -561,6 +673,12 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
561
673
|
}
|
|
562
674
|
|
|
563
675
|
try {
|
|
676
|
+
if (member.completionGroup) {
|
|
677
|
+
if (!member.lifecycleRunId) throw new Error("Grouped admission requires an actual lifecycle run.");
|
|
678
|
+
await new CompletionGroup(teamName, member.completionGroup.groupId).apply({
|
|
679
|
+
type: "running", ...member.completionGroup, runId: member.lifecycleRunId,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
564
682
|
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
565
683
|
assertPending?.();
|
|
566
684
|
} catch (error) {
|
|
@@ -608,9 +726,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
608
726
|
sessionContextReference = null;
|
|
609
727
|
}
|
|
610
728
|
}
|
|
729
|
+
const continuedPrompt = continuationPrompt(member.checkpointAssignment, prompt);
|
|
611
730
|
const launchPrompt = sessionContextReference
|
|
612
|
-
? `${
|
|
613
|
-
:
|
|
731
|
+
? `${continuedPrompt}${sessionContextReference.promptSuffix}`
|
|
732
|
+
: continuedPrompt;
|
|
614
733
|
const removeReference = () => {
|
|
615
734
|
try {
|
|
616
735
|
removeSessionContextReference(sessionContextReference);
|
|
@@ -622,6 +741,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
622
741
|
try {
|
|
623
742
|
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
624
743
|
assertPending?.();
|
|
744
|
+
if (member.checkpointAssignment?.parent) readCheckpoint(member.checkpointAssignment.parent.id);
|
|
625
745
|
commitLaunch?.();
|
|
626
746
|
const launch = options.runReadAgentInProcess(teamName, member, launchPrompt, ctx, options.readAgentOptions());
|
|
627
747
|
return {
|
|
@@ -654,10 +774,21 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
654
774
|
releaseNameOnFailure = true,
|
|
655
775
|
queuedAcceptance?: PendingChildAcceptance,
|
|
656
776
|
assertPending?: () => void,
|
|
777
|
+
signal?: AbortSignal,
|
|
657
778
|
commitLaunch?: () => void,
|
|
658
779
|
): Promise<boolean> {
|
|
659
780
|
const key = options.readAgentKey(teamName, member.name);
|
|
781
|
+
if (checkpointAdmissions.has(key)) throw new Error(`Checkpoint admission for ${member.name} is already in progress.`);
|
|
660
782
|
reserveReadAdmission(teamName, key, member.role || "read");
|
|
783
|
+
const controller = member.checkpointAssignment ? new AbortController() : undefined;
|
|
784
|
+
let settleCheckpoint = () => {};
|
|
785
|
+
if (controller) checkpointAdmissions.set(key, { controller, settled: new Promise<void>(resolve => { settleCheckpoint = resolve; }) });
|
|
786
|
+
const checkpointSignal = controller ? AbortSignal.any([controller.signal, AbortSignal.timeout(30_000), ...(signal ? [signal] : [])]) : undefined;
|
|
787
|
+
const finishCheckpoint = () => {
|
|
788
|
+
if (controller && checkpointAdmissions.get(key)?.controller === controller) checkpointAdmissions.delete(key);
|
|
789
|
+
settleCheckpoint();
|
|
790
|
+
};
|
|
791
|
+
const assertAdmissionPending = () => { checkpointSignal?.throwIfAborted(); assertPending?.(); };
|
|
661
792
|
|
|
662
793
|
const releaseNameReservation = () => {
|
|
663
794
|
releaseNestedReadName(teamName, member.name, nameReservationId);
|
|
@@ -688,7 +819,15 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
688
819
|
};
|
|
689
820
|
|
|
690
821
|
try {
|
|
691
|
-
|
|
822
|
+
if (member.checkpointAssignment && checkpointSignal) {
|
|
823
|
+
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
824
|
+
assertAdmissionPending();
|
|
825
|
+
member.checkpointAssignment = await captureCheckpointAssignment(member.checkpointAssignment, member.cwd, checkpointSignal);
|
|
826
|
+
}
|
|
827
|
+
const admitted = await admitAndLaunchReadAgentMember(teamName, member, prompt, ctx, queuedAcceptance, assertAdmissionPending, () => {
|
|
828
|
+
finishCheckpoint();
|
|
829
|
+
commitLaunch?.();
|
|
830
|
+
});
|
|
692
831
|
releaseNameReservation();
|
|
693
832
|
void Promise.resolve(admitted.launch).then(
|
|
694
833
|
() => drainAfterRun(admitted.pendingChildRun),
|
|
@@ -699,7 +838,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
699
838
|
settlePendingChild(queuedAcceptance);
|
|
700
839
|
releaseReservationAndDrain();
|
|
701
840
|
throw error;
|
|
702
|
-
}
|
|
841
|
+
} finally { finishCheckpoint(); }
|
|
703
842
|
}
|
|
704
843
|
|
|
705
844
|
async function enqueueReadSpawn(
|
|
@@ -709,13 +848,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
709
848
|
params: any,
|
|
710
849
|
resolved: ReturnType<typeof resolveModel>,
|
|
711
850
|
ctx: any,
|
|
712
|
-
nameReservationId?: string
|
|
851
|
+
nameReservationId?: string,
|
|
852
|
+
signal?: AbortSignal,
|
|
713
853
|
): 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
|
-
}
|
|
854
|
+
const append = async (pendingChildAcceptance?: PendingChildAcceptance): Promise<QueuedReadSpawn> => {
|
|
719
855
|
const queued: QueuedReadSpawn = {
|
|
720
856
|
id: crypto.randomUUID(),
|
|
721
857
|
teamName,
|
|
@@ -726,9 +862,21 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
726
862
|
ctx,
|
|
727
863
|
requestedAt: Date.now(),
|
|
728
864
|
nameReservationId,
|
|
865
|
+
signal,
|
|
729
866
|
pendingChildAcceptance,
|
|
730
867
|
};
|
|
731
|
-
|
|
868
|
+
if (member.completionGroup) {
|
|
869
|
+
const group = new CompletionGroup(teamName, member.completionGroup.groupId);
|
|
870
|
+
await group.apply({ type: "queued", ...member.completionGroup, queueId: queued.id });
|
|
871
|
+
if (lifecycleProbeCleanedUp) {
|
|
872
|
+
await group.apply({ type: "cancelled", ...member.completionGroup, reason: "Agent session is closing." });
|
|
873
|
+
throw new Error("Agent session is closing; admission cancelled.");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (readQueue(teamName).some(item => item.member.name === member.name)) {
|
|
877
|
+
throw new Error(`Nested read agent ${member.name} is already active or queued; nested delegation cannot replace an existing run.`);
|
|
878
|
+
}
|
|
879
|
+
setReadQueue(teamName, [...readQueue(teamName), queued]);
|
|
732
880
|
void drainQueuedReadSpawns(teamName);
|
|
733
881
|
return queued;
|
|
734
882
|
};
|
|
@@ -744,7 +892,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
744
892
|
if (!parent) throw new Error(`Nested read agent ${member.name} is missing exact parent identity.`);
|
|
745
893
|
const pendingChildAcceptance = pendingChildController.acceptChild(parent, member.name);
|
|
746
894
|
try {
|
|
747
|
-
return append(pendingChildAcceptance);
|
|
895
|
+
return await append(pendingChildAcceptance);
|
|
748
896
|
} catch (error) {
|
|
749
897
|
pendingChildController.settleAcceptance(pendingChildAcceptance);
|
|
750
898
|
throw error;
|
|
@@ -753,7 +901,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
753
901
|
}
|
|
754
902
|
|
|
755
903
|
async function drainQueuedReadSpawns(teamName: string): Promise<void> {
|
|
756
|
-
if (lifecycleProbeCleanedUp) return;
|
|
904
|
+
if (lifecycleProbeCleanedUp || options.isHostAttached?.() === false) return;
|
|
757
905
|
if (readQueueDrainingTeams.has(teamName)) {
|
|
758
906
|
pendingQueueDrains.add(teamName);
|
|
759
907
|
return;
|
|
@@ -761,7 +909,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
761
909
|
readQueueDrainingTeams.add(teamName);
|
|
762
910
|
try {
|
|
763
911
|
let progressed = true;
|
|
764
|
-
while (progressed && !lifecycleProbeCleanedUp) {
|
|
912
|
+
while (progressed && !lifecycleProbeCleanedUp && options.isHostAttached?.() !== false) {
|
|
765
913
|
progressed = false;
|
|
766
914
|
for (const queued of readQueue(teamName)) {
|
|
767
915
|
if (queued.admissionError) continue;
|
|
@@ -785,7 +933,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
785
933
|
if (activeAgentCount(teamName, role, true) >= capacity.maxConcurrent) continue;
|
|
786
934
|
queued.member.joinedAt = Date.now();
|
|
787
935
|
await startReadAgentMember(teamName, queued.member, queued.prompt, queued.ctx,
|
|
788
|
-
queued.nameReservationId, false, queued.pendingChildAcceptance, assertPending,
|
|
936
|
+
queued.nameReservationId, false, queued.pendingChildAcceptance, assertPending, queued.signal,
|
|
789
937
|
() => { queued.launchCommitted = true; });
|
|
790
938
|
removeQueuedReadSpawnById(teamName, queued.id, false);
|
|
791
939
|
progressed = true;
|
|
@@ -799,10 +947,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
799
947
|
}
|
|
800
948
|
queued.admissionError = error instanceof Error ? error.message : String(error);
|
|
801
949
|
failedAdmissionsByTeam.set(teamName, [...(failedAdmissionsByTeam.get(teamName) ?? []), queued].slice(-20));
|
|
802
|
-
removeQueuedReadSpawnById(teamName, queued.id);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
950
|
+
const removed = removeQueuedReadSpawnById(teamName, queued.id);
|
|
951
|
+
let groupSettlementFailed = false;
|
|
952
|
+
await removed?.groupSettlement?.catch(error => {
|
|
953
|
+
groupSettlementFailed = true;
|
|
954
|
+
queued.admissionError += ` Group settlement failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
955
|
+
});
|
|
956
|
+
if (!queued.member.completionGroup || groupSettlementFailed) {
|
|
957
|
+
await messaging.sendPlainMessage(teamName, "system", queued.member.parentAgentName || "team-lead",
|
|
958
|
+
`Queued agent ${queued.member.name} failed admission: ${queued.admissionError}`,
|
|
959
|
+
`Queued agent ${queued.member.name} failed`, "red").catch(() => {});
|
|
960
|
+
}
|
|
806
961
|
}
|
|
807
962
|
}
|
|
808
963
|
}
|
|
@@ -817,8 +972,38 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
817
972
|
return paths.sanitizeName(`session-${sessionId}`);
|
|
818
973
|
}
|
|
819
974
|
|
|
975
|
+
let admittingPublicTeam: string | undefined;
|
|
976
|
+
let publicAdmissions = 0;
|
|
977
|
+
async function withPublicScope<T>(teamName: string, action: () => Promise<T>): Promise<T> {
|
|
978
|
+
if (teamName.startsWith("prompt-build-")) return action();
|
|
979
|
+
if (publicAdmissions > 0 && admittingPublicTeam !== teamName) {
|
|
980
|
+
throw new Error(`Cannot switch to ${teamName}: ${admittingPublicTeam} has unfinished agents being admitted.`);
|
|
981
|
+
}
|
|
982
|
+
admittingPublicTeam = teamName;
|
|
983
|
+
publicAdmissions += 1;
|
|
984
|
+
try {
|
|
985
|
+
return await action();
|
|
986
|
+
} finally {
|
|
987
|
+
if (--publicAdmissions === 0) admittingPublicTeam = undefined;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function requireSettledPublicScope(nextTeamName: string): Promise<void> {
|
|
992
|
+
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
993
|
+
const current = options.getTeamName();
|
|
994
|
+
if (!current || current === nextTeamName || nextTeamName.startsWith("prompt-build-")) return;
|
|
995
|
+
const [config, fences, queued] = await Promise.all([
|
|
996
|
+
teams.readConfig(current), listLifecycleTombstones(current), listQueuedAgentStatuses(current),
|
|
997
|
+
]);
|
|
998
|
+
const unfinished = config.members.some(member => member.name !== "team-lead")
|
|
999
|
+
|| fences.length > 0 || queued.some(item => !item.failed)
|
|
1000
|
+
|| Array.from(options.runningReadAgents.values()).some(agent => agent.teamName === current && agent.teardownState !== "finalized");
|
|
1001
|
+
if (unfinished) throw new Error(`Cannot switch from ${current} to ${nextTeamName}: unfinished agents must remain visible. Finish or stop them first.`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
820
1004
|
async function ensureCurrentSessionAgentGroup(ctx: any, explicitDefaultModel: string): Promise<string> {
|
|
821
1005
|
const sessionName = currentSessionAgentGroupName(ctx);
|
|
1006
|
+
await requireSettledPublicScope(sessionName);
|
|
822
1007
|
if (teams.teamExists(sessionName)) {
|
|
823
1008
|
options.adoptTeamAsLead(sessionName, ctx);
|
|
824
1009
|
return sessionName;
|
|
@@ -837,10 +1022,23 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
837
1022
|
}
|
|
838
1023
|
|
|
839
1024
|
async function spawnTeammate(params: any, ctx: any, spawnOptions: SpawnTeammateOptions = {}): Promise<{ content: any[]; details: any }> {
|
|
1025
|
+
const checkpointAssignment = createCheckpointAssignment(params.checkpoint, params.prompt, params.continue_from);
|
|
1026
|
+
const assignedChecks = normalizeCheckPolicy(params.checks);
|
|
1027
|
+
const repairPolicy = normalizeRepairPolicy(params.repair);
|
|
1028
|
+
if (repairPolicy && !assignedChecks?.length) throw new Error("Repair policy requires explicitly assigned checks.");
|
|
840
1029
|
const safeName = paths.sanitizeName(params.name);
|
|
841
1030
|
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
842
1031
|
const cwd = params.cwd || ctx.cwd;
|
|
843
1032
|
const teamConfig = await teams.readConfig(safeTeamName);
|
|
1033
|
+
if (checkpointAssignment?.parent) {
|
|
1034
|
+
requireSpawnLevel(params, `Continuation ${safeName}`);
|
|
1035
|
+
if (safeName.toLowerCase() === checkpointAssignment.parent.author.agentName.toLowerCase()
|
|
1036
|
+
|| teamConfig.members.some(member => member.name.toLowerCase() === safeName.toLowerCase())
|
|
1037
|
+
|| options.runningReadAgents.has(options.readAgentKey(safeTeamName, safeName)) || findQueuedReadSpawn(safeTeamName, { name: safeName })) {
|
|
1038
|
+
throw new Error(`Continuation recipient ${safeName} is not unused; choose a new prefix and retry.`);
|
|
1039
|
+
}
|
|
1040
|
+
assertUnusedContinuationRecipient(safeTeamName, safeName);
|
|
1041
|
+
}
|
|
844
1042
|
failedAdmissionsByTeam.set(safeTeamName, (failedAdmissionsByTeam.get(safeTeamName) ?? []).filter(item => item.member.name !== safeName));
|
|
845
1043
|
let nestedNameReservationId: string | undefined;
|
|
846
1044
|
let nestedNameReservationTransferred = false;
|
|
@@ -871,7 +1069,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
871
1069
|
}
|
|
872
1070
|
|
|
873
1071
|
try {
|
|
874
|
-
if (spawnOptions.once) {
|
|
1072
|
+
if (spawnOptions.once && !checkpointAssignment?.parent) {
|
|
875
1073
|
const existingOnceMember = teamConfig.members.find(m => m.agentType === "teammate" && (m.name === safeName || memberMatchesOperation(m, params)));
|
|
876
1074
|
if (existingOnceMember) {
|
|
877
1075
|
return {
|
|
@@ -910,6 +1108,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
910
1108
|
}
|
|
911
1109
|
}
|
|
912
1110
|
|
|
1111
|
+
if (checkpointAdmissions.has(options.readAgentKey(safeTeamName, safeName)) && !findQueuedReadSpawn(safeTeamName, { name: safeName })) {
|
|
1112
|
+
throw new Error(`Checkpoint admission for ${safeName} is already in progress.`);
|
|
1113
|
+
}
|
|
913
1114
|
const existingMember = teamConfig.members.find(m => m.name === safeName && m.agentType === "teammate");
|
|
914
1115
|
if (existingMember) {
|
|
915
1116
|
const key = options.readAgentKey(safeTeamName, existingMember.name);
|
|
@@ -980,6 +1181,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
980
1181
|
color: role === "read" ? "cyan" : "blue",
|
|
981
1182
|
thinking: chosenThinking,
|
|
982
1183
|
planModeRequired: params.plan_mode_required,
|
|
1184
|
+
assignedChecks,
|
|
1185
|
+
repairPolicy,
|
|
1186
|
+
checkpointAssignment,
|
|
1187
|
+
completionGroup: spawnOptions.completionGroup,
|
|
983
1188
|
metadata: operationMetadataFromParams(params),
|
|
984
1189
|
delegationDepth: spawnOptions.nestedParent ? 1 : 0,
|
|
985
1190
|
allowNestedReadAgents: !spawnOptions.nestedParent && spawnOptions.allowNestedReadAgents === true,
|
|
@@ -991,7 +1196,10 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
991
1196
|
};
|
|
992
1197
|
|
|
993
1198
|
if (role === "write") await writeQueue.removeQueuedWriteSpawnsByName(safeTeamName, safeName);
|
|
994
|
-
if (!spawnOptions.nestedParent)
|
|
1199
|
+
if (!spawnOptions.nestedParent) {
|
|
1200
|
+
const removed = removeQueuedReadSpawnsByName(safeTeamName, safeName);
|
|
1201
|
+
if (removed.some(item => item.groupSettlement)) await Promise.all(removed.map(item => item.groupSettlement));
|
|
1202
|
+
}
|
|
995
1203
|
if (lifecycleProbeCleanedUp) throw new Error("Agent session is closing; admission cancelled.");
|
|
996
1204
|
const capacity = role === "write" ? settings.writeAgents : settings.readAgents;
|
|
997
1205
|
const activeCount = activeAgentCount(safeTeamName, role, true);
|
|
@@ -999,7 +1207,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
999
1207
|
if (!capacity.queueOverflow) {
|
|
1000
1208
|
throw new Error(`${role === "write" ? "Edit" : "Read"}-agent capacity reached (${activeCount}/${capacity.maxConcurrent}) and queueOverflow is disabled.`);
|
|
1001
1209
|
}
|
|
1002
|
-
const queued = await enqueueReadSpawn(safeTeamName, member, params.prompt, params, resolved, ctx, nestedNameReservationId);
|
|
1210
|
+
const queued = await enqueueReadSpawn(safeTeamName, member, params.prompt, params, resolved, ctx, nestedNameReservationId, spawnOptions.signal);
|
|
1003
1211
|
nestedNameReservationTransferred = !!nestedNameReservationId;
|
|
1004
1212
|
const queuePosition = readQueue(safeTeamName).findIndex(item => item.id === queued.id) + 1;
|
|
1005
1213
|
return {
|
|
@@ -1008,7 +1216,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1008
1216
|
};
|
|
1009
1217
|
}
|
|
1010
1218
|
|
|
1011
|
-
const sessionContextAvailable = await startReadAgentMember(safeTeamName, member, params.prompt, ctx, nestedNameReservationId);
|
|
1219
|
+
const sessionContextAvailable = await startReadAgentMember(safeTeamName, member, params.prompt, ctx, nestedNameReservationId, true, undefined, undefined, spawnOptions.signal);
|
|
1012
1220
|
if (role === "write") {
|
|
1013
1221
|
await writeTeamsDebugEvent(safeTeamName, "write-agent.spawn.success", {
|
|
1014
1222
|
agentName: safeName, cwd, requestedRole: role, model: chosenModel,
|
|
@@ -1033,7 +1241,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1033
1241
|
}
|
|
1034
1242
|
}
|
|
1035
1243
|
|
|
1036
|
-
|
|
1244
|
+
async function handleOrchestrationRequest(payload: any): Promise<void> {
|
|
1037
1245
|
const requestId = payload?.requestId;
|
|
1038
1246
|
const type = String(payload?.type || "");
|
|
1039
1247
|
const params = payload?.params || {};
|
|
@@ -1045,6 +1253,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1045
1253
|
if (options.isTeammate) throw new Error("Teammates cannot satisfy orchestration requests directly.");
|
|
1046
1254
|
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
1255
|
|
|
1256
|
+
if (type === "ensure_team" || type === "spawn_teammate_once") {
|
|
1257
|
+
await requireSettledPublicScope(paths.sanitizeName(params.team_name));
|
|
1258
|
+
}
|
|
1048
1259
|
if (type === "ensure_team") {
|
|
1049
1260
|
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
1050
1261
|
if (teams.teamExists(safeTeamName)) {
|
|
@@ -1078,7 +1289,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1078
1289
|
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
1079
1290
|
if (!teams.teamExists(safeTeamName)) throw new Error(`Team ${params.team_name} does not exist`);
|
|
1080
1291
|
options.adoptTeamAsLead(safeTeamName, ctx);
|
|
1081
|
-
const
|
|
1292
|
+
const prepared = params.continue_from === undefined ? params : { ...params, name: freshContinuationName(params.name || "agent") };
|
|
1293
|
+
const result = await spawnTeammate(prepared, ctx, {
|
|
1294
|
+
once: true,
|
|
1295
|
+
allowNestedReadAgents: params.allow_nested_read_agents === true,
|
|
1296
|
+
});
|
|
1297
|
+
emitOrchestrationResponse(requestId, type, { ok: true, details: result.details, content: result.content });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (type === "spawn_agent") {
|
|
1302
|
+
const result = await spawnPublicAgent(params, ctx);
|
|
1082
1303
|
emitOrchestrationResponse(requestId, type, { ok: true, details: result.details, content: result.content });
|
|
1083
1304
|
return;
|
|
1084
1305
|
}
|
|
@@ -1087,43 +1308,35 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1087
1308
|
} catch (error) {
|
|
1088
1309
|
emitOrchestrationResponse(requestId, type, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1089
1310
|
}
|
|
1311
|
+
}
|
|
1312
|
+
pi.events?.on?.("pi-extended-teams:orchestration-request", (payload: any) => {
|
|
1313
|
+
if (payload?.type !== "ensure_team" && payload?.type !== "spawn_teammate_once") return handleOrchestrationRequest(payload);
|
|
1314
|
+
return trackAdmission(() => withPublicScope(String(payload?.params?.team_name || ""), () => handleOrchestrationRequest(payload)))
|
|
1315
|
+
.catch(error => emitOrchestrationResponse(payload?.requestId, payload?.type, { ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1090
1316
|
});
|
|
1091
1317
|
|
|
1092
|
-
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.";
|
|
1093
|
-
const publicAgentBaseParams = {
|
|
1094
|
-
name: Type.Optional(Type.String({ description: "Stable display name. Defaults to a generated agent name." })),
|
|
1095
|
-
prompt: Type.String({ description: "The agent's assignment, relevant prior context, evidence already gathered, constraints, and report shape." }),
|
|
1096
|
-
cwd: Type.Optional(Type.String({ description: "Working directory. Defaults to the lead session cwd." })),
|
|
1097
|
-
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
|
-
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
1099
|
-
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 })),
|
|
1100
|
-
};
|
|
1101
|
-
const publicAgentParams = {
|
|
1102
|
-
...publicAgentBaseParams,
|
|
1103
|
-
model_slot: StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription, default: "read-review" }),
|
|
1104
|
-
};
|
|
1105
|
-
const publicSwarmAgentParams = {
|
|
1106
|
-
...publicAgentBaseParams,
|
|
1107
|
-
model_slot: Type.Optional(StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription })),
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
1318
|
function generatedAgentName(index?: number): string {
|
|
1111
1319
|
const position = index === undefined ? "" : `-${index + 1}`;
|
|
1112
1320
|
return `agent-${Date.now().toString(36)}${position}-${crypto.randomUUID().slice(0, 8)}`;
|
|
1113
1321
|
}
|
|
1114
1322
|
|
|
1115
|
-
|
|
1323
|
+
function spawnPublicAgent(params: any, ctx: any, signal?: AbortSignal): Promise<{ content: any[]; details: any }> {
|
|
1324
|
+
return trackAdmission(() => withPublicScope(currentSessionAgentGroupName(ctx), () => spawnPublicAgentInScope(params, ctx, signal)));
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function spawnPublicAgentInScope(params: any, ctx: any, signal?: AbortSignal): Promise<{ content: any[]; details: any }> {
|
|
1116
1328
|
if (options.isTeammate) throw new Error("Only the lead session can spawn agents.");
|
|
1117
1329
|
if (!ctx) throw new Error("No active Pi session context is available for spawn_agent.");
|
|
1118
1330
|
|
|
1331
|
+
params = { ...params, checkpoint: createCheckpointAssignment(params.checkpoint, params.prompt, params.continue_from)?.policy };
|
|
1332
|
+
const name = params.continue_from === undefined ? params.name || generatedAgentName() : freshContinuationName(params.name || "agent");
|
|
1119
1333
|
const sessionDefaultModel = configuredFavoriteModelForSpawn(params, ctx);
|
|
1120
1334
|
const sessionName = await ensureCurrentSessionAgentGroup(ctx, sessionDefaultModel);
|
|
1121
|
-
const name = params.name || generatedAgentName();
|
|
1122
1335
|
const result = await spawnTeammate({
|
|
1123
1336
|
...params,
|
|
1124
1337
|
name,
|
|
1125
1338
|
team_name: sessionName,
|
|
1126
|
-
}, ctx, { allowNestedReadAgents: params.allow_nested_read_agents === true });
|
|
1339
|
+
}, ctx, { allowNestedReadAgents: params.allow_nested_read_agents === true, signal });
|
|
1127
1340
|
|
|
1128
1341
|
const outcome = result.details.queued
|
|
1129
1342
|
? `Agent ${name} queued at position ${result.details.queuePosition}.`
|
|
@@ -1145,13 +1358,18 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1145
1358
|
model_slot: Type.Optional(StringEnum(NESTED_READ_MODEL_SLOTS, { description: "Canonical read-* tier, or inherit defaults.model_slot." })),
|
|
1146
1359
|
};
|
|
1147
1360
|
|
|
1148
|
-
|
|
1361
|
+
function spawnNestedReadAgent(params: any, binding: NestedReadAgentToolBinding, context = "spawn_agent") {
|
|
1362
|
+
return trackAdmission(() => spawnNestedReadAgentInScope(params, binding, context));
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
async function spawnNestedReadAgentInScope(
|
|
1149
1366
|
params: any,
|
|
1150
1367
|
binding: NestedReadAgentToolBinding,
|
|
1151
1368
|
context = "spawn_agent"
|
|
1152
1369
|
): Promise<{ content: any[]; details: any }> {
|
|
1153
1370
|
rejectUnexpectedNestedParams(params, ["name", "prompt", "model_slot"], context);
|
|
1154
1371
|
requireNestedReadSpawnLevel(params, context);
|
|
1372
|
+
if (options.isHostAttached?.() === false) await options.waitForHost?.();
|
|
1155
1373
|
await assertNestedParentAuthorized(binding.teamName, binding.parent.name, binding.parentRunId, binding.parent.cwd);
|
|
1156
1374
|
const name = params.name || generatedAgentName();
|
|
1157
1375
|
const result = await spawnTeammate({
|
|
@@ -1160,7 +1378,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1160
1378
|
model_slot: params.model_slot,
|
|
1161
1379
|
team_name: binding.teamName,
|
|
1162
1380
|
cwd: binding.parent.cwd,
|
|
1163
|
-
}, binding.outerCtx, {
|
|
1381
|
+
}, options.isHostAttached ? options.getSessionCtx?.() ?? binding.outerCtx : binding.outerCtx, {
|
|
1164
1382
|
nestedParent: {
|
|
1165
1383
|
teamName: binding.teamName,
|
|
1166
1384
|
name: binding.parent.name,
|
|
@@ -1248,72 +1466,112 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1248
1466
|
pi.registerTool(statusTool());
|
|
1249
1467
|
|
|
1250
1468
|
pi.registerTool({
|
|
1251
|
-
|
|
1252
|
-
label: "Spawn Agent",
|
|
1253
|
-
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
|
-
parameters: Type.Object(publicAgentParams),
|
|
1469
|
+
...publicTeamToolMetadata.spawn_agent,
|
|
1255
1470
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
1256
|
-
return spawnPublicAgent(params, ctx);
|
|
1471
|
+
return spawnPublicAgent(params, ctx, _signal);
|
|
1257
1472
|
},
|
|
1258
1473
|
});
|
|
1259
1474
|
|
|
1260
1475
|
pi.registerTool({
|
|
1261
|
-
|
|
1262
|
-
label: "Spawn Swarm Agents",
|
|
1263
|
-
description: "Spawn a batch by intent tier only. Configured favorites take priority; unconfigured tiers inherit the current lead model and thinking. Give each lane the relevant goal, decisions, prior attempts, inspected evidence, constraints, and expected delta. Use session_context=lazy selectively as an on-demand fallback, never instead of a good mission prompt. Use read-review as the normal default, read-collect for bounded collection lanes, read-analyze for connected explanation, and read-critical only for irreducible high-stakes reasoning; choose write-patch/feature/system/critical by edit scope and risk. Each spawned lane is delegation-locked: do not duplicate or take it over. After unrelated work is done, end the turn so automatic reports can resume you. One get_agent_status snapshot is allowed when current status is needed; never sleep, busy-wait, repeatedly read inbox/status, or intervene early. Synthesize only after actual reports; intervene only on blocker/error, actual failure, or explicit cancellation. Each agent gets model_slot directly or from defaults; do not pass role, model, or thinking directly.",
|
|
1264
|
-
parameters: Type.Object({
|
|
1265
|
-
defaults: Type.Optional(Type.Object({
|
|
1266
|
-
cwd: Type.Optional(Type.String()),
|
|
1267
|
-
model_slot: Type.Optional(StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription })),
|
|
1268
|
-
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
1269
|
-
session_context: Type.Optional(StringEnum(["none", "lazy"] as const, { description: "Shared lazy session-reference policy.", default: "none" })),
|
|
1270
|
-
allow_nested_read_agents: Type.Optional(Type.Boolean({ description: "Shared opt-in for eligible depth-0 write-feature/write-critical agents.", default: false })),
|
|
1271
|
-
})),
|
|
1272
|
-
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
|
-
}),
|
|
1476
|
+
...publicTeamToolMetadata.spawn_swarm_agents,
|
|
1274
1477
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1478
|
+
return trackAdmission(async () => {
|
|
1479
|
+
if (options.isTeammate) throw new Error("Only the lead session can spawn agents.");
|
|
1480
|
+
if (!ctx) throw new Error("No active Pi session context is available for spawn_swarm_agents.");
|
|
1481
|
+
if (!Array.isArray(params.agents) || params.agents.length === 0) throw new Error("spawn_swarm_agents requires at least one agent.");
|
|
1482
|
+
|
|
1483
|
+
const groupPolicy = normalizeCompletionGroupPolicy(params.completion_group);
|
|
1484
|
+
const sessionId = getPiSessionId(ctx);
|
|
1485
|
+
if (groupPolicy && (!sessionId || !_toolCallId?.trim())) throw new Error("Completion groups require a bound session and runtime submission identity.");
|
|
1486
|
+
const mergedAgents = params.agents.map((agent: any, index: number) => {
|
|
1487
|
+
const merged = mergeSwarmAgentParams(params.defaults || {}, agent);
|
|
1488
|
+
merged.checkpoint = createCheckpointAssignment(merged.checkpoint, merged.prompt, merged.continue_from)?.policy;
|
|
1489
|
+
if (merged.continue_from !== undefined) merged.name = freshContinuationName(merged.name || "agent", JSON.stringify([sessionId, _toolCallId, index, merged.continue_from]));
|
|
1490
|
+
return merged;
|
|
1491
|
+
});
|
|
1492
|
+
const defaultModel = configuredFavoriteModelForSpawn(mergedAgents[0], ctx, "spawn_swarm_agents");
|
|
1493
|
+
for (let index = 0; index < mergedAgents.length; index += 1) {
|
|
1494
|
+
configuredFavoriteModelForSpawn(mergedAgents[index], ctx, `spawn_swarm_agents agent ${mergedAgents[index].name || index + 1}`);
|
|
1495
|
+
}
|
|
1496
|
+
const sessionName = await ensureCurrentSessionAgentGroup(ctx, defaultModel);
|
|
1497
|
+
let group: CompletionGroup | undefined;
|
|
1498
|
+
if (groupPolicy && sessionId) {
|
|
1499
|
+
const identity = { teamName: sessionName, sessionId, submissionId: `tool:${_toolCallId}` };
|
|
1500
|
+
const groupId = completionGroupIdentity(identity);
|
|
1501
|
+
options.onCompletionGroupUse?.(groupId);
|
|
1502
|
+
const config = await teams.readConfig(sessionName);
|
|
1503
|
+
const reports = await listStoredTeamReportEvents(sessionName);
|
|
1504
|
+
const inbox = await messaging.readInbox(sessionName, "team-lead", false, false);
|
|
1505
|
+
const entries = ctx.sessionManager?.getBranch?.() ?? [];
|
|
1506
|
+
const references = [...config.members.map(member => member.completionGroup),
|
|
1507
|
+
...readQueue(sessionName).map(queued => queued.member.completionGroup),
|
|
1508
|
+
...reports.map(report => report.completionGroup), ...inbox.map(message => message.metadata?.completionGroup),
|
|
1509
|
+
...entries.map((entry: any) => entry.message?.details?.completionGroup)];
|
|
1510
|
+
if (references.some(binding => binding?.groupId === groupId)) new CompletionGroup(sessionName, groupId).read();
|
|
1511
|
+
group = await CompletionGroup.create({ ...identity, policy: groupPolicy, members: mergedAgents.map((agent: any, index: number) => ({
|
|
1512
|
+
name: paths.sanitizeName(agent.name || `agent-${crypto.createHash("sha256").update(`${_toolCallId}:${index}`).digest("hex").slice(0, 12)}`),
|
|
1513
|
+
assignmentKey: crypto.createHash("sha256").update(JSON.stringify([agent, ctx.cwd])).digest("hex"),
|
|
1514
|
+
suppressed: sessionName.startsWith("prompt-build-") || shouldSuppressLeadReportInjection(agent),
|
|
1515
|
+
})) });
|
|
1516
|
+
if (group && !group.created) return {
|
|
1517
|
+
content: [{ type: "text", text: `Completion group ${group.groupId} already exists; no assignments replayed. State: ${group.journalPath}\n${AGENT_WAIT_CONTRACT}` }],
|
|
1518
|
+
details: { session: sessionName, completionGroup: { groupId: group.groupId, journalPath: group.journalPath },
|
|
1519
|
+
idempotent: true, spawned: [], failed: [], members: group.read().members },
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const spawned: any[] = [];
|
|
1523
|
+
const failed: Array<{ name: string; error: string }> = [];
|
|
1287
1524
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1525
|
+
for (let index = 0; index < mergedAgents.length; index += 1) {
|
|
1526
|
+
const merged = mergedAgents[index];
|
|
1527
|
+
const name = group?.read().members[index].name || merged.name || generatedAgentName(index);
|
|
1528
|
+
try {
|
|
1529
|
+
const result = await spawnTeammate({
|
|
1530
|
+
...merged,
|
|
1531
|
+
name,
|
|
1532
|
+
team_name: sessionName,
|
|
1533
|
+
}, ctx, { allowNestedReadAgents: merged.allow_nested_read_agents === true, completionGroup: group?.binding(index), signal: _signal });
|
|
1534
|
+
spawned.push({ ...result.details, name });
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1537
|
+
failed.push({ name, error: message });
|
|
1538
|
+
if (group) await group.apply({ type: "rejected", ...group.binding(index), runId: group.read().members[index].runId, reason: message });
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (group) {
|
|
1542
|
+
await group.seal();
|
|
1543
|
+
await enqueueCompletionGroupDeliveries(group);
|
|
1300
1544
|
}
|
|
1301
|
-
}
|
|
1302
1545
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1546
|
+
const lines = [`Spawned ${spawned.length}/${params.agents.length} agents in the current Pi session.`];
|
|
1547
|
+
for (const item of spawned) lines.push(`- ${item.name}: ${item.queued ? `queued at position ${item.queuePosition}` : `${item.role}, ${item.mode || "in-process"}`}`);
|
|
1548
|
+
for (const item of failed) lines.push(`- ${item.name}: failed — ${item.error}`);
|
|
1549
|
+
lines.push(AGENT_WAIT_CONTRACT);
|
|
1550
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { session: sessionName, spawned, failed,
|
|
1551
|
+
...(group ? { completionGroup: { groupId: group.groupId, journalPath: group.journalPath } } : {}) } };
|
|
1552
|
+
});
|
|
1308
1553
|
},
|
|
1309
1554
|
});
|
|
1310
1555
|
|
|
1311
1556
|
return {
|
|
1312
1557
|
createNestedReadAgentTools,
|
|
1558
|
+
nestedChildSnapshot,
|
|
1559
|
+
closeAdmission,
|
|
1560
|
+
hasPendingWork: () => (
|
|
1561
|
+
admissions.size > 0
|
|
1562
|
+
|| queuedReadSpawnsByTeam.size > 0
|
|
1563
|
+
|| checkpointAdmissions.size > 0
|
|
1564
|
+
|| readQueueDrainingTeams.size > 0
|
|
1565
|
+
),
|
|
1566
|
+
resumeQueues: () => { for (const teamName of queuedReadSpawnsByTeam.keys()) void drainQueuedReadSpawns(teamName); },
|
|
1313
1567
|
cancelQueuedAgent: (teamName, agentName) => {
|
|
1314
1568
|
// Retain the entry for admission bookkeeping, but let active teardown own cancellation after launch commits.
|
|
1315
1569
|
if (readQueue(teamName).some(queued => queued.member.name === agentName && queued.launchCommitted)) return false;
|
|
1316
|
-
|
|
1570
|
+
const pending = cancelCheckpointAdmission(teamName, agentName);
|
|
1571
|
+
const removed = removeQueuedReadSpawnsByName(teamName, agentName);
|
|
1572
|
+
return pending || removed.some(item => item.groupSettlement)
|
|
1573
|
+
? Promise.all([pending, ...removed.map(item => item.groupSettlement)]).then(() => !!pending || removed.length > 0)
|
|
1574
|
+
: removed.length > 0;
|
|
1317
1575
|
},
|
|
1318
1576
|
};
|
|
1319
1577
|
}
|