pi-subagents 0.53.0 → 0.55.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 (76) hide show
  1. package/CHANGELOG.md +66 -5
  2. package/README.md +1 -1
  3. package/agents/reviewer.md +12 -2
  4. package/docs/agents.md +4 -4
  5. package/docs/configuration.md +6 -6
  6. package/docs/extension-api.md +14 -4
  7. package/docs/models.md +41 -6
  8. package/docs/observability.md +14 -3
  9. package/docs/tool-reference.md +21 -6
  10. package/docs/workflows.md +7 -1
  11. package/index.ts +10 -1
  12. package/package.json +1 -1
  13. package/prompts/council.md +19 -7
  14. package/prompts/parallel-review.md +5 -1
  15. package/prompts/review-loop.md +9 -5
  16. package/skills/council-mode/SKILL.md +50 -26
  17. package/skills/pi-subagents/SKILL.md +5 -1
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  19. package/skills/pi-subagents/references/execution-controls.md +13 -4
  20. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  21. package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
  22. package/src/agents/agent-management.ts +115 -14
  23. package/src/agents/agent-serializer.ts +2 -2
  24. package/src/agents/agents.ts +195 -47
  25. package/src/api/external-job-provider.ts +10 -1
  26. package/src/api/preflight.ts +33 -19
  27. package/src/api/project-panes.ts +2 -0
  28. package/src/extension/doctor.ts +10 -0
  29. package/src/extension/fanout-child.ts +3 -2
  30. package/src/extension/index.ts +24 -4
  31. package/src/extension/public-execution.ts +12 -9
  32. package/src/extension/rpc.ts +77 -3
  33. package/src/extension/schemas.ts +2 -1
  34. package/src/extension/tool-description.ts +9 -6
  35. package/src/extension/tool-result.ts +19 -0
  36. package/src/inspectors/herdr/client.ts +3 -3
  37. package/src/inspectors/herdr/focus.ts +55 -0
  38. package/src/inspectors/herdr/project-panes.ts +228 -44
  39. package/src/integrations/herdr-status.ts +26 -4
  40. package/src/runs/background/async-execution.ts +112 -13
  41. package/src/runs/background/async-job-tracker.ts +33 -14
  42. package/src/runs/background/async-resume.ts +20 -2
  43. package/src/runs/background/async-retention.ts +1 -1
  44. package/src/runs/background/async-status.ts +10 -0
  45. package/src/runs/background/chain-root-attachment.ts +5 -0
  46. package/src/runs/background/control-channel.ts +98 -10
  47. package/src/runs/background/notify.ts +62 -1
  48. package/src/runs/background/result-watcher.ts +4 -3
  49. package/src/runs/background/run-status.ts +15 -1
  50. package/src/runs/background/stale-run-reconciler.ts +3 -0
  51. package/src/runs/background/subagent-runner.ts +251 -38
  52. package/src/runs/background/wait-completions.ts +2 -0
  53. package/src/runs/background/wait-tool.ts +4 -3
  54. package/src/runs/foreground/async-stop-action.ts +23 -2
  55. package/src/runs/foreground/execution.ts +110 -8
  56. package/src/runs/foreground/subagent-executor.ts +361 -45
  57. package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
  58. package/src/runs/shared/acceptance.ts +1 -0
  59. package/src/runs/shared/child-identity.ts +36 -0
  60. package/src/runs/shared/completion-guard.ts +50 -1
  61. package/src/runs/shared/external-job-bridge.ts +53 -37
  62. package/src/runs/shared/external-job-runner.ts +126 -23
  63. package/src/runs/shared/model-fallback.ts +46 -15
  64. package/src/runs/shared/model-scope.ts +106 -39
  65. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  66. package/src/runs/shared/parallel-utils.ts +12 -0
  67. package/src/runs/shared/pi-args.ts +31 -1
  68. package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
  69. package/src/runs/shared/tool-availability.ts +1 -3
  70. package/src/shared/launch-contract.ts +6 -5
  71. package/src/shared/thinking-ceiling.ts +52 -0
  72. package/src/shared/types.ts +72 -5
  73. package/src/tui/fleet-status.ts +66 -13
  74. package/src/tui/render.ts +5 -4
  75. package/src/watchdog/permission-arbiter.ts +59 -51
  76. package/src/workflows/scripted-workflow.ts +107 -10
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import * as fs from "node:fs";
3
2
  import * as path from "node:path";
4
3
  import { fileURLToPath } from "node:url";
@@ -7,19 +6,22 @@ import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
7
6
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
8
7
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
9
8
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
9
+ import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
10
10
  import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
11
11
  import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
12
12
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
13
13
  import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
+ import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
14
15
  import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
15
16
  import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
16
17
  import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
18
+ import { resolvePermissionRules } from "../runs/shared/permissions.ts";
17
19
  import type { ResolvedTurnBudget } from "../shared/types.ts";
18
20
  import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
19
21
  import { resolveStepBehavior } from "../shared/settings.ts";
20
22
  import { canPreferForkFromSnapshot, resolveSubagentLaunchContext } from "../shared/fork-context.ts";
21
23
  import { loadConfig } from "../extension/config.ts";
22
- import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts";
24
+ import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest, stableJsonDigest } from "../shared/launch-contract.ts";
23
25
  import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
24
26
  import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
25
27
  import { resultFilePath } from "../runs/background/result-files.ts";
@@ -35,7 +37,8 @@ export type SubagentLaunchContractReasonCode =
35
37
  | "invalid_artifact_dir"
36
38
  | "invalid_cwd"
37
39
  | "unsupported_mode"
38
- | "restricted_agent";
40
+ | "restricted_agent"
41
+ | "thinking_ceiling";
39
42
 
40
43
  export interface SubagentLaunchContractDiagnostic {
41
44
  code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
@@ -51,6 +54,8 @@ export interface SubagentLaunchContractInput {
51
54
  context?: "fresh" | "fork";
52
55
  model?: string;
53
56
  thinking?: string | false;
57
+ thinkingCeiling?: ThinkingLevel;
58
+ inheritedThinkingCeiling?: ThinkingLevel;
54
59
  parentModel?: ParentModel;
55
60
  availableModels?: ReadonlyArray<AvailableModelInfo | { provider: string; id: string; fullId?: string; reasoning?: boolean }>;
56
61
  preferredProvider?: string;
@@ -145,6 +150,7 @@ export interface SubagentLaunchContract {
145
150
  model?: string;
146
151
  modelCandidates: string[];
147
152
  thinking?: string;
153
+ thinkingCeiling?: ThinkingLevel;
148
154
  systemPromptMode: AgentConfig["systemPromptMode"];
149
155
  inheritProjectContext: boolean;
150
156
  inheritSkills: boolean;
@@ -174,20 +180,8 @@ function packageVersion(): string {
174
180
  return parsed.version;
175
181
  }
176
182
 
177
- function stableJson(value: unknown): string {
178
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
179
- if (value && typeof value === "object") {
180
- return `{${Object.entries(value as Record<string, unknown>)
181
- .filter(([, entry]) => entry !== undefined)
182
- .sort(([a], [b]) => a.localeCompare(b))
183
- .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
184
- .join(",")}}`;
185
- }
186
- return JSON.stringify(value);
187
- }
188
-
189
183
  function digestContract(contract: Omit<SubagentLaunchContract, "digest">): string {
190
- return createHash("sha256").update(stableJson(contract)).digest("hex");
184
+ return stableJsonDigest(contract);
191
185
  }
192
186
 
193
187
  function normalizeAvailableModels(models: SubagentLaunchContractInput["availableModels"]): AvailableModelInfo[] {
@@ -287,20 +281,38 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
287
281
 
288
282
  const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
289
283
  const availableModels = normalizeAvailableModels(input.availableModels);
290
- const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
284
+ const preferredProvider = agent.modelProvider ?? input.preferredProvider ?? input.parentModel?.provider;
285
+ const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
291
286
  const primaryModel = externalRunner
292
287
  ? undefined
293
- : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: discovered.modelScope });
288
+ : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: modelScopes });
294
289
  const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
290
+ const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
291
+ discovered.maxThinking,
292
+ input.thinkingCeiling,
293
+ input.inheritedThinkingCeiling,
294
+ decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
295
+ );
295
296
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
296
297
  const modelCandidates = externalRunner
297
298
  ? []
298
299
  : buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
299
- scope: discovered.modelScope,
300
+ scope: modelScopes,
300
301
  primaryModelFromParent: inheritsParentModel(input.model, agent.model, input.parentModel),
301
302
  })
302
303
  .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
304
+ if (!externalRunner) {
305
+ try {
306
+ assertThinkingWithinCeiling({ model, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
307
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
308
+ } catch (error) {
309
+ const message = error instanceof Error ? error.message : String(error);
310
+ diagnostics.push({ code: "thinking_ceiling", severity: "error", message });
311
+ return { ok: false, code: "thinking_ceiling", message, diagnostics };
312
+ }
313
+ }
303
314
  let toolPlan: PiLaunchToolPlan;
315
+ const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
304
316
  try {
305
317
  toolPlan = resolvePiLaunchToolPlan({
306
318
  tools: agent.tools,
@@ -312,6 +324,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
312
324
  structuredOutput: Boolean(input.outputSchema),
313
325
  capabilityCeiling: effectiveCapabilityCeiling,
314
326
  agentName: agent.name,
327
+ permissionRules,
315
328
  });
316
329
  } catch (error) {
317
330
  const message = error instanceof Error ? error.message : String(error);
@@ -367,6 +380,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
367
380
  ...(model ? { model } : {}),
368
381
  modelCandidates,
369
382
  ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
383
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
370
384
  systemPromptMode: agent.systemPromptMode,
371
385
  inheritProjectContext: agent.inheritProjectContext,
372
386
  inheritSkills: agent.inheritSkills,
@@ -10,6 +10,7 @@ export {
10
10
  createProjectPaneManager,
11
11
  openProjectPane,
12
12
  getProjectPaneStatus,
13
+ focusProjectPane,
13
14
  closeProjectPane,
14
15
  readProjectPaneBinding,
15
16
  projectPaneBindingPath,
@@ -21,6 +22,7 @@ export {
21
22
  type CloseProjectPaneOptions,
22
23
  type OpenProjectPaneData,
23
24
  type ProjectPaneStatusData,
25
+ type FocusProjectPaneData,
24
26
  type CloseProjectPaneData,
25
27
  type ProjectPaneRuntime,
26
28
  type ProjectPaneError,
@@ -224,6 +224,13 @@ function formatPermissionSystemSection(): string[] {
224
224
  return lines;
225
225
  }
226
226
 
227
+ function formatWorkflowScriptSection(): string[] {
228
+ return [
229
+ "- helpers: runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console",
230
+ "- recovery: if runs.all is missing, reload or update pi-subagents; await Promise.all([runs.run(...)]) is also supported",
231
+ ];
232
+ }
233
+
227
234
  export function buildDoctorReport(input: DoctorReportInput): string {
228
235
  const paths = input.paths ?? defaultPaths();
229
236
  const deps = { ...DEFAULT_DEPS, ...input.deps };
@@ -253,6 +260,9 @@ export function buildDoctorReport(input: DoctorReportInput): string {
253
260
  "Active async capacity",
254
261
  ...formatActiveAsyncCapacitySection(input),
255
262
  "",
263
+ "Workflow script",
264
+ ...formatWorkflowScriptSection(),
265
+ "",
256
266
  "Permission system",
257
267
  ...formatPermissionSystemSection(),
258
268
  "",
@@ -11,6 +11,7 @@ import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute,
11
11
  import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
12
  import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
13
  import { createSubagentParamsSchema } from "./schemas.ts";
14
+ import { finalizeToolResult } from "./tool-result.ts";
14
15
  import { loadConfig, resolveAsyncByDefault } from "./config.ts";
15
16
  import { type Details, type SubagentState } from "../shared/types.ts";
16
17
 
@@ -181,8 +182,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
181
182
  "Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
182
183
  ].join("\n"),
183
184
  parameters: params,
184
- execute(id, params, signal, onUpdate, ctx) {
185
- return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
185
+ async execute(id, params, signal, onUpdate, ctx) {
186
+ return finalizeToolResult(await executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
186
187
  },
187
188
  };
188
189
 
@@ -43,6 +43,7 @@ import { registerMainWatchdog } from "../watchdog/register-main.ts";
43
43
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
44
44
  import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
45
45
  import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
46
+ import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
46
47
  import { registerSubagentRpcBridge } from "./rpc.ts";
47
48
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
48
49
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
@@ -57,6 +58,7 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
57
58
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
58
59
  import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
59
60
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
61
+ import { finalizeToolResult } from "./tool-result.ts";
60
62
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
61
63
  import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
62
64
  import { resolveMissionStoreLocation } from "../missions/store.ts";
@@ -435,6 +437,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
435
437
  grantHistory: [],
436
438
  },
437
439
  activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
440
+ herdrProjectPanes: new Map(),
438
441
  asyncJobs: new Map(),
439
442
  fleetJobs: new Map(),
440
443
  foregroundRuns: new Map(),
@@ -500,7 +503,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
500
503
  ...all.user,
501
504
  ...all.project,
502
505
  ];
503
- return mergeRuntimeAgents(pi, discovered, configuredAgents);
506
+ const merged = mergeRuntimeAgents(pi, discovered, configuredAgents);
507
+ if (discovered.maxThinking === undefined) return merged;
508
+ return {
509
+ ...merged,
510
+ agents: merged.agents.map((agent) => agent.maxThinking === discovered.maxThinking ? agent : { ...agent, maxThinking: discovered.maxThinking }),
511
+ };
504
512
  };
505
513
  const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
506
514
  widgetEnabled: asyncWidgetEnabled,
@@ -517,7 +525,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
517
525
  observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
518
526
  hasDeliveryDemand: hasResultDeliveryDemand,
519
527
  deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
520
- resultScanLogging: config.resultScanLogging ?? "all",
528
+ resultScanLogging: config.resultScanLogging,
521
529
  },
522
530
  );
523
531
  const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
@@ -599,6 +607,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
599
607
  const expandKey = keyText("app.tools.expand");
600
608
  text += `\n ${theme.fg("dim", `${expandKey} full notification`)}`;
601
609
  }
610
+ if (details.workflowRunId) {
611
+ text += `\n ${theme.fg("muted", `workflow: ${details.workflowRunId}`)}`;
612
+ }
613
+ if (details.childRuns?.length) {
614
+ text += `\n ${theme.fg("muted", `children: ${details.childRuns.map((child) => `${child.workflowKey ?? child.agent ?? "child"}=${child.runId}`).join(", ")}`)}`;
615
+ }
616
+ if (details.reconciledFromDetachedChild) {
617
+ text += `\n ${theme.fg("muted", `reconciled child: ${details.reconciledFromDetachedChild}`)}`;
618
+ }
602
619
  if (details.sessionLabel && details.sessionValue) {
603
620
  text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
604
621
  }
@@ -658,8 +675,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
658
675
  ...buildSubagentToolPromptMetadata(config),
659
676
  parameters,
660
677
 
661
- execute(id, params, signal, onUpdate, ctx) {
662
- return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
678
+ async execute(id, params, signal, onUpdate, ctx) {
679
+ return finalizeToolResult(await executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
663
680
  },
664
681
 
665
682
  renderCall(args, theme) {
@@ -731,6 +748,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
731
748
  const herdrStatusBridge = registerHerdrStatusBridge({
732
749
  events: pi.events,
733
750
  getRuns: activeHerdrRuns,
751
+ getProjectPaneCount: () => [...(state.herdrProjectPanes?.values() ?? [])].filter((pane) => pane.state === "open").length,
734
752
  async runHerdr(args) {
735
753
  await pi.exec(process.env.HERDR_BIN || "herdr", [...args], { timeout: 5_000 });
736
754
  },
@@ -839,6 +857,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
839
857
  granted: 0,
840
858
  grantHistory: [],
841
859
  };
860
+ const projectPaneOwnerRoot = path.resolve(ctx.cwd);
861
+ restoreHerdrProjectPaneSnapshots(state, [...new Set([...(state.herdrProjectPanes?.keys() ?? []), ...listHerdrProjectPaneRoots(projectPaneOwnerRoot), projectPaneOwnerRoot])]);
842
862
  // Set PI_SUBAGENT_PARENT_SESSION for permission-system forwarding.
843
863
  // Only set in the root session (the interactive UI session), not in
844
864
  // child subagent processes — children inherit the parent's value
@@ -17,6 +17,12 @@ export interface PublicSubagentExecutionParams {
17
17
  output?: unknown;
18
18
  resume?: unknown;
19
19
  clarify?: unknown;
20
+ workflowParentRunId?: unknown;
21
+ workflowKey?: unknown;
22
+ workflowChildAsyncId?: unknown;
23
+ workflowAwaitAsync?: unknown;
24
+ workflowParentDeadlineAt?: unknown;
25
+ suppressRoutineResultIntercom?: unknown;
20
26
  runFanoutBudget?: unknown;
21
27
  runFanoutAdmitted?: unknown;
22
28
  }
@@ -46,6 +52,9 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
46
52
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
47
53
  return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
48
54
  }
55
+ if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
56
+ return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
57
+ }
49
58
  const action = params.action;
50
59
  if (action !== undefined && (typeof action !== "string" || !action.trim())) {
51
60
  return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
@@ -111,18 +120,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
111
120
  if (params.task !== undefined && typeof params.task !== "string") {
112
121
  return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
113
122
  }
114
- const { agent: _agent, task: _task, output, ...workflowDefaults } = params;
115
- const child = {
116
- agent: params.agent.trim(),
117
- ...(params.task !== undefined ? { task: params.task } : {}),
118
- output: output === undefined ? true : output,
119
- };
120
123
  return {
121
124
  ok: true,
122
125
  params: {
123
- ...workflowDefaults,
124
- ...(params.async === undefined && options.asyncByDefault === false ? { async: false } : {}),
125
- workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
126
+ ...params,
127
+ agent: params.agent.trim(),
128
+ output: params.output === undefined ? true : params.output,
126
129
  } as T,
127
130
  };
128
131
  }
@@ -13,14 +13,17 @@ import {
13
13
  type SubagentState,
14
14
  DIRS,
15
15
  SUBAGENT_ASYNC_COMPLETE_EVENT,
16
+ SUBAGENT_CHILD_STATUS_EVENT,
16
17
  SUBAGENT_PROCESS_TERMINAL_EVENT,
17
18
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
19
+ type SubagentChildStatusEvent,
18
20
  } from "../shared/types.ts";
19
21
  import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
20
22
  import { readStatus } from "../shared/utils.ts";
21
23
  import { SubagentParams } from "./schemas.ts";
22
24
  import { normalizePublicSubagentExecution } from "./public-execution.ts";
23
25
  import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
26
+ import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, type ResolvedAsyncStatusChild } from "../runs/shared/child-identity.ts";
24
27
 
25
28
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
26
29
  export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
@@ -405,6 +408,7 @@ function pingData(ctx: ExtensionContext | null) {
405
408
  request: SUBAGENT_RPC_REQUEST_EVENT,
406
409
  replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
407
410
  asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
411
+ childStatus: SUBAGENT_CHILD_STATUS_EVENT,
408
412
  processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
409
413
  },
410
414
  session: sessionData(ctx),
@@ -500,8 +504,14 @@ function stopAsyncRun(
500
504
  params: unknown,
501
505
  options: RegisterSubagentRpcBridgeOptions,
502
506
  ctx: ExtensionContext,
503
- ): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string } {
504
- const target = normalizeTargetParams(params, "stop");
507
+ ): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string; childId?: string } {
508
+ const input = assertRecordParams(params, "stop");
509
+ const rawChildId = input.childId;
510
+ if (rawChildId !== undefined && (typeof rawChildId !== "string" || !rawChildId.trim() || /[\r\n]/.test(rawChildId) || rawChildId.length > 256)) {
511
+ throw new SubagentRpcError("invalid_params", "RPC stop childId must be a non-empty string without newlines and at most 256 characters.");
512
+ }
513
+ const childId = typeof rawChildId === "string" ? rawChildId : undefined;
514
+ const target = normalizeTargetParams(input, "stop");
505
515
  assertSubagentParams({ action: "status", ...target }, "RPC stop target params");
506
516
  const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
507
517
  const resultsDir = options.resultsDir ?? DIRS.results;
@@ -523,7 +533,60 @@ function stopAsyncRun(
523
533
  throw new SubagentRpcError("not_found", `Async run '${initialRunId}' was not found in the active session.`);
524
534
  }
525
535
 
536
+ let child: ResolvedAsyncStatusChild | undefined;
537
+ const emitChildStopping = (runId: string, asyncDir: string, stoppedChild: ResolvedAsyncStatusChild, ts = options.now?.() ?? Date.now()): void => {
538
+ options.events.emit(SUBAGENT_CHILD_STATUS_EVENT, {
539
+ type: "subagent.child-status",
540
+ version: 1,
541
+ runId,
542
+ childId: stoppedChild.id,
543
+ status: "stopping",
544
+ ts,
545
+ reason: "user",
546
+ source: "rpc",
547
+ asyncDir,
548
+ stepIndex: stoppedChild.index,
549
+ agent: stoppedChild.step.agent,
550
+ ...(stoppedChild.step.runId ? { childRunId: stoppedChild.step.runId } : {}),
551
+ ...(stoppedChild.step.workflowKey ? { workflowKey: stoppedChild.step.workflowKey } : {}),
552
+ ...(stoppedChild.step.phase ? { phase: stoppedChild.step.phase } : {}),
553
+ ...(stoppedChild.step.label ? { label: stoppedChild.step.label } : {}),
554
+ } satisfies SubagentChildStatusEvent);
555
+ };
556
+ if (childId !== undefined) {
557
+ const resolution = resolveAsyncStatusChild(initialStatus, childId);
558
+ if (!resolution.ok) throw new SubagentRpcError(resolution.code === "not_found" ? "not_found" : "invalid_params", resolution.message);
559
+ child = resolution.child;
560
+ if (!isStoppableAsyncStatusStep(child.step)) {
561
+ throw new SubagentRpcError("invalid_state", `Child '${childId}' in async run '${initialRunId}' is ${child.step.status}; stop only supports pending or running children.`);
562
+ }
563
+ }
526
564
  if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
565
+ if (child) {
566
+ const stopChild = options.state?.workflowChildStops?.get(initialRunId);
567
+ if (!stopChild) throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; child stop is unavailable.`);
568
+ if (!stopChild(child.id, `Workflow child '${child.id}' stopped by RPC.`)) throw new SubagentRpcError("invalid_state", `Child '${childId}' in workflow ${initialRunId} is not available to stop.`);
569
+ emitChildStopping(initialRunId, location.asyncDir, child);
570
+ return {
571
+ runId: initialRunId,
572
+ asyncDir: location.asyncDir,
573
+ previousState: initialStatus.state,
574
+ state: "stopping",
575
+ childId: child.id,
576
+ message: `Stop requested for child ${child.id} in async run ${initialRunId}.`,
577
+ };
578
+ }
579
+ const workflowController = options.state?.workflowControllers?.get(initialRunId);
580
+ if (workflowController) {
581
+ workflowController.abort(new Error("Workflow stopped by RPC."));
582
+ return {
583
+ runId: initialRunId,
584
+ asyncDir: location.asyncDir,
585
+ previousState: initialStatus.state,
586
+ state: "stopping",
587
+ message: `Stop requested for async run ${initialRunId}.`,
588
+ };
589
+ }
527
590
  throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; reload recovery cannot stop it safely.`);
528
591
  }
529
592
 
@@ -541,6 +604,14 @@ function stopAsyncRun(
541
604
  if (status.state !== "running") {
542
605
  throw new SubagentRpcError("invalid_state", `Async run ${runId} is ${status.state}; stop only supports running async runs.`);
543
606
  }
607
+ if (childId !== undefined) {
608
+ const resolution = resolveAsyncStatusChild(status, childId);
609
+ if (!resolution.ok) throw new SubagentRpcError(resolution.code === "not_found" ? "not_found" : "invalid_params", resolution.message);
610
+ child = resolution.child;
611
+ if (!isStoppableAsyncStatusStep(child.step)) {
612
+ throw new SubagentRpcError("invalid_state", `Child '${childId}' in async run '${runId}' is ${child.step.status}; stop only supports pending or running children.`);
613
+ }
614
+ }
544
615
 
545
616
  try {
546
617
  deliverStopRequest({
@@ -549,17 +620,20 @@ function stopAsyncRun(
549
620
  kill: options.kill,
550
621
  now: options.now,
551
622
  source: "rpc-stop",
623
+ ...(child ? { targetIndex: child.index, childId: child.id } : {}),
552
624
  });
553
625
  } catch (error) {
554
626
  throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
555
627
  }
628
+ if (child) emitChildStopping(runId, location.asyncDir, child);
556
629
 
557
630
  return {
558
631
  runId,
559
632
  asyncDir: location.asyncDir,
560
633
  previousState: status.state,
561
634
  state: "stopping",
562
- message: `Stop requested for async run ${runId}.`,
635
+ ...(child ? { childId: child.id } : {}),
636
+ message: child ? `Stop requested for child ${child.id} in async run ${runId}.` : `Stop requested for async run ${runId}.`,
563
637
  };
564
638
  }
565
639
 
@@ -271,6 +271,7 @@ const SubagentParamProperties = {
271
271
  })),
272
272
  handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
273
273
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
274
+ childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
274
275
  view: Type.Optional(Type.String({
275
276
  enum: ["fleet", "transcript"],
276
277
  description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.",
@@ -307,7 +308,7 @@ const SubagentParamProperties = {
307
308
  ],
308
309
  description: "Agent config for create/update. Object or JSON string."
309
310
  })),
310
- workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
311
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
311
312
  chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
312
313
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
313
314
  worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
@@ -6,7 +6,7 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
6
6
  const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
7
7
  const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
- export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child or workflowScript for orchestration. For multi-step or parallel work, make exactly one top-level subagent call with workflowScript and async:true; launch children only inside that script and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and pass completed child results via .output. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
9
+ export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child or workflowScript for orchestration. For multi-step or parallel work, make exactly one top-level subagent call with workflowScript and async:true; launch children only inside that script and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
10
10
 
11
11
  export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
12
12
 
@@ -14,8 +14,9 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
14
14
  "Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
15
15
  "Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
16
16
  "workflowScript means exactly one top-level subagent tool call with async:true. Inside it, use runs.run/runs.all to launch children; do not make another top-level subagent call for those children.",
17
- "For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all.",
17
+ "For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all.",
18
18
  "Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
19
+ "To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids.",
19
20
  "Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
20
21
  ];
21
22
 
@@ -32,8 +33,9 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
32
33
 
33
34
  EXECUTION:
34
35
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
35
- • SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
36
- • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
36
+ • When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids.
37
+ • SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action or workflowScript.
38
+ • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
37
39
  • Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
38
40
  • Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
39
41
  • Optional context is "fresh", "fork", or "profile". profile requires the selected agent's declared defaultContext and ignores config defaultSubagentContext. Explicit fresh/fork wins. When omitted, config defaultSubagentContext wins over agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
@@ -50,8 +52,9 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
50
52
 
51
53
  EXECUTE:
52
54
  • Call { action:"list" } first and use only executable/non-disabled agents.
53
- • SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
54
- • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
55
+ • Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids.
56
+ • SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action or workflowScript.
57
+ • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
55
58
  • Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
56
59
  • context can be fresh, fork, or profile. profile requires the selected agent's declared defaultContext and ignores defaultSubagentContext. Explicit fresh/fork wins; omitted context follows defaultSubagentContext before agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
57
60
 
@@ -0,0 +1,19 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+
3
+ /**
4
+ * Convert pi-subagents' internal logical-error result into the rejection Pi's
5
+ * public tool boundary uses to emit a canonical errored ToolResult.
6
+ *
7
+ * Keep this at registered tool boundaries. Internal workflows intentionally
8
+ * retain their return-based error handling.
9
+ */
10
+ export function finalizeToolResult<T>(result: AgentToolResult<T>): AgentToolResult<T> {
11
+ if (result.isError !== true) return result;
12
+
13
+ const message = result.content
14
+ .flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
15
+ .join("\n")
16
+ .trim();
17
+
18
+ throw new Error(message || "pi-subagents reported a logical tool failure.");
19
+ }
@@ -1,4 +1,4 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
 
3
3
  export type HerdrErrorCode =
4
4
  | "HERDR_UNAVAILABLE"
@@ -16,7 +16,7 @@ export interface HerdrClient {
16
16
  run<T = unknown>(args: string[], options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean }): Promise<HerdrResult<T>>;
17
17
  }
18
18
 
19
- type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ChildProcess;
19
+ type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ReturnType<typeof spawn>;
20
20
 
21
21
  function error(code: HerdrErrorCode, message: string, details?: unknown): HerdrResult<never> {
22
22
  return { ok: false, error: { code, message, ...(details !== undefined ? { details } : {}) } };
@@ -46,7 +46,7 @@ export function createHerdrClient(options: { bin?: string; spawn?: SpawnHerdr }
46
46
  return {
47
47
  run<T>(args: string[], runOptions: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean } = {}): Promise<HerdrResult<T>> {
48
48
  return new Promise((resolve) => {
49
- let child: ChildProcess;
49
+ let child: ReturnType<typeof spawn>;
50
50
  try {
51
51
  child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
52
52
  } catch (cause) {