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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -12
  3. package/SECURITY.md +19 -0
  4. package/docs/access.md +35 -0
  5. package/extensions/agents/read-agent.ts +153 -4
  6. package/extensions/agents/write-agent.ts +8 -5
  7. package/extensions/events/register-events.ts +1 -1
  8. package/extensions/index.ts +49 -6
  9. package/extensions/runtime/nested-read-agents.ts +1 -1
  10. package/extensions/runtime/teammate-interrupt.ts +222 -0
  11. package/extensions/runtime/types.ts +15 -0
  12. package/extensions/team/team-contracts.json +1 -1
  13. package/extensions/tools/agent-status-tool.ts +270 -0
  14. package/extensions/tools/task-runtime-tools.ts +24 -3
  15. package/extensions/tools/team-tools.ts +76 -16
  16. package/extensions/ui/agent-follow-view.ts +128 -89
  17. package/extensions/ui/agent-navigation.ts +2 -0
  18. package/extensions/ui/frame.ts +33 -19
  19. package/extensions/ui/renderers.ts +4 -4
  20. package/extensions/ui/status-widget.ts +53 -20
  21. package/extensions/ui/theme.ts +34 -0
  22. package/package.json +15 -10
  23. package/skills/teams.md +14 -11
  24. package/src/adapters/tmux-adapter.ts +11 -0
  25. package/src/orchestration/index.ts +1 -1
  26. package/src/utils/runtime.ts +4 -0
  27. package/src/utils/settings.ts +23 -1
  28. package/src/utils/terminal-adapter.ts +7 -0
  29. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  30. package/assets/pi-extended-teams-in-action.png +0 -0
  31. package/extensions/agents/read-agent-session-lifecycle.test.ts +0 -605
  32. package/extensions/agents/read-agent.test.ts +0 -3077
  33. package/extensions/agents/write-agent.test.ts +0 -513
  34. package/extensions/events/register-events.test.ts +0 -465
  35. package/extensions/index.test.ts +0 -1659
  36. package/extensions/internal/agent-session-files.test.ts +0 -164
  37. package/extensions/internal/pi-command.test.ts +0 -191
  38. package/extensions/internal/pi-runtime-api.test.ts +0 -43
  39. package/extensions/internal/session-context-reference.test.ts +0 -290
  40. package/extensions/internal/session-files.test.ts +0 -225
  41. package/extensions/resources/spawn-resource-plan.test.ts +0 -233
  42. package/extensions/runtime/active-agent-sleep.test.ts +0 -157
  43. package/extensions/runtime/pending-child-controller.test.ts +0 -169
  44. package/extensions/team/contracts.test.ts +0 -111
  45. package/extensions/team/lifecycle.test.ts +0 -1095
  46. package/extensions/team/recipient-closure.test.ts +0 -97
  47. package/extensions/team/roster.test.ts +0 -129
  48. package/extensions/team/writer-screens.test.ts +0 -50
  49. package/extensions/tools/agent-communication-tools.test.ts +0 -306
  50. package/extensions/tools/coordination-tools.test.ts +0 -670
  51. package/extensions/tools/delegation-guard.test.ts +0 -95
  52. package/extensions/tools/file-claim-tools.test.ts +0 -104
  53. package/extensions/tools/predefined-tools.test.ts +0 -647
  54. package/extensions/tools/read-helper.test.ts +0 -35
  55. package/extensions/tools/task-runtime-tools.test.ts +0 -334
  56. package/extensions/tools/team-tools.read-agent.test.ts +0 -1924
  57. package/extensions/ui/agent-follow-view.test.ts +0 -520
  58. package/extensions/ui/agent-navigation.test.ts +0 -52
  59. package/extensions/ui/extensions-command.test.ts +0 -336
  60. package/extensions/ui/favorite-models-command.test.ts +0 -280
  61. package/extensions/ui/read-agent-status.test.ts +0 -117
  62. package/extensions/ui/status-widget.test.ts +0 -143
  63. package/extensions/ui-frame.test.ts +0 -69
  64. package/src/adapters/tmux-adapter.test.ts +0 -276
  65. package/src/orchestration/orchestrator.test.ts +0 -396
  66. package/src/utils/claims.test.ts +0 -137
  67. package/src/utils/hooks.test.ts +0 -75
  68. package/src/utils/lifecycle-tombstone.test.ts +0 -105
  69. package/src/utils/lock.race.child.ts +0 -44
  70. package/src/utils/lock.race.test.ts +0 -198
  71. package/src/utils/lock.test.ts +0 -90
  72. package/src/utils/messaging.test.ts +0 -337
  73. package/src/utils/model-resolution.test.ts +0 -231
  74. package/src/utils/models.test.ts +0 -8
  75. package/src/utils/predefined-teams.save-template.test.ts +0 -74
  76. package/src/utils/predefined-teams.test.ts +0 -441
  77. package/src/utils/report-events.test.ts +0 -154
  78. package/src/utils/runtime.test.ts +0 -314
  79. package/src/utils/security.test.ts +0 -43
  80. package/src/utils/settings.test.ts +0 -480
  81. package/src/utils/shared-memory.test.ts +0 -80
  82. package/src/utils/tasks.race.test.ts +0 -44
  83. package/src/utils/tasks.test.ts +0 -229
  84. package/src/utils/thinking-levels.test.ts +0 -56
  85. package/src/utils/workflow-metadata.test.ts +0 -11
  86. 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 a configured intent tier from /agents-favorite-models. See README.md for tier examples.`
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 configured model_slot intent tier: ${FAVORITE_MODEL_SLOTS.join(", ")}. Define tiers with /agents-favorite-models and see README.md for examples.`
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
- return requireFavoriteModelLevel(settings, slot).model;
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 requestedLevel = requireFavoriteModelLevel(settings, modelSlot);
868
- const requestedFavoriteModel = requestedLevel.model;
869
- const { availableModels } = await getModelSelectionState(ctx, ctx.cwd, [teamConfig.defaultModel, requestedFavoriteModel].filter(Boolean) as string[]);
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: null,
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
- "No model could be resolved from the configured model_slot level. Define levels with /agents-favorite-models before spawning agents."
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 = requestedLevel.thinking as Member["thinking"];
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 configured intent tier. 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.";
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: `Agent ${name} started (${result.details.role}, ${result.details.mode || "in-process"}).` }],
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 configured intent tier only. 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 wait literally idle for the automatic report—never sleep, poll, 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.",
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 configured intent tiers only. 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/take it over, and after unrelated work is done wait literally idle for automatic reports without sleep, polling, repeated inbox/status reads, or premature intervention. 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.",
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
  });