pi-subagents 0.54.0 → 0.56.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 (71) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/agents/reviewer.md +12 -2
  3. package/docs/agents.md +4 -4
  4. package/docs/configuration.md +4 -4
  5. package/docs/extension-api.md +14 -4
  6. package/docs/models.md +30 -3
  7. package/docs/observability.md +14 -3
  8. package/docs/tool-reference.md +21 -6
  9. package/docs/workflows.md +7 -1
  10. package/package.json +1 -1
  11. package/prompts/council.md +9 -0
  12. package/prompts/parallel-review.md +5 -1
  13. package/prompts/review-loop.md +9 -5
  14. package/skills/council-mode/SKILL.md +33 -10
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  16. package/skills/pi-subagents/references/execution-controls.md +11 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  18. package/skills/pi-subagents/references/prompting-and-roles.md +8 -8
  19. package/src/agents/agent-management.ts +30 -9
  20. package/src/agents/agent-serializer.ts +4 -2
  21. package/src/agents/agents.ts +168 -41
  22. package/src/api/external-job-provider.ts +10 -1
  23. package/src/api/preflight.ts +45 -17
  24. package/src/api/project-panes.ts +2 -0
  25. package/src/extension/index.ts +20 -1
  26. package/src/extension/public-execution.ts +12 -9
  27. package/src/extension/rpc.ts +77 -3
  28. package/src/extension/schemas.ts +7 -1
  29. package/src/extension/tool-description.ts +9 -6
  30. package/src/inspectors/herdr/focus.ts +55 -0
  31. package/src/inspectors/herdr/project-panes.ts +228 -44
  32. package/src/integrations/herdr-status.ts +26 -4
  33. package/src/runs/background/async-execution.ts +137 -13
  34. package/src/runs/background/async-job-tracker.ts +33 -14
  35. package/src/runs/background/async-resume.ts +23 -2
  36. package/src/runs/background/async-status.ts +10 -0
  37. package/src/runs/background/control-channel.ts +98 -10
  38. package/src/runs/background/notify.ts +62 -1
  39. package/src/runs/background/run-status.ts +15 -1
  40. package/src/runs/background/subagent-runner.ts +308 -47
  41. package/src/runs/foreground/async-stop-action.ts +23 -2
  42. package/src/runs/foreground/execution.ts +115 -12
  43. package/src/runs/foreground/foreground-history.ts +3 -2
  44. package/src/runs/foreground/subagent-executor.ts +404 -45
  45. package/src/runs/foreground/workflow-detach-reconcile.ts +18 -6
  46. package/src/runs/shared/acceptance.ts +11 -5
  47. package/src/runs/shared/agent-contract.ts +1 -1
  48. package/src/runs/shared/child-identity.ts +36 -0
  49. package/src/runs/shared/completion-guard.ts +52 -2
  50. package/src/runs/shared/dynamic-fanout.ts +1 -1
  51. package/src/runs/shared/extension-bindings.ts +78 -0
  52. package/src/runs/shared/external-cli-runner.ts +2 -0
  53. package/src/runs/shared/external-job-bridge.ts +53 -37
  54. package/src/runs/shared/external-job-runner.ts +126 -23
  55. package/src/runs/shared/fast-mode-extension.ts +10 -0
  56. package/src/runs/shared/model-exclusions.ts +2 -1
  57. package/src/runs/shared/model-fallback.ts +12 -7
  58. package/src/runs/shared/mutation-evidence.ts +145 -0
  59. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  60. package/src/runs/shared/parallel-utils.ts +15 -0
  61. package/src/runs/shared/pi-args.ts +56 -12
  62. package/src/runs/shared/structured-output.ts +18 -4
  63. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  64. package/src/runs/shared/tool-availability.ts +1 -3
  65. package/src/shared/launch-contract.ts +12 -5
  66. package/src/shared/settings.ts +6 -1
  67. package/src/shared/thinking-ceiling.ts +52 -0
  68. package/src/shared/types.ts +107 -2
  69. package/src/tui/fleet-status.ts +66 -13
  70. package/src/tui/render.ts +5 -4
  71. package/src/workflows/scripted-workflow.ts +156 -12
@@ -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";
@@ -12,6 +11,7 @@ import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } f
12
11
  import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
13
12
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
14
13
  import { resolveEffectiveThinking } from "../shared/model-info.ts";
14
+ import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
15
15
  import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
16
16
  import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
17
17
  import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
@@ -21,11 +21,12 @@ import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-t
21
21
  import { resolveStepBehavior } from "../shared/settings.ts";
22
22
  import { canPreferForkFromSnapshot, resolveSubagentLaunchContext } from "../shared/fork-context.ts";
23
23
  import { loadConfig } from "../extension/config.ts";
24
- 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";
25
25
  import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
26
26
  import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
27
27
  import { resultFilePath } from "../runs/background/result-files.ts";
28
28
  import { nestedResultsPath } from "../runs/shared/nested-events.ts";
29
+ import { normalizeExtensionBindings, type ExtensionBindings } from "../runs/shared/extension-bindings.ts";
29
30
 
30
31
  export const SUBAGENT_LAUNCH_CONTRACT_VERSION = 2 as const;
31
32
 
@@ -37,7 +38,9 @@ export type SubagentLaunchContractReasonCode =
37
38
  | "invalid_artifact_dir"
38
39
  | "invalid_cwd"
39
40
  | "unsupported_mode"
40
- | "restricted_agent";
41
+ | "restricted_agent"
42
+ | "thinking_ceiling"
43
+ | "invalid_extension_bindings";
41
44
 
42
45
  export interface SubagentLaunchContractDiagnostic {
43
46
  code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
@@ -52,7 +55,10 @@ export interface SubagentLaunchContractInput {
52
55
  agentScope?: AgentScope;
53
56
  context?: "fresh" | "fork";
54
57
  model?: string;
58
+ fast?: boolean;
55
59
  thinking?: string | false;
60
+ thinkingCeiling?: ThinkingLevel;
61
+ inheritedThinkingCeiling?: ThinkingLevel;
56
62
  parentModel?: ParentModel;
57
63
  availableModels?: ReadonlyArray<AvailableModelInfo | { provider: string; id: string; fullId?: string; reasoning?: boolean }>;
58
64
  preferredProvider?: string;
@@ -60,6 +66,7 @@ export interface SubagentLaunchContractInput {
60
66
  output?: string | boolean;
61
67
  outputMode?: OutputMode;
62
68
  outputSchema?: JsonSchemaObject;
69
+ extensionBindings?: ExtensionBindings;
63
70
  turnBudget?: ResolvedTurnBudget;
64
71
  artifacts?: boolean;
65
72
  artifactDir?: ArtifactDirPreference;
@@ -147,6 +154,7 @@ export interface SubagentLaunchContract {
147
154
  model?: string;
148
155
  modelCandidates: string[];
149
156
  thinking?: string;
157
+ thinkingCeiling?: ThinkingLevel;
150
158
  systemPromptMode: AgentConfig["systemPromptMode"];
151
159
  inheritProjectContext: boolean;
152
160
  inheritSkills: boolean;
@@ -176,20 +184,8 @@ function packageVersion(): string {
176
184
  return parsed.version;
177
185
  }
178
186
 
179
- function stableJson(value: unknown): string {
180
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
181
- if (value && typeof value === "object") {
182
- return `{${Object.entries(value as Record<string, unknown>)
183
- .filter(([, entry]) => entry !== undefined)
184
- .sort(([a], [b]) => a.localeCompare(b))
185
- .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
186
- .join(",")}}`;
187
- }
188
- return JSON.stringify(value);
189
- }
190
-
191
187
  function digestContract(contract: Omit<SubagentLaunchContract, "digest">): string {
192
- return createHash("sha256").update(stableJson(contract)).digest("hex");
188
+ return stableJsonDigest(contract);
193
189
  }
194
190
 
195
191
  function normalizeAvailableModels(models: SubagentLaunchContractInput["availableModels"]): AvailableModelInfo[] {
@@ -258,6 +254,15 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
258
254
  return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
259
255
  }
260
256
  const agent = resolvedAgent.agent;
257
+ let extensionBindings: ExtensionBindings | undefined;
258
+ try {
259
+ extensionBindings = normalizeExtensionBindings(input.extensionBindings)?.value;
260
+ } catch (error) {
261
+ return { ok: false, code: "invalid_extension_bindings", message: error instanceof Error ? error.message : String(error), diagnostics };
262
+ }
263
+ if (extensionBindings !== undefined && (agent.runner?.type === "external-cli" || agent.runner?.type === "external-job")) {
264
+ return { ok: false, code: "unsupported_mode", message: `extensionBindings is not supported for runner.type='${agent.runner.type}'.`, diagnostics };
265
+ }
261
266
  const context = resolveLaunchContractContext(input, agent);
262
267
  if (context === "fork") {
263
268
  diagnostics.push({ code: "host_required", severity: "host-required", message: "Exact fork session branching and fork-thinking downgrade checks require Pi host session and model-registry snapshots." });
@@ -289,12 +294,18 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
289
294
 
290
295
  const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
291
296
  const availableModels = normalizeAvailableModels(input.availableModels);
292
- const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
297
+ const preferredProvider = agent.modelProvider ?? input.preferredProvider ?? input.parentModel?.provider;
293
298
  const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
294
299
  const primaryModel = externalRunner
295
300
  ? undefined
296
301
  : resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: modelScopes });
297
302
  const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
303
+ const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
304
+ discovered.maxThinking,
305
+ input.thinkingCeiling,
306
+ input.inheritedThinkingCeiling,
307
+ decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
308
+ );
298
309
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
299
310
  const modelCandidates = externalRunner
300
311
  ? []
@@ -303,8 +314,19 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
303
314
  primaryModelFromParent: inheritsParentModel(input.model, agent.model, input.parentModel),
304
315
  })
305
316
  .map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
317
+ if (!externalRunner) {
318
+ try {
319
+ assertThinkingWithinCeiling({ model, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
320
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
321
+ } catch (error) {
322
+ const message = error instanceof Error ? error.message : String(error);
323
+ diagnostics.push({ code: "thinking_ceiling", severity: "error", message });
324
+ return { ok: false, code: "thinking_ceiling", message, diagnostics };
325
+ }
326
+ }
306
327
  let toolPlan: PiLaunchToolPlan;
307
328
  const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
329
+ const fast = input.fast ?? agent.fast;
308
330
  try {
309
331
  toolPlan = resolvePiLaunchToolPlan({
310
332
  tools: agent.tools,
@@ -314,6 +336,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
314
336
  cwd: effectiveCwd,
315
337
  requireReadTool: resolvedSkills.resolved.length > 0,
316
338
  structuredOutput: Boolean(input.outputSchema),
339
+ fast,
340
+ model,
341
+ modelCandidates,
317
342
  capabilityCeiling: effectiveCapabilityCeiling,
318
343
  agentName: agent.name,
319
344
  permissionRules,
@@ -372,6 +397,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
372
397
  ...(model ? { model } : {}),
373
398
  modelCandidates,
374
399
  ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
400
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
375
401
  systemPromptMode: agent.systemPromptMode,
376
402
  inheritProjectContext: agent.inheritProjectContext,
377
403
  inheritSkills: agent.inheritSkills,
@@ -424,6 +450,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
424
450
  definitionDigest,
425
451
  ...(model ? { model } : {}),
426
452
  modelCandidates,
453
+ ...(fast !== undefined ? { fast } : {}),
427
454
  ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
428
455
  systemPrompt: effectiveSystemPrompt,
429
456
  systemPromptMode: agent.systemPromptMode,
@@ -436,6 +463,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
436
463
  ...(outputPath ? { outputPath } : {}),
437
464
  outputMode: behavior.outputMode,
438
465
  ...(input.outputSchema ? { structuredOutputSchema: input.outputSchema } : {}),
466
+ ...(extensionBindings ? { extensionBindings } : {}),
439
467
  }),
440
468
  };
441
469
  return { ok: true, contract: { ...contractBase, digest: digestContract(contractBase) } };
@@ -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,
@@ -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";
@@ -436,6 +437,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
436
437
  grantHistory: [],
437
438
  },
438
439
  activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
440
+ herdrProjectPanes: new Map(),
439
441
  asyncJobs: new Map(),
440
442
  fleetJobs: new Map(),
441
443
  foregroundRuns: new Map(),
@@ -501,7 +503,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
501
503
  ...all.user,
502
504
  ...all.project,
503
505
  ];
504
- 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
+ };
505
512
  };
506
513
  const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
507
514
  widgetEnabled: asyncWidgetEnabled,
@@ -600,6 +607,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
600
607
  const expandKey = keyText("app.tools.expand");
601
608
  text += `\n ${theme.fg("dim", `${expandKey} full notification`)}`;
602
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
+ }
603
619
  if (details.sessionLabel && details.sessionValue) {
604
620
  text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
605
621
  }
@@ -732,6 +748,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
732
748
  const herdrStatusBridge = registerHerdrStatusBridge({
733
749
  events: pi.events,
734
750
  getRuns: activeHerdrRuns,
751
+ getProjectPaneCount: () => [...(state.herdrProjectPanes?.values() ?? [])].filter((pane) => pane.state === "open").length,
735
752
  async runHerdr(args) {
736
753
  await pi.exec(process.env.HERDR_BIN || "herdr", [...args], { timeout: 5_000 });
737
754
  },
@@ -840,6 +857,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
840
857
  granted: 0,
841
858
  grantHistory: [],
842
859
  };
860
+ const projectPaneOwnerRoot = path.resolve(ctx.cwd);
861
+ restoreHerdrProjectPaneSnapshots(state, [...new Set([...(state.herdrProjectPanes?.keys() ?? []), ...listHerdrProjectPaneRoots(projectPaneOwnerRoot), projectPaneOwnerRoot])]);
843
862
  // Set PI_SUBAGENT_PARENT_SESSION for permission-system forwarding.
844
863
  // Only set in the root session (the interactive UI session), not in
845
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: `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
 
@@ -145,6 +145,7 @@ export const ParallelTaskSchema = Type.Object({
145
145
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
146
146
  skill: Type.Optional(SkillOverride),
147
147
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
148
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. This can increase quota or cost." })),
148
149
  toolBudget: Type.Optional(ToolBudgetOverride),
149
150
  acceptance: Type.Optional(AcceptanceOverride),
150
151
  agentContract: Type.Optional(AgentContractOverride),
@@ -175,6 +176,7 @@ export const DynamicParallelTemplateSchema = Type.Object({
175
176
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
176
177
  skill: Type.Optional(SkillOverride),
177
178
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
179
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. This can increase quota or cost." })),
178
180
  toolBudget: Type.Optional(ToolBudgetOverride),
179
181
  acceptance: Type.Optional(AcceptanceOverride),
180
182
  agentContract: Type.Optional(AgentContractOverride),
@@ -203,6 +205,7 @@ export const ChainItem = Type.Object({
203
205
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
204
206
  skill: Type.Optional(SkillOverride),
205
207
  model: Type.Optional(Type.String({ description: "Override model for this step" })),
208
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. This can increase quota or cost." })),
206
209
  toolBudget: Type.Optional(ToolBudgetOverride),
207
210
  acceptance: Type.Optional(AcceptanceOverride),
208
211
  agentContract: Type.Optional(AgentContractOverride),
@@ -255,6 +258,7 @@ const ControlOverrides = Type.Object({
255
258
  const SubagentParamProperties = {
256
259
  agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
257
260
  task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
261
+ extensionBindings: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Namespaced, bounded plain-JSON metadata delivered only to the child runtime. Namespace keys use package.name/1 syntax." })),
258
262
  // Management action (when present, tool operates in management mode)
259
263
  action: Type.Optional(Type.String({ minLength: 1,
260
264
  description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
@@ -271,6 +275,7 @@ const SubagentParamProperties = {
271
275
  })),
272
276
  handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
273
277
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
278
+ childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
274
279
  view: Type.Optional(Type.String({
275
280
  enum: ["fleet", "transcript"],
276
281
  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 +312,7 @@ const SubagentParamProperties = {
307
312
  ],
308
313
  description: "Agent config for create/update. Object or JSON string."
309
314
  })),
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." })),
315
+ 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
316
  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
317
  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
318
  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." })),
@@ -342,6 +347,7 @@ const SubagentParamProperties = {
342
347
  outputMode: Type.Optional(OutputModeOverride),
343
348
  skill: Type.Optional(SkillOverride),
344
349
  model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry." })),
350
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. Default false. This can increase quota or cost." })),
345
351
  outputSchema: Type.Optional(JsonSchemaObject),
346
352
  agentContract: Type.Optional(AgentContractOverride),
347
353
  acceptance: Type.Optional(AcceptanceOverride),
@@ -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,55 @@
1
+ import type { HerdrClient, HerdrErrorCode } from "./client.ts";
2
+
3
+ export type HerdrFocusErrorCode = HerdrErrorCode | "PANE_FOCUS_UNSUPPORTED" | "INVALID_PANE_RESPONSE";
4
+
5
+ export type HerdrPaneFocusResult =
6
+ | { ok: true; data: { paneId: string; tabId?: string; workspaceId?: string } }
7
+ | { ok: false; error: { code: HerdrFocusErrorCode; message: string; details?: unknown } };
8
+
9
+ export function herdrPaneRecord(value: unknown): Record<string, unknown> | undefined {
10
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
11
+ const record = value as Record<string, unknown>;
12
+ return record.pane && typeof record.pane === "object" && !Array.isArray(record.pane)
13
+ ? record.pane as Record<string, unknown>
14
+ : record;
15
+ }
16
+
17
+ function text(record: Record<string, unknown>, ...keys: string[]): string | undefined {
18
+ for (const key of keys) if (typeof record[key] === "string" && record[key]) return record[key] as string;
19
+ return undefined;
20
+ }
21
+
22
+ export function herdrPaneFocusTarget(value: unknown): { paneId?: string; tabId?: string; workspaceId?: string } {
23
+ const pane = herdrPaneRecord(value);
24
+ if (!pane) return {};
25
+ return {
26
+ paneId: text(pane, "pane_id", "paneId", "id"),
27
+ tabId: text(pane, "tab_id", "tabId"),
28
+ workspaceId: text(pane, "workspace_id", "workspaceId"),
29
+ };
30
+ }
31
+
32
+ export async function focusHerdrPane(client: HerdrClient, paneId: string, signal?: AbortSignal): Promise<HerdrPaneFocusResult> {
33
+ const live = await client.run(["pane", "get", paneId], { timeoutMs: 5_000, signal });
34
+ if (live.ok === false) return live;
35
+ const target = herdrPaneFocusTarget(live.data);
36
+ if (!target.paneId) {
37
+ return { ok: false, error: { code: "INVALID_PANE_RESPONSE", message: `Herdr pane get returned no pane id for '${paneId}'.`, details: live.data } };
38
+ }
39
+ if (target.tabId) {
40
+ const focused = await client.run(["tab", "focus", target.tabId], { timeoutMs: 5_000, signal });
41
+ return focused.ok ? { ok: true, data: { paneId: target.paneId, tabId: target.tabId, ...(target.workspaceId ? { workspaceId: target.workspaceId } : {}) } } : focused;
42
+ }
43
+ if (target.workspaceId) {
44
+ const focused = await client.run(["workspace", "focus", target.workspaceId], { timeoutMs: 5_000, signal });
45
+ return focused.ok ? { ok: true, data: { paneId: target.paneId, workspaceId: target.workspaceId } } : focused;
46
+ }
47
+ return {
48
+ ok: false,
49
+ error: {
50
+ code: "PANE_FOCUS_UNSUPPORTED",
51
+ message: `Herdr pane '${paneId}' has no tab_id or workspace_id. Select it in Herdr manually, or upgrade Herdr focus support.`,
52
+ details: live.data,
53
+ },
54
+ };
55
+ }