pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -5,8 +5,8 @@
5
5
  * - Sync (default): Streams output, renders markdown, tracks usage
6
6
  * - Async: Background execution, emits events when done
7
7
  *
8
- * Modes: single (agent + task), parallel (tasks[]), chain (chain[] with {previous})
9
- * Toggle: async parameter (default: false, configurable via config.json)
8
+ * Public execution modes: single (agent + task) and workflow (workflowScript)
9
+ * Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
12
12
  * { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
@@ -24,11 +24,10 @@ import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
24
24
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
25
25
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
26
26
  import { cleanupOldChainDirs } from "../shared/settings.ts";
27
- import { clearLegacyResultAnimationTimer, renderSubagentResult } from "../tui/render.ts";
27
+ import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
28
28
  import { openSubagentFleet } from "../tui/fleet.ts";
29
29
  import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-status.ts";
30
30
  import { SubagentParams } from "./schemas.ts";
31
- import { validateChainInput } from "./chain-validation.ts";
32
31
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
33
32
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
34
33
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
@@ -38,25 +37,27 @@ import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template
38
37
  import { registerMainWatchdog } from "../watchdog/register-main.ts";
39
38
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
40
39
  import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
40
+ import { registerHerdrStatusBridge } from "../integrations/herdr-status.ts";
41
41
  import { registerSubagentRpcBridge } from "./rpc.ts";
42
42
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
43
43
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
44
44
  import { resolveWaitToolConfig } from "../runs/background/subagent-wait.ts";
45
45
  import { registerWaitTool } from "../runs/background/wait-tool.ts";
46
+ import { createWaitSubscriptionManager } from "../runs/background/wait-subscriptions.ts";
46
47
  import { drainOutstandingWork } from "../runs/background/auto-drain.ts";
47
48
  import registerSubagentNotify, { parseSubagentNotifyContent, type SubagentNotifyDetails } from "../runs/background/notify.ts";
48
49
  import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_MESSAGE_TYPE, type SubagentSteeringMessageDetails } from "./steering-notices.ts";
49
50
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
50
51
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
51
52
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
52
- import { loadConfig } from "./config.ts";
53
+ import { loadConfig, resolveAsyncByDefault } from "./config.ts";
53
54
  import { buildSubagentToolDescription } from "./tool-description.ts";
55
+ import { syncMissionFromAsyncCompletion } from "../missions/lifecycle.ts";
54
56
  import {
55
57
  type Details,
56
58
  type SubagentState,
57
- ASYNC_DIR,
59
+ DIRS,
58
60
  DEFAULT_ARTIFACT_CONFIG,
59
- RESULTS_DIR,
60
61
  SLASH_RESULT_TYPE,
61
62
  SLASH_TEXT_RESULT_TYPE,
62
63
  SUBAGENT_ASYNC_COMPLETE_EVENT,
@@ -67,14 +68,146 @@ import {
67
68
  resolveMaxSubagentSpawnsPerSession,
68
69
  } from "../shared/types.ts";
69
70
  import {
70
- clearPendingForegroundControlNotices,
71
71
  formatSubagentControlNotice,
72
72
  handleSubagentControlNotice,
73
73
  SUBAGENT_CONTROL_MESSAGE_TYPE,
74
74
  type SubagentControlMessageDetails,
75
75
  } from "./control-notices.ts";
76
76
 
77
- export { loadConfig } from "./config.ts";
77
+ export { loadConfig, resolveAsyncByDefault } from "./config.ts";
78
+
79
+ function workflowLaneKeys(script: string): string[] {
80
+ const keys: string[] = [];
81
+ const seen = new Set<string>();
82
+ const add = (key: string): void => {
83
+ if (!seen.has(key)) {
84
+ seen.add(key);
85
+ keys.push(key);
86
+ }
87
+ };
88
+ const isIdentifier = (char: string | undefined): boolean => char !== undefined && /[\w$]/.test(char);
89
+ const skipTrivia = (start: number): number => {
90
+ let index = start;
91
+ while (index < script.length) {
92
+ if (/\s/.test(script[index]!)) index += 1;
93
+ else if (script.startsWith("//", index)) {
94
+ const end = script.indexOf("\n", index + 2);
95
+ index = end === -1 ? script.length : end + 1;
96
+ } else if (script.startsWith("/*", index)) {
97
+ const end = script.indexOf("*/", index + 2);
98
+ index = end === -1 ? script.length : end + 2;
99
+ } else break;
100
+ }
101
+ return index;
102
+ };
103
+ const readLiteral = (start: number): { key?: string; end: number } | undefined => {
104
+ const quote = script[start];
105
+ if (quote !== "'" && quote !== '"' && quote !== "`") return undefined;
106
+ let index = start + 1;
107
+ let dynamicTemplate = false;
108
+ while (index < script.length) {
109
+ if (script[index] === "\\") {
110
+ index += 2;
111
+ continue;
112
+ }
113
+ if (quote === "`" && script.startsWith("${", index)) dynamicTemplate = true;
114
+ if (script[index] === quote) return { key: dynamicTemplate ? undefined : script.slice(start + 1, index), end: index + 1 };
115
+ if (quote !== "`" && /[\r\n]/.test(script[index]!)) return { end: index + 1 };
116
+ index += 1;
117
+ }
118
+ return { end: script.length };
119
+ };
120
+
121
+ const collectRunsAllKeys = (start: number): number => {
122
+ let index = skipTrivia(start);
123
+ if (script[index] !== "(") return start;
124
+ index = skipTrivia(index + 1);
125
+ if (script[index] !== "[") return start;
126
+ let arrayDepth = 1;
127
+ let objectDepth = 0;
128
+ let directChildObject = false;
129
+ let expectingElement = true;
130
+ for (index += 1; index < script.length; index += 1) {
131
+ index = skipTrivia(index);
132
+ const literal = readLiteral(index);
133
+ if (literal) {
134
+ index = literal.end - 1;
135
+ continue;
136
+ }
137
+ if (script[index] === "[") {
138
+ arrayDepth += 1;
139
+ expectingElement = false;
140
+ continue;
141
+ }
142
+ if (script[index] === "]") {
143
+ arrayDepth -= 1;
144
+ if (arrayDepth === 0) return index + 1;
145
+ continue;
146
+ }
147
+ if (script[index] === "{") {
148
+ objectDepth += 1;
149
+ if (objectDepth === 1) directChildObject = arrayDepth === 1 && expectingElement;
150
+ expectingElement = false;
151
+ continue;
152
+ }
153
+ if (script[index] === "}") {
154
+ objectDepth -= 1;
155
+ if (objectDepth === 0) directChildObject = false;
156
+ continue;
157
+ }
158
+ if (script[index] === "," && arrayDepth === 1 && objectDepth === 0) {
159
+ expectingElement = true;
160
+ continue;
161
+ }
162
+ if (directChildObject && objectDepth === 1 && !isIdentifier(script[index - 1]) && script.startsWith("key", index) && !isIdentifier(script[index + 3])) {
163
+ const colon = skipTrivia(index + 3);
164
+ const key = script[colon] === ":" ? readLiteral(skipTrivia(colon + 1)) : undefined;
165
+ if (key) {
166
+ const next = skipTrivia(key.end);
167
+ if (key.key !== undefined && (script[next] === "," || script[next] === "}")) add(key.key);
168
+ index = key.end - 1;
169
+ }
170
+ }
171
+ }
172
+ return index;
173
+ };
174
+
175
+ for (let index = 0; index < script.length;) {
176
+ index = skipTrivia(index);
177
+ const literal = readLiteral(index);
178
+ if (literal) {
179
+ index = literal.end;
180
+ continue;
181
+ }
182
+ if (!isIdentifier(script[index - 1]) && script.startsWith("runs.run", index) && !isIdentifier(script[index + 8])) {
183
+ const open = skipTrivia(index + 8);
184
+ const key = script[open] === "(" ? readLiteral(skipTrivia(open + 1)) : undefined;
185
+ if (key) {
186
+ const next = skipTrivia(key.end);
187
+ if (key.key !== undefined && (script[next] === "," || script[next] === ")")) add(key.key);
188
+ index = key.end;
189
+ continue;
190
+ }
191
+ }
192
+ if (!isIdentifier(script[index - 1]) && script.startsWith("runs.all", index) && !isIdentifier(script[index + 8])) {
193
+ index = collectRunsAllKeys(index + 8);
194
+ continue;
195
+ }
196
+ index += 1;
197
+ }
198
+ return keys;
199
+ }
200
+
201
+ function formatWorkflowManifest(script: string, async: unknown, clarify: unknown): string {
202
+ if (clarify === true) return "workflow script · rejected: clarify UI unsupported";
203
+ const keys = workflowLaneKeys(script);
204
+ // The workflow executor starts background work unless callers pass async:false.
205
+ const mode = async === false ? "foreground" : "background";
206
+ if (keys.length === 0) return `workflow script · ${mode}`;
207
+ const visibleKeys = keys.slice(0, 4).join(", ");
208
+ const remainder = keys.length > 4 ? `, +${keys.length - 4}` : "";
209
+ return `workflow · ${mode} · ${keys.length} lane${keys.length === 1 ? "" : "s"}: ${visibleKeys}${remainder}`;
210
+ }
78
211
 
79
212
  /**
80
213
  * Derive subagent session base directory from parent session file.
@@ -188,16 +321,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
188
321
  }
189
322
  }
190
323
 
191
- ensureAccessibleDir(RESULTS_DIR);
192
- ensureAccessibleDir(ASYNC_DIR);
324
+ DIRS.results = ensureAccessibleDir(DIRS.results);
325
+ DIRS.async = ensureAccessibleDir(DIRS.async);
193
326
  cleanupOldChainDirs();
194
327
 
195
328
  const config = loadConfig();
196
329
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
197
- const asyncByDefault = config.asyncByDefault === true;
330
+ const asyncByDefault = resolveAsyncByDefault(config);
198
331
  const fleetViewEnabled = config.fleetView !== false;
199
332
  const fleetViewPlacement = resolveFleetViewPlacement(config.fleetViewPlacement);
200
- const asyncWidgetEnabled = config.asyncWidget === true || (!fleetViewEnabled && config.asyncWidget !== false);
333
+ const asyncWidgetEnabled = config.asyncWidget !== false;
334
+ const summaryInlineToolDisplay = config.inlineToolDisplay === "summary";
201
335
  const tempArtifactsDir = getArtifactsDir(null);
202
336
  cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
203
337
 
@@ -205,6 +339,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
205
339
  baseCwd: "",
206
340
  currentSessionId: null,
207
341
  artifactDirPreference: config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
342
+ ...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
343
+ ...(config.missions ? { missionStoreConfig: config.missions } : {}),
208
344
  parentSessionFile: null,
209
345
  subagentInProgress: false,
210
346
  subagentSpawns: {
@@ -219,7 +355,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
219
355
  foregroundRuns: new Map(),
220
356
  foregroundControls: new Map(),
221
357
  lastForegroundControlId: null,
222
- pendingForegroundControlNotices: new Map(),
223
358
  cleanupTimers: new Map(),
224
359
  lastUiContext: null,
225
360
  poller: null,
@@ -233,23 +368,40 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
233
368
  };
234
369
 
235
370
  const supervisorChannel = createNativeSupervisorChannel(pi, state);
371
+ const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
236
372
  const mainWatchdog = registerMainWatchdog(pi);
237
373
  const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
238
374
  const fleetStatus = fleetViewEnabled
239
375
  ? new SubagentFleetStatus(state, async (itemKey) => {
240
376
  const ctx = state.lastUiContext;
241
377
  if (!ctx?.hasUI) return;
242
- await openSubagentFleet(ctx, state, { initialKey: itemKey });
378
+ await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
243
379
  }, { placement: fleetViewPlacement })
244
380
  : undefined;
381
+ let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
382
+ const scheduledRunManager = createScheduledRunManager({
383
+ config,
384
+ launch: (params, ctx, signal) => {
385
+ if (!executorScheduled) {
386
+ return Promise.resolve({
387
+ content: [{ type: "text", text: "Scheduled subagent launch is unavailable (executor not ready)." }],
388
+ isError: true,
389
+ details: { mode: "management" as const, results: [] },
390
+ });
391
+ }
392
+ return executorScheduled(randomUUID(), params, signal, ctx);
393
+ },
394
+ resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
395
+ });
245
396
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
246
397
  pi,
247
398
  state,
248
- RESULTS_DIR,
399
+ DIRS.results,
249
400
  10 * 60 * 1000,
250
401
  {
251
402
  notifier: completionNotifier,
252
- deliverIntercomResults: config.intercomBridge?.resultDelivery !== false,
403
+ observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
404
+ deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
253
405
  },
254
406
  );
255
407
 
@@ -260,8 +412,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
260
412
  mainWatchdog.dispose();
261
413
  scheduledRunManager.stop();
262
414
  supervisorChannel.dispose();
415
+ waitSubscriptionManager.dispose();
263
416
  fleetStatus?.dispose();
264
- clearPendingForegroundControlNotices(state);
265
417
  if (state.poller) {
266
418
  clearInterval(state.poller);
267
419
  state.poller = null;
@@ -269,24 +421,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
269
421
  };
270
422
  globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
271
423
 
272
- const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR, {
424
+ const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, DIRS.async, {
273
425
  widgetEnabled: asyncWidgetEnabled,
274
426
  });
275
- let executorExecute: ((id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
276
- const scheduledRunManager = createScheduledRunManager({
277
- config,
278
- launch: (params, ctx, signal) => {
279
- if (!executorExecute) {
280
- return Promise.resolve({
281
- content: [{ type: "text", text: "Scheduled subagent launch is unavailable (executor not ready)." }],
282
- isError: true,
283
- details: { mode: "management" as const, results: [] },
284
- });
285
- }
286
- return executorExecute(randomUUID(), params, signal, undefined, ctx);
287
- },
288
- resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
289
- });
290
427
  const executor = createSubagentExecutor({
291
428
  pi,
292
429
  state,
@@ -300,7 +437,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
300
437
  expandTilde,
301
438
  discoverAgents,
302
439
  });
303
- executorExecute = executor.execute;
440
+ executorScheduled = executor.executeScheduled;
304
441
 
305
442
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
306
443
  const details = resolveSlashMessageDetails(message.details);
@@ -379,7 +516,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
379
516
  getContext: () => state.lastUiContext,
380
517
  execute: (requestId, params, signal, ctx, onUpdate) =>
381
518
  executeSubagentCollapsed(requestId, params, signal, onUpdate, ctx),
382
- executeVersioned: (requestId, params, signal, ctx, onUpdate) => {
519
+ executeStructured: (requestId, params, signal, ctx, onUpdate) => {
383
520
  if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
384
521
  return executor.executeDelegated(requestId, params, signal, onUpdate, ctx);
385
522
  },
@@ -392,13 +529,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
392
529
  state,
393
530
  });
394
531
 
395
- function effectiveParallelTaskCount(tasks: Array<{ count?: unknown }> | undefined): number {
396
- if (!tasks || tasks.length === 0) return 0;
397
- return tasks.reduce((total, task) => {
398
- const count = typeof task.count === "number" && Number.isInteger(task.count) && task.count >= 1 ? task.count : 1;
399
- return total + count;
400
- }, 0);
401
- }
402
532
 
403
533
  const tool: ToolDefinition<typeof SubagentParams, Details> = {
404
534
  name: "subagent",
@@ -406,16 +536,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
406
536
  description: buildSubagentToolDescription(config),
407
537
  parameters: SubagentParams,
408
538
 
409
- prepareArguments(args) {
410
- // Run friendly chain validation before pi-ai's raw TypeBox schema check
411
- // so the model sees which property is disallowed, what is allowed, and a
412
- // valid example instead of `chain.N: must not have additional properties`.
413
- validateChainInput(args);
414
- return args as never;
415
- },
416
-
417
539
  execute(id, params, signal, onUpdate, ctx) {
418
- return executeSubagentCollapsed(id, params, signal, onUpdate, ctx);
540
+ const input = params as SubagentParamsLike;
541
+ if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
542
+ return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
543
+ }
544
+ return executeSubagentCollapsed(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
419
545
  },
420
546
 
421
547
  renderCall(args, theme) {
@@ -426,21 +552,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
426
552
  0, 0,
427
553
  );
428
554
  }
429
- const isParallel = (args.tasks?.length ?? 0) > 0;
430
- const parallelCount = effectiveParallelTaskCount(args.tasks as Array<{ count?: unknown }> | undefined);
431
- const asyncLabel = args.async === true && args.clarify !== true ? theme.fg("warning", " [async]") : "";
432
- if (args.chain?.length)
433
- return new Text(
434
- `${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
435
- 0,
436
- 0,
437
- );
438
- if (isParallel)
555
+ if (args.workflowScript)
439
556
  return new Text(
440
- `${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${parallelCount})${asyncLabel}`,
557
+ `${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, args.clarify)}`,
441
558
  0,
442
559
  0,
443
560
  );
561
+ const asyncLabel = args.async === true && args.clarify !== true ? theme.fg("warning", " [async]") : "";
444
562
  return new Text(
445
563
  `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
446
564
  0,
@@ -450,14 +568,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
450
568
 
451
569
  renderResult(result, options, theme, context) {
452
570
  clearLegacyResultAnimationTimer(context);
453
- return renderSubagentResult(result, options, theme);
571
+ const renderedResult = { ...result, isError: context.isError };
572
+ return summaryInlineToolDisplay
573
+ ? renderSubagentSummary(renderedResult, options, theme)
574
+ : renderSubagentResult(renderedResult, options, theme);
454
575
  },
455
576
 
456
577
  };
457
578
 
458
579
  pi.registerTool(tool);
459
580
 
460
- registerWaitTool(pi, state, waitToolConfig.enabled);
581
+ registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager);
461
582
 
462
583
  pi.on("agent_end", async (_event, ctx) => {
463
584
  if (ctx.hasUI) return;
@@ -482,6 +603,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
482
603
  const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
483
604
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
484
605
  globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
606
+ const activeHerdrRuns = () => [...state.asyncJobs.values()]
607
+ .filter((job) => job.status === "queued" || job.status === "running")
608
+ .map((job) => ({
609
+ id: job.asyncId,
610
+ agents: job.agents,
611
+ needsAttention: job.activityState === "needs_attention",
612
+ }));
613
+ const herdrStatusBridge = registerHerdrStatusBridge({
614
+ events: pi.events,
615
+ getRuns: activeHerdrRuns,
616
+ async runHerdr(args) {
617
+ await pi.exec(process.env.HERDR_BIN || "herdr", [...args], { timeout: 5_000 });
618
+ },
619
+ });
485
620
  const controlEventHandler = (payload: unknown) => {
486
621
  handleSubagentControlNotice({
487
622
  pi,
@@ -499,6 +634,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
499
634
  };
500
635
  const asyncCompleteHandler = (payload: unknown) => {
501
636
  handleComplete(payload);
637
+ scheduledRunManager.handleAsyncCompletion(payload);
638
+ try {
639
+ syncMissionFromAsyncCompletion(payload);
640
+ } catch (error) {
641
+ console.error("Failed to update mission from async completion:", error);
642
+ }
502
643
  fleetStatus?.refresh();
503
644
  };
504
645
  const eventUnsubscribes = [
@@ -506,6 +647,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
506
647
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
507
648
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
508
649
  pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
650
+ herdrStatusBridge.dispose,
509
651
  rpcBridge.dispose,
510
652
  ];
511
653
  globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
@@ -514,6 +656,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
514
656
  if (event.toolName !== "subagent") return;
515
657
  if (!ctx.hasUI) return;
516
658
  state.lastUiContext = ctx;
659
+ restoreActiveJobs(ctx);
517
660
  fleetStatus?.setContext(ctx);
518
661
  fleetStatus?.refresh();
519
662
  if (state.asyncJobs.size > 0) {
@@ -557,26 +700,47 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
557
700
  }
558
701
  state.lastUiContext = ctx;
559
702
  cleanupSessionArtifacts(ctx);
560
- clearPendingForegroundControlNotices(state);
561
703
  state.foregroundControls.clear();
562
704
  state.lastForegroundControlId = null;
563
705
  resetJobs(ctx);
564
706
  restoreActiveJobs(ctx);
565
707
  scheduledRunManager.bindSession(ctx);
566
708
  restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
709
+ waitSubscriptionManager.restore();
567
710
  startResultWatcher();
568
711
  primeExistingResults({ triggerTurn: !recovering });
569
712
  fleetStatus?.setContext(ctx);
570
713
  };
571
714
 
715
+ pi.on("agent_start", () => {
716
+ herdrStatusBridge.agentStarted();
717
+ });
718
+
719
+ pi.on("session_compact", () => {
720
+ const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
721
+ if (!hasActiveAsyncWork || state.lastUiContext?.hasUI !== true) return;
722
+ pi.sendMessage(
723
+ {
724
+ customType: "subagent-compaction-resume",
725
+ content: "Compaction is complete. Resume the parent task now; background subagent results will arrive separately when ready.",
726
+ display: false,
727
+ },
728
+ { triggerTurn: true },
729
+ );
730
+ });
731
+
572
732
  pi.on("session_start", (event, ctx) => {
573
733
  const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
574
734
  resetSessionState(ctx, recovering);
735
+ herdrStatusBridge.sessionStarted({
736
+ hasUI: ctx.hasUI === true,
737
+ runs: activeHerdrRuns(),
738
+ });
575
739
  rpcBridge.emitReady(ctx);
576
740
  supervisorChannel.start();
577
741
  });
578
742
 
579
- pi.on("session_shutdown", () => {
743
+ pi.on("session_shutdown", async () => {
580
744
  stopResultWatcher();
581
745
  state.currentSessionId = null;
582
746
  state.parentSessionFile = null;
@@ -595,7 +759,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
595
759
  scheduledRunManager.stop();
596
760
  if (state.poller) clearInterval(state.poller);
597
761
  state.poller = null;
598
- clearPendingForegroundControlNotices(state);
599
762
  for (const timer of state.cleanupTimers.values()) {
600
763
  clearTimeout(timer);
601
764
  }
@@ -618,5 +781,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
618
781
  } catch (error) {
619
782
  if (!isStaleExtensionContextError(error)) throw error;
620
783
  }
784
+ await herdrStatusBridge.flush();
621
785
  });
622
786
  }
@@ -11,15 +11,14 @@ import {
11
11
  type AsyncJobStep,
12
12
  type Details,
13
13
  type SubagentState,
14
- ASYNC_DIR,
15
- RESULTS_DIR,
14
+ DIRS,
16
15
  SUBAGENT_ASYNC_COMPLETE_EVENT,
17
16
  SUBAGENT_PROCESS_TERMINAL_EVENT,
18
17
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
19
18
  } from "../shared/types.ts";
20
19
  import { readStatus } from "../shared/utils.ts";
21
20
  import { SubagentParams } from "./schemas.ts";
22
- import { validateChainInput } from "./chain-validation.ts";
21
+ import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
23
22
 
24
23
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
25
24
  export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
@@ -193,6 +192,17 @@ function buildFleetStatus(
193
192
  for (const job of state.asyncJobs.values()) {
194
193
  if (job.sessionId !== authoritativeSessionId || !activeState(job.status)) continue;
195
194
  const startedAt = job.startedAt ?? job.updatedAt;
195
+ if (job.mode === "workflow") {
196
+ const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
197
+ addCandidate({
198
+ internalKey: `async:${job.asyncId}`,
199
+ agent: "workflow",
200
+ startedAt,
201
+ tokens: job.totalTokens,
202
+ goal: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
203
+ });
204
+ continue;
205
+ }
196
206
  const steps: AsyncJobStep[] | undefined = job.steps?.length
197
207
  ? job.steps
198
208
  : job.agents?.map((agent, index) => ({
@@ -318,13 +328,6 @@ function assertRecordParams(params: unknown, method: SubagentRpcMethod): Record<
318
328
  }
319
329
 
320
330
  function assertSubagentParams(params: SubagentParamsLike, label: string): void {
321
- // Friendly chain validation first: name the disallowed property, list allowed
322
- // ones, and show a valid example instead of raw TypeBox diagnostics.
323
- try {
324
- validateChainInput(params);
325
- } catch (error) {
326
- throw new SubagentRpcError("invalid_params", `${label}: ${error instanceof Error ? error.message : String(error)}`);
327
- }
328
331
  if (subagentParamsValidator.Check(params)) return;
329
332
  const messages = [...subagentParamsValidator.Errors(params)]
330
333
  .slice(0, 4)
@@ -417,6 +420,9 @@ async function executeChecked(
417
420
 
418
421
  function spawnParams(params: unknown): SubagentParamsLike {
419
422
  const input = assertRecordParams(params, "spawn");
423
+ if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
424
+ throw new SubagentRpcError("invalid_params", "RPC spawn no longer accepts top-level chain or parallel inputs; use workflowScript.");
425
+ }
420
426
  if (input.action !== undefined) {
421
427
  throw new SubagentRpcError("invalid_params", "RPC spawn does not accept management/control actions. Use status or interrupt RPC methods instead.");
422
428
  }
@@ -468,8 +474,8 @@ function stopAsyncRun(
468
474
  ): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string } {
469
475
  const target = normalizeTargetParams(params, "stop");
470
476
  assertSubagentParams({ action: "status", ...target }, "RPC stop target params");
471
- const asyncDirRoot = options.asyncDirRoot ?? ASYNC_DIR;
472
- const resultsDir = options.resultsDir ?? RESULTS_DIR;
477
+ const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
478
+ const resultsDir = options.resultsDir ?? DIRS.results;
473
479
  let location;
474
480
  try {
475
481
  location = resolveAsyncRunLocation(target, asyncDirRoot, resultsDir);