pi-subagents 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +66 -5
  2. package/README.md +1 -1
  3. package/agents/reviewer.md +12 -2
  4. package/docs/agents.md +4 -4
  5. package/docs/configuration.md +6 -6
  6. package/docs/extension-api.md +14 -4
  7. package/docs/models.md +41 -6
  8. package/docs/observability.md +14 -3
  9. package/docs/tool-reference.md +21 -6
  10. package/docs/workflows.md +7 -1
  11. package/index.ts +10 -1
  12. package/package.json +1 -1
  13. package/prompts/council.md +19 -7
  14. package/prompts/parallel-review.md +5 -1
  15. package/prompts/review-loop.md +9 -5
  16. package/skills/council-mode/SKILL.md +50 -26
  17. package/skills/pi-subagents/SKILL.md +5 -1
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  19. package/skills/pi-subagents/references/execution-controls.md +13 -4
  20. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  21. package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
  22. package/src/agents/agent-management.ts +115 -14
  23. package/src/agents/agent-serializer.ts +2 -2
  24. package/src/agents/agents.ts +195 -47
  25. package/src/api/external-job-provider.ts +10 -1
  26. package/src/api/preflight.ts +33 -19
  27. package/src/api/project-panes.ts +2 -0
  28. package/src/extension/doctor.ts +10 -0
  29. package/src/extension/fanout-child.ts +3 -2
  30. package/src/extension/index.ts +24 -4
  31. package/src/extension/public-execution.ts +12 -9
  32. package/src/extension/rpc.ts +77 -3
  33. package/src/extension/schemas.ts +2 -1
  34. package/src/extension/tool-description.ts +9 -6
  35. package/src/extension/tool-result.ts +19 -0
  36. package/src/inspectors/herdr/client.ts +3 -3
  37. package/src/inspectors/herdr/focus.ts +55 -0
  38. package/src/inspectors/herdr/project-panes.ts +228 -44
  39. package/src/integrations/herdr-status.ts +26 -4
  40. package/src/runs/background/async-execution.ts +112 -13
  41. package/src/runs/background/async-job-tracker.ts +33 -14
  42. package/src/runs/background/async-resume.ts +20 -2
  43. package/src/runs/background/async-retention.ts +1 -1
  44. package/src/runs/background/async-status.ts +10 -0
  45. package/src/runs/background/chain-root-attachment.ts +5 -0
  46. package/src/runs/background/control-channel.ts +98 -10
  47. package/src/runs/background/notify.ts +62 -1
  48. package/src/runs/background/result-watcher.ts +4 -3
  49. package/src/runs/background/run-status.ts +15 -1
  50. package/src/runs/background/stale-run-reconciler.ts +3 -0
  51. package/src/runs/background/subagent-runner.ts +251 -38
  52. package/src/runs/background/wait-completions.ts +2 -0
  53. package/src/runs/background/wait-tool.ts +4 -3
  54. package/src/runs/foreground/async-stop-action.ts +23 -2
  55. package/src/runs/foreground/execution.ts +110 -8
  56. package/src/runs/foreground/subagent-executor.ts +361 -45
  57. package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
  58. package/src/runs/shared/acceptance.ts +1 -0
  59. package/src/runs/shared/child-identity.ts +36 -0
  60. package/src/runs/shared/completion-guard.ts +50 -1
  61. package/src/runs/shared/external-job-bridge.ts +53 -37
  62. package/src/runs/shared/external-job-runner.ts +126 -23
  63. package/src/runs/shared/model-fallback.ts +46 -15
  64. package/src/runs/shared/model-scope.ts +106 -39
  65. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  66. package/src/runs/shared/parallel-utils.ts +12 -0
  67. package/src/runs/shared/pi-args.ts +31 -1
  68. package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
  69. package/src/runs/shared/tool-availability.ts +1 -3
  70. package/src/shared/launch-contract.ts +6 -5
  71. package/src/shared/thinking-ceiling.ts +52 -0
  72. package/src/shared/types.ts +72 -5
  73. package/src/tui/fleet-status.ts +66 -13
  74. package/src/tui/render.ts +5 -4
  75. package/src/watchdog/permission-arbiter.ts +59 -51
  76. package/src/workflows/scripted-workflow.ts +107 -10
@@ -26,14 +26,16 @@ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
26
26
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
27
27
  import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
28
28
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
29
- import type { ModelScopeConfig } from "../shared/model-scope.ts";
29
+ import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
30
30
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
31
+ import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
31
32
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
32
33
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
33
34
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
34
35
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
35
36
  import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
36
37
  import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
38
+ import { validateImplementationToolContract } from "../shared/completion-guard.ts";
37
39
  import {
38
40
  type AcceptanceInput,
39
41
  type AgentContract,
@@ -187,6 +189,7 @@ interface AsyncChainParams {
187
189
  /** Global cap on simultaneously-running subagent tasks within the async run. */
188
190
  globalConcurrencyLimit?: number;
189
191
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
192
+ thinkingCeiling?: ThinkingLevel;
190
193
  runFanoutBudget?: RunFanoutBudgetDescriptor;
191
194
  parentWorkflowRunId?: string;
192
195
  workflowKey?: string;
@@ -249,10 +252,19 @@ interface AsyncSingleParams {
249
252
  toolTimeoutMsEnv?: string | undefined;
250
253
  allowZeroToolBudget?: boolean;
251
254
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
255
+ thinkingCeiling?: ThinkingLevel;
252
256
  runFanoutBudget?: RunFanoutBudgetDescriptor;
253
257
  parentWorkflowRunId?: string;
254
258
  workflowKey?: string;
259
+ workflowAwaitAsync?: boolean;
255
260
  activeAsyncCapacity?: ActiveAsyncCapacityHandle;
261
+ externalJobFollowUp?: {
262
+ sourceRunId: string;
263
+ sourceStepIndex: number;
264
+ parentProviderJobId: string;
265
+ requestId: string;
266
+ requestDigest: string;
267
+ };
256
268
  }
257
269
 
258
270
  interface AsyncExecutionResult {
@@ -292,6 +304,7 @@ export interface AsyncRunnerStepBuildParams {
292
304
  /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
293
305
  toolTimeoutMsEnv?: string | undefined;
294
306
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
307
+ thinkingCeiling?: ThinkingLevel;
295
308
  }
296
309
 
297
310
  export type AsyncRunnerStepBuildResult =
@@ -791,18 +804,33 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
791
804
  const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
792
805
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
793
806
 
807
+ const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
808
+ const primaryModelFromParent = inheritsParentModel(s.model, a.model, ctx.currentModel);
794
809
  const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
795
810
  s.model,
796
811
  a.model,
797
812
  ctx.currentModel,
798
813
  availableModels,
799
- ctx.currentModelProvider,
800
- { scope: ctx.modelScope },
814
+ a.modelProvider ?? ctx.currentModelProvider,
815
+ { scope: modelScopes },
801
816
  );
802
817
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
803
818
  const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
804
819
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
820
+ const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
821
+ params.thinkingCeiling,
822
+ a.maxThinking,
823
+ decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
824
+ );
825
+ if (!externalRunner) {
826
+ try {
827
+ assertThinkingWithinCeiling({ model, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
828
+ } catch (error) {
829
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
830
+ }
831
+ }
805
832
  const agentContract = s.agentContract ?? params.agentContract;
833
+ const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
806
834
  const toolPlan = resolvePiLaunchToolPlan({
807
835
  tools: a.tools,
808
836
  extensions: a.extensions,
@@ -814,12 +842,40 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
814
842
  capabilityCeiling: params.capabilityCeiling,
815
843
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
816
844
  agentName: a.name,
845
+ permissionRules,
817
846
  });
818
847
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
819
- const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
820
848
  if (externalRunner && permissionRules) {
821
849
  throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
822
850
  }
851
+ if (!externalRunner) {
852
+ const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
853
+ const contractError = validateImplementationToolContract({
854
+ agent: a.name,
855
+ task,
856
+ tools: contractTools,
857
+ mcpDirectTools: toolPlan.effectiveMcpTools,
858
+ configuredExtensions: toolPlan.configuredExtensions,
859
+ requestedTools: toolPlan.requestedBuiltinTools,
860
+ acceptanceRole: a.acceptanceRole,
861
+ completionGuard: a.completionGuard,
862
+ });
863
+ if (contractError) throw new AsyncStartValidationError(contractError);
864
+ }
865
+ const modelCandidates = externalRunner ? [] : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
866
+ scope: modelScopes,
867
+ primaryModelFromParent,
868
+ }).flatMap((candidate) => {
869
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
870
+ return resolved ? [resolved] : [];
871
+ });
872
+ if (!externalRunner) {
873
+ try {
874
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
875
+ } catch (error) {
876
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
877
+ }
878
+ }
823
879
  return {
824
880
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
825
881
  permissionRules,
@@ -837,13 +893,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
837
893
  cwd: stepCwd,
838
894
  model,
839
895
  thinking: resolveEffectiveThinking(model, effectiveThinking),
896
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
840
897
  launchResolvedExtensions,
841
- modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
842
- scope: ctx.modelScope,
843
- primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
844
- }).map((candidate) =>
845
- applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
846
- ),
898
+ modelCandidates: externalRunner ? undefined : modelCandidates,
899
+ ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
900
+ ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
847
901
  tools: a.tools,
848
902
  extensions: a.extensions,
849
903
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -960,6 +1014,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
960
1014
  acceptanceRole: agent.acceptanceRole,
961
1015
  ...(s.agentContract ?? params.agentContract ? { agentContract: s.agentContract ?? params.agentContract } : {}),
962
1016
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1017
+ ...(parallel.thinkingCeiling ? { thinkingCeiling: parallel.thinkingCeiling } : {}),
963
1018
  };
964
1019
  }
965
1020
  const sequential = s as SequentialStep;
@@ -1094,6 +1149,7 @@ export function executeAsyncChain(
1094
1149
  configToolTimeoutMs: params.configToolTimeoutMs,
1095
1150
  toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1096
1151
  capabilityCeiling,
1152
+ thinkingCeiling: params.thinkingCeiling,
1097
1153
  });
1098
1154
  if ("error" in built) {
1099
1155
  try {
@@ -1418,6 +1474,7 @@ export function executeAsyncSingle(
1418
1474
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
1419
1475
  : "";
1420
1476
  const taskText = readsInstruction + taskWithOutputInstruction;
1477
+ const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
1421
1478
  const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
1422
1479
  ? params.modelOverride
1423
1480
  : resolveSubagentModelOverride(
@@ -1425,9 +1482,22 @@ export function executeAsyncSingle(
1425
1482
  ctx.currentModel,
1426
1483
  availableModels,
1427
1484
  ctx.currentModelProvider,
1485
+ { scope: modelScopes },
1428
1486
  );
1429
1487
  const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1430
1488
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1489
+ const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
1490
+ params.thinkingCeiling,
1491
+ agentConfig.maxThinking,
1492
+ decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
1493
+ );
1494
+ if (!externalRunner) {
1495
+ try {
1496
+ assertThinkingWithinCeiling({ model, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1497
+ } catch (error) {
1498
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1499
+ }
1500
+ }
1431
1501
  const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
1432
1502
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, params.toolBudget ? "toolBudget" : agentConfig.toolBudget ? "agent.toolBudget" : "config.toolBudget");
1433
1503
  if (resolvedToolBudget.error) return formatAsyncStartError("single", resolvedToolBudget.error);
@@ -1452,14 +1522,21 @@ export function executeAsyncSingle(
1452
1522
  : undefined;
1453
1523
  const modelCandidates = externalRunner
1454
1524
  ? []
1455
- : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
1456
- scope: ctx.modelScope,
1525
+ : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1526
+ scope: modelScopes,
1457
1527
  primaryModelFromParent: params.modelOverrideFromParent,
1458
1528
  })
1459
1529
  .flatMap((candidate) => {
1460
1530
  const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1461
1531
  return resolved ? [resolved] : [];
1462
1532
  });
1533
+ if (!externalRunner) {
1534
+ try {
1535
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1536
+ } catch (error) {
1537
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1538
+ }
1539
+ }
1463
1540
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1464
1541
  const toolPlan = resolvePiLaunchToolPlan({
1465
1542
  tools: agentConfig.tools,
@@ -1472,14 +1549,30 @@ export function executeAsyncSingle(
1472
1549
  capabilityCeiling,
1473
1550
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1474
1551
  agentName: agentConfig.name,
1552
+ permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1475
1553
  });
1476
1554
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1555
+ if (!externalRunner) {
1556
+ const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
1557
+ const contractError = validateImplementationToolContract({
1558
+ agent: agentConfig.name,
1559
+ task: taskText,
1560
+ tools: contractTools,
1561
+ mcpDirectTools: toolPlan.effectiveMcpTools,
1562
+ configuredExtensions: toolPlan.configuredExtensions,
1563
+ requestedTools: toolPlan.requestedBuiltinTools,
1564
+ acceptanceRole: agentConfig.acceptanceRole,
1565
+ completionGuard: agentConfig.completionGuard,
1566
+ });
1567
+ if (contractError) return formatAsyncStartError("single", contractError);
1568
+ }
1477
1569
  const launchContractDigest = launchBindingDigest({
1478
1570
  definitionDigest: agentDefinitionDigest(agentConfig),
1479
1571
  task,
1480
1572
  ...(model ? { model } : {}),
1481
1573
  modelCandidates,
1482
1574
  ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1575
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
1483
1576
  systemPrompt: effectiveSystemPrompt,
1484
1577
  systemPromptMode: agentConfig.systemPromptMode,
1485
1578
  inheritProjectContext: agentConfig.inheritProjectContext,
@@ -1513,9 +1606,11 @@ export function executeAsyncSingle(
1513
1606
  ...(sessionFile ? { sessionFile } : {}),
1514
1607
  cwd: runnerCwd,
1515
1608
  ...(model ? { model } : {}),
1609
+ ...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
1516
1610
  ...(params.modelOverrideFromParent ? { modelOverrideFromParent: true } : {}),
1517
1611
  ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1518
1612
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1613
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
1519
1614
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1520
1615
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1521
1616
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
@@ -1567,11 +1662,15 @@ export function executeAsyncSingle(
1567
1662
  agent,
1568
1663
  task: taskText,
1569
1664
  ...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
1665
+ ...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
1570
1666
  ...(params.context ? { context: params.context } : {}),
1571
1667
  cwd: runnerCwd,
1572
1668
  model,
1573
1669
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1670
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
1574
1671
  modelCandidates,
1672
+ ...(params.modelOverrideFromParent ? { skipPrimaryModelVerification: true } : {}),
1673
+ ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1575
1674
  tools: agentConfig.tools,
1576
1675
  extensions: agentConfig.extensions,
1577
1676
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1599,7 +1698,7 @@ export function executeAsyncSingle(
1599
1698
  ...(params.worktree === true ? { worktree: true } : {}),
1600
1699
  },
1601
1700
  ],
1602
- resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
1701
+ resultPath: params.parentWorkflowRunId !== undefined && (params.revivalLease !== undefined || params.workflowAwaitAsync === true)
1603
1702
  ? workflowAwaitedAsyncResultPath(asyncDir)
1604
1703
  : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1605
1704
  cwd: runnerCwd,
@@ -8,12 +8,14 @@ import {
8
8
  type AsyncStartedEvent,
9
9
  type ControlEvent,
10
10
  type SteeringNotice,
11
+ type SubagentChildStatusEvent,
11
12
  type SubagentState,
12
- POLL_INTERVAL_MS,
13
13
  DIRS,
14
+ SUBAGENT_CHILD_STATUS_EVENT,
14
15
  SUBAGENT_CONTROL_EVENT,
15
16
  SUBAGENT_CONTROL_INTERCOM_EVENT,
16
17
  SUBAGENT_STEERING_NOTICE_EVENT,
18
+ WIDGET_ANIMATION_INTERVAL_MS,
17
19
  } from "../../shared/types.ts";
18
20
  import { readStatus, resolveWatchPath } from "../../shared/utils.ts";
19
21
  import { normalizeParallelGroups } from "./parallel-groups.ts";
@@ -71,6 +73,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
71
73
  const runningJobIds = new Set<string>();
72
74
  let rootWatcher: fs.FSWatcher | undefined;
73
75
  let nextLivenessAt = Date.now() + livenessIntervalMs;
76
+ let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
74
77
  const watch = options.watch ?? fs.watch;
75
78
  const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
76
79
  const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
@@ -93,17 +96,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
93
96
  }
94
97
  };
95
98
  const requestLastWidgetRender = () => {
96
- const ctx = state.lastUiContext;
97
- if (!ctx || state.widgetsSuspended || options.widgetEnabled === false) return;
98
- try {
99
- if (ctx.hasUI) (ctx.ui as { requestRender?: () => void }).requestRender?.();
100
- } catch (error) {
101
- if (error instanceof Error && error.message.includes("extension ctx is stale")) {
102
- state.lastUiContext = null;
103
- return;
104
- }
105
- throw error;
106
- }
99
+ if (options.widgetEnabled === false) return;
100
+ rerenderLastWidget();
107
101
  };
108
102
  const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
109
103
  const restoredControlEventCursor = (asyncDir: string) => {
@@ -227,6 +221,28 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
227
221
  return;
228
222
  }
229
223
  if (!parsed || typeof parsed !== "object") return;
224
+ if ((parsed as { type?: unknown }).type === "subagent.child-status") {
225
+ const event = parsed as Partial<SubagentChildStatusEvent>;
226
+ if (event.version !== 1 || typeof event.runId !== "string" || typeof event.childId !== "string" || (event.status !== "stopping" && event.status !== "stopped") || typeof event.ts !== "number") return;
227
+ pi.events.emit(SUBAGENT_CHILD_STATUS_EVENT, {
228
+ type: "subagent.child-status",
229
+ version: 1,
230
+ runId: event.runId,
231
+ childId: event.childId,
232
+ status: event.status,
233
+ ts: event.ts,
234
+ ...(typeof event.reason === "string" ? { reason: event.reason } : {}),
235
+ source: event.source === "rpc" ? "rpc" : "async",
236
+ asyncDir: job.asyncDir,
237
+ ...(typeof event.stepIndex === "number" ? { stepIndex: event.stepIndex } : {}),
238
+ ...(typeof event.agent === "string" ? { agent: event.agent } : {}),
239
+ ...(typeof event.childRunId === "string" ? { childRunId: event.childRunId } : {}),
240
+ ...(typeof event.workflowKey === "string" ? { workflowKey: event.workflowKey } : {}),
241
+ ...(typeof event.phase === "string" ? { phase: event.phase } : {}),
242
+ ...(typeof event.label === "string" ? { label: event.label } : {}),
243
+ } satisfies SubagentChildStatusEvent);
244
+ return;
245
+ }
230
246
  if ((parsed as { type?: unknown }).type === "subagent.steering.notice") {
231
247
  const notice = parsed as Partial<SteeringNotice>;
232
248
  if (typeof notice.requestId !== "string" || typeof notice.runId !== "string" || (notice.state !== "failed" && notice.state !== "partial" && notice.state !== "recovered") || typeof notice.message !== "string") return;
@@ -556,8 +572,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
556
572
  }
557
573
  if (widgetChanged) rerenderLastWidget();
558
574
  }
559
- if (runningJobIds.size > 0) requestLastWidgetRender();
560
- }, Math.min(POLL_INTERVAL_MS, livenessIntervalMs));
575
+ if (runningJobIds.size > 0 && now >= nextWidgetAnimationAt) {
576
+ nextWidgetAnimationAt = now + WIDGET_ANIMATION_INTERVAL_MS;
577
+ requestLastWidgetRender();
578
+ }
579
+ }, Math.min(WIDGET_ANIMATION_INTERVAL_MS, livenessIntervalMs));
561
580
  state.poller.unref?.();
562
581
  };
563
582
 
@@ -11,6 +11,7 @@ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
11
11
  import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
12
12
  import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
13
13
  import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
14
+ import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
14
15
 
15
16
  export interface AsyncResumeParams {
16
17
  id?: string;
@@ -43,9 +44,12 @@ export type AsyncResumeTarget = {
43
44
  sessionFile?: string;
44
45
  model?: string;
45
46
  thinking?: string;
47
+ thinkingCeiling?: ThinkingLevel;
46
48
  recoveryDescriptor?: SteeringRecoveryDescriptor;
47
49
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
48
50
  launchContractDigest?: string;
51
+ runner?: NonNullable<AsyncStatus["steps"]>[number]["runner"];
52
+ externalJob?: NonNullable<AsyncStatus["steps"]>[number]["externalJob"];
49
53
  };
50
54
 
51
55
  interface AsyncResultFile {
@@ -265,6 +269,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
265
269
  if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
266
270
  if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
267
271
  if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
272
+ if (stepRecord.thinkingCeiling !== undefined) stepRecord.thinkingCeiling = parseThinkingLevel(stepRecord.thinkingCeiling, `async status '${source}' steps[${index}].thinkingCeiling`);
268
273
  if (stepRecord.launchContractDigest !== undefined && typeof stepRecord.launchContractDigest !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].launchContractDigest must be a string.`);
269
274
  if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
270
275
  });
@@ -318,7 +323,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
318
323
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
319
324
  const parsed = value as Record<string, unknown>;
320
325
  const allowedFields = new Set([
321
- "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
326
+ "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "extensions",
322
327
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
323
328
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
324
329
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
@@ -338,6 +343,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
338
343
  throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
339
344
  }
340
345
  if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
346
+ if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
341
347
  if (parsed.agentContract !== undefined) {
342
348
  if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
343
349
  const contract = parsed.agentContract as Record<string, unknown>;
@@ -356,7 +362,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
356
362
  if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
357
363
  }
358
364
  if (parsed.systemPrompt !== undefined && typeof parsed.systemPrompt !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPrompt must be a string.`);
359
- for (const field of ["launchContractDigest", "sessionFile", "model", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
365
+ for (const field of ["launchContractDigest", "sessionFile", "model", "modelProvider", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
360
366
  if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
361
367
  }
362
368
  if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
@@ -487,7 +493,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
487
493
  model: selectedStep.model,
488
494
  thinking: selectedStep.thinking,
489
495
  launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
496
+ ...(selectedStep.runner ? { runner: selectedStep.runner } : {}),
497
+ ...(selectedStep.externalJob ? { externalJob: selectedStep.externalJob } : {}),
490
498
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
499
+ ...(selectedStep.thinkingCeiling ? { thinkingCeiling: selectedStep.thinkingCeiling } : {}),
491
500
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
492
501
  };
493
502
  }
@@ -515,7 +524,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
515
524
  model: selected.step.model,
516
525
  thinking: selected.step.thinking,
517
526
  launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
527
+ ...(selected.step.runner ? { runner: selected.step.runner } : {}),
528
+ ...(selected.step.externalJob ? { externalJob: selected.step.externalJob } : {}),
518
529
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
530
+ ...(selected.step.thinkingCeiling ? { thinkingCeiling: selected.step.thinkingCeiling } : {}),
519
531
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
520
532
  };
521
533
  }
@@ -537,6 +549,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
537
549
  const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
538
550
  const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
539
551
  const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
552
+ const thinkingCeiling = statusSteps[index]?.thinkingCeiling ?? (stepCount === 1 ? recoveryDescriptor?.thinkingCeiling : undefined);
540
553
  const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
541
554
  const managedWorktreeCwd = location.asyncDir
542
555
  ? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
@@ -556,7 +569,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
556
569
  ...(stepModel ? { model: stepModel } : {}),
557
570
  ...(stepThinking ? { thinking: stepThinking } : {}),
558
571
  launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
572
+ ...(statusSteps[index]?.runner ? { runner: statusSteps[index]!.runner } : {}),
573
+ ...(statusSteps[index]?.externalJob ? { externalJob: statusSteps[index]!.externalJob } : {}),
559
574
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
575
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
560
576
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
561
577
  };
562
578
  }
@@ -565,8 +581,10 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
565
581
  return {
566
582
  ...agentConfig,
567
583
  model: descriptor.model,
584
+ modelProvider: descriptor.modelProvider,
568
585
  fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
569
586
  thinking: descriptor.thinking,
587
+ maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
570
588
  tools: descriptor.tools ? [...descriptor.tools] : undefined,
571
589
  extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
572
590
  subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
@@ -600,7 +600,7 @@ function runRetentionDiscovery(input: {
600
600
  };
601
601
  const onAbort = (): void => fail(new RetentionCancelledError("Retention discovery was cancelled."));
602
602
  input.signal?.addEventListener("abort", onAbort, { once: true });
603
- worker.once("error", (error) => fail(error));
603
+ worker.once("error", (error) => fail(error instanceof Error ? error : new Error(String(error))));
604
604
  worker.once("exit", (code) => {
605
605
  if (!settled) fail(new Error(`Retention discovery worker exited before replying (${code}).`));
606
606
  });
@@ -15,14 +15,18 @@ import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal
15
15
  import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRunMarkerAgeMs, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
16
16
  import { readRecentTerminalRunIndex, TERMINAL_RUN_INDEX_DIR } from "./terminal-run-index.ts";
17
17
  import { canScanAsyncRunPrefix } from "./run-id-query.ts";
18
+ import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
18
19
 
19
20
  interface AsyncRunStepSummary {
20
21
  index: number;
22
+ childId?: string;
21
23
  agent: string;
22
24
  context?: ContextMode;
23
25
  label?: string;
24
26
  description?: string;
25
27
  phase?: string;
28
+ workflowKey?: string;
29
+ runId?: string;
26
30
  outputName?: string;
27
31
  structured?: boolean;
28
32
  status: AsyncJobStep["status"];
@@ -251,11 +255,14 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
251
255
  const stepLastActivityAt = step.lastActivityAt;
252
256
  return {
253
257
  index,
258
+ childId: asyncStatusChildIdentity(step, index),
254
259
  agent: step.agent,
255
260
  ...(step.context ? { context: step.context } : {}),
256
261
  ...(step.label ? { label: step.label } : {}),
257
262
  ...(step.description ? { description: step.description } : {}),
258
263
  ...(step.phase ? { phase: step.phase } : {}),
264
+ ...(step.workflowKey ? { workflowKey: step.workflowKey } : {}),
265
+ ...(step.runId ? { runId: step.runId } : {}),
259
266
  ...(step.outputName ? { outputName: step.outputName } : {}),
260
267
  ...(step.structured ? { structured: step.structured } : {}),
261
268
  status: step.status,
@@ -277,12 +284,15 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
277
284
  ...(step.skills ? { skills: step.skills } : {}),
278
285
  ...(step.model ? { model: step.model } : {}),
279
286
  ...(step.thinking ? { thinking: step.thinking } : {}),
287
+ ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
280
288
  ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
281
289
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
282
290
  ...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
283
291
  ...(step.error ? { error: step.error } : {}),
284
292
  ...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
285
293
  ...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
294
+ ...(step.stopRequested !== undefined ? { stopRequested: step.stopRequested } : {}),
295
+ ...(step.stopRequestedAt !== undefined ? { stopRequestedAt: step.stopRequestedAt } : {}),
286
296
  ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
287
297
  ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
288
298
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
@@ -21,6 +21,7 @@ export interface ImportedAsyncRootResult {
21
21
  model?: string;
22
22
  attemptedModels?: string[];
23
23
  modelAttempts?: ModelAttempt[];
24
+ contextOverflow?: boolean;
24
25
  totalCost?: CostSummary;
25
26
  structuredOutput?: unknown;
26
27
  structuredOutputPath?: string;
@@ -49,6 +50,7 @@ interface AsyncResultFile {
49
50
  model?: string;
50
51
  attemptedModels?: string[];
51
52
  modelAttempts?: ModelAttempt[];
53
+ contextOverflow?: boolean;
52
54
  totalCost?: CostSummary;
53
55
  structuredOutput?: unknown;
54
56
  structuredOutputPath?: string;
@@ -110,6 +112,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
110
112
  ...(step?.model ? { model: step.model } : {}),
111
113
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
112
114
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
115
+ ...(step?.contextOverflow ? { contextOverflow: true } : {}),
113
116
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
114
117
  ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
115
118
  ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
@@ -131,6 +134,7 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
131
134
  ...(step?.model ? { model: step.model } : {}),
132
135
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
133
136
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
137
+ ...(step?.contextOverflow ? { contextOverflow: true } : {}),
134
138
  ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
135
139
  };
136
140
  }
@@ -158,6 +162,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
158
162
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
159
163
  ...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
160
164
  ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
165
+ ...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
161
166
  ...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
162
167
  ...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
163
168
  ...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),