pi-subagents 0.53.0 → 0.54.0

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 (36) hide show
  1. package/CHANGELOG.md +31 -4
  2. package/README.md +1 -1
  3. package/docs/configuration.md +3 -3
  4. package/docs/models.md +17 -3
  5. package/index.ts +10 -1
  6. package/package.json +1 -1
  7. package/prompts/council.md +10 -7
  8. package/skills/council-mode/SKILL.md +17 -16
  9. package/skills/pi-subagents/SKILL.md +5 -1
  10. package/skills/pi-subagents/references/execution-controls.md +2 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
  12. package/src/agents/agent-management.ts +85 -5
  13. package/src/agents/agents.ts +67 -24
  14. package/src/api/preflight.ts +7 -2
  15. package/src/extension/doctor.ts +10 -0
  16. package/src/extension/fanout-child.ts +3 -2
  17. package/src/extension/index.ts +4 -3
  18. package/src/extension/public-execution.ts +1 -1
  19. package/src/extension/tool-result.ts +19 -0
  20. package/src/inspectors/herdr/client.ts +3 -3
  21. package/src/runs/background/async-execution.ts +10 -5
  22. package/src/runs/background/async-retention.ts +1 -1
  23. package/src/runs/background/chain-root-attachment.ts +5 -0
  24. package/src/runs/background/result-watcher.ts +4 -3
  25. package/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/src/runs/background/subagent-runner.ts +26 -2
  27. package/src/runs/background/wait-completions.ts +2 -0
  28. package/src/runs/background/wait-tool.ts +4 -3
  29. package/src/runs/foreground/execution.ts +29 -1
  30. package/src/runs/foreground/subagent-executor.ts +14 -6
  31. package/src/runs/shared/model-fallback.ts +36 -15
  32. package/src/runs/shared/model-scope.ts +106 -39
  33. package/src/runs/shared/pi-args.ts +34 -1
  34. package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
  35. package/src/shared/types.ts +13 -3
  36. package/src/watchdog/permission-arbiter.ts +59 -51
@@ -110,6 +110,9 @@ export interface AgentConfig {
110
110
  runner?: AgentRunnerConfig;
111
111
  localName?: string;
112
112
  packageName?: string;
113
+ packageSourceName?: string;
114
+ packageSourceVersion?: string;
115
+ packageSourceRoot?: string;
113
116
  description: string;
114
117
  aliases?: string[];
115
118
  tools?: string[];
@@ -255,8 +258,15 @@ function getUserChainDir(): string {
255
258
  return path.join(getAgentDir(), "chains");
256
259
  }
257
260
 
261
+ interface PackageSubagentPath {
262
+ dir: string;
263
+ packageName?: string;
264
+ packageVersion?: string;
265
+ packageRoot: string;
266
+ }
267
+
258
268
  interface PackageSubagentPaths {
259
- agents: string[];
269
+ agents: PackageSubagentPath[];
260
270
  chains: string[];
261
271
  }
262
272
 
@@ -391,18 +401,30 @@ function stringArray(value: unknown): string[] {
391
401
  return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
392
402
  }
393
403
 
404
+ function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omit<PackageSubagentPath, "dir"> {
405
+ const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : undefined;
406
+ const version = typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : undefined;
407
+ return {
408
+ packageRoot,
409
+ ...(name ? { packageName: name } : {}),
410
+ ...(version ? { packageVersion: version } : {}),
411
+ };
412
+ }
413
+
394
414
  function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
395
415
  const packageJsonPath = path.join(packageRoot, "package.json");
396
416
  const pkg = readJsonFileBestEffort(packageJsonPath);
397
417
  if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
418
+ const pkgRecord = pkg as Record<string, unknown>;
419
+ const metadata = packageMetadata(pkgRecord, packageRoot);
398
420
 
399
421
  const roots: Record<string, unknown>[] = [];
400
- const piSubagents = (pkg as { "pi-subagents"?: unknown })["pi-subagents"];
422
+ const piSubagents = pkgRecord["pi-subagents"];
401
423
  if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) {
402
424
  roots.push(piSubagents as Record<string, unknown>);
403
425
  }
404
426
 
405
- const pi = (pkg as { pi?: unknown }).pi;
427
+ const pi = pkgRecord.pi;
406
428
  if (pi && typeof pi === "object" && !Array.isArray(pi)) {
407
429
  const subagents = (pi as { subagents?: unknown }).subagents;
408
430
  if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) {
@@ -410,10 +432,10 @@ function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubage
410
432
  }
411
433
  }
412
434
 
413
- const agents: string[] = [];
435
+ const agents: PackageSubagentPath[] = [];
414
436
  const chains: string[] = [];
415
437
  for (const root of roots) {
416
- for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry));
438
+ for (const entry of stringArray(root.agents)) agents.push({ dir: path.resolve(packageRoot, entry), ...metadata });
417
439
  for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
418
440
  }
419
441
  return { agents, chains };
@@ -505,17 +527,17 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
505
527
  const seenRoots = new Set<string>();
506
528
  const seenAgents = new Set<string>();
507
529
  const seenChains = new Set<string>();
508
- const agents: string[] = [];
530
+ const agents: PackageSubagentPath[] = [];
509
531
  const chains: string[] = [];
510
532
  for (const packageRoot of packageRoots) {
511
533
  const resolvedRoot = path.resolve(packageRoot);
512
534
  if (seenRoots.has(resolvedRoot)) continue;
513
535
  seenRoots.add(resolvedRoot);
514
536
  const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
515
- for (const agentDir of paths.agents) {
516
- if (seenAgents.has(agentDir)) continue;
517
- seenAgents.add(agentDir);
518
- agents.push(agentDir);
537
+ for (const agentPath of paths.agents) {
538
+ if (seenAgents.has(agentPath.dir)) continue;
539
+ seenAgents.add(agentPath.dir);
540
+ agents.push(agentPath);
519
541
  }
520
542
  for (const chainDir of paths.chains) {
521
543
  if (seenChains.has(chainDir)) continue;
@@ -1233,7 +1255,18 @@ function applyCustomAgentOverride(
1233
1255
  if (override.acceptanceRole !== undefined) {
1234
1256
  fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole);
1235
1257
  }
1236
- if (override.disabled !== undefined && agent.disabled === undefined) {
1258
+ if (override.disabled !== undefined) {
1259
+ // Unconditional, matching applyBuiltinOverride: custom agents have no
1260
+ // frontmatter concept of `disabled` to protect (unlike model/thinking/
1261
+ // etc., which use `fill()` + agentHasFrontmatterField to defer to the
1262
+ // agent's own file). A guard here previously read `agent.disabled ===
1263
+ // undefined`, which was a no-op before user+project layering existed
1264
+ // (this function only ever ran once, against the pristine base agent,
1265
+ // whose `.disabled` is always undefined for custom agents) but became a
1266
+ // real bug once a user-scope override could run first: the guard then
1267
+ // silently blocked a later project-scope override from ever changing
1268
+ // `disabled`, breaking this PR's own "project wins" precedence for this
1269
+ // one field.
1237
1270
  mutable().disabled = override.disabled;
1238
1271
  anyFilled = true;
1239
1272
  }
@@ -1275,18 +1308,25 @@ function applyCustomAgentOverrides(
1275
1308
  userSettingsPath: string,
1276
1309
  projectSettingsPath: string | null,
1277
1310
  ): AgentConfig[] {
1311
+ // Both scopes are applied, user first then project, so a project override that
1312
+ // only sets a subset of fields (e.g. just `extensions`) layers on top of the
1313
+ // user's override instead of silently discarding it (#1341 / issue writeup:
1314
+ // "agentOverrides project override drops user-only fields for custom agents").
1315
+ // Per-field precedence still favors project over user: applyCustomAgentOverride
1316
+ // only fills fields present in the override it's given, and project is applied
1317
+ // last, so any field set at both scopes ends up with the project's value.
1278
1318
  return agents.map((agent) => {
1319
+ const userOverride = userSettings.overrides[agent.name];
1320
+ const withUserOverride = userOverride
1321
+ ? applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath })
1322
+ : agent;
1323
+
1279
1324
  const projectOverride = projectSettings.overrides[agent.name];
1280
1325
  if (projectOverride && projectSettingsPath) {
1281
- return applyCustomAgentOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
1282
- }
1283
-
1284
- const userOverride = userSettings.overrides[agent.name];
1285
- if (userOverride) {
1286
- return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
1326
+ return applyCustomAgentOverride(withUserOverride, projectOverride, { scope: "project", path: projectSettingsPath });
1287
1327
  }
1288
1328
 
1289
- return agent;
1329
+ return withUserOverride;
1290
1330
  });
1291
1331
  }
1292
1332
 
@@ -1615,7 +1655,7 @@ function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFi
1615
1655
  });
1616
1656
  }
1617
1657
 
1618
- function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1658
+ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1619
1659
  const agents: AgentConfig[] = [];
1620
1660
  const diagnostics: AgentDiscoveryDiagnostic[] = [];
1621
1661
 
@@ -1750,6 +1790,9 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1750
1790
  ...(runner !== undefined ? { runner } : {}),
1751
1791
  localName,
1752
1792
  ...(packageName !== undefined ? { packageName } : {}),
1793
+ ...(packageSource?.packageName ? { packageSourceName: packageSource.packageName } : {}),
1794
+ ...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
1795
+ ...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
1753
1796
  description: frontmatter.description,
1754
1797
  ...(aliases !== undefined ? { aliases } : {}),
1755
1798
  ...(rawTools !== undefined ? { tools } : {}),
@@ -1797,8 +1840,8 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1797
1840
  return { agents, diagnostics };
1798
1841
  }
1799
1842
 
1800
- function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1801
- return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority);
1843
+ function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1844
+ return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority, packageSource);
1802
1845
  }
1803
1846
 
1804
1847
  function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
@@ -1924,7 +1967,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1924
1967
  userSettingsPath,
1925
1968
  projectSettingsPath,
1926
1969
  );
1927
- const packageLoaded = packageSubagentPaths.agents.map((dir, index) => loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index));
1970
+ const packageLoaded = packageSubagentPaths.agents.map((entry, index) => loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry));
1928
1971
  const packageMap = new Map<string, AgentConfig>();
1929
1972
  for (const loaded of packageLoaded) {
1930
1973
  for (const agent of loaded.agents) {
@@ -1998,8 +2041,8 @@ export function discoverAgentsAll(cwd: string): {
1998
2041
  );
1999
2042
  const packageMap = new Map<string, AgentConfig>();
2000
2043
  const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
2001
- for (const [index, dir] of packageSubagentPaths.agents.entries()) {
2002
- const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
2044
+ for (const [index, entry] of packageSubagentPaths.agents.entries()) {
2045
+ const loaded = loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry);
2003
2046
  packageAgentDiagnostics.push(...loaded.diagnostics);
2004
2047
  for (const agent of loaded.agents) {
2005
2048
  if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
@@ -7,6 +7,7 @@ import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
7
7
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
8
8
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
9
9
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
10
+ import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
10
11
  import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
11
12
  import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
12
13
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
@@ -14,6 +15,7 @@ import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
15
  import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
15
16
  import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
16
17
  import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
18
+ import { resolvePermissionRules } from "../runs/shared/permissions.ts";
17
19
  import type { ResolvedTurnBudget } from "../shared/types.ts";
18
20
  import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
19
21
  import { resolveStepBehavior } from "../shared/settings.ts";
@@ -288,19 +290,21 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
288
290
  const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
289
291
  const availableModels = normalizeAvailableModels(input.availableModels);
290
292
  const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
293
+ const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
291
294
  const primaryModel = externalRunner
292
295
  ? undefined
293
- : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: discovered.modelScope });
296
+ : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: modelScopes });
294
297
  const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
295
298
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
296
299
  const modelCandidates = externalRunner
297
300
  ? []
298
301
  : buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
299
- scope: discovered.modelScope,
302
+ scope: modelScopes,
300
303
  primaryModelFromParent: inheritsParentModel(input.model, agent.model, input.parentModel),
301
304
  })
302
305
  .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
303
306
  let toolPlan: PiLaunchToolPlan;
307
+ const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
304
308
  try {
305
309
  toolPlan = resolvePiLaunchToolPlan({
306
310
  tools: agent.tools,
@@ -312,6 +316,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
312
316
  structuredOutput: Boolean(input.outputSchema),
313
317
  capabilityCeiling: effectiveCapabilityCeiling,
314
318
  agentName: agent.name,
319
+ permissionRules,
315
320
  });
316
321
  } catch (error) {
317
322
  const message = error instanceof Error ? error.message : String(error);
@@ -224,6 +224,13 @@ function formatPermissionSystemSection(): string[] {
224
224
  return lines;
225
225
  }
226
226
 
227
+ function formatWorkflowScriptSection(): string[] {
228
+ return [
229
+ "- helpers: runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console",
230
+ "- recovery: if runs.all is missing, reload or update pi-subagents; await Promise.all([runs.run(...)]) is also supported",
231
+ ];
232
+ }
233
+
227
234
  export function buildDoctorReport(input: DoctorReportInput): string {
228
235
  const paths = input.paths ?? defaultPaths();
229
236
  const deps = { ...DEFAULT_DEPS, ...input.deps };
@@ -253,6 +260,9 @@ export function buildDoctorReport(input: DoctorReportInput): string {
253
260
  "Active async capacity",
254
261
  ...formatActiveAsyncCapacitySection(input),
255
262
  "",
263
+ "Workflow script",
264
+ ...formatWorkflowScriptSection(),
265
+ "",
256
266
  "Permission system",
257
267
  ...formatPermissionSystemSection(),
258
268
  "",
@@ -11,6 +11,7 @@ import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute,
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
12
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
13
  import { createSubagentParamsSchema } from "./schemas.ts";
14
+ import { finalizeToolResult } from "./tool-result.ts";
14
15
  import { loadConfig, resolveAsyncByDefault } from "./config.ts";
15
16
  import { type Details, type SubagentState } from "../shared/types.ts";
16
17
 
@@ -181,8 +182,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
181
182
  "Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
182
183
  ].join("\n"),
183
184
  parameters: params,
184
- execute(id, params, signal, onUpdate, ctx) {
185
- return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
185
+ async execute(id, params, signal, onUpdate, ctx) {
186
+ return finalizeToolResult(await executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
186
187
  },
187
188
  };
188
189
 
@@ -57,6 +57,7 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
57
57
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
58
58
  import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
59
59
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
60
+ import { finalizeToolResult } from "./tool-result.ts";
60
61
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
61
62
  import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
62
63
  import { resolveMissionStoreLocation } from "../missions/store.ts";
@@ -517,7 +518,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
517
518
  observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
518
519
  hasDeliveryDemand: hasResultDeliveryDemand,
519
520
  deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
520
- resultScanLogging: config.resultScanLogging ?? "all",
521
+ resultScanLogging: config.resultScanLogging,
521
522
  },
522
523
  );
523
524
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
@@ -658,8 +659,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
658
659
  ...buildSubagentToolPromptMetadata(config),
659
660
  parameters,
660
661
 
661
- execute(id, params, signal, onUpdate, ctx) {
662
- return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
662
+ async execute(id, params, signal, onUpdate, ctx) {
663
+ return finalizeToolResult(await executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
663
664
  },
664
665
 
665
666
  renderCall(args, theme) {
@@ -122,7 +122,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
122
122
  params: {
123
123
  ...workflowDefaults,
124
124
  ...(params.async === undefined && options.asyncByDefault === false ? { async: false } : {}),
125
- workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
125
+ workflowScript: `return runs.run("main", ${JSON.stringify(child)})`,
126
126
  } as T,
127
127
  };
128
128
  }
@@ -0,0 +1,19 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+
3
+ /**
4
+ * Convert pi-subagents' internal logical-error result into the rejection Pi's
5
+ * public tool boundary uses to emit a canonical errored ToolResult.
6
+ *
7
+ * Keep this at registered tool boundaries. Internal workflows intentionally
8
+ * retain their return-based error handling.
9
+ */
10
+ export function finalizeToolResult<T>(result: AgentToolResult<T>): AgentToolResult<T> {
11
+ if (result.isError !== true) return result;
12
+
13
+ const message = result.content
14
+ .flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
15
+ .join("\n")
16
+ .trim();
17
+
18
+ throw new Error(message || "pi-subagents reported a logical tool failure.");
19
+ }
@@ -1,4 +1,4 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
 
3
3
  export type HerdrErrorCode =
4
4
  | "HERDR_UNAVAILABLE"
@@ -16,7 +16,7 @@ export interface HerdrClient {
16
16
  run<T = unknown>(args: string[], options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean }): Promise<HerdrResult<T>>;
17
17
  }
18
18
 
19
- type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ChildProcess;
19
+ type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ReturnType<typeof spawn>;
20
20
 
21
21
  function error(code: HerdrErrorCode, message: string, details?: unknown): HerdrResult<never> {
22
22
  return { ok: false, error: { code, message, ...(details !== undefined ? { details } : {}) } };
@@ -46,7 +46,7 @@ export function createHerdrClient(options: { bin?: string; spawn?: SpawnHerdr }
46
46
  return {
47
47
  run<T>(args: string[], runOptions: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean } = {}): Promise<HerdrResult<T>> {
48
48
  return new Promise((resolve) => {
49
- let child: ChildProcess;
49
+ let child: ReturnType<typeof spawn>;
50
50
  try {
51
51
  child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
52
52
  } catch (cause) {
@@ -26,7 +26,7 @@ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
26
26
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
27
27
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
28
28
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
29
- import type { ModelScopeConfig } from "../shared/model-scope.ts";
29
+ import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
30
30
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
31
31
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
32
32
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
@@ -791,18 +791,20 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
791
791
  const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
792
792
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
793
793
 
794
+ const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
794
795
  const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
795
796
  s.model,
796
797
  a.model,
797
798
  ctx.currentModel,
798
799
  availableModels,
799
800
  ctx.currentModelProvider,
800
- { scope: ctx.modelScope },
801
+ { scope: modelScopes },
801
802
  );
802
803
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
803
804
  const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
804
805
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
805
806
  const agentContract = s.agentContract ?? params.agentContract;
807
+ const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
806
808
  const toolPlan = resolvePiLaunchToolPlan({
807
809
  tools: a.tools,
808
810
  extensions: a.extensions,
@@ -814,9 +816,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
814
816
  capabilityCeiling: params.capabilityCeiling,
815
817
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
816
818
  agentName: a.name,
819
+ permissionRules,
817
820
  });
818
821
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
819
- const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
820
822
  if (externalRunner && permissionRules) {
821
823
  throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
822
824
  }
@@ -839,7 +841,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
839
841
  thinking: resolveEffectiveThinking(model, effectiveThinking),
840
842
  launchResolvedExtensions,
841
843
  modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
842
- scope: ctx.modelScope,
844
+ scope: modelScopes,
843
845
  primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
844
846
  }).map((candidate) =>
845
847
  applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
@@ -1418,6 +1420,7 @@ export function executeAsyncSingle(
1418
1420
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
1419
1421
  : "";
1420
1422
  const taskText = readsInstruction + taskWithOutputInstruction;
1423
+ const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
1421
1424
  const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
1422
1425
  ? params.modelOverride
1423
1426
  : resolveSubagentModelOverride(
@@ -1425,6 +1428,7 @@ export function executeAsyncSingle(
1425
1428
  ctx.currentModel,
1426
1429
  availableModels,
1427
1430
  ctx.currentModelProvider,
1431
+ { scope: modelScopes },
1428
1432
  );
1429
1433
  const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1430
1434
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
@@ -1453,7 +1457,7 @@ export function executeAsyncSingle(
1453
1457
  const modelCandidates = externalRunner
1454
1458
  ? []
1455
1459
  : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
1456
- scope: ctx.modelScope,
1460
+ scope: modelScopes,
1457
1461
  primaryModelFromParent: params.modelOverrideFromParent,
1458
1462
  })
1459
1463
  .flatMap((candidate) => {
@@ -1472,6 +1476,7 @@ export function executeAsyncSingle(
1472
1476
  capabilityCeiling,
1473
1477
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1474
1478
  agentName: agentConfig.name,
1479
+ permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1475
1480
  });
1476
1481
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1477
1482
  const launchContractDigest = launchBindingDigest({
@@ -600,7 +600,7 @@ function runRetentionDiscovery(input: {
600
600
  };
601
601
  const onAbort = (): void => fail(new RetentionCancelledError("Retention discovery was cancelled."));
602
602
  input.signal?.addEventListener("abort", onAbort, { once: true });
603
- worker.once("error", (error) => fail(error));
603
+ worker.once("error", (error) => fail(error instanceof Error ? error : new Error(String(error))));
604
604
  worker.once("exit", (code) => {
605
605
  if (!settled) fail(new Error(`Retention discovery worker exited before replying (${code}).`));
606
606
  });
@@ -21,6 +21,7 @@ export interface ImportedAsyncRootResult {
21
21
  model?: string;
22
22
  attemptedModels?: string[];
23
23
  modelAttempts?: ModelAttempt[];
24
+ contextOverflow?: boolean;
24
25
  totalCost?: CostSummary;
25
26
  structuredOutput?: unknown;
26
27
  structuredOutputPath?: string;
@@ -49,6 +50,7 @@ interface AsyncResultFile {
49
50
  model?: string;
50
51
  attemptedModels?: string[];
51
52
  modelAttempts?: ModelAttempt[];
53
+ contextOverflow?: boolean;
52
54
  totalCost?: CostSummary;
53
55
  structuredOutput?: unknown;
54
56
  structuredOutputPath?: string;
@@ -110,6 +112,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
110
112
  ...(step?.model ? { model: step.model } : {}),
111
113
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
112
114
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
115
+ ...(step?.contextOverflow ? { contextOverflow: true } : {}),
113
116
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
114
117
  ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
115
118
  ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
@@ -131,6 +134,7 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
131
134
  ...(step?.model ? { model: step.model } : {}),
132
135
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
133
136
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
137
+ ...(step?.contextOverflow ? { contextOverflow: true } : {}),
134
138
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
135
139
  };
136
140
  }
@@ -158,6 +162,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
158
162
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
159
163
  ...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
160
164
  ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
165
+ ...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
161
166
  ...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
162
167
  ...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
163
168
  ...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
@@ -57,7 +57,7 @@ type ResultWatcherDeps = {
57
57
  coalesceDelayMs?: number;
58
58
  /** Returns true while a durable completion source needs periodic delivery checks. */
59
59
  hasDeliveryDemand?: () => boolean;
60
- /** Control how slow result-index scans are logged. Defaults to \"all\". */
60
+ /** Control how slow result-index scans are logged. Defaults to \"activity\". */
61
61
  resultScanLogging?: "all" | "activity" | "off";
62
62
  platform?: NodeJS.Platform;
63
63
  };
@@ -606,12 +606,13 @@ export function createResultWatcher(
606
606
  const logScanStats = (stats: ResultScanStats) => {
607
607
  const elapsed = Date.now() - stats.startedAt;
608
608
  if (elapsed < SLOW_RESULT_SCAN_MS) return;
609
- if (deps.resultScanLogging === "off") return;
609
+ const resultScanLogging = deps.resultScanLogging ?? "activity";
610
+ if (resultScanLogging === "off") return;
610
611
  // A scan that inspected and scheduled nothing is a quiet no-op (e.g. the
611
612
  // healthy periodic rescan while no async runs are pending). Under
612
613
  // "activity", skip it so empty scans do not burn context tokens in the
613
614
  // session transcript.
614
- if (deps.resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
615
+ if (resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
615
616
  console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
616
617
  };
617
618
  const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
@@ -97,6 +97,7 @@ interface ResultChildOutcome {
97
97
  thinking?: string;
98
98
  attemptedModels?: string[];
99
99
  modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
100
+ contextOverflow?: boolean;
100
101
  }
101
102
 
102
103
  interface ResultRepairData {
@@ -163,6 +164,7 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
163
164
  thinking,
164
165
  attemptedModels: child?.attemptedModels ?? step.attemptedModels,
165
166
  modelAttempts: child?.modelAttempts ?? step.modelAttempts,
167
+ contextOverflow: child?.contextOverflow ?? step.contextOverflow,
166
168
  };
167
169
  });
168
170
  const terminalStatus: AsyncStatus = {
@@ -258,6 +260,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
258
260
  model: step.model,
259
261
  attemptedModels: step.attemptedModels,
260
262
  modelAttempts: step.modelAttempts,
263
+ contextOverflow: step.contextOverflow,
261
264
  sessionFile: step.sessionFile,
262
265
  })),
263
266
  exitCode: 1,