pi-extended-teams 2.5.0 → 2.5.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.
@@ -1,4 +1,4 @@
1
- import type { AgentSession } from "@mariozechner/pi-coding-agent";
1
+ import type { AgentSession, ExtensionFactory } from "@mariozechner/pi-coding-agent";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
@@ -36,6 +36,7 @@ import {
36
36
  type SpawnResourcePlan,
37
37
  } from "../resources/spawn-resource-plan";
38
38
  import { loadPiRuntimeApi } from "../internal/pi-runtime-api";
39
+ import { createNativeExtensionFactories, isNativeExtensionPath } from "../resources/native-extensions";
39
40
  import { preparePrivateAgentSessionDirectory } from "../internal/agent-session-files";
40
41
  import { isEligibleNestedReadParent, NESTED_DELEGATION_TOOL_NAMES } from "../runtime/nested-read-agents";
41
42
  import { CHILD_AGENT_LIFECYCLE_PROBE, type NestedReadAgentToolBinding } from "../tools/team-tools";
@@ -1019,6 +1020,7 @@ export async function runReadAgentInProcess(
1019
1020
  }, 5000);
1020
1021
  }
1021
1022
 
1023
+ const piRuntimeApi = await loadPiRuntimeApi();
1022
1024
  const {
1023
1025
  createAgentSession,
1024
1026
  createEventBus,
@@ -1026,7 +1028,7 @@ export async function runReadAgentInProcess(
1026
1028
  getAgentDir,
1027
1029
  SessionManager,
1028
1030
  SettingsManager,
1029
- } = await loadPiRuntimeApi();
1031
+ } = piRuntimeApi;
1030
1032
  const agentDir = getAgentDir();
1031
1033
  const projectTrusted = parentProjectTrustForSpawn(ctx, member.cwd);
1032
1034
  const resourcePlan = await (options.createResourcePlan ?? createSpawnResourcePlan)({
@@ -1054,6 +1056,11 @@ export async function runReadAgentInProcess(
1054
1056
  : [];
1055
1057
  const nestedReadDelegationEnabled = nestedReadAgentTools.length > 0;
1056
1058
  const childEventBus = createEventBus();
1059
+ const nativeExtensions = createNativeExtensionFactories(piRuntimeApi, resourcePlan.extensionPaths);
1060
+ const extensionFactories = [
1061
+ (pi: Parameters<ExtensionFactory>[0]) => registerSpawnedAgentCommunicationGuard(pi, () => submittedFinalReport !== undefined),
1062
+ ...nativeExtensions,
1063
+ ];
1057
1064
  const loader = new DefaultResourceLoader({
1058
1065
  eventBus: childEventBus,
1059
1066
  cwd: member.cwd,
@@ -1061,7 +1068,8 @@ export async function runReadAgentInProcess(
1061
1068
  settingsManager: childSettingsManager,
1062
1069
  noExtensions: true,
1063
1070
  additionalExtensionPaths: [...resourcePlan.extensionPaths],
1064
- extensionFactories: [registerSpawnedAgentCommunicationGuard],
1071
+ // Named builtin descriptors exist only on capable hosts; legacy SDK types accept functions only.
1072
+ extensionFactories: extensionFactories as ExtensionFactory[],
1065
1073
  noSkills: false,
1066
1074
  appendSystemPrompt: [
1067
1075
  `You are ${roleLabel} agent '${member.name}' in Pi session '${readTeamName}', running in-process so the lead can follow and control you from Pi.`,
@@ -1143,9 +1151,14 @@ export async function runReadAgentInProcess(
1143
1151
  const communicationToolNames = communicationTools.map(tool => tool.name);
1144
1152
  const communicationToolNameSet = new Set(communicationToolNames);
1145
1153
  const delegationToolNameSet = new Set<string>(NESTED_DELEGATION_TOOL_NAMES);
1146
- const extensionToolNames = loader.getExtensions().extensions.flatMap(extension => {
1154
+ const loadedExtensions = loader.getExtensions().extensions;
1155
+ const nativeToolNames = new Set(loadedExtensions
1156
+ .filter(extension => isNativeExtensionPath(extension.path))
1157
+ .flatMap(extension => Array.from(extension.tools.keys())));
1158
+ const extensionToolNames = loadedExtensions.flatMap(extension => {
1147
1159
  return Array.from(extension.tools.keys()).filter(name =>
1148
- !communicationToolNameSet.has(name) && !delegationToolNameSet.has(name) && !USER_INTERACTION_TOOLS.includes(name)
1160
+ !communicationToolNameSet.has(name) && !delegationToolNameSet.has(name)
1161
+ && !USER_INTERACTION_TOOLS.includes(name) && !nativeToolNames.has(name)
1149
1162
  );
1150
1163
  });
1151
1164
  const nestedReadAgentToolNames = nestedReadAgentTools.map(tool => tool.name);
@@ -1186,8 +1199,12 @@ export async function runReadAgentInProcess(
1186
1199
  // sessions retain custom providers and runtime-scoped credentials.
1187
1200
  modelRuntime: parentModelRuntime,
1188
1201
  modelRegistry: ctx.modelRegistry,
1189
- tools: activeToolNames,
1190
- excludeTools: USER_INTERACTION_TOOLS,
1202
+ // An SDK allowlist would hide MCP tools registered later during session_start.
1203
+ ...(nativeExtensions.length ? {} : { tools: activeToolNames }),
1204
+ excludeTools: [
1205
+ ...USER_INTERACTION_TOOLS,
1206
+ ...(nativeExtensions.length ? NESTED_DELEGATION_TOOL_NAMES.filter(name => !nestedReadAgentToolNames.includes(name)) : []),
1207
+ ],
1191
1208
  customTools: [...communicationTools, ...nestedReadAgentTools],
1192
1209
  resourceLoader: loader,
1193
1210
  settingsManager: childSettingsManager,
@@ -1195,9 +1212,21 @@ export async function runReadAgentInProcess(
1195
1212
  } as Parameters<typeof createAgentSession>[0] & { modelRuntime?: unknown });
1196
1213
 
1197
1214
  state.session = session;
1198
- // Current hosts capture this public hook when a prompt starts; legacy hosts
1199
- // retain their existing loop behavior. Never abort from the reporting tool.
1200
- if (session.agent && "shouldStopAfterTurn" in session.agent) {
1215
+ if (nativeExtensions.length) {
1216
+ const activeNativeTools = session.getActiveToolNames().filter(name => nativeToolNames.has(name));
1217
+ session.setActiveToolsByName([...activeToolNames, ...activeNativeTools]);
1218
+ }
1219
+ // Hosts capture these public hooks when a prompt starts. Never abort from the reporting tool.
1220
+ if (session.agent && "finishTurn" in session.agent) {
1221
+ const agent = session.agent as typeof session.agent & {
1222
+ finishTurn?: (...args: unknown[]) => unknown;
1223
+ };
1224
+ const previous = agent.finishTurn;
1225
+ agent.finishTurn = async (...args) => {
1226
+ const decision = await previous?.(...args);
1227
+ return submittedFinalReport !== undefined ? { action: "end" } : decision;
1228
+ };
1229
+ } else if (session.agent && "shouldStopAfterTurn" in session.agent) {
1201
1230
  const agent = session.agent as typeof session.agent & {
1202
1231
  shouldStopAfterTurn?: (...args: unknown[]) => boolean | Promise<boolean>;
1203
1232
  };
@@ -852,7 +852,7 @@ function createExecutionRuntime(pi: ExtensionAPI, host?: ReturnType<typeof creat
852
852
  if (result.status === "occupied" && result.tombstone.phase === "persistence_closed"
853
853
  && runningAgents.some(agent => agent.name === quarantinedName && agent.runId === result.tombstone.runId)) {
854
854
  const entryIndex = entries.findIndex(entry => entry.name === quarantinedName);
855
- footerStatuses[entryIndex] += " · Finishing cleanup";
855
+ entries[entryIndex].statusNote = "Finishing cleanup";
856
856
  continue;
857
857
  }
858
858
  const persistedMember = activityMembers.find(member => member.name === quarantinedName);
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import * as nodeFs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { isNativeExtensionPath } from "../resources/native-extensions";
4
5
 
5
6
  /**
6
7
  * Build the command used to relaunch pi for teammate processes.
@@ -40,6 +41,7 @@ export function shellQuote(value: string): string {
40
41
  }
41
42
 
42
43
  function extensionPathIdentity(source: string): string {
44
+ if (isNativeExtensionPath(source)) return source;
43
45
  const absolute = path.resolve(source);
44
46
  try {
45
47
  return nodeFs.realpathSync(absolute);
@@ -0,0 +1,36 @@
1
+ import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
2
+
3
+ const NATIVE_FACTORIES = {
4
+ "builtin:mcp": "createMcpExtension",
5
+ "builtin:codemode": "createCodemodeExtension",
6
+ "builtin:tool-search": "createToolSearchExtension",
7
+ } as const;
8
+
9
+ type NativeExtensionPath = keyof typeof NATIVE_FACTORIES;
10
+
11
+ export function isNativeExtensionPath(value: string): value is NativeExtensionPath {
12
+ return Object.hasOwn(NATIVE_FACTORIES, value);
13
+ }
14
+
15
+ export interface NativeExtensionFactory {
16
+ name: string;
17
+ factory: ExtensionFactory;
18
+ builtin: true;
19
+ replaceable: true;
20
+ }
21
+
22
+ /** Only selected builtins are instantiated, using exports from the running host. */
23
+ export function createNativeExtensionFactories(api: object, paths: readonly string[]): NativeExtensionFactory[] {
24
+ return paths.filter(isNativeExtensionPath).map(identity => {
25
+ const createFactory: unknown = Reflect.get(api, NATIVE_FACTORIES[identity]);
26
+ if (typeof createFactory !== "function") {
27
+ throw new Error(`Selected extension ${identity} is not supported by this Pi host.`);
28
+ }
29
+ return {
30
+ name: identity.slice("builtin:".length),
31
+ factory: createFactory(),
32
+ builtin: true,
33
+ replaceable: true,
34
+ };
35
+ });
36
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { loadSettings, type PiExtendedTeamsSettings } from "../../src/utils/settings";
4
+ import { isNativeExtensionPath } from "./native-extensions";
4
5
 
5
6
  const SELF_COMMAND_NAME = "agents-extensions";
6
7
  export const EXTENSIONS_COMMAND_DESCRIPTION = "Select which observable loaded Pi extensions spawned agents receive.";
@@ -19,7 +20,7 @@ export interface ExtensionSourceInfo {
19
20
  export interface LeadExtensionSnapshot {
20
21
  /** Stable, human-readable selector when unique in the current lead snapshot. */
21
22
  name: string;
22
- /** Canonical filesystem identity used for deterministic deduplication. */
23
+ /** Canonical filesystem path or native builtin identity used for deduplication. */
23
24
  identity: string;
24
25
  /** Pi's registered extension entrypoint path, passed to the child unchanged. */
25
26
  path: string;
@@ -81,6 +82,7 @@ export interface CreateSpawnResourcePlanOptions {
81
82
  }
82
83
 
83
84
  function canonicalPath(filePath: string): string {
85
+ if (isNativeExtensionPath(filePath)) return filePath;
84
86
  const absolute = path.resolve(filePath);
85
87
  try {
86
88
  return fs.realpathSync(absolute);
@@ -104,7 +106,8 @@ function usableSourceInfo(sourceInfo: ExtensionSourceInfo | undefined): sourceIn
104
106
  return !!sourceInfo
105
107
  && typeof sourceInfo.path === "string"
106
108
  && sourceInfo.path.length > 0
107
- && !sourceInfo.path.startsWith("<");
109
+ && !sourceInfo.path.startsWith("<")
110
+ && (!sourceInfo.path.startsWith("builtin:") || isNativeExtensionPath(sourceInfo.path));
108
111
  }
109
112
 
110
113
  /**
@@ -130,7 +133,8 @@ export function snapshotLeadExtensions(pi: LeadRegistrationApi): readonly LeadEx
130
133
  .filter((command) => command.source === "extension")
131
134
  .map((command) => command.sourceInfo),
132
135
  ...tools
133
- .filter((tool) => !GENERIC_TOOL_SOURCES.has(tool.sourceInfo?.source ?? ""))
136
+ .filter((tool) => !GENERIC_TOOL_SOURCES.has(tool.sourceInfo?.source ?? "")
137
+ || isNativeExtensionPath(tool.sourceInfo?.path ?? ""))
134
138
  .map((tool) => tool.sourceInfo),
135
139
  ].filter(usableSourceInfo);
136
140
 
@@ -13,8 +13,14 @@ export function requireLeadRecipient(recipient: string): void {
13
13
  }
14
14
  }
15
15
 
16
- export function registerSpawnedAgentCommunicationGuard(pi: Pick<ExtensionAPI, "on">): void {
16
+ export function registerSpawnedAgentCommunicationGuard(
17
+ pi: Pick<ExtensionAPI, "on">,
18
+ reportAccepted: () => boolean = () => false,
19
+ ): void {
17
20
  pi.on("tool_call", event => {
21
+ if (reportAccepted()) {
22
+ return { block: true, reason: "This agent's final report has been accepted. No further tool calls are allowed." };
23
+ }
18
24
  if (USER_INTERACTION_TOOLS.includes(event.toolName)) {
19
25
  return { block: true, reason: "Spawned agents cannot contact the user. Send your question to team-lead using send_message." };
20
26
  }
@@ -9,6 +9,7 @@ export interface TeamActivityStatusEntry {
9
9
  status?: string;
10
10
  detail?: string;
11
11
  displayText?: string;
12
+ statusNote?: string;
12
13
  }
13
14
 
14
15
  export type TeamActivityStatusCounts = Record<string, number>;
@@ -399,9 +400,13 @@ export function teamActivityStatusWidget(
399
400
  transitionHint = candidate;
400
401
  }
401
402
  }
402
- const animated = animateEntry(entry, branch, now, width, transitionHint, index, hasCachedTransitionEntry);
403
+ const note = entry.statusNote ? ` · ${entry.statusNote}` : "";
404
+ const progressWidth = note ? Math.max(0, width - visibleWidth(note)) : width;
405
+ const animated = animateEntry(entry, branch, now, progressWidth, transitionHint, index, hasCachedTransitionEntry);
403
406
  animationActive ||= animated.active;
404
- lines.push(animated.text);
407
+ lines.push(note
408
+ ? truncateToWidth(`${animated.text.trimEnd()}${theme.fg("dim", note)}`, width, "…", true)
409
+ : animated.text);
405
410
  }
406
411
  if (rosterChanged) {
407
412
  const activeKeys = new Set<string>();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extended-teams",
3
- "version": "2.5.0",
3
+ "version": "2.5.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",