pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -335,7 +335,6 @@ function resolveProbeStatus(text: string, timedOut: boolean): ProbeStatus {
335
335
 
336
336
  async function probeModel(
337
337
  pi: Pick<ExtensionAPI, "exec"> | { exec?: ExtensionAPI["exec"] },
338
- ctx: Pick<ExtensionContext, "cwd">,
339
338
  fullId: string,
340
339
  ): Promise<{ status: ProbeStatus; message?: string }> {
341
340
  if (typeof pi.exec !== "function") {
@@ -401,7 +400,7 @@ function filterDominatedModels(models: ProviderModelCatalogModel[]): ProviderMod
401
400
  return models.filter((candidate, index) => !models.some((other, otherIndex) => otherIndex !== index && dominatesModel(other, candidate)));
402
401
  }
403
402
 
404
- function buildProfileFile(kind: ProfileKind, models: { cheap: string; medium: string; strong: string }): SubagentProfileFile {
403
+ function buildProfileFile(models: { cheap: string; medium: string; strong: string }): SubagentProfileFile {
405
404
  return {
406
405
  subagents: {
407
406
  agentOverrides: {
@@ -544,7 +543,7 @@ export async function refreshProviderModelCatalog(
544
543
  const fullId = `${modelRecord.provider}/${modelRecord.id}`;
545
544
  const probe = options.probe === false
546
545
  ? { status: "skipped" as const, message: "Live probing disabled." }
547
- : await probeModel(pi, ctx, fullId);
546
+ : await probeModel(pi, fullId);
548
547
  observedModels.push({ rawModel, modelRecord, fullId, probe });
549
548
  }
550
549
  const classificationContext = buildClassificationContext(observedModels.map(({ modelRecord }) => ({
@@ -624,8 +623,8 @@ export async function generateProfilesForProvider(
624
623
  const dir = ensureSubagentProfilesDir();
625
624
  const quotaPath = path.join(dir, `${normalizedProvider}.quota.json`);
626
625
  const qualityPath = path.join(dir, `${normalizedProvider}.quality.json`);
627
- writeJsonFile(quotaPath, buildProfileFile("quota", quotaModels));
628
- writeJsonFile(qualityPath, buildProfileFile("quality", qualityModels));
626
+ writeJsonFile(quotaPath, buildProfileFile(quotaModels));
627
+ writeJsonFile(qualityPath, buildProfileFile(qualityModels));
629
628
  const selectedModels = new Set([...Object.values(quotaModels), ...Object.values(qualityModels)]);
630
629
  const selectedHeuristicFallbackCount = profileModels.filter((model) => selectedModels.has(model.fullId) && modelUsesHeuristicClassification(model)).length;
631
630
  return { quotaPath, qualityPath, catalogPath, quotaModels, qualityModels, heuristicFallbackCount, selectedHeuristicFallbackCount };
@@ -649,7 +648,7 @@ export async function checkSubagentProfile(
649
648
  const probeModelId = modelInfo ? `${modelInfo.fullId}${thinkingSuffix}` : entry.model;
650
649
  let probe = probeCache.get(probeModelId);
651
650
  if (!probe) {
652
- probe = await probeModel(pi, ctx, probeModelId);
651
+ probe = await probeModel(pi, probeModelId);
653
652
  probeCache.set(probeModelId, probe);
654
653
  }
655
654
  results.push({
@@ -230,10 +230,10 @@ function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncSt
230
230
  && proof.runId === owner.runId
231
231
  && proof.runnerProcessInstanceId === owner.runnerProcessInstanceId
232
232
  ? { state: "releasable", reason: "matching observed process-terminal proof is present" }
233
- : abandonedRunnerReleaseVerdict(owner, status, proof?.state ?? "missing", options);
233
+ : abandonedRunnerReleaseVerdict(status, proof?.state ?? "missing", options);
234
234
  }
235
235
 
236
- function abandonedRunnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus, proofState: string, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
236
+ function abandonedRunnerReleaseVerdict(status: AsyncStatus, proofState: string, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
237
237
  const proofReason = `process-terminal proof is ${proofState}`;
238
238
  const thresholdMs = resolveAbandonedSlotReleaseAfterMs(options.abandonedSlotReleaseAfterMs);
239
239
  if (thresholdMs === false) return { state: "retained", reason: `${proofReason}; abandoned-timeout policy is disabled` };
@@ -5,7 +5,6 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
- import * as os from "node:os";
9
8
  import * as path from "node:path";
10
9
  import { fileURLToPath } from "node:url";
11
10
  import { createRequire } from "node:module";
@@ -14,9 +13,10 @@ import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext,
14
13
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
15
14
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
16
15
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
+ import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
18
18
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
- import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveExistingReadPaths, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
19
+ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
20
20
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
21
  import type { ContextMode } from "../shared/context-mode.ts";
22
22
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
@@ -25,7 +25,7 @@ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
25
25
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
26
26
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
27
27
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
28
- import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
28
+ import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
29
29
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
30
30
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
31
31
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -49,12 +49,12 @@ import {
49
49
  type MaxOutputConfig,
50
50
  type NestedRouteInfo,
51
51
  type ResolvedControlConfig,
52
- type ResolvedTurnBudget,
53
52
  type ResolvedToolBudget,
54
53
  type RunFanoutBudgetDescriptor,
55
54
  type ToolBudgetConfig,
56
55
  type SubagentRunMode,
57
56
  type SteeringRecoveryDescriptor,
57
+ type WorkflowLaneMetadata,
58
58
  type UsageBudgetConfig,
59
59
  DIRS,
60
60
  SUBAGENT_ASYNC_STARTED_EVENT,
@@ -65,7 +65,6 @@ import {
65
65
  } from "../../shared/types.ts";
66
66
  import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
67
67
  import { resultFilePath } from "./result-files.ts";
68
- import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
69
68
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
70
69
  import { usageBudgetState } from "../shared/usage-budget.ts";
71
70
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
@@ -78,6 +77,7 @@ import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling,
78
77
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
79
78
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
80
79
  import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
80
+ import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
81
81
 
82
82
  const require = createRequire(import.meta.url);
83
83
  const piPackageRoot = resolvePiPackageRoot();
@@ -173,6 +173,7 @@ interface AsyncChainParams {
173
173
  dynamicFanoutMaxItems?: number;
174
174
  maxSubagentDepth: number;
175
175
  waitToolEnabled?: boolean;
176
+ waitToolDefaultTimeoutMs?: number;
176
177
  worktreeSetupHook?: string;
177
178
  worktreeSetupHookTimeoutMs?: number;
178
179
  worktreeBaseDir?: string;
@@ -183,7 +184,6 @@ interface AsyncChainParams {
183
184
  acceptance?: AcceptanceInput;
184
185
  fast?: boolean;
185
186
  timeoutMs?: number;
186
- turnBudget?: ResolvedTurnBudget;
187
187
  toolBudget?: ResolvedToolBudget;
188
188
  usageBudget?: UsageBudgetConfig;
189
189
  configToolBudget?: ResolvedToolBudget;
@@ -200,6 +200,7 @@ interface AsyncChainParams {
200
200
  runFanoutBudget?: RunFanoutBudgetDescriptor;
201
201
  parentWorkflowRunId?: string;
202
202
  workflowKey?: string;
203
+ lane?: WorkflowLaneMetadata;
203
204
  activeAsyncCapacity?: ActiveAsyncCapacityHandle;
204
205
  }
205
206
 
@@ -228,6 +229,7 @@ interface AsyncSingleParams {
228
229
  reads?: string[] | false;
229
230
  outputMode?: "inline" | "file-only";
230
231
  outputBaseDir?: string;
232
+ outputClaimPath?: string;
231
233
  agentContract?: AgentContract;
232
234
  structuredOutputSchema?: JsonSchemaObject;
233
235
  modelOverride?: string;
@@ -237,6 +239,7 @@ interface AsyncSingleParams {
237
239
  availableModels?: AvailableModelInfo[];
238
240
  maxSubagentDepth: number;
239
241
  waitToolEnabled?: boolean;
242
+ waitToolDefaultTimeoutMs?: number;
240
243
  worktreeSetupHook?: string;
241
244
  worktreeSetupHookTimeoutMs?: number;
242
245
  worktreeBaseDir?: string;
@@ -251,7 +254,6 @@ interface AsyncSingleParams {
251
254
  absoluteDeadlineAt?: number;
252
255
  /** Optional per-call hard toolTimeoutMs override (highest precedence). */
253
256
  toolTimeoutMs?: number;
254
- turnBudget?: { maxTurns: number; graceTurns?: number };
255
257
  toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
256
258
  usageBudget?: UsageBudgetConfig;
257
259
  configToolBudget?: ResolvedToolBudget;
@@ -265,6 +267,7 @@ interface AsyncSingleParams {
265
267
  runFanoutBudget?: RunFanoutBudgetDescriptor;
266
268
  parentWorkflowRunId?: string;
267
269
  workflowKey?: string;
270
+ lane?: WorkflowLaneMetadata;
268
271
  workflowAwaitAsync?: boolean;
269
272
  activeAsyncCapacity?: ActiveAsyncCapacityHandle;
270
273
  externalJobFollowUp?: {
@@ -303,6 +306,7 @@ export interface AsyncRunnerStepBuildParams {
303
306
  dynamicFanoutMaxItems?: number;
304
307
  maxSubagentDepth: number;
305
308
  waitToolEnabled?: boolean;
309
+ waitToolDefaultTimeoutMs?: number;
306
310
  worktreeBaseDir?: string;
307
311
  asyncDir: string;
308
312
  outputBaseDir?: string;
@@ -804,19 +808,21 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
804
808
  envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
805
809
  });
806
810
  if (resolvedToolTimeout.error) throw new AsyncStartValidationError(resolvedToolTimeout.error);
807
- const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
808
- const instructionCwd = behaviorCwd ?? stepCwd;
809
- const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
810
- let behavior = suppressProgressForReadOnlyTask(resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
811
- const inheritedRelativeParallelOutput = parallelOutputNamespace && s.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output);
812
- if (inheritedRelativeParallelOutput && parallelOutputNamespace.taskIndex !== undefined) {
813
- behavior = {
814
- ...behavior,
815
- output: path.join(`parallel-${parallelOutputNamespace.stepIndex}`, `${parallelOutputNamespace.taskIndex}-${s.agent}`, behavior.output as string),
816
- };
817
- }
818
- const namespaceOutputPath = Boolean(inheritedRelativeParallelOutput && parallelOutputNamespace.taskIndex === undefined);
819
- const skillNames = behavior.skills === false ? [] : behavior.skills;
811
+ const launchPlan = planChildLaunch({
812
+ agentConfig: a,
813
+ stepOverrides: buildStepOverrides(s),
814
+ task: s.task,
815
+ originalTask,
816
+ runnerCwd,
817
+ runtimeCwd: ctx.cwd,
818
+ stepCwdInput: s.cwd,
819
+ behaviorCwd,
820
+ chainSkills,
821
+ outputBaseDir,
822
+ parallelOutputNamespace,
823
+ resolvedBehavior,
824
+ });
825
+ const { stepCwd, instructionCwd, readExistenceCwd, behavior, namespaceOutputPath, outputPath, skillNames } = launchPlan;
820
826
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
821
827
  skillNames,
822
828
  stepCwd,
@@ -841,7 +847,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
841
847
  const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
842
848
  if (behavior.progress) progressInstructionCreated = true;
843
849
  const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
844
- const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd, outputBaseDir);
845
850
  if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
846
851
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
847
852
  if (validationError) throw new AsyncStartValidationError(validationError);
@@ -896,6 +901,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
896
901
  const fast = s.fast ?? params.fast ?? a.fast;
897
902
  const toolPlan = resolvePiLaunchToolPlan({
898
903
  tools: a.tools,
904
+ allowNestedSubagents: a.allowNestedSubagents,
899
905
  extensions: a.extensions,
900
906
  subagentOnlyExtensions: a.subagentOnlyExtensions,
901
907
  mcpDirectTools: a.mcpDirectTools,
@@ -957,6 +963,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
957
963
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
958
964
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
959
965
  tools: a.tools,
966
+ allowNestedSubagents: a.allowNestedSubagents,
960
967
  extensions: a.extensions,
961
968
  subagentOnlyExtensions: a.subagentOnlyExtensions,
962
969
  mcpDirectTools: a.mcpDirectTools,
@@ -976,6 +983,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
976
983
  timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
977
984
  toolTimeoutMs: resolvedToolTimeout.toolTimeoutMs,
978
985
  waitToolEnabled: params.waitToolEnabled,
986
+ waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
979
987
  effectiveAcceptance: resolveEffectiveAcceptance({
980
988
  explicit: s.acceptance,
981
989
  agentName: s.agent,
@@ -1203,6 +1211,7 @@ export function executeAsyncChain(
1203
1211
  dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
1204
1212
  maxSubagentDepth,
1205
1213
  waitToolEnabled: params.waitToolEnabled,
1214
+ waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1206
1215
  worktreeBaseDir,
1207
1216
  asyncDir,
1208
1217
  fast: params.fast,
@@ -1224,7 +1233,6 @@ export function executeAsyncChain(
1224
1233
  }
1225
1234
  const { steps, runnerCwd, workflowGraph, eventChain } = built;
1226
1235
  const deadlineAt = params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined;
1227
- const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1228
1236
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1229
1237
  let childTargetIndex = 0;
1230
1238
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
@@ -1281,7 +1289,6 @@ export function executeAsyncChain(
1281
1289
  worktreeSetupHookTimeoutMs,
1282
1290
  worktreeBaseDir,
1283
1291
  controlConfig,
1284
- turnBudget: params.turnBudget,
1285
1292
  toolBudget: params.toolBudget,
1286
1293
  usageBudget: params.usageBudget,
1287
1294
  controlIntercomTarget,
@@ -1400,7 +1407,6 @@ export function executeAsyncChain(
1400
1407
  chainStepCount: eventChain.length,
1401
1408
  parallelGroups,
1402
1409
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1403
- ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1404
1410
  startedAt: now,
1405
1411
  lastUpdate: now,
1406
1412
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
@@ -1431,7 +1437,6 @@ export function executeAsyncChain(
1431
1437
  asyncDir,
1432
1438
  ...(sessionRoot ? { sessionRoot } : {}),
1433
1439
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1434
- ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1435
1440
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
1436
1441
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1437
1442
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
@@ -1448,7 +1453,7 @@ export function executeAsyncChain(
1448
1453
 
1449
1454
  return {
1450
1455
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`, ctx.interactive === true) }],
1451
- details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}), ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1456
+ details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}), ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1452
1457
  };
1453
1458
  }
1454
1459
 
@@ -1483,6 +1488,13 @@ export function executeAsyncSingle(
1483
1488
  childIntercomTarget,
1484
1489
  nestedRoute,
1485
1490
  } = params;
1491
+ let lane: WorkflowLaneMetadata | undefined;
1492
+ try {
1493
+ lane = normalizeWorkflowLaneMetadata(params.lane, "lane");
1494
+ assertWorkflowLaneKey(lane, params.workflowKey, "lane");
1495
+ } catch (error) {
1496
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1497
+ }
1486
1498
  const task = params.task ?? "";
1487
1499
  let extensionBindings: ExtensionBindings | undefined;
1488
1500
  try {
@@ -1616,7 +1628,6 @@ export function executeAsyncSingle(
1616
1628
  });
1617
1629
  if (resolvedToolTimeout.error) return formatAsyncStartError("single", resolvedToolTimeout.error);
1618
1630
  const toolTimeoutMs = resolvedToolTimeout.toolTimeoutMs;
1619
- const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1620
1631
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1621
1632
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1622
1633
  const structuredOutput = params.structuredOutputSchema
@@ -1639,9 +1650,9 @@ export function executeAsyncSingle(
1639
1650
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1640
1651
  }
1641
1652
  }
1642
- const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1643
1653
  const toolPlan = resolvePiLaunchToolPlan({
1644
1654
  tools: agentConfig.tools,
1655
+ allowNestedSubagents: agentConfig.allowNestedSubagents,
1645
1656
  extensions: agentConfig.extensions,
1646
1657
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1647
1658
  mcpDirectTools: agentConfig.mcpDirectTools,
@@ -1680,7 +1691,7 @@ export function executeAsyncSingle(
1680
1691
  ...((params.fast ?? agentConfig.fast) !== undefined ? { fast: params.fast ?? agentConfig.fast } : {}),
1681
1692
  ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1682
1693
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1683
- systemPrompt: effectiveSystemPrompt,
1694
+ systemPrompt,
1684
1695
  systemPromptMode: agentConfig.systemPromptMode,
1685
1696
  inheritProjectContext: agentConfig.inheritProjectContext,
1686
1697
  inheritGlobalContext: agentConfig.inheritGlobalContext,
@@ -1706,6 +1717,7 @@ export function executeAsyncSingle(
1706
1717
  const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
1707
1718
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1708
1719
  version: 1,
1720
+ ...(lane ? { lane } : {}),
1709
1721
  launchContractDigest,
1710
1722
  ...(extensionBindings ? { extensionBindings } : {}),
1711
1723
  runFanoutBudget,
@@ -1723,6 +1735,7 @@ export function executeAsyncSingle(
1723
1735
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1724
1736
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1725
1737
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1738
+ ...(recoveryAgentConfig.allowNestedSubagents !== undefined ? { allowNestedSubagents: recoveryAgentConfig.allowNestedSubagents } : {}),
1726
1739
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1727
1740
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
1728
1741
  ...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
@@ -1745,7 +1758,6 @@ export function executeAsyncSingle(
1745
1758
  ...(params.context ? { context: params.context } : {}),
1746
1759
  ...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
1747
1760
  ...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
1748
- ...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
1749
1761
  ...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
1750
1762
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
1751
1763
  ...(maxOutput ? { maxOutput } : {}),
@@ -1790,6 +1802,7 @@ export function executeAsyncSingle(
1790
1802
  ...(params.modelOverrideFromParent ? { skipPrimaryModelVerification: true } : {}),
1791
1803
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1792
1804
  tools: agentConfig.tools,
1805
+ allowNestedSubagents: agentConfig.allowNestedSubagents,
1793
1806
  extensions: agentConfig.extensions,
1794
1807
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1795
1808
  mcpDirectTools: agentConfig.mcpDirectTools,
@@ -1804,10 +1817,12 @@ export function executeAsyncSingle(
1804
1817
  inheritSkills: agentConfig.inheritSkills,
1805
1818
  skills: resolvedSkills.map((r) => r.name),
1806
1819
  outputPath,
1820
+ ...(params.outputClaimPath ? { outputClaimPath: params.outputClaimPath } : {}),
1807
1821
  outputMode,
1808
1822
  ...(!externalRunner && sessionFile ? { sessionFile } : {}),
1809
1823
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
1810
1824
  waitToolEnabled: params.waitToolEnabled,
1825
+ waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1811
1826
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1812
1827
  definitionDigest: agentDefinitionDigest(agentConfig),
1813
1828
  launchBindingTask: task,
@@ -1819,6 +1834,7 @@ export function executeAsyncSingle(
1819
1834
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1820
1835
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1821
1836
  ...(params.worktree === true ? { worktree: true } : {}),
1837
+ ...(lane ? { lane } : {}),
1822
1838
  },
1823
1839
  ],
1824
1840
  resultPath: params.parentWorkflowRunId !== undefined && (params.revivalLease !== undefined || params.workflowAwaitAsync === true)
@@ -1844,7 +1860,6 @@ export function executeAsyncSingle(
1844
1860
  timeoutMs,
1845
1861
  deadlineAt,
1846
1862
  toolTimeoutMs,
1847
- turnBudget: params.turnBudget,
1848
1863
  toolBudget: params.toolBudget,
1849
1864
  usageBudget: params.usageBudget,
1850
1865
  controlIntercomTarget,
@@ -1855,6 +1870,7 @@ export function executeAsyncSingle(
1855
1870
  runFanoutBudget,
1856
1871
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1857
1872
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1873
+ ...(lane ? { lane } : {}),
1858
1874
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
1859
1875
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1860
1876
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
@@ -1877,7 +1893,8 @@ export function executeAsyncSingle(
1877
1893
  lastUpdate: initialStatusAt,
1878
1894
  currentStep: 0,
1879
1895
  chainStepCount: 1,
1880
- steps: [{ agent, status: "pending", ...(model ? { model } : {}), ...(contextLimit !== undefined ? { contextLimit } : {}) }],
1896
+ ...(lane ? { lane } : {}),
1897
+ steps: [{ agent, status: "pending", ...(lane ? { lane } : {}), ...(model ? { model } : {}), ...(contextLimit !== undefined ? { contextLimit } : {}) }],
1881
1898
  },
1882
1899
  path.join(asyncDir, "status.json"),
1883
1900
  (proof) => emitProcessTerminalEvent(ctx, proof),
@@ -1927,7 +1944,6 @@ export function executeAsyncSingle(
1927
1944
  agents: [agent],
1928
1945
  chainStepCount: 1,
1929
1946
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1930
- ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1931
1947
  startedAt: now,
1932
1948
  lastUpdate: now,
1933
1949
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
@@ -1955,7 +1971,6 @@ export function executeAsyncSingle(
1955
1971
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1956
1972
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1957
1973
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1958
- ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1959
1974
  ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
1960
1975
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1961
1976
  nestedRoute,
@@ -1964,6 +1979,6 @@ export function executeAsyncSingle(
1964
1979
 
1965
1980
  return {
1966
1981
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
1967
- details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, launchResolvedExtensions, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: resolvedToolBudget.budget ?? params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) } as Details,
1982
+ details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, launchResolvedExtensions, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.toolBudget ? { toolBudget: resolvedToolBudget.budget ?? params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) } as Details,
1968
1983
  };
1969
1984
  }
@@ -25,6 +25,8 @@ import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
25
25
  import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } from "../shared/external-job-bridge.ts";
26
26
  import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
27
27
  import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
28
+ import { validHostStepNodes } from "../shared/host-step-status.ts";
29
+ import { withCachedUiContext } from "../../shared/extension-context.ts";
28
30
 
29
31
  interface AsyncJobTrackerOptions {
30
32
  completionRetentionMs?: number;
@@ -78,23 +80,19 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
78
80
  const watch = options.watch ?? fs.watch;
79
81
  const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
80
82
  const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
83
+ const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
84
+ const cached = state.lastUiContext;
85
+ return withCachedUiContext(cached, () => {
86
+ if (state.lastUiContext === cached) state.lastUiContext = null;
87
+ }, run);
88
+ };
81
89
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
82
90
  if (state.widgetsSuspended) return;
83
91
  renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
84
92
  (ctx.ui as { requestRender?: () => void }).requestRender?.();
85
93
  };
86
94
  const rerenderLastWidget = (jobs = Array.from(state.asyncJobs.values())) => {
87
- const ctx = state.lastUiContext;
88
- if (!ctx) return;
89
- try {
90
- if (ctx.hasUI) rerenderWidget(ctx, jobs);
91
- } catch (error) {
92
- if (error instanceof Error && error.message.includes("extension ctx is stale")) {
93
- state.lastUiContext = null;
94
- return;
95
- }
96
- throw error;
97
- }
95
+ withLastUiContext((ctx) => rerenderWidget(ctx, jobs));
98
96
  };
99
97
  const requestLastWidgetRender = () => {
100
98
  if (options.widgetEnabled === false) return;
@@ -147,6 +145,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
147
145
  currentStep: run.currentStep,
148
146
  chainStepCount: run.chainStepCount,
149
147
  parallelGroups: groups,
148
+ hostSteps: run.hostSteps,
149
+ preflight: run.preflight,
150
150
  steps: visibleSteps,
151
151
  stepsTotal: visibleSteps.length,
152
152
  runningSteps: visibleSteps.filter((step) => step.status === "running").length,
@@ -342,7 +342,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
342
342
  };
343
343
 
344
344
  const refreshJob = (job: AsyncJobState): boolean => {
345
- const widgetExpanded = state.lastUiContext?.hasUI ? state.lastUiContext.ui.getToolsExpanded?.() ?? false : false;
345
+ const widgetExpanded = withLastUiContext((ctx) => ctx.ui.getToolsExpanded?.() ?? false) ?? false;
346
346
  const widgetStateBefore = widgetRenderKey(job, widgetExpanded);
347
347
  let nestedRefreshFailed = false;
348
348
  const refreshNestedProjection = () => {
@@ -399,7 +399,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
399
399
  job.mode = status.mode;
400
400
  job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
401
401
  job.workflowKey = status.workflowKey ?? job.workflowKey;
402
+ job.lane = status.lane ?? job.lane;
402
403
  job.workflow = status.workflow ?? job.workflow;
404
+ job.hostSteps = validHostStepNodes(status.workflowGraph);
403
405
  const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
404
406
  if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
405
407
  job.workflowChildren = workflowChildren ?? job.workflowChildren;