pi-extended-teams 2.1.17 → 2.2.3
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/LICENSE +21 -0
- package/README.md +25 -12
- package/SECURITY.md +19 -0
- package/docs/access.md +35 -0
- package/extensions/agents/read-agent.ts +153 -4
- package/extensions/agents/write-agent.ts +8 -5
- package/extensions/events/register-events.ts +1 -1
- package/extensions/index.ts +49 -6
- package/extensions/runtime/nested-read-agents.ts +1 -1
- package/extensions/runtime/teammate-interrupt.ts +222 -0
- package/extensions/runtime/types.ts +15 -0
- package/extensions/team/team-contracts.json +1 -1
- package/extensions/tools/agent-status-tool.ts +270 -0
- package/extensions/tools/task-runtime-tools.ts +24 -3
- package/extensions/tools/team-tools.ts +76 -16
- package/extensions/ui/agent-follow-view.ts +128 -89
- package/extensions/ui/agent-navigation.ts +2 -0
- package/extensions/ui/frame.ts +33 -19
- package/extensions/ui/renderers.ts +4 -4
- package/extensions/ui/status-widget.ts +53 -20
- package/extensions/ui/theme.ts +34 -0
- package/package.json +15 -10
- package/skills/teams.md +14 -11
- package/src/adapters/tmux-adapter.ts +11 -0
- package/src/orchestration/index.ts +1 -1
- package/src/utils/runtime.ts +4 -0
- package/src/utils/settings.ts +23 -1
- package/src/utils/terminal-adapter.ts +7 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +0 -605
- package/extensions/agents/read-agent.test.ts +0 -3077
- package/extensions/agents/write-agent.test.ts +0 -513
- package/extensions/events/register-events.test.ts +0 -465
- package/extensions/index.test.ts +0 -1659
- package/extensions/internal/agent-session-files.test.ts +0 -164
- package/extensions/internal/pi-command.test.ts +0 -191
- package/extensions/internal/pi-runtime-api.test.ts +0 -43
- package/extensions/internal/session-context-reference.test.ts +0 -290
- package/extensions/internal/session-files.test.ts +0 -225
- package/extensions/resources/spawn-resource-plan.test.ts +0 -233
- package/extensions/runtime/active-agent-sleep.test.ts +0 -157
- package/extensions/runtime/pending-child-controller.test.ts +0 -169
- package/extensions/team/contracts.test.ts +0 -111
- package/extensions/team/lifecycle.test.ts +0 -1095
- package/extensions/team/recipient-closure.test.ts +0 -97
- package/extensions/team/roster.test.ts +0 -129
- package/extensions/team/writer-screens.test.ts +0 -50
- package/extensions/tools/agent-communication-tools.test.ts +0 -306
- package/extensions/tools/coordination-tools.test.ts +0 -670
- package/extensions/tools/delegation-guard.test.ts +0 -95
- package/extensions/tools/file-claim-tools.test.ts +0 -104
- package/extensions/tools/predefined-tools.test.ts +0 -647
- package/extensions/tools/read-helper.test.ts +0 -35
- package/extensions/tools/task-runtime-tools.test.ts +0 -334
- package/extensions/tools/team-tools.read-agent.test.ts +0 -1924
- package/extensions/ui/agent-follow-view.test.ts +0 -520
- package/extensions/ui/agent-navigation.test.ts +0 -52
- package/extensions/ui/extensions-command.test.ts +0 -336
- package/extensions/ui/favorite-models-command.test.ts +0 -280
- package/extensions/ui/read-agent-status.test.ts +0 -117
- package/extensions/ui/status-widget.test.ts +0 -143
- package/extensions/ui-frame.test.ts +0 -69
- package/src/adapters/tmux-adapter.test.ts +0 -276
- package/src/orchestration/orchestrator.test.ts +0 -396
- package/src/utils/claims.test.ts +0 -137
- package/src/utils/hooks.test.ts +0 -75
- package/src/utils/lifecycle-tombstone.test.ts +0 -105
- package/src/utils/lock.race.child.ts +0 -44
- package/src/utils/lock.race.test.ts +0 -198
- package/src/utils/lock.test.ts +0 -90
- package/src/utils/messaging.test.ts +0 -337
- package/src/utils/model-resolution.test.ts +0 -231
- package/src/utils/models.test.ts +0 -8
- package/src/utils/predefined-teams.save-template.test.ts +0 -74
- package/src/utils/predefined-teams.test.ts +0 -441
- package/src/utils/report-events.test.ts +0 -154
- package/src/utils/runtime.test.ts +0 -314
- package/src/utils/security.test.ts +0 -43
- package/src/utils/settings.test.ts +0 -480
- package/src/utils/shared-memory.test.ts +0 -80
- package/src/utils/tasks.race.test.ts +0 -44
- package/src/utils/tasks.test.ts +0 -229
- package/src/utils/thinking-levels.test.ts +0 -56
- package/src/utils/workflow-metadata.test.ts +0 -11
- package/src/utils/write-queue.test.ts +0 -251
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { StringEnum } from "../internal/schema";
|
|
4
4
|
import { isTeamsDebugEnabled, teamDebugLogPath, writeTeamsDebugEvent } from "../internal/debug";
|
|
5
|
-
import { getModelSelectionState, requireQualifiedKnownModel } from "../internal/model-selection";
|
|
5
|
+
import { getCurrentQualifiedModel, getModelSelectionState, requireQualifiedKnownModel } from "../internal/model-selection";
|
|
6
6
|
import { getPiSessionId } from "../internal/session-files";
|
|
7
7
|
import { createSessionContextReference, removeSessionContextReference } from "../internal/session-context-reference";
|
|
8
8
|
import * as paths from "../../src/utils/paths";
|
|
@@ -30,6 +30,12 @@ import {
|
|
|
30
30
|
type PendingChildController,
|
|
31
31
|
type PendingChildRun,
|
|
32
32
|
} from "../runtime/pending-child-controller";
|
|
33
|
+
import {
|
|
34
|
+
AGENT_WAIT_CONTRACT,
|
|
35
|
+
createAgentStatusTool,
|
|
36
|
+
type AgentStatusScope,
|
|
37
|
+
type QueuedAgentStatus,
|
|
38
|
+
} from "./agent-status-tool";
|
|
33
39
|
|
|
34
40
|
export const CHILD_AGENT_LIFECYCLE_PROBE = "pi-extended-teams:child-agent-lifecycle-probe";
|
|
35
41
|
|
|
@@ -118,7 +124,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
118
124
|
const forbidden = ["model", "thinking", "role"].filter((key) => hasOwnParam(params, key));
|
|
119
125
|
if (forbidden.length === 0) return;
|
|
120
126
|
throw new Error(
|
|
121
|
-
`${context} must use model_slot only. Do not pass ${forbidden.join(", ")}; choose
|
|
127
|
+
`${context} must use model_slot only. Do not pass ${forbidden.join(", ")}; choose an intent tier. Configured favorites are optional; see README.md for tier examples.`
|
|
122
128
|
);
|
|
123
129
|
}
|
|
124
130
|
|
|
@@ -130,7 +136,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
130
136
|
const slot = normalizeFavoriteModelSlot(params?.model_slot);
|
|
131
137
|
if (!slot) {
|
|
132
138
|
throw new Error(
|
|
133
|
-
`${context} requires a
|
|
139
|
+
`${context} requires a model_slot intent tier: ${FAVORITE_MODEL_SLOTS.join(", ")}. Unconfigured tiers inherit the current lead model and thinking; see README.md for examples.`
|
|
134
140
|
);
|
|
135
141
|
}
|
|
136
142
|
return slot;
|
|
@@ -139,7 +145,16 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
139
145
|
function configuredFavoriteModelForSpawn(params: any, ctx: any, context = "spawn_agent"): string {
|
|
140
146
|
const slot = requireSpawnLevel(params, context);
|
|
141
147
|
const settings = loadSettings({ projectDir: params.cwd || ctx.cwd });
|
|
142
|
-
|
|
148
|
+
const configured = settings.favoriteModels[slot];
|
|
149
|
+
if (configured?.model && configured.thinking) return configured.model;
|
|
150
|
+
|
|
151
|
+
const currentModel = getCurrentQualifiedModel(ctx);
|
|
152
|
+
if (!currentModel) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`${context} could not resolve model_slot "${slot}": it is not configured and the current lead session has no model.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return currentModel;
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
function requireNestedReadSpawnLevel(params: any, context: string): NestedReadModelSlot {
|
|
@@ -279,6 +294,35 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
279
294
|
return queuedReadSpawnsByTeam.get(teamName) ?? [];
|
|
280
295
|
}
|
|
281
296
|
|
|
297
|
+
async function listQueuedAgentStatuses(teamName: string): Promise<QueuedAgentStatus[]> {
|
|
298
|
+
const readers = readQueue(teamName).map((queued, index) => ({
|
|
299
|
+
name: queued.member.name,
|
|
300
|
+
role: queued.member.role || "read",
|
|
301
|
+
queuedAt: queued.requestedAt,
|
|
302
|
+
queuePosition: index + 1,
|
|
303
|
+
parentAgentName: queued.member.parentAgentName,
|
|
304
|
+
parentLifecycleRunId: queued.member.parentLifecycleRunId,
|
|
305
|
+
}));
|
|
306
|
+
const writers = (await writeQueue.listWriteQueue(teamName)).map((queued, index) => ({
|
|
307
|
+
name: queued.name,
|
|
308
|
+
role: "write",
|
|
309
|
+
queuedAt: queued.requestedAt,
|
|
310
|
+
queuePosition: index + 1,
|
|
311
|
+
}));
|
|
312
|
+
return [...readers, ...writers];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function statusTool(scope?: AgentStatusScope, fixedTeamName?: string): any {
|
|
316
|
+
return createAgentStatusTool({
|
|
317
|
+
getTeamName: () => fixedTeamName ?? options.getTeamName(),
|
|
318
|
+
runningReadAgents: options.runningReadAgents,
|
|
319
|
+
readAgentKey: options.readAgentKey,
|
|
320
|
+
terminal: options.terminal,
|
|
321
|
+
listQueuedAgents: listQueuedAgentStatuses,
|
|
322
|
+
scope,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
282
326
|
const lifecycleFenceUnsubscribe = onLifecycleTombstoneCleared((clearedTeamName) => {
|
|
283
327
|
if (readQueue(clearedTeamName).length > 0) void drainQueuedReadSpawns(clearedTeamName);
|
|
284
328
|
});
|
|
@@ -864,26 +908,28 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
864
908
|
? requireNestedReadSpawnLevel(params, `Nested read agent ${safeName}`)
|
|
865
909
|
: requireSpawnLevel(params, `Agent ${safeName}`);
|
|
866
910
|
const role: AgentRole = roleForFavoriteModelSlot(modelSlot);
|
|
867
|
-
const
|
|
868
|
-
const requestedFavoriteModel =
|
|
869
|
-
const
|
|
911
|
+
const configuredFavorite = settings.favoriteModels[modelSlot];
|
|
912
|
+
const requestedFavoriteModel = configuredFavorite?.model && configuredFavorite.thinking ? configuredFavorite.model : undefined;
|
|
913
|
+
const currentModel = getCurrentQualifiedModel(ctx);
|
|
914
|
+
const { availableModels } = await getModelSelectionState(ctx, ctx.cwd, [teamConfig.defaultModel, requestedFavoriteModel, currentModel].filter(Boolean) as string[]);
|
|
870
915
|
const resolved = resolveModel(settings, {
|
|
871
916
|
role,
|
|
872
917
|
modelSlot,
|
|
873
918
|
explicitModel: null,
|
|
874
919
|
explicitThinking: null,
|
|
875
920
|
teamDefaultModel: teamConfig.defaultModel,
|
|
876
|
-
currentModel
|
|
921
|
+
currentModel,
|
|
922
|
+
currentThinking: ctx.thinkingLevel,
|
|
877
923
|
});
|
|
878
924
|
|
|
879
925
|
const chosenModel = requireQualifiedKnownModel(resolved.model ?? undefined, availableModels, "resolved model");
|
|
880
926
|
if (!chosenModel) {
|
|
881
927
|
throw new Error(
|
|
882
|
-
|
|
928
|
+
`Agent ${safeName} could not resolve model_slot "${modelSlot}": it is not configured and neither the team nor current lead session has a model.`
|
|
883
929
|
);
|
|
884
930
|
}
|
|
885
931
|
|
|
886
|
-
const chosenThinking =
|
|
932
|
+
const chosenThinking = (resolved.thinking ?? ctx.thinkingLevel ?? null) as Member["thinking"];
|
|
887
933
|
const debugLogPath = role === "write" && isTeamsDebugEnabled(settings) ? teamDebugLogPath(safeTeamName) : undefined;
|
|
888
934
|
|
|
889
935
|
const member: Member = {
|
|
@@ -1061,7 +1107,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1061
1107
|
}
|
|
1062
1108
|
});
|
|
1063
1109
|
|
|
1064
|
-
const levelDescription = "Required
|
|
1110
|
+
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.";
|
|
1065
1111
|
const publicAgentBaseParams = {
|
|
1066
1112
|
name: Type.Optional(Type.String({ description: "Stable display name. Defaults to a generated agent name." })),
|
|
1067
1113
|
prompt: Type.String({ description: "The agent's assignment, relevant prior context, evidence already gathered, constraints, and report shape." }),
|
|
@@ -1097,8 +1143,11 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1097
1143
|
team_name: sessionName,
|
|
1098
1144
|
}, ctx, { allowNestedReadAgents: params.allow_nested_read_agents === true });
|
|
1099
1145
|
|
|
1146
|
+
const outcome = result.details.queued
|
|
1147
|
+
? `Agent ${name} queued at position ${result.details.queuePosition}.`
|
|
1148
|
+
: `Agent ${name} started (${result.details.role}, ${result.details.mode || "in-process"}).`;
|
|
1100
1149
|
return {
|
|
1101
|
-
content: [{ type: "text", text:
|
|
1150
|
+
content: [{ type: "text", text: `${outcome}\n${AGENT_WAIT_CONTRACT}` }],
|
|
1102
1151
|
details: { ...result.details, name, session: sessionName },
|
|
1103
1152
|
};
|
|
1104
1153
|
}
|
|
@@ -1138,7 +1187,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1138
1187
|
},
|
|
1139
1188
|
});
|
|
1140
1189
|
return {
|
|
1141
|
-
content: result.content,
|
|
1190
|
+
content: [...result.content, { type: "text", text: AGENT_WAIT_CONTRACT }],
|
|
1142
1191
|
details: { ...result.details, name, session: binding.teamName },
|
|
1143
1192
|
};
|
|
1144
1193
|
}
|
|
@@ -1150,7 +1199,14 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1150
1199
|
return [];
|
|
1151
1200
|
}
|
|
1152
1201
|
|
|
1202
|
+
const statusScope: AgentStatusScope = {
|
|
1203
|
+
parentName: binding.parent.name,
|
|
1204
|
+
parentRunId: binding.parentRunId,
|
|
1205
|
+
parentStartedAt: binding.parent.joinedAt,
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1153
1208
|
return [
|
|
1209
|
+
statusTool(statusScope, binding.teamName),
|
|
1154
1210
|
{
|
|
1155
1211
|
name: "spawn_agent",
|
|
1156
1212
|
label: "Spawn Read Agent",
|
|
@@ -1200,16 +1256,19 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1200
1256
|
const lines = [`Spawned ${spawned.length}/${params.agents.length} nested read agents.`];
|
|
1201
1257
|
for (const item of spawned) lines.push(`- ${item.name}: ${item.queued ? "queued" : "started"}`);
|
|
1202
1258
|
for (const item of failed) lines.push(`- ${item.name}: failed — ${item.error}`);
|
|
1259
|
+
lines.push(AGENT_WAIT_CONTRACT);
|
|
1203
1260
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { session: binding.teamName, spawned, failed } };
|
|
1204
1261
|
},
|
|
1205
1262
|
},
|
|
1206
1263
|
];
|
|
1207
1264
|
}
|
|
1208
1265
|
|
|
1266
|
+
pi.registerTool(statusTool());
|
|
1267
|
+
|
|
1209
1268
|
pi.registerTool({
|
|
1210
1269
|
name: "spawn_agent",
|
|
1211
1270
|
label: "Spawn Agent",
|
|
1212
|
-
description: "Spawn one agent by
|
|
1271
|
+
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.",
|
|
1213
1272
|
parameters: Type.Object(publicAgentParams),
|
|
1214
1273
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
1215
1274
|
return spawnPublicAgent(params, ctx);
|
|
@@ -1219,7 +1278,7 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1219
1278
|
pi.registerTool({
|
|
1220
1279
|
name: "spawn_swarm_agents",
|
|
1221
1280
|
label: "Spawn Swarm Agents",
|
|
1222
|
-
description: "Spawn a batch by
|
|
1281
|
+
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.",
|
|
1223
1282
|
parameters: Type.Object({
|
|
1224
1283
|
defaults: Type.Optional(Type.Object({
|
|
1225
1284
|
cwd: Type.Optional(Type.String()),
|
|
@@ -1260,8 +1319,9 @@ export function registerTeamTools(pi: any, options: TeamToolsOptions): TeamTools
|
|
|
1260
1319
|
}
|
|
1261
1320
|
|
|
1262
1321
|
const lines = [`Spawned ${spawned.length}/${params.agents.length} agents in the current Pi session.`];
|
|
1263
|
-
for (const item of spawned) lines.push(`- ${item.name}: ${item.role}, ${item.mode || "in-process"}`);
|
|
1322
|
+
for (const item of spawned) lines.push(`- ${item.name}: ${item.queued ? `queued at position ${item.queuePosition}` : `${item.role}, ${item.mode || "in-process"}`}`);
|
|
1264
1323
|
for (const item of failed) lines.push(`- ${item.name}: failed — ${item.error}`);
|
|
1324
|
+
lines.push(AGENT_WAIT_CONTRACT);
|
|
1265
1325
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { session: sessionName, spawned, failed } };
|
|
1266
1326
|
},
|
|
1267
1327
|
});
|