pi-subagents 0.52.1 → 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 (67) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +4 -0
  3. package/docs/configuration.md +12 -2
  4. package/docs/extension-api.md +3 -1
  5. package/docs/models.md +17 -3
  6. package/docs/workflows.md +2 -0
  7. package/index.ts +10 -1
  8. package/package.json +2 -1
  9. package/prompts/council.md +51 -0
  10. package/skills/council-mode/SKILL.md +231 -0
  11. package/skills/pi-subagents/SKILL.md +5 -1
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
  13. package/skills/pi-subagents/references/execution-controls.md +13 -0
  14. package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
  15. package/src/agents/agent-management.ts +107 -8
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +96 -37
  18. package/src/agents/builtin-names.ts +9 -0
  19. package/src/agents/runtime-agent-registry.ts +418 -0
  20. package/src/api/agents.ts +7 -0
  21. package/src/api/preflight.ts +8 -3
  22. package/src/extension/config.ts +3 -0
  23. package/src/extension/doctor.ts +11 -0
  24. package/src/extension/fanout-child.ts +3 -2
  25. package/src/extension/index.ts +20 -4
  26. package/src/extension/public-execution.ts +1 -1
  27. package/src/extension/rpc.ts +41 -1
  28. package/src/extension/schemas.ts +6 -3
  29. package/src/extension/tool-description.ts +2 -2
  30. package/src/extension/tool-result.ts +19 -0
  31. package/src/inspectors/herdr/client.ts +3 -3
  32. package/src/runs/background/async-execution.ts +12 -6
  33. package/src/runs/background/async-job-tracker.ts +4 -3
  34. package/src/runs/background/async-resume.ts +2 -1
  35. package/src/runs/background/async-retention.ts +1 -1
  36. package/src/runs/background/async-status-snapshot.ts +14 -5
  37. package/src/runs/background/auto-drain.ts +1 -0
  38. package/src/runs/background/chain-root-attachment.ts +5 -0
  39. package/src/runs/background/result-watcher.ts +9 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -0
  41. package/src/runs/background/subagent-runner.ts +32 -5
  42. package/src/runs/background/subagent-wait.ts +9 -5
  43. package/src/runs/background/terminal-run-index.ts +15 -6
  44. package/src/runs/background/wait-completions.ts +2 -0
  45. package/src/runs/background/wait-tool.ts +5 -3
  46. package/src/runs/foreground/execution.ts +34 -2
  47. package/src/runs/foreground/subagent-executor.ts +196 -52
  48. package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
  49. package/src/runs/shared/acceptance.ts +44 -1
  50. package/src/runs/shared/model-exclusions.ts +242 -0
  51. package/src/runs/shared/model-fallback.ts +72 -16
  52. package/src/runs/shared/model-scope.ts +106 -39
  53. package/src/runs/shared/pi-args.ts +34 -1
  54. package/src/runs/shared/subagent-control.ts +25 -3
  55. package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
  56. package/src/shared/fork-context.ts +17 -1
  57. package/src/shared/model-info.ts +20 -0
  58. package/src/shared/settings.ts +2 -2
  59. package/src/shared/types.ts +47 -2
  60. package/src/slash/slash-commands.ts +20 -6
  61. package/src/slash/slash-live-state.ts +3 -3
  62. package/src/tui/fleet-status.ts +86 -1
  63. package/src/tui/fleet.ts +55 -2
  64. package/src/tui/render.ts +73 -3
  65. package/src/watchdog/permission-arbiter.ts +59 -51
  66. package/src/workflows/scripted-workflow.ts +100 -12
  67. package/src/workflows/workflow-receipt.ts +140 -0
@@ -86,6 +86,8 @@ export interface SubagentWaitParams {
86
86
  all?: boolean;
87
87
  /** Give up after this many milliseconds. Defaults to 30 minutes. */
88
88
  timeoutMs?: number;
89
+ /** False keeps a blocking wait open through idle attention; supervisor/contact requests still stop the wait. */
90
+ stopOnAttention?: boolean;
89
91
  }
90
92
 
91
93
  /** Minimal event-bus surface wait subscribes to (matches pi.events). */
@@ -110,6 +112,8 @@ export interface SubagentWaitDeps {
110
112
  stopOnAttention?: boolean;
111
113
  /** Internal auto-drain mode surfaces failed terminal subagent runs as errors. */
112
114
  failOnFailedRuns?: boolean;
115
+ /** Internal auto-drain mode surfaces actionable attention as an error. */
116
+ failOnAttention?: boolean;
113
117
  /** Arm a durable exact-target wait subscription in a long-lived interactive runtime. */
114
118
  subscribe?: (input: { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number }) => { token: string; expiresAt: number };
115
119
  /** Injectable provider protocol surfaces for deterministic tests. */
@@ -453,7 +457,7 @@ async function waitForDetachedForegroundRun(
453
457
  return result(`Remembered foreground run "${run.runId}" disappeared before a terminal child result was recorded. Completion cannot be confirmed; do not launch a replacement without checking the originating child session.`, true);
454
458
  }
455
459
  const pending = current.children.filter((child) => initialDetachedIndices.has(child.index) && child.status === "detached");
456
- const attention = deps.stopOnAttention === false ? [] : foregroundChildrenNeedingAttention(current, initialDetachedIndices);
460
+ const attention = foregroundChildrenNeedingAttention(current, initialDetachedIndices);
457
461
  if (attention.length > 0) return formatForegroundAttention(current, attention, now() - startedAt);
458
462
  if (pending.length === 0) {
459
463
  const outcome = summarizeForegroundChildren(current, initialDetachedIndices);
@@ -555,11 +559,11 @@ export async function waitForSubagents(
555
559
  const initialProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
556
560
  const initialProviderNames = new Set(providerActive.map((item) => item.provider));
557
561
  const initialCount = initialAsyncIds.size + initialProviderIds.size;
558
- const stopOnAttention = deps.stopOnAttention !== false;
562
+ const stopOnAttention = params.stopOnAttention ?? deps.stopOnAttention !== false;
559
563
  let attention = active.filter((run) => needsAttention(run));
560
564
 
561
565
  const isDone = (): boolean => {
562
- if (stopOnAttention && attention.some((run) => initialAsyncIds.has(run.id))) return true;
566
+ if (attention.some((run) => initialAsyncIds.has(run.id) && (stopOnAttention || hasSupervisorTool(run)))) return true;
563
567
  const activeAsyncIds = new Set(active.map((run) => run.id));
564
568
  const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
565
569
  if (waitForAll) {
@@ -643,7 +647,7 @@ export async function waitForSubagents(
643
647
  const status = relevantAttention.length > 0 ? "attention required" : "done";
644
648
  return result(
645
649
  `Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
646
- deps.failOnFailedRuns === true && failedAsyncCount > 0,
650
+ (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
647
651
  completions,
648
652
  );
649
653
  }
@@ -660,7 +664,7 @@ export async function waitForSubagents(
660
664
  : `${finishedCount} of ${initialCount} ${subject} finished`;
661
665
  return result(
662
666
  `Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
663
- deps.failOnFailedRuns === true && failedAsyncCount > 0,
667
+ (deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
664
668
  completions,
665
669
  );
666
670
  }
@@ -68,11 +68,16 @@ function removeInvalidMarker(marker: string): void {
68
68
  }
69
69
  }
70
70
 
71
- function markerFiles(dir: string): string[] {
71
+ interface MarkerFile {
72
+ dir: string;
73
+ name: string;
74
+ }
75
+
76
+ function markerFiles(dir: string): MarkerFile[] {
72
77
  try {
73
78
  return fs.readdirSync(dir, { withFileTypes: true })
74
79
  .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
75
- .map((entry) => path.join(dir, entry.name));
80
+ .map((entry) => ({ dir, name: entry.name }));
76
81
  } catch (error) {
77
82
  const code = (error as NodeJS.ErrnoException).code;
78
83
  if (code === "ENOENT" || code === "ENOTDIR") return [];
@@ -93,12 +98,16 @@ function sessionDirs(asyncDirRoot: string, sessionId: string | undefined): strin
93
98
  }
94
99
  }
95
100
 
101
+ function recentMarkerFiles(dirs: string[], limit: number): string[] {
102
+ return dirs.flatMap(markerFiles)
103
+ .sort((left, right) => right.name.localeCompare(left.name))
104
+ .slice(0, limit)
105
+ .map((marker) => path.join(marker.dir, marker.name));
106
+ }
107
+
96
108
  export function readRecentTerminalRunIndex(asyncDirRoot: string, options: { sessionId?: string; limit?: number } = {}): string[] {
97
109
  const limit = options.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(options.limit));
98
- const candidates = sessionDirs(asyncDirRoot, options.sessionId)
99
- .flatMap(markerFiles)
100
- .sort((left, right) => path.basename(right).localeCompare(path.basename(left)))
101
- .slice(0, limit);
110
+ const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit);
102
111
  const runIds: string[] = [];
103
112
  const seen = new Set<string>();
104
113
  for (const marker of candidates) {
@@ -44,6 +44,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
44
44
  const childRunId = asNonEmptyString(child.runId);
45
45
  const error = asNonEmptyString(child.error);
46
46
  const model = asNonEmptyString(child.model);
47
+ const contextOverflow = child.contextOverflow === true;
47
48
  return [{
48
49
  ...(agent ? { agent } : {}),
49
50
  ...(childRunId ? { runId: childRunId } : {}),
@@ -51,6 +52,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
51
52
  ...(outputState ? { outputState } : {}),
52
53
  ...(error ? { error } : {}),
53
54
  ...(model ? { model } : {}),
55
+ ...(contextOverflow ? { contextOverflow: true } : {}),
54
56
  ...(artifactPaths ? { artifactPaths } : {}),
55
57
  }];
56
58
  })
@@ -3,6 +3,7 @@ import { SubagentWaitParams } from "../../extension/schemas.ts";
3
3
  import type { Details, SubagentState } from "../../shared/types.ts";
4
4
  import { resolveWaitToolConfig, waitForSubagents } from "./subagent-wait.ts";
5
5
  import type { WaitSubscriptionManager } from "./wait-subscriptions.ts";
6
+ import { finalizeToolResult } from "../../extension/tool-result.ts";
6
7
 
7
8
  export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled, subscriptions?: Pick<WaitSubscriptionManager, "arm">): void {
8
9
  const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
@@ -16,18 +17,19 @@ In an interactive chat, do not call this merely to wait: return control to the u
16
17
  • { all: true } — wait for every async run and provider item that was active when the call began.
17
18
  • { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
18
19
  • { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. The originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
20
+ • { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
19
21
  • { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running.
20
22
 
21
23
  Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
22
24
  parameters: SubagentWaitParams,
23
- execute(_id, params, signal, onUpdate, ctx) {
24
- return waitForSubagents(params, signal, {
25
+ async execute(_id, params, signal, onUpdate, ctx) {
26
+ return finalizeToolResult(await waitForSubagents(params, signal, {
25
27
  state,
26
28
  events: pi.events,
27
29
  enabled,
28
30
  onUpdate,
29
31
  ...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
30
- });
32
+ }));
31
33
  },
32
34
  };
33
35
  pi.registerTool(tool);
@@ -73,7 +73,9 @@ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleO
73
73
  import {
74
74
  buildModelCandidates,
75
75
  formatModelAttemptNote,
76
+ isContextOverflow,
76
77
  isRetryableModelFailure,
78
+ recordRetryableModelFailure,
77
79
  } from "../shared/model-fallback.ts";
78
80
  import {
79
81
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
@@ -307,6 +309,7 @@ async function runSingleAttempt(
307
309
  originalTask?: string;
308
310
  taskDelivery?: SubagentTaskDelivery;
309
311
  orcaProgressTab?: OrcaProgressTab;
312
+ launchWarnings: { emitted: boolean };
310
313
  },
311
314
  ): Promise<SingleResult> {
312
315
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
@@ -325,7 +328,7 @@ async function runSingleAttempt(
325
328
  const permissionAuditPath = permissionRules && options.artifactsDir
326
329
  ? path.join(options.artifactsDir, "permission-audit", `${options.runId}-${options.index ?? 0}.jsonl`)
327
330
  : undefined;
328
- const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
331
+ const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit, warnings } = buildPiArgs({
329
332
  baseArgs: ["--mode", "json", "-p"],
330
333
  task,
331
334
  taskDelivery: shared.taskDelivery,
@@ -368,6 +371,10 @@ async function runSingleAttempt(
368
371
  waitToolEnabled: options.waitToolEnabled,
369
372
  capabilityCeiling: options.capabilityCeiling,
370
373
  });
374
+ if (!shared.launchWarnings.emitted && warnings.length > 0) {
375
+ for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
376
+ shared.launchWarnings.emitted = true;
377
+ }
371
378
 
372
379
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
373
380
  const toolPlan = resolvePiLaunchToolPlan({
@@ -381,6 +388,7 @@ async function runSingleAttempt(
381
388
  capabilityCeiling: options.capabilityCeiling,
382
389
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
383
390
  agentName: agent.name,
391
+ permissionRules,
384
392
  });
385
393
  const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
386
394
  const launchContractDigest = launchBindingDigest({
@@ -872,6 +880,7 @@ async function runSingleAttempt(
872
880
  startedAt: startTime,
873
881
  lastActivityAt: progress.lastActivityAt,
874
882
  currentTool: progress.currentTool,
883
+ thinking: resolvedThinking,
875
884
  now,
876
885
  });
877
886
  if (idleState === "needs_attention") {
@@ -1049,6 +1058,20 @@ async function runSingleAttempt(
1049
1058
  }
1050
1059
 
1051
1060
  if (evt.type === "tool_result_end" && evt.message) {
1061
+ const toolResultCompletion = {
1062
+ toolCallId: (evt.message as { toolCallId?: unknown }).toolCallId ?? (evt as { toolCallId?: unknown }).toolCallId,
1063
+ toolName: (evt.message as { toolName?: unknown }).toolName ?? (evt as { toolName?: unknown }).toolName,
1064
+ };
1065
+ clearActiveToolTimeout(toolResultCompletion);
1066
+ const endedTool = removeActiveToolCall(toolResultCompletion);
1067
+ if (endedTool) {
1068
+ progress.recentTools.push({
1069
+ tool: endedTool.tool,
1070
+ args: endedTool.args,
1071
+ endMs: now,
1072
+ });
1073
+ refreshCurrentTool();
1074
+ }
1052
1075
  result.messages!.push(evt.message);
1053
1076
  const resultText = extractTextFromContent(evt.message.content);
1054
1077
  if (options.toolBudget && pendingToolResult && resultText.includes("Tool budget hard limit reached")) {
@@ -1679,6 +1702,7 @@ async function runSyncCompletionInner(
1679
1702
  const modelAttempts: ModelAttempt[] = [];
1680
1703
  const aggregateUsage = emptyUsage();
1681
1704
  const attemptNotes: string[] = [];
1705
+ const launchWarnings = { emitted: false };
1682
1706
  let totalToolCount = 0;
1683
1707
  let totalDurationMs = 0;
1684
1708
 
@@ -1770,6 +1794,7 @@ async function runSyncCompletionInner(
1770
1794
  originalTask: task,
1771
1795
  taskDelivery: taskDeliveryOverride,
1772
1796
  orcaProgressTab,
1797
+ launchWarnings,
1773
1798
  });
1774
1799
  lastResult = result;
1775
1800
  if (startupAttemptIndex === 0) {
@@ -1860,7 +1885,14 @@ async function runSyncCompletionInner(
1860
1885
  attempt.error = startupError;
1861
1886
  break modelAttemptsLoop;
1862
1887
  }
1863
- if (!isRetryableModelFailure(result.error) || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
1888
+ const retryableModelFailure = isRetryableModelFailure(result.error);
1889
+ if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
1890
+ if (isContextOverflow(result.error)) {
1891
+ result.contextOverflow = true;
1892
+ attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
1893
+ break modelAttemptsLoop;
1894
+ }
1895
+ if (!retryableModelFailure || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
1864
1896
  attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
1865
1897
  break;
1866
1898
  }