pi-subagents 0.56.0 → 0.57.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 (83) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +111 -15
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +2 -2
  11. package/docs/observability.md +8 -7
  12. package/docs/tool-reference.md +11 -1
  13. package/docs/workflows.md +20 -0
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +1 -1
  16. package/src/agents/agent-management.ts +13 -5
  17. package/src/agents/agent-refinements.ts +4 -4
  18. package/src/agents/agents.ts +219 -48
  19. package/src/agents/builtin-names.ts +6 -0
  20. package/src/agents/runtime-agent-registry.ts +8 -2
  21. package/src/api/preflight.ts +2 -2
  22. package/src/extension/config.ts +70 -0
  23. package/src/extension/doctor.ts +3 -3
  24. package/src/extension/index.ts +18 -4
  25. package/src/extension/public-execution.ts +29 -13
  26. package/src/extension/rpc.ts +20 -5
  27. package/src/extension/schemas.ts +4 -3
  28. package/src/extension/tool-description.ts +11 -9
  29. package/src/inspectors/herdr/actions.ts +2 -1
  30. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  31. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  32. package/src/integrations/herdr-status.ts +46 -3
  33. package/src/runs/background/active-async-capacity.ts +77 -10
  34. package/src/runs/background/async-execution.ts +100 -9
  35. package/src/runs/background/async-job-tracker.ts +5 -0
  36. package/src/runs/background/async-retention.ts +20 -3
  37. package/src/runs/background/async-status.ts +5 -0
  38. package/src/runs/background/chain-root-attachment.ts +15 -1
  39. package/src/runs/background/fleet-view.ts +16 -10
  40. package/src/runs/background/inspect-rpc.ts +8 -8
  41. package/src/runs/background/result-files.ts +27 -14
  42. package/src/runs/background/result-watcher.ts +9 -5
  43. package/src/runs/background/run-status.ts +33 -6
  44. package/src/runs/background/scheduled-runs.ts +7 -1
  45. package/src/runs/background/subagent-runner.ts +177 -44
  46. package/src/runs/background/wait-completions.ts +4 -0
  47. package/src/runs/foreground/execution.ts +72 -10
  48. package/src/runs/foreground/foreground-control.ts +6 -0
  49. package/src/runs/foreground/foreground-history.ts +22 -1
  50. package/src/runs/foreground/subagent-executor.ts +270 -81
  51. package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
  52. package/src/runs/shared/child-protocol.ts +21 -7
  53. package/src/runs/shared/claude-code-adapter.ts +129 -0
  54. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  55. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  56. package/src/runs/shared/external-cli-contract.ts +167 -0
  57. package/src/runs/shared/external-cli-preflight.ts +122 -0
  58. package/src/runs/shared/external-cli-runner.ts +348 -55
  59. package/src/runs/shared/model-exclusions.ts +57 -7
  60. package/src/runs/shared/model-fallback.ts +23 -1
  61. package/src/runs/shared/mutation-evidence.ts +7 -2
  62. package/src/runs/shared/nested-events.ts +3 -1
  63. package/src/runs/shared/nested-render.ts +2 -2
  64. package/src/runs/shared/process-signal.ts +13 -0
  65. package/src/runs/shared/run-history.ts +21 -1
  66. package/src/shared/fork-context.ts +21 -0
  67. package/src/shared/formatters.ts +7 -1
  68. package/src/shared/pruned-fork.ts +450 -0
  69. package/src/shared/session-file-trust.ts +19 -0
  70. package/src/shared/session-tokens.ts +14 -3
  71. package/src/shared/settings.ts +10 -2
  72. package/src/shared/shortcuts.ts +17 -0
  73. package/src/shared/types.ts +118 -9
  74. package/src/shared/workflow-child-permit.ts +116 -0
  75. package/src/slash/delegation-adapters.ts +0 -1
  76. package/src/slash/slash-commands.ts +8 -6
  77. package/src/tui/fleet-status.ts +27 -10
  78. package/src/tui/fleet-transcript.ts +11 -5
  79. package/src/tui/fleet.ts +28 -13
  80. package/src/tui/render.ts +47 -21
  81. package/src/workflows/scripted-workflow.ts +248 -27
  82. package/src/workflows/workflow-child-summary.ts +117 -0
  83. package/src/workflows/workflow-receipt.ts +119 -4
@@ -14,6 +14,7 @@ import type { AuthorityPolicyConfig } from "../policy/authority.ts";
14
14
  import type { ThinkingLevel } from "./model-info.ts";
15
15
  import type { GlobalMissionIndexRecord, MissionRecord, MissionStoreConfig } from "../missions/types.ts";
16
16
  import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
17
+ import type { WorkflowChildPermitContext } from "./workflow-child-permit.ts";
17
18
 
18
19
  // ============================================================================
19
20
  // Basic Types
@@ -75,6 +76,22 @@ export interface WorkflowGraphSnapshot {
75
76
 
76
77
  export type WorkflowReceiptState = "complete" | "failed" | "paused" | "stopped";
77
78
 
79
+ export interface WorkflowChildSummaryV1 {
80
+ version: 1;
81
+ parentToolCallId: string;
82
+ workflowRunId: string;
83
+ inventoryComplete: boolean;
84
+ workflowState: "queued" | "running" | "completed" | "failed" | "paused" | "stopped";
85
+ children: Array<{
86
+ childId: string;
87
+ runId?: string;
88
+ agent?: string;
89
+ model?: string;
90
+ thinking?: string;
91
+ state: "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "rejected" | "detached";
92
+ }>;
93
+ }
94
+
78
95
  type WorkflowReceiptEntryResumability =
79
96
  | { latestRunId: string; resumability: { state: "resumable" } }
80
97
  | { latestRunId?: string; resumability: { state: "not-resumable"; reason: string } };
@@ -85,6 +102,7 @@ export type WorkflowReceiptEntry = WorkflowReceiptEntryResumability & {
85
102
  requestedContext?: "fresh" | "fork";
86
103
  resolvedContext?: "fresh" | "fork" | "mixed";
87
104
  outputReference?: string;
105
+ externalAdapter?: ExternalCliReceiptMetadata;
88
106
  continuation: { runIds: string[] };
89
107
  };
90
108
 
@@ -94,6 +112,7 @@ export interface WorkflowReceipt {
94
112
  state: WorkflowReceiptState;
95
113
  createdAt: number;
96
114
  entries: Record<string, WorkflowReceiptEntry>;
115
+ workflowChildren?: WorkflowChildSummaryV1;
97
116
  }
98
117
 
99
118
  export interface SavedOutputReference {
@@ -166,6 +185,10 @@ export interface TokenUsage {
166
185
  input: number;
167
186
  output: number;
168
187
  total: number;
188
+ /** Input plus cache-read tokens for the latest assistant turn. */
189
+ window?: number;
190
+ /** Largest window observed in this usage scope. */
191
+ windowPeak?: number;
169
192
  }
170
193
 
171
194
  export type ActivityState = "active_long_running" | "needs_attention";
@@ -703,6 +726,8 @@ export interface AgentProgress {
703
726
  thinking?: string;
704
727
  inputTokens?: number;
705
728
  outputTokens?: number;
729
+ window?: number;
730
+ windowPeak?: number;
706
731
  durationMs: number;
707
732
  error?: string;
708
733
  failedTool?: string;
@@ -1044,6 +1069,8 @@ export interface SingleResult {
1044
1069
  watchdog?: ChildWatchdogProgress;
1045
1070
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1046
1071
  capabilityAudit?: SubagentCapabilityAudit;
1072
+ runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
1073
+ externalProcess?: ExternalProcessStatus;
1047
1074
  }
1048
1075
 
1049
1076
  export interface SpawnBudgetGrant {
@@ -1090,6 +1117,7 @@ export interface WaitCompletion {
1090
1117
  /** Versioned bounded output archive retained with the durable completion replay. */
1091
1118
  archivePath?: string;
1092
1119
  results?: WaitCompletionChild[];
1120
+ workflowChildren?: WorkflowChildSummaryV1;
1093
1121
  }
1094
1122
 
1095
1123
  export interface Details {
@@ -1100,6 +1128,7 @@ export interface Details {
1100
1128
  /** Run-level context summary. "mixed" when children resolved to different modes. */
1101
1129
  context?: "fresh" | "fork" | "mixed";
1102
1130
  results: SingleResult[];
1131
+ workflowChildren?: WorkflowChildSummaryV1;
1103
1132
  /**
1104
1133
  * Terminal completion payloads for runs this subagent_wait call observed
1105
1134
  * finishing. Async completions travel as result files that are consumed and
@@ -1375,9 +1404,11 @@ export type AgentRunnerConfig =
1375
1404
  | { type: "pi" }
1376
1405
  | {
1377
1406
  type: "external-cli";
1407
+ adapter?: "codex-exec" | "codex-exec-writer" | "claude-code" | "claude-code-writer" | "cursor-agent" | "cursor-agent-writer";
1378
1408
  command: string;
1379
1409
  args?: string[];
1380
1410
  promptDelivery?: "stdin";
1411
+ capabilities?: ExternalCliCapabilityNarrowing;
1381
1412
  }
1382
1413
  | {
1383
1414
  type: "external-job";
@@ -1385,18 +1416,46 @@ export type AgentRunnerConfig =
1385
1416
  options?: Record<string, unknown>;
1386
1417
  };
1387
1418
 
1419
+ export type ExternalCliCapabilityNarrowing = Partial<Record<"steer" | "resume" | "structuredOutput" | "toolEvents" | "supervisor" | "forkContext" | "extensionBindings", false>>;
1420
+
1421
+ export interface ExternalCliCapabilities {
1422
+ stop: true;
1423
+ steer: false;
1424
+ resume: false;
1425
+ structuredOutput: false;
1426
+ toolEvents: false;
1427
+ supervisor: "unsupported";
1428
+ forkContext: false;
1429
+ extensionBindings: false;
1430
+ }
1431
+
1432
+ export interface ExternalCliReceiptMetadata {
1433
+ adapter: { id: "external-cli" | "codex-exec" | "codex-exec-writer" | "claude-code" | "claude-code-writer" | "cursor-agent" | "cursor-agent-writer" | "grok-build"; version: 1; executionMode: "one-shot-stdin" | "one-shot-prompt-file" };
1434
+ capabilities: ExternalCliCapabilities;
1435
+ safety?:
1436
+ | { sandbox: "read-only"; approvalPolicy: "never"; ephemeral: true }
1437
+ | { access: "workspace-write"; sandbox: "workspace-write"; approvalPolicy: "never"; ephemeral: true }
1438
+ | { permissionMode: "plan"; tools: "none"; mcp: "empty-strict"; settingSources: "none"; sessionPersistence: false }
1439
+ | { access: "read-only"; authentication: "existing-cli-required"; permissionMode: "plan"; tools: "none"; mcp: "empty-strict"; settingSources: "user"; userSettingsTrust: "required"; sessionPersistence: false }
1440
+ | { access: "workspace-write"; authentication: "existing-cli-required"; permissionMode: "acceptEdits"; tools: "Read,Write,Edit,Glob,Grep"; mcp: "empty-strict"; settingSources: "user"; userSettingsTrust: "required"; sessionPersistence: false }
1441
+ | { access: "read-only"; authentication: "cursor-api-key-or-existing-login"; mode: "ask"; sandbox: "enabled"; workspaceTrust: "existing-required"; sessionReuse: false }
1442
+ | { access: "workspace-write"; authentication: "cursor-api-key-or-existing-login"; mode: "print"; sandbox: "enabled"; workspaceTrust: "existing-required"; sessionReuse: false };
1443
+ outputArtifacts?: { stdoutPath?: string; stderrPath?: string; finalOutputPath?: string };
1444
+ handoff: { mode: "fresh" };
1445
+ supervisor: { mode: "unsupported"; reason: string };
1446
+ nonResumableReason: string;
1447
+ }
1448
+
1388
1449
  export interface ExternalCliRunnerStatus {
1389
1450
  type: "external-cli";
1390
1451
  command: string;
1391
1452
  args: string[];
1392
- promptDelivery: "stdin";
1393
- capabilities: {
1394
- stop: true;
1395
- steer: false;
1396
- resume: false;
1397
- structuredOutput: false;
1398
- toolEvents: false;
1399
- };
1453
+ promptDelivery: "stdin" | "prompt-file";
1454
+ adapter: ExternalCliReceiptMetadata["adapter"];
1455
+ safety?: ExternalCliReceiptMetadata["safety"];
1456
+ capabilities: ExternalCliCapabilities;
1457
+ unsupportedReasons: Record<Exclude<keyof ExternalCliCapabilities, "stop">, string>;
1458
+ nonResumableReason: string;
1400
1459
  }
1401
1460
 
1402
1461
  export interface ExternalJobRunnerStatus {
@@ -1443,6 +1502,11 @@ export interface ExternalProcessStatus {
1443
1502
  processSignal?: string | null;
1444
1503
  stdoutPath: string;
1445
1504
  stderrPath: string;
1505
+ finalOutputPath?: string;
1506
+ stdoutBytes?: number;
1507
+ stderrBytes?: number;
1508
+ stdoutTruncated?: boolean;
1509
+ stderrTruncated?: boolean;
1446
1510
  }
1447
1511
 
1448
1512
  export interface AsyncStatus {
@@ -1499,6 +1563,7 @@ export interface AsyncStatus {
1499
1563
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1500
1564
  capabilityAudit?: SubagentCapabilityAudit;
1501
1565
  workflow?: Details["workflow"];
1566
+ workflowChildren?: WorkflowChildSummaryV1;
1502
1567
  parentWorkflowRunId?: string;
1503
1568
  workflowKey?: string;
1504
1569
  /** Set when a durable schedule launched this run, so completions can name their origin. */
@@ -1654,6 +1719,7 @@ export interface AsyncJobState {
1654
1719
  parentWorkflowRunId?: string;
1655
1720
  workflowKey?: string;
1656
1721
  workflow?: Details["workflow"];
1722
+ workflowChildren?: WorkflowChildSummaryV1;
1657
1723
  }
1658
1724
 
1659
1725
  export interface ForegroundResumeChild {
@@ -1671,6 +1737,8 @@ export interface ForegroundResumeChild {
1671
1737
  currentPath?: string;
1672
1738
  turnCount?: number;
1673
1739
  tokens?: number;
1740
+ window?: number;
1741
+ windowPeak?: number;
1674
1742
  toolCount?: number;
1675
1743
  exitCode?: number;
1676
1744
  error?: string;
@@ -1685,6 +1753,14 @@ export interface ForegroundResumeChild {
1685
1753
  detachedReason?: string;
1686
1754
  acceptance?: AcceptanceLedger;
1687
1755
  agentContract?: AgentContract;
1756
+ /** Private bounded launch fields needed to preserve the child contract on resume. */
1757
+ resumeContract?: {
1758
+ outputSchema?: JsonSchemaObject;
1759
+ agentContract?: AgentContract;
1760
+ acceptance?: AcceptanceInput;
1761
+ output?: string | boolean;
1762
+ outputMode?: OutputMode;
1763
+ };
1688
1764
  launchContractDigest?: string;
1689
1765
  /** Private retained launch authority. Never project into status or result output. */
1690
1766
  extensionBindings?: ExtensionBindings;
@@ -1723,6 +1799,8 @@ export interface ForegroundChildControl {
1723
1799
  tokens?: number;
1724
1800
  inputTokens?: number;
1725
1801
  outputTokens?: number;
1802
+ window?: number;
1803
+ windowPeak?: number;
1726
1804
  model?: string;
1727
1805
  thinking?: string;
1728
1806
  toolCount?: number;
@@ -1758,6 +1836,8 @@ export interface ForegroundRunControl {
1758
1836
  tokens?: number;
1759
1837
  inputTokens?: number;
1760
1838
  outputTokens?: number;
1839
+ window?: number;
1840
+ windowPeak?: number;
1761
1841
  model?: string;
1762
1842
  thinking?: string;
1763
1843
  toolCount?: number;
@@ -1808,6 +1888,8 @@ export interface SubagentState {
1808
1888
  parentSessionFile?: string | null;
1809
1889
  /** Extension-owned roots trusted for child session transcript reads. */
1810
1890
  trustedSessionRoots?: string[];
1891
+ /** Pi sessions base that caps exact runtime-recorded session file trust. */
1892
+ trustedSessionFileRoot?: string;
1811
1893
  /** Live async session roots created by this parent executor, keyed by run id. */
1812
1894
  liveAsyncSessionRoots?: Map<string, string>;
1813
1895
  /** Last valid parent session model observed for this session; used when continuation contexts omit ctx.model. */
@@ -1932,6 +2014,8 @@ export interface SubagentChildStatusEvent {
1932
2014
  // ============================================================================
1933
2015
 
1934
2016
  export interface RunSyncOptions {
2017
+ /** Exact discovery provenance for an unknown-agent error; omission uses defensive fallback discovery. */
2018
+ unknownAgentDiagnosticContext?: import("../agents/agents.ts").UnknownAgentDiagnosticContext;
1935
2019
  /** Opt-in global permission rules; missing tools remain allowed. */
1936
2020
  permissions?: import("../runs/shared/permissions.ts").PermissionConfig;
1937
2021
  /** Session id of the direct parent session for permission-system ask forwarding. */
@@ -1998,6 +2082,8 @@ export interface RunSyncOptions {
1998
2082
  thinkingOverride?: AgentConfig["thinking"];
1999
2083
  thinkingCeiling?: ThinkingLevel;
2000
2084
  extensionBindings?: ExtensionBindings;
2085
+ /** Package-internal one-use authorization for one foreground workflow child. */
2086
+ workflowChildPermitLaunch?: WorkflowChildPermitContext;
2001
2087
  /** Registry models available for heuristic bare-model resolution */
2002
2088
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
2003
2089
  /** Current parent-session provider to prefer for ambiguous bare model ids */
@@ -2063,6 +2149,11 @@ export interface ScheduledRunsConfig {
2063
2149
  storeRoot?: string;
2064
2150
  }
2065
2151
 
2152
+ export interface ModelExclusionsConfig {
2153
+ /** Default duration in milliseconds. A lower configured value also shortens active cached exclusions. */
2154
+ defaultTtlMs?: number;
2155
+ }
2156
+
2066
2157
  export type FleetViewPlacement = "aboveEditor" | "belowEditor";
2067
2158
 
2068
2159
  export const FLEET_KEYBINDING_ACTIONS = [
@@ -2097,10 +2188,24 @@ export interface MainWindowRendererConfig {
2097
2188
  compactResultMaxLines?: number;
2098
2189
  }
2099
2190
 
2191
+ export interface ForkContextConfig {
2192
+ /** Keep the complete fork by default, or summarize large text-only tool results before launch. */
2193
+ mode?: "full" | "pruned";
2194
+ /** Required pruning model when mode is "pruned". */
2195
+ model?: string;
2196
+ }
2197
+
2198
+ export interface ActiveAsyncCapacityConfig {
2199
+ /** Reclaim failed runner slots after this age when process proof is unknown; false keeps strict retention. */
2200
+ abandonedSlotReleaseAfterMs?: number | false;
2201
+ }
2202
+
2100
2203
  export interface ExtensionConfig {
2101
2204
  asyncByDefault?: boolean;
2102
2205
  /** Set the context for launches that omit an explicit context. */
2103
2206
  defaultSubagentContext?: "fresh" | "fork";
2207
+ /** Configure how every resolved fork session is prepared before child spawn. */
2208
+ forkContext?: ForkContextConfig;
2104
2209
  /** Optional shortcut that detaches the active foreground single-subagent run. */
2105
2210
  foregroundDetachShortcut?: string;
2106
2211
  /** Show the Claude Code-style navigable fleet. Defaults to true. */
@@ -2111,6 +2216,8 @@ export interface ExtensionConfig {
2111
2216
  fleetKeybindings?: FleetKeybindingsConfig;
2112
2217
  /** Show the under-editor async runs widget. Defaults to true, including when FleetView is enabled. */
2113
2218
  asyncWidget?: boolean;
2219
+ /** Configure the process-wide TTL policy for persisted model exclusions. */
2220
+ modelExclusions?: ModelExclusionsConfig;
2114
2221
  /** Tool description variant registered for the parent-facing subagent tool. Defaults to split metadata. */
2115
2222
  toolDescriptionMode?: ToolDescriptionMode;
2116
2223
  /** Inline chat rendering for the subagent tool. Defaults to rich. */
@@ -2130,6 +2237,8 @@ export interface ExtensionConfig {
2130
2237
  maxSubagentSpawnsPerRun?: number;
2131
2238
  /** Optional active top-level async run cap per parent session. Unset or 0 means unlimited. */
2132
2239
  maxActiveAsyncRunsPerSession?: number;
2240
+ /** Active-capacity cleanup policy. */
2241
+ capacity?: ActiveAsyncCapacityConfig;
2133
2242
  /** Global cap on simultaneously-running subagent tasks within a single run. Defaults to 20. */
2134
2243
  globalConcurrencyLimit?: number;
2135
2244
  /**
@@ -2285,7 +2394,7 @@ export const POLL_INTERVAL_MS = 250;
2285
2394
  export const WIDGET_ANIMATION_INTERVAL_MS = 1000;
2286
2395
  export const MAX_WIDGET_JOBS = 4;
2287
2396
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
2288
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2397
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "validate", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
2289
2398
 
2290
2399
  export const DEFAULT_FORK_PREAMBLE =
2291
2400
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -0,0 +1,116 @@
1
+ import { stableJsonDigest } from "./launch-contract.ts";
2
+
3
+ export interface WorkflowChildPermitInput {
4
+ issuerPackage: string;
5
+ workflowRunId: string;
6
+ childKey: string;
7
+ agent: string;
8
+ launchContractDigest: string;
9
+ context: "fresh" | "fork";
10
+ }
11
+
12
+ export interface WorkflowChildPermitLaunch {
13
+ workflowRunId: string;
14
+ childKey: string;
15
+ agent: string;
16
+ launchContractDigest: string;
17
+ context: "fresh" | "fork";
18
+ runner: "pi";
19
+ }
20
+
21
+ export interface WorkflowChildPermitContext {
22
+ permit: WorkflowChildPermit;
23
+ workflowRunId: string;
24
+ childKey: string;
25
+ }
26
+
27
+ export interface WorkflowChildPermit {
28
+ readonly __workflowChildPermit: unique symbol;
29
+ }
30
+
31
+ interface WorkflowChildPermitRecord {
32
+ issuerPackage: string;
33
+ workflowRunId: string;
34
+ childKey: string;
35
+ agent: string;
36
+ expectedProjectionDigest: string;
37
+ state: "available" | "claimed" | "consumed";
38
+ }
39
+
40
+ const records = new WeakMap<object, WorkflowChildPermitRecord>();
41
+
42
+ function projectionDigest(input: Omit<WorkflowChildPermitLaunch, "workflowRunId">): string {
43
+ return stableJsonDigest({
44
+ version: 1,
45
+ childKey: input.childKey,
46
+ agent: input.agent,
47
+ launchContractDigest: input.launchContractDigest,
48
+ context: input.context,
49
+ runner: input.runner,
50
+ });
51
+ }
52
+
53
+ function required(value: string, label: string): string {
54
+ if (!value.trim() || value !== value.trim()) throw new Error(`${label} must be a non-empty trimmed string.`);
55
+ return value;
56
+ }
57
+
58
+ /** Package-internal first-slice permit. It is opaque, in-memory, and not serializable. */
59
+ export function createWorkflowChildPermit(input: WorkflowChildPermitInput): WorkflowChildPermit {
60
+ const permit = Object.freeze(Object.create(null)) as WorkflowChildPermit;
61
+ const record: WorkflowChildPermitRecord = {
62
+ issuerPackage: required(input.issuerPackage, "issuerPackage"),
63
+ workflowRunId: required(input.workflowRunId, "workflowRunId"),
64
+ childKey: required(input.childKey, "childKey"),
65
+ agent: required(input.agent, "agent"),
66
+ expectedProjectionDigest: projectionDigest({
67
+ childKey: input.childKey,
68
+ agent: input.agent,
69
+ launchContractDigest: required(input.launchContractDigest, "launchContractDigest"),
70
+ context: input.context,
71
+ runner: "pi",
72
+ }),
73
+ state: "available",
74
+ };
75
+ records.set(permit as object, record);
76
+ return permit;
77
+ }
78
+
79
+ export function validateWorkflowChildPermitRoot(permit: WorkflowChildPermit, workflowRunId: string): string | undefined {
80
+ const record = records.get(permit as object);
81
+ if (!record) return "Workflow child permit is invalid.";
82
+ if (record.state !== "available") return "Workflow child permit is already consumed.";
83
+ if (record.workflowRunId !== workflowRunId) return "Workflow child permit does not match this workflow root.";
84
+ return undefined;
85
+ }
86
+
87
+ /** Claim the first distinct launch attempt before validating its model-authored shape. */
88
+ export function claimWorkflowChildPermit(permit: WorkflowChildPermit, workflowRunId: string, childKey: string): string | undefined {
89
+ const record = records.get(permit as object);
90
+ if (!record) return "Workflow child permit is invalid.";
91
+ if (record.state !== "available") return "Workflow child permit is already consumed.";
92
+ if (record.workflowRunId !== workflowRunId) return "Workflow child permit does not match this workflow root.";
93
+ record.state = record.childKey === childKey ? "claimed" : "consumed";
94
+ if (record.childKey !== childKey) return "Workflow child permit child key mismatch.";
95
+ return undefined;
96
+ }
97
+
98
+ /** Verify and permanently consume the permit before the one native process spawn. */
99
+ export function consumeWorkflowChildPermit(permit: WorkflowChildPermit, launch: WorkflowChildPermitLaunch): string | undefined {
100
+ const record = records.get(permit as object);
101
+ if (!record) return "Workflow child permit is invalid.";
102
+ if (record.state === "available") return "Workflow child permit launch was not claimed.";
103
+ if (record.state === "consumed") return "Workflow child permit is already consumed.";
104
+ if (record.workflowRunId !== launch.workflowRunId) return "Workflow child permit does not match this workflow root.";
105
+ record.state = "consumed";
106
+ if (record.childKey !== launch.childKey) return "Workflow child permit child key mismatch.";
107
+ if (record.agent !== launch.agent) return "Workflow child permit agent mismatch.";
108
+ if (launch.runner !== "pi") return "Workflow child permit supports native Pi children only.";
109
+ if (record.expectedProjectionDigest !== projectionDigest(launch)) return "Workflow child permit does not match the final launch projection.";
110
+ return undefined;
111
+ }
112
+
113
+ export function workflowChildPermitConsumed(permit: WorkflowChildPermit): boolean {
114
+ const state = records.get(permit as object)?.state;
115
+ return state === "claimed" || state === "consumed";
116
+ }
@@ -298,7 +298,6 @@ export function toSubagentDelegationExecutionParams(request: SubagentDelegationR
298
298
  enforceHardTurnLimit: true,
299
299
  toolBudget: request.toolBudget,
300
300
  skill: request.skill,
301
- output: false,
302
301
  ...(request.result.kind === "structured" ? { outputSchema: request.result.schema } : {}),
303
302
  acceptance: false,
304
303
  artifacts: request.artifacts,
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { Key, matchesKey, truncateToWidth, type Component, type KeyId, type TUI } from "@earendil-works/pi-tui";
6
- import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope } from "../agents/agents.ts";
6
+ import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope, type UnknownAgentDiagnosticContext } from "../agents/agents.ts";
7
7
  import { listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
8
8
  import { resolveExistingReadPaths } from "../shared/settings.ts";
9
9
  import {
@@ -22,6 +22,7 @@ import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from
22
22
  import { encodeInspectReply, handleInspectRpcArgs, INSPECT_WIDGET_KEY } from "../runs/background/inspect-rpc.ts";
23
23
  import { listScheduledRunSummaries } from "../runs/background/scheduled-runs.ts";
24
24
  import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
25
+ import { FLEET_OPEN_SHORTCUT } from "../shared/shortcuts.ts";
25
26
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
26
27
  import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
27
28
  import { openSubagentsAdmin } from "./subagents-admin.ts";
@@ -108,9 +109,9 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
108
109
  return { args, bg, fork };
109
110
  };
110
111
 
111
- function discoverSlashAgents(pi: ExtensionAPI, cwd: string, scope: AgentScope): { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[] } {
112
+ function discoverSlashAgents(pi: ExtensionAPI, cwd: string, scope: AgentScope): { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; unknownAgentDiagnosticContext: UnknownAgentDiagnosticContext } {
112
113
  const discovered = discoverAgents(cwd, scope);
113
- if (listRuntimeAgentConfigs(pi).length === 0) return discovered;
114
+ if (listRuntimeAgentConfigs(pi).length === 0) return { ...discovered, unknownAgentDiagnosticContext: unknownAgentDiagnosticContext(discovered) };
114
115
  const all = discoverAgentsAll(cwd);
115
116
  const configuredAgents: AgentConfig[] = [
116
117
  ...all.builtin,
@@ -118,7 +119,8 @@ function discoverSlashAgents(pi: ExtensionAPI, cwd: string, scope: AgentScope):
118
119
  ...(scope !== "project" ? all.user : []),
119
120
  ...(scope !== "user" ? all.project : []),
120
121
  ];
121
- return mergeRuntimeAgents(pi, discovered, configuredAgents);
122
+ const merged = mergeRuntimeAgents(pi, discovered, configuredAgents);
123
+ return { ...merged, unknownAgentDiagnosticContext: { ...unknownAgentDiagnosticContext(discovered), agents: merged.agents } };
122
124
  }
123
125
 
124
126
  const makeAgentCompletions = (pi: ExtensionAPI, state: SubagentState) => (prefix: string) => {
@@ -693,7 +695,7 @@ export function registerSlashCommands(
693
695
  : resolvedAgent.agent;
694
696
  const diagnostic = findBlockingAgentDiagnostic(agentName, candidates, discovered.agentDiagnostics);
695
697
  if (diagnostic || resolvedAgent.error || !resolvedAgent.agent) {
696
- ctx.ui.notify(diagnostic ? `Agent '${agentName}' has invalid configuration: ${diagnostic.error}` : resolvedAgent.error ?? `Unknown agent: ${agentName}`, "error");
698
+ ctx.ui.notify(diagnostic ? `Agent '${agentName}' has invalid configuration: ${diagnostic.error}` : resolvedAgent.error ?? formatUnknownAgentError(agentName, discovered.unknownAgentDiagnosticContext), "error");
697
699
  return;
698
700
  }
699
701
 
@@ -776,7 +778,7 @@ export function registerSlashCommands(
776
778
  handler: async (_args, ctx) => showFleet(ctx),
777
779
  });
778
780
 
779
- pi.registerShortcut(Key.ctrlAlt("f"), {
781
+ pi.registerShortcut(FLEET_OPEN_SHORTCUT, {
780
782
  description: "Open subagent fleet inspector",
781
783
  handler: async (ctx) => showFleet(ctx),
782
784
  });
@@ -25,6 +25,7 @@ type FleetStatusEntry = {
25
25
  description?: string;
26
26
  startedAt: number;
27
27
  tokens: number;
28
+ window?: number;
28
29
  state: string;
29
30
  external?: true;
30
31
  projectPane?: HerdrProjectPaneSnapshot;
@@ -39,6 +40,7 @@ type FleetWorkflowRow = {
39
40
  activity?: string;
40
41
  startedAt?: number;
41
42
  tokens?: number;
43
+ window?: number;
42
44
  overflow?: number;
43
45
  };
44
46
 
@@ -72,12 +74,15 @@ export function formatFleetElapsed(ms: number): string {
72
74
  return `${Math.max(0, Math.round(ms / 1000))}s`;
73
75
  }
74
76
 
75
- export function formatFleetTokens(count: number): string {
76
- let compact: string;
77
- if (count >= 1_000_000) compact = `${(count / 1_000_000).toFixed(1)}M`;
78
- else if (count >= 1_000) compact = `${(count / 1_000).toFixed(1)}k`;
79
- else compact = `${Math.max(0, Math.round(count))}`;
80
- return `↓ ${compact} tokens`;
77
+ export function formatFleetTokens(count: number, window?: number): string {
78
+ const compact = (value: number): string => value >= 1_000_000
79
+ ? `${(value / 1_000_000).toFixed(1)}M`
80
+ : value >= 1_000
81
+ ? `${(value / 1_000).toFixed(1)}k`
82
+ : `${Math.max(0, Math.round(value))}`;
83
+ return window !== undefined
84
+ ? `↓ ${compact(window)} window · ${compact(count)} spent`
85
+ : `↓ ${compact(count)} tokens`;
81
86
  }
82
87
 
83
88
  function rightAlign(left: string, right: string, width: number): string {
@@ -134,6 +139,7 @@ function workflowFleetRows(steps: AsyncJobStep[] | undefined): FleetWorkflowRow[
134
139
  ...(activity ? { activity } : {}),
135
140
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
136
141
  ...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
142
+ ...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
137
143
  };
138
144
  });
139
145
  }
@@ -332,7 +338,8 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
332
338
  ...(modelThinking ? { modelThinking } : {}),
333
339
  description: foregroundDescription(control, child.description),
334
340
  startedAt: child.startedAt,
335
- tokens: child.tokens ?? 0,
341
+ tokens: child.tokens ?? 0,
342
+ ...(child.window !== undefined ? { window: child.window } : {}),
336
343
  state: "running",
337
344
  ...((control.nestedChildren?.filter((nested) => nested.parentStepIndex === child.index).length ?? 0) > 0
338
345
  ? { nestedChildren: control.nestedChildren!.filter((nested) => nested.parentStepIndex === child.index) }
@@ -350,6 +357,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
350
357
  description: foregroundDescription(control, control.description),
351
358
  startedAt: control.startedAt,
352
359
  tokens: control.tokens ?? 0,
360
+ ...(control.window !== undefined ? { window: control.window } : {}),
353
361
  state: "running",
354
362
  ...(control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
355
363
  });
@@ -367,6 +375,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
367
375
  description: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
368
376
  startedAt,
369
377
  tokens: job.totalTokens?.total ?? 0,
378
+ ...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
370
379
  state: job.status,
371
380
  ...(job.steps?.length ? { workflowRows: workflowFleetRows(job.steps) } : {}),
372
381
  ...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
@@ -387,6 +396,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
387
396
  description: job.description,
388
397
  startedAt,
389
398
  tokens: job.totalTokens?.total ?? 0,
399
+ ...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
390
400
  state: job.status,
391
401
  ...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
392
402
  });
@@ -404,6 +414,9 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
404
414
  description: step.description ?? job.description,
405
415
  startedAt: step.startedAt ?? startedAt,
406
416
  tokens: step.tokens?.total ?? (steps.length === 1 ? job.totalTokens?.total ?? 0 : 0),
417
+ ...((step.tokens?.window ?? (steps.length === 1 ? job.totalTokens?.window : undefined)) !== undefined
418
+ ? { window: step.tokens?.window ?? job.totalTokens?.window }
419
+ : {}),
407
420
  state: step.status,
408
421
  ...((step.children?.length ?? 0) > 0
409
422
  ? { nestedChildren: step.children }
@@ -611,6 +624,10 @@ export class SubagentFleetStatus {
611
624
  const workEntries = this.entries.filter((entry) => !entry.surface);
612
625
  const projectEntries = this.entries.filter((entry) => entry.surface === "project-pane");
613
626
  const tokens = workEntries.reduce((total, entry) => total + entry.tokens, 0);
627
+ const nativeEntries = workEntries.filter((entry) => !entry.external);
628
+ const window = nativeEntries.length > 0 && nativeEntries.every((entry) => entry.window !== undefined)
629
+ ? nativeEntries.reduce((total, entry) => total + entry.window!, 0)
630
+ : undefined;
614
631
  const capacity = this.state.activeAsyncCapacity;
615
632
  const hasNativeRows = workEntries.some((entry) => !entry.external);
616
633
  const showNativeSummary = hasNativeRows || Boolean(capacity?.used);
@@ -621,7 +638,7 @@ export class SubagentFleetStatus {
621
638
  const paneAttention = projectEntries.filter((entry) => entry.projectPane && projectPaneNeedsAttention(entry.projectPane)).length;
622
639
  const panes = projectEntries.length > 0 ? `${projectEntries.length} pane${projectEntries.length === 1 ? "" : "s"}${paneAttention ? ` (${paneAttention} ⚠)` : ""}` : "";
623
640
  const label = [agents, asyncRuns, panes].filter(Boolean).join(" · ");
624
- const detail = [showNativeSummary ? formatFleetTokens(tokens) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
641
+ const detail = [showNativeSummary ? formatFleetTokens(tokens, window) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
625
642
  return [truncateToWidth(` ${theme.fg("muted", label)}${label && detail ? " · " : ""}${theme.fg("dim", detail)}`, width)];
626
643
  }
627
644
  const roster = this.rosterKeys();
@@ -670,7 +687,7 @@ export class SubagentFleetStatus {
670
687
  const elapsed = Date.now() - entry.startedAt;
671
688
  const rightText = entry.projectPane
672
689
  ? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
673
- : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`;
690
+ : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens, entry.window)}`;
674
691
  const right = theme.fg("dim", rightText);
675
692
  return rightAlign(left, right, width);
676
693
  }
@@ -695,7 +712,7 @@ export class SubagentFleetStatus {
695
712
  const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
696
713
  const details = [
697
714
  row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
698
- row.tokens !== undefined ? formatFleetTokens(row.tokens) : undefined,
715
+ row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
699
716
  ].filter(Boolean).join(" · ");
700
717
  return truncateToWidth(`${left}${details ? theme.fg("dim", ` · ${details}`) : ""}`, width);
701
718
  }