pi-extended-teams 2.3.0 → 2.3.2
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 +31 -271
- package/assets/pi-extended-teams-demo.gif +0 -0
- package/docs/reference.md +270 -0
- package/extensions/events/register-events.ts +15 -11
- package/extensions/index.ts +125 -22
- 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/team/session-cost.ts +117 -47
- package/extensions/tools/public-team-tools.ts +51 -0
- package/extensions/tools/team-tools.ts +138 -127
- package/extensions/ui/activity-colors.ts +17 -26
- package/package.json +4 -2
- package/src/utils/settings.ts +24 -0
|
@@ -10,15 +10,14 @@ 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 {
|
|
16
|
-
import {
|
|
17
|
-
import { CheckpointPolicySchema } from "../../src/results/checkpoint-policy";
|
|
15
|
+
import { normalizeCheckPolicy } from "../../src/results/check-policy";
|
|
16
|
+
import { normalizeRepairPolicy } from "../../src/results/repair-policy";
|
|
18
17
|
import { createCheckpointAssignment, captureCheckpointAssignment, continuationPrompt } from "../../src/results/checkpoint-assignment";
|
|
19
18
|
import { assertUnusedContinuationRecipient, freshContinuationName } from "../../src/results/continuation-recipient";
|
|
20
19
|
import { readCheckpoint } from "../../src/results/specialist-checkpoint";
|
|
21
|
-
import { CompletionGroup,
|
|
20
|
+
import { CompletionGroup, completionGroupIdentity, normalizeCompletionGroupPolicy, type CompletionGroupBinding } from "../../src/results/completion-group";
|
|
22
21
|
import { listStoredTeamReportEvents } from "../../src/utils/report-events";
|
|
23
22
|
import { enqueueCompletionGroupDeliveries } from "../../src/results/completion-group-delivery";
|
|
24
23
|
import { shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
|
|
@@ -48,6 +47,8 @@ import {
|
|
|
48
47
|
type QueuedAgentStatus,
|
|
49
48
|
} from "./agent-status-tool";
|
|
50
49
|
|
|
50
|
+
import { publicTeamToolMetadata } from "./public-team-tools";
|
|
51
|
+
|
|
51
52
|
export const CHILD_AGENT_LIFECYCLE_PROBE = "pi-extended-teams:child-agent-lifecycle-probe";
|
|
52
53
|
|
|
53
54
|
interface ChildAgentLifecycleProbe {
|
|
@@ -74,6 +75,8 @@ export interface TeamToolsOptions {
|
|
|
74
75
|
getSessionCtx?(): any;
|
|
75
76
|
setSessionCtx?(ctx: any): void;
|
|
76
77
|
pendingChildController?: PendingChildController;
|
|
78
|
+
isHostAttached?(): boolean;
|
|
79
|
+
waitForHost?(): Promise<void>;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export interface NestedReadAgentToolBinding {
|
|
@@ -92,6 +95,9 @@ export interface TeamToolsRuntime {
|
|
|
92
95
|
createNestedReadAgentTools(binding: NestedReadAgentToolBinding): any[];
|
|
93
96
|
nestedChildSnapshot(binding: NestedReadAgentToolBinding): NestedChildSnapshot;
|
|
94
97
|
cancelQueuedAgent(teamName: string, agentName: string): boolean | Promise<boolean>;
|
|
98
|
+
hasPendingWork(): boolean;
|
|
99
|
+
closeAdmission(): void;
|
|
100
|
+
resumeQueues(): void;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
interface SpawnTeammateOptions {
|
|
@@ -131,6 +137,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
131
137
|
createNestedReadAgentTools: () => [],
|
|
132
138
|
nestedChildSnapshot: () => ({ running: 0, queued: 0 }),
|
|
133
139
|
cancelQueuedAgent: () => false,
|
|
140
|
+
hasPendingWork: () => false,
|
|
141
|
+
closeAdmission: () => {},
|
|
142
|
+
resumeQueues: () => {},
|
|
134
143
|
};
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -389,10 +398,29 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
389
398
|
return pending?.settled;
|
|
390
399
|
}
|
|
391
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
|
+
}
|
|
392
419
|
let pendingChildCancelUnsubscribe = (): void => {};
|
|
393
420
|
pi.on?.("session_shutdown", async () => {
|
|
394
|
-
if (
|
|
395
|
-
|
|
421
|
+
if (shutdownStarted) return;
|
|
422
|
+
shutdownStarted = true;
|
|
423
|
+
closeAdmission();
|
|
396
424
|
const settlements: Promise<void>[] = [];
|
|
397
425
|
for (const pending of checkpointAdmissions.values()) {
|
|
398
426
|
pending.controller.abort(new Error("Checkpoint preparation cancelled: session is closing."));
|
|
@@ -404,8 +432,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
404
432
|
if (removed?.groupSettlement) settlements.push(removed.groupSettlement);
|
|
405
433
|
}
|
|
406
434
|
}
|
|
407
|
-
|
|
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]); }
|
|
408
444
|
finally {
|
|
445
|
+
clearTimeout(deadline);
|
|
409
446
|
if (typeof lifecycleProbeUnsubscribe === "function") lifecycleProbeUnsubscribe();
|
|
410
447
|
pendingChildCancelUnsubscribe();
|
|
411
448
|
lifecycleFenceUnsubscribe();
|
|
@@ -864,7 +901,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
864
901
|
}
|
|
865
902
|
|
|
866
903
|
async function drainQueuedReadSpawns(teamName: string): Promise<void> {
|
|
867
|
-
if (lifecycleProbeCleanedUp) return;
|
|
904
|
+
if (lifecycleProbeCleanedUp || options.isHostAttached?.() === false) return;
|
|
868
905
|
if (readQueueDrainingTeams.has(teamName)) {
|
|
869
906
|
pendingQueueDrains.add(teamName);
|
|
870
907
|
return;
|
|
@@ -872,7 +909,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
872
909
|
readQueueDrainingTeams.add(teamName);
|
|
873
910
|
try {
|
|
874
911
|
let progressed = true;
|
|
875
|
-
while (progressed && !lifecycleProbeCleanedUp) {
|
|
912
|
+
while (progressed && !lifecycleProbeCleanedUp && options.isHostAttached?.() !== false) {
|
|
876
913
|
progressed = false;
|
|
877
914
|
for (const queued of readQueue(teamName)) {
|
|
878
915
|
if (queued.admissionError) continue;
|
|
@@ -1274,39 +1311,17 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1274
1311
|
}
|
|
1275
1312
|
pi.events?.on?.("pi-extended-teams:orchestration-request", (payload: any) => {
|
|
1276
1313
|
if (payload?.type !== "ensure_team" && payload?.type !== "spawn_teammate_once") return handleOrchestrationRequest(payload);
|
|
1277
|
-
return withPublicScope(String(payload?.params?.team_name || ""), () => handleOrchestrationRequest(payload))
|
|
1314
|
+
return trackAdmission(() => withPublicScope(String(payload?.params?.team_name || ""), () => handleOrchestrationRequest(payload)))
|
|
1278
1315
|
.catch(error => emitOrchestrationResponse(payload?.requestId, payload?.type, { ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1279
1316
|
});
|
|
1280
1317
|
|
|
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.";
|
|
1282
|
-
const publicAgentBaseParams = {
|
|
1283
|
-
name: Type.Optional(Type.String({ description: "Stable display name. Defaults to a generated agent name." })),
|
|
1284
|
-
prompt: Type.String({ description: "The agent's assignment, relevant prior context, evidence already gathered, constraints, and report shape." }),
|
|
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." })),
|
|
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" })),
|
|
1291
|
-
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
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 })),
|
|
1293
|
-
};
|
|
1294
|
-
const publicAgentParams = {
|
|
1295
|
-
...publicAgentBaseParams,
|
|
1296
|
-
model_slot: StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription, default: "read-review" }),
|
|
1297
|
-
};
|
|
1298
|
-
const publicSwarmAgentParams = {
|
|
1299
|
-
...publicAgentBaseParams,
|
|
1300
|
-
model_slot: Type.Optional(StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription })),
|
|
1301
|
-
};
|
|
1302
|
-
|
|
1303
1318
|
function generatedAgentName(index?: number): string {
|
|
1304
1319
|
const position = index === undefined ? "" : `-${index + 1}`;
|
|
1305
1320
|
return `agent-${Date.now().toString(36)}${position}-${crypto.randomUUID().slice(0, 8)}`;
|
|
1306
1321
|
}
|
|
1307
1322
|
|
|
1308
1323
|
function spawnPublicAgent(params: any, ctx: any, signal?: AbortSignal): Promise<{ content: any[]; details: any }> {
|
|
1309
|
-
return withPublicScope(currentSessionAgentGroupName(ctx), () => spawnPublicAgentInScope(params, ctx, signal));
|
|
1324
|
+
return trackAdmission(() => withPublicScope(currentSessionAgentGroupName(ctx), () => spawnPublicAgentInScope(params, ctx, signal)));
|
|
1310
1325
|
}
|
|
1311
1326
|
|
|
1312
1327
|
async function spawnPublicAgentInScope(params: any, ctx: any, signal?: AbortSignal): Promise<{ content: any[]; details: any }> {
|
|
@@ -1343,13 +1358,18 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1343
1358
|
model_slot: Type.Optional(StringEnum(NESTED_READ_MODEL_SLOTS, { description: "Canonical read-* tier, or inherit defaults.model_slot." })),
|
|
1344
1359
|
};
|
|
1345
1360
|
|
|
1346
|
-
|
|
1361
|
+
function spawnNestedReadAgent(params: any, binding: NestedReadAgentToolBinding, context = "spawn_agent") {
|
|
1362
|
+
return trackAdmission(() => spawnNestedReadAgentInScope(params, binding, context));
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
async function spawnNestedReadAgentInScope(
|
|
1347
1366
|
params: any,
|
|
1348
1367
|
binding: NestedReadAgentToolBinding,
|
|
1349
1368
|
context = "spawn_agent"
|
|
1350
1369
|
): Promise<{ content: any[]; details: any }> {
|
|
1351
1370
|
rejectUnexpectedNestedParams(params, ["name", "prompt", "model_slot"], context);
|
|
1352
1371
|
requireNestedReadSpawnLevel(params, context);
|
|
1372
|
+
if (options.isHostAttached?.() === false) await options.waitForHost?.();
|
|
1353
1373
|
await assertNestedParentAuthorized(binding.teamName, binding.parent.name, binding.parentRunId, binding.parent.cwd);
|
|
1354
1374
|
const name = params.name || generatedAgentName();
|
|
1355
1375
|
const result = await spawnTeammate({
|
|
@@ -1358,7 +1378,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1358
1378
|
model_slot: params.model_slot,
|
|
1359
1379
|
team_name: binding.teamName,
|
|
1360
1380
|
cwd: binding.parent.cwd,
|
|
1361
|
-
}, binding.outerCtx, {
|
|
1381
|
+
}, options.isHostAttached ? options.getSessionCtx?.() ?? binding.outerCtx : binding.outerCtx, {
|
|
1362
1382
|
nestedParent: {
|
|
1363
1383
|
teamName: binding.teamName,
|
|
1364
1384
|
name: binding.parent.name,
|
|
@@ -1446,113 +1466,104 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1446
1466
|
pi.registerTool(statusTool());
|
|
1447
1467
|
|
|
1448
1468
|
pi.registerTool({
|
|
1449
|
-
|
|
1450
|
-
label: "Spawn Agent",
|
|
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.",
|
|
1452
|
-
parameters: Type.Object(publicAgentParams),
|
|
1469
|
+
...publicTeamToolMetadata.spawn_agent,
|
|
1453
1470
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
1454
1471
|
return spawnPublicAgent(params, ctx, _signal);
|
|
1455
1472
|
},
|
|
1456
1473
|
});
|
|
1457
1474
|
|
|
1458
1475
|
pi.registerTool({
|
|
1459
|
-
|
|
1460
|
-
label: "Spawn Swarm Agents",
|
|
1461
|
-
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.",
|
|
1462
|
-
parameters: Type.Object({
|
|
1463
|
-
defaults: Type.Optional(Type.Object({
|
|
1464
|
-
cwd: Type.Optional(Type.String()),
|
|
1465
|
-
checks: Type.Optional(CheckPolicySchema),
|
|
1466
|
-
repair: Type.Optional(RepairPolicySchema),
|
|
1467
|
-
checkpoint: Type.Optional(CheckpointPolicySchema),
|
|
1468
|
-
model_slot: Type.Optional(StringEnum(ACCEPTED_FAVORITE_MODEL_SLOTS, { description: levelDescription })),
|
|
1469
|
-
metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
1470
|
-
session_context: Type.Optional(StringEnum(["none", "lazy"] as const, { description: "Shared lazy session-reference policy.", default: "none" })),
|
|
1471
|
-
allow_nested_read_agents: Type.Optional(Type.Boolean({ description: "Shared opt-in for eligible depth-0 write-feature/write-critical agents.", default: false })),
|
|
1472
|
-
})),
|
|
1473
|
-
completion_group: Type.Optional(CompletionGroupPolicySchema),
|
|
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." }),
|
|
1475
|
-
}),
|
|
1476
|
+
...publicTeamToolMetadata.spawn_swarm_agents,
|
|
1476
1477
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
...
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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 }> = [];
|
|
1522
1524
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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);
|
|
1537
1544
|
}
|
|
1538
|
-
}
|
|
1539
|
-
if (group) {
|
|
1540
|
-
await group.seal();
|
|
1541
|
-
await enqueueCompletionGroupDeliveries(group);
|
|
1542
|
-
}
|
|
1543
1545
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
+
});
|
|
1550
1553
|
},
|
|
1551
1554
|
});
|
|
1552
1555
|
|
|
1553
1556
|
return {
|
|
1554
1557
|
createNestedReadAgentTools,
|
|
1555
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); },
|
|
1556
1567
|
cancelQueuedAgent: (teamName, agentName) => {
|
|
1557
1568
|
// Retain the entry for admission bookkeeping, but let active teardown own cancellation after launch commits.
|
|
1558
1569
|
if (readQueue(teamName).some(queued => queued.member.name === agentName && queued.launchCommitted)) return false;
|
|
@@ -1,35 +1,26 @@
|
|
|
1
|
-
import { canonicalPersistedModelSlot } from "../../src/utils/settings";
|
|
1
|
+
import { canonicalPersistedModelSlot, loadSettings, type ActivityColors } from "../../src/utils/settings";
|
|
2
2
|
|
|
3
3
|
export const CONTEXT_USAGE_STATUS_SUFFIX = /^(?:\?|[\d.]+[kM]?) tok(?: \((?:\?|[\d.]+)%\))?$/;
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
warning: "255;215;0",
|
|
13
|
-
error: "255;85;85",
|
|
14
|
-
} as const;
|
|
15
|
-
|
|
16
|
-
const TIER_PINKS: Record<string, string> = {
|
|
17
|
-
"read-collect": "255;214;235",
|
|
18
|
-
"write-patch": "255;214;235",
|
|
19
|
-
"read-review": "255;189;222",
|
|
20
|
-
"write-feature": "255;189;222",
|
|
21
|
-
"read-analyze": "255;167;211",
|
|
22
|
-
"write-system": "255;167;211",
|
|
23
|
-
"read-critical": ACTIVITY_COLORS.tier,
|
|
24
|
-
"write-critical": ACTIVITY_COLORS.tier,
|
|
5
|
+
const TIER_COLORS: Record<string, keyof ActivityColors> = {
|
|
6
|
+
"read-collect": "tierCollect",
|
|
7
|
+
"write-patch": "tierCollect",
|
|
8
|
+
"read-review": "tierReview",
|
|
9
|
+
"write-feature": "tierReview",
|
|
10
|
+
"read-analyze": "tierAnalyze",
|
|
11
|
+
"write-system": "tierAnalyze",
|
|
25
12
|
};
|
|
26
13
|
|
|
27
|
-
//
|
|
14
|
+
// Views share settings; surrounding chrome remains theme-owned.
|
|
28
15
|
export function createActivityColors(enabled: boolean) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
16
|
+
const palette = loadSettings({ projectDir: process.cwd() }).activityColors;
|
|
17
|
+
const color = (token: keyof ActivityColors, text: string): string => {
|
|
18
|
+
if (!enabled) return text;
|
|
19
|
+
const hex = palette[token];
|
|
20
|
+
const rgb = [1, 3, 5].map(offset => parseInt(hex.slice(offset, offset + 2), 16)).join(";");
|
|
21
|
+
return `\x1b[38;2;${rgb}m${text}\x1b[39m`;
|
|
22
|
+
};
|
|
23
|
+
const tier = (text: string): string => color(TIER_COLORS[canonicalPersistedModelSlot(text)] ?? "tier", text);
|
|
33
24
|
const metadata = (text: string, name: string): string => {
|
|
34
25
|
const identity = `(${name})`;
|
|
35
26
|
return text.split(" · ").map((part, index) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extended-teams",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Control-first, session-connected subagents for Pi with live navigation and intent-tier routing",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
"README.md",
|
|
53
53
|
"LICENSE",
|
|
54
54
|
"SECURITY.md",
|
|
55
|
-
"docs/access.md"
|
|
55
|
+
"docs/access.md",
|
|
56
|
+
"docs/reference.md",
|
|
57
|
+
"assets/pi-extended-teams-demo.gif"
|
|
56
58
|
],
|
|
57
59
|
"dependencies": {
|
|
58
60
|
"uuid": "^11.1.0"
|
package/src/utils/settings.ts
CHANGED
|
@@ -116,7 +116,23 @@ export interface AgentSessionsConfig {
|
|
|
116
116
|
showInResume: boolean;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export const DEFAULT_ACTIVITY_COLORS = {
|
|
120
|
+
name: "#50FA7B",
|
|
121
|
+
model: "#FFD700",
|
|
122
|
+
thinking: "#FFF2A8",
|
|
123
|
+
tier: "#FF92C8",
|
|
124
|
+
tierCollect: "#FFD6EB",
|
|
125
|
+
tierReview: "#FFBDDE",
|
|
126
|
+
tierAnalyze: "#FFA7D3",
|
|
127
|
+
text: "#F8F8F2",
|
|
128
|
+
message: "#969CAB",
|
|
129
|
+
warning: "#FFD700",
|
|
130
|
+
error: "#FF5555",
|
|
131
|
+
};
|
|
132
|
+
export type ActivityColors = typeof DEFAULT_ACTIVITY_COLORS;
|
|
133
|
+
|
|
119
134
|
export interface PiExtendedTeamsSettings {
|
|
135
|
+
activityColors: ActivityColors;
|
|
120
136
|
watchdog: WatchdogConfig;
|
|
121
137
|
writeAgents: WriteAgentsConfig;
|
|
122
138
|
readAgents: ReadAgentsConfig;
|
|
@@ -134,6 +150,7 @@ export const DEFAULT_READ_AGENT_MAX_CONCURRENT = 25;
|
|
|
134
150
|
export const DEFAULT_READ_HELPER_MAX_CONCURRENT = 10;
|
|
135
151
|
|
|
136
152
|
export const DEFAULT_SETTINGS: PiExtendedTeamsSettings = {
|
|
153
|
+
activityColors: DEFAULT_ACTIVITY_COLORS,
|
|
137
154
|
watchdog: { bufferSeconds: 30 },
|
|
138
155
|
writeAgents: { maxConcurrent: DEFAULT_WRITE_AGENT_MAX_CONCURRENT, queueOverflow: true },
|
|
139
156
|
readAgents: { maxConcurrent: DEFAULT_READ_AGENT_MAX_CONCURRENT, queueOverflow: true },
|
|
@@ -269,6 +286,13 @@ function applyLayer(acc: PiExtendedTeamsSettings, raw: any, options: { favoriteM
|
|
|
269
286
|
if (!raw || typeof raw !== "object") return;
|
|
270
287
|
const includeFavoriteModels = options.favoriteModels !== false;
|
|
271
288
|
|
|
289
|
+
if (raw.activityColors && typeof raw.activityColors === "object" && !Array.isArray(raw.activityColors)) {
|
|
290
|
+
for (const key of Object.keys(DEFAULT_ACTIVITY_COLORS) as Array<keyof ActivityColors>) {
|
|
291
|
+
const value = raw.activityColors[key];
|
|
292
|
+
if (typeof value === "string" && /^#[\da-f]{6}$/i.test(value)) acc.activityColors[key] = value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
272
296
|
if (raw.watchdog && typeof raw.watchdog === "object") {
|
|
273
297
|
const b = Number(raw.watchdog.bufferSeconds);
|
|
274
298
|
if (Number.isFinite(b) && b >= 0) acc.watchdog.bufferSeconds = b;
|