pi-subagents 0.38.0 → 0.40.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 (59) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +85 -13
  3. package/agents/worker.md +1 -0
  4. package/package.json +1 -1
  5. package/prompts/review-loop.md +1 -1
  6. package/skills/pi-subagents/SKILL.md +4 -3
  7. package/skills/pi-subagents/references/execution-controls.md +22 -3
  8. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -1
  9. package/skills/pi-subagents/references/prompting-and-roles.md +17 -3
  10. package/src/agents/agent-management.ts +71 -30
  11. package/src/agents/agent-serializer.ts +4 -0
  12. package/src/agents/agents.ts +60 -1
  13. package/src/api/preflight.ts +16 -12
  14. package/src/extension/chain-validation.ts +17 -1
  15. package/src/extension/rpc.ts +2 -0
  16. package/src/extension/schemas.ts +16 -3
  17. package/src/extension/tool-description.ts +6 -5
  18. package/src/intercom/result-intercom.ts +33 -4
  19. package/src/runs/background/async-execution.ts +63 -16
  20. package/src/runs/background/async-job-tracker.ts +5 -1
  21. package/src/runs/background/async-resume.ts +1 -0
  22. package/src/runs/background/async-status.ts +18 -2
  23. package/src/runs/background/chain-append.ts +48 -5
  24. package/src/runs/background/control-channel.ts +60 -0
  25. package/src/runs/background/notify.ts +26 -4
  26. package/src/runs/background/result-watcher.ts +20 -3
  27. package/src/runs/background/run-status.ts +29 -3
  28. package/src/runs/background/subagent-runner.ts +302 -18
  29. package/src/runs/foreground/async-stop-action.ts +65 -0
  30. package/src/runs/foreground/chain-execution.ts +143 -43
  31. package/src/runs/foreground/execution.ts +384 -202
  32. package/src/runs/foreground/foreground-control.ts +30 -0
  33. package/src/runs/foreground/subagent-executor.ts +314 -77
  34. package/src/runs/shared/capability-ceiling.ts +51 -19
  35. package/src/runs/shared/chain-outputs.ts +3 -1
  36. package/src/runs/shared/dynamic-fanout.ts +1 -1
  37. package/src/runs/shared/nested-events.ts +97 -20
  38. package/src/runs/shared/parallel-utils.ts +43 -12
  39. package/src/runs/shared/pi-args.ts +45 -3
  40. package/src/runs/shared/process-signal.ts +19 -0
  41. package/src/runs/shared/run-history.ts +45 -9
  42. package/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  43. package/src/runs/shared/subagent-prompt-runtime.ts +30 -0
  44. package/src/runs/shared/usage-budget.ts +65 -0
  45. package/src/runs/shared/workflow-graph.ts +26 -1
  46. package/src/shared/settings.ts +17 -1
  47. package/src/shared/types.ts +124 -8
  48. package/src/shared/utils.ts +12 -1
  49. package/src/slash/slash-commands.ts +43 -1
  50. package/src/slash/slash-live-state.ts +5 -3
  51. package/src/tui/fleet-status.ts +12 -2
  52. package/src/tui/fleet.ts +181 -8
  53. package/src/tui/render.ts +4 -2
  54. package/src/watchdog/register-main.ts +14 -7
  55. package/src/watchdog/review.ts +4 -3
  56. package/src/watchdog/runtime.ts +170 -17
  57. package/src/watchdog/scope.ts +62 -0
  58. package/src/watchdog/settings.ts +41 -1
  59. package/src/watchdog/types.ts +10 -0
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
+ import { isUnexplainedProcessSignal } from "../runs/shared/process-signal.ts";
3
4
  import {
4
5
  type Details,
5
6
  type IntercomEventBus,
@@ -10,6 +11,7 @@ import {
10
11
  type SubagentResultIntercomChild,
11
12
  type SubagentResultIntercomPayload,
12
13
  type SubagentResultStatus,
14
+ type SubagentOutputState,
13
15
  type SubagentRunMode,
14
16
  SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
15
17
  SUBAGENT_RESULT_INTERCOM_EVENT,
@@ -21,11 +23,17 @@ export function resolveSubagentResultStatus(input: {
21
23
  state?: string;
22
24
  interrupted?: boolean;
23
25
  detached?: boolean;
26
+ processSignal?: string | null;
27
+ timedOut?: boolean;
28
+ stopped?: boolean;
29
+ turnBudgetExceeded?: boolean;
24
30
  }): SubagentResultStatus {
25
31
  if (input.detached) return "detached";
26
- if (input.state === "stopped") return "stopped";
32
+ if (input.stopped || input.state === "stopped") return "stopped";
27
33
  if (input.interrupted || input.state === "paused") return "paused";
28
- if (typeof input.success === "boolean") return input.success ? "completed" : "failed";
34
+ if (input.success === true) return "completed";
35
+ if (isUnexplainedProcessSignal(input) && input.exitCode !== 0) return "stopped";
36
+ if (input.success === false) return "failed";
29
37
  if (input.state === "complete") return "completed";
30
38
  if (input.state === "failed") return "failed";
31
39
  if (typeof input.exitCode === "number") return input.exitCode === 0 ? "completed" : "failed";
@@ -57,6 +65,21 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
57
65
  return parts.length ? parts.join(", ") : "0 results";
58
66
  }
59
67
 
68
+ function countOutputStates(children: SubagentResultIntercomChild[]): Record<SubagentOutputState, number> {
69
+ const counts: Record<SubagentOutputState, number> = { present: 0, absent: 0, unknown: 0 };
70
+ for (const child of children) counts[child.outputState ?? "unknown"] += 1;
71
+ return counts;
72
+ }
73
+
74
+ function formatOutputCounts(counts: Record<SubagentOutputState, number>): string {
75
+ const parts = [
76
+ counts.present ? `${counts.present} present` : undefined,
77
+ counts.absent ? `${counts.absent} absent` : undefined,
78
+ counts.unknown ? `${counts.unknown} unknown` : undefined,
79
+ ].filter((part): part is string => Boolean(part));
80
+ return parts.length ? parts.join(", ") : "0 outputs";
81
+ }
82
+
60
83
  function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
61
84
  const counts = countStatuses(children);
62
85
  if (counts.failed > 0) return "failed";
@@ -212,14 +235,19 @@ function formatSubagentResultIntercomMessage(input: {
212
235
  parallelHandoff?: ParallelHandoffReference;
213
236
  }): string {
214
237
  const counts = countStatuses(input.children);
238
+ const outputCounts = countOutputStates(input.children);
215
239
  const lines: string[] = [
216
240
  "subagent results",
217
241
  "",
218
242
  `Run: ${input.runId}`,
219
243
  `Mode: ${input.mode}`,
220
- `Status: ${input.status}`,
244
+ `Process status: ${input.status}`,
221
245
  `Children: ${formatStatusCounts(counts)}`,
246
+ `Outputs: ${formatOutputCounts(outputCounts)} (semantic adequacy unassessed)`,
222
247
  ];
248
+ if (input.children.some((child) => child.status === "failed" && child.outputState === "present")) {
249
+ lines.push("Recovery: At least one failed process produced output. Inspect that output before retrying; output presence does not establish task completion.");
250
+ }
223
251
  if (input.mode === "chain" && typeof input.chainSteps === "number") {
224
252
  lines.push(`Chain steps: ${input.chainSteps}`);
225
253
  }
@@ -238,7 +266,7 @@ function formatSubagentResultIntercomMessage(input: {
238
266
  for (let index = 0; index < input.children.length; index++) {
239
267
  const child = input.children[index]!;
240
268
  lines.push("");
241
- lines.push(`${index + 1}. ${child.agent} — ${child.status}`);
269
+ lines.push(`${index + 1}. ${child.agent} — process ${child.status} · output ${child.outputState ?? "unknown"}`);
242
270
  if (child.intercomTarget) lines.push(`${input.source === "async" ? "Previous intercom target" : "Run intercom target"}: ${child.intercomTarget}`);
243
271
  if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
244
272
  if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
@@ -253,6 +281,7 @@ function formatSubagentResultIntercomMessage(input: {
253
281
  export function buildSubagentResultIntercomPayload(input: GroupedResultIntercomMessageInput): SubagentResultIntercomPayload {
254
282
  const children = input.children.map((child) => ({
255
283
  ...child,
284
+ outputState: child.outputState ?? "unknown",
256
285
  summary: child.summary.trim() || "(no output)",
257
286
  children: compactNestedResultChildren(child.children),
258
287
  }));
@@ -12,9 +12,9 @@ import { createRequire } from "node:module";
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import type { AgentConfig } from "../../agents/agents.ts";
14
14
  import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
15
- import { applyThinkingSuffix, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
15
+ import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
16
16
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
17
- import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
17
+ import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
18
18
  import type { RunnerStep } from "../shared/parallel-utils.ts";
19
19
  import type { ContextMode } from "../shared/context-mode.ts";
20
20
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
@@ -42,6 +42,7 @@ import {
42
42
  type ResolvedToolBudget,
43
43
  type SubagentRunMode,
44
44
  type SteeringRecoveryDescriptor,
45
+ type UsageBudgetConfig,
45
46
  ASYNC_DIR,
46
47
  RESULTS_DIR,
47
48
  SUBAGENT_ASYNC_STARTED_EVENT,
@@ -53,11 +54,12 @@ import {
53
54
  import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
54
55
  import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
55
56
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
57
+ import { usageBudgetState } from "../shared/usage-budget.ts";
56
58
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
57
59
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
58
60
  import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
59
61
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
60
- import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
62
+ import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
61
63
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
62
64
 
63
65
  const require = createRequire(import.meta.url);
@@ -159,6 +161,7 @@ interface AsyncChainParams {
159
161
  timeoutMs?: number;
160
162
  turnBudget?: ResolvedTurnBudget;
161
163
  toolBudget?: ResolvedToolBudget;
164
+ usageBudget?: UsageBudgetConfig;
162
165
  configToolBudget?: ResolvedToolBudget;
163
166
  /** Global cap on simultaneously-running subagent tasks within the async run. */
164
167
  globalConcurrencyLimit?: number;
@@ -205,6 +208,7 @@ interface AsyncSingleParams {
205
208
  absoluteDeadlineAt?: number;
206
209
  turnBudget?: ResolvedTurnBudget;
207
210
  toolBudget?: ResolvedToolBudget;
211
+ usageBudget?: UsageBudgetConfig;
208
212
  configToolBudget?: ResolvedToolBudget;
209
213
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
210
214
  }
@@ -592,11 +596,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
592
596
  : chain;
593
597
  const firstStep = chain[0];
594
598
  const originalTask = params.task ?? (firstStep
595
- ? (isParallelStep(firstStep)
596
- ? firstStep.parallel[0]?.task
597
- : isDynamicParallelStep(firstStep)
598
- ? firstStep.parallel.task
599
- : (firstStep as SequentialStep).task)
599
+ ? (isCheckpointStep(firstStep)
600
+ ? undefined
601
+ : isParallelStep(firstStep)
602
+ ? firstStep.parallel[0]?.task
603
+ : isDynamicParallelStep(firstStep)
604
+ ? firstStep.parallel.task
605
+ : (firstStep as SequentialStep).task)
600
606
  : undefined);
601
607
  try {
602
608
  if (params.validateOutputBindings !== false) {
@@ -609,11 +615,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
609
615
  const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
610
616
 
611
617
  for (const s of chain) {
612
- const stepAgents = isParallelStep(s)
613
- ? s.parallel.map((t) => t.agent)
614
- : isDynamicParallelStep(s)
615
- ? [s.parallel.agent]
616
- : [(s as SequentialStep).agent];
618
+ const stepAgents = isCheckpointStep(s)
619
+ ? []
620
+ : isParallelStep(s)
621
+ ? s.parallel.map((t) => t.agent)
622
+ : isDynamicParallelStep(s)
623
+ ? [s.parallel.agent]
624
+ : [(s as SequentialStep).agent];
617
625
  for (const agentName of stepAgents) {
618
626
  if (!agents.find((x) => x.name === agentName)) {
619
627
  return { error: `Unknown agent: ${agentName}` };
@@ -635,6 +643,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
635
643
  };
636
644
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }) => {
637
645
  const a = agents.find((x) => x.name === s.agent)!;
646
+ try {
647
+ assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])));
648
+ } catch (error) {
649
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
650
+ }
638
651
  const toolBudgetInput = s.toolBudget ?? params.toolBudget ?? a.toolBudget ?? params.configToolBudget;
639
652
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, s.toolBudget ? "toolBudget" : a.toolBudget ? "agent.toolBudget" : "config.toolBudget");
640
653
  if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
@@ -695,6 +708,19 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
695
708
  const effectiveThinking = thinkingOverride ?? a.thinking;
696
709
  const model = applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
697
710
  const agentContract = s.agentContract ?? params.agentContract;
711
+ const toolPlan = resolvePiLaunchToolPlan({
712
+ tools: a.tools,
713
+ extensions: a.extensions,
714
+ subagentOnlyExtensions: a.subagentOnlyExtensions,
715
+ mcpDirectTools: a.mcpDirectTools,
716
+ cwd: stepCwd,
717
+ requireReadTool: Boolean(resolvedSkills.length),
718
+ structuredOutput: Boolean(s.outputSchema),
719
+ capabilityCeiling: params.capabilityCeiling,
720
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
721
+ agentName: a.name,
722
+ });
723
+ const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
698
724
  return {
699
725
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
700
726
  ...(params.capabilityCeiling ? { capabilityCeiling: params.capabilityCeiling } : {}),
@@ -709,6 +735,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
709
735
  cwd: stepCwd,
710
736
  model,
711
737
  thinking: resolveEffectiveThinking(model, effectiveThinking),
738
+ launchResolvedExtensions,
712
739
  modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
713
740
  applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
714
741
  ),
@@ -762,6 +789,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
762
789
 
763
790
  try {
764
791
  const builtSteps = chain.map((s, stepIndex) => {
792
+ if (isCheckpointStep(s)) {
793
+ return { checkpoint: s.checkpoint, ...(s.message ? { message: s.message } : {}), phase: s.phase, label: s.label };
794
+ }
765
795
  if (isParallelStep(s)) {
766
796
  const parallelBehaviors = s.parallel.map((task) => {
767
797
  const agent = agents.find((candidate) => candidate.name === task.agent)!;
@@ -948,6 +978,7 @@ export function executeAsyncChain(
948
978
  const { steps, runnerCwd, workflowGraph, eventChain } = built;
949
979
  const deadlineAt = params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined;
950
980
  const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
981
+ const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
951
982
  let childTargetIndex = 0;
952
983
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
953
984
  if (!("parallel" in step) && step.importAsyncRoot) {
@@ -989,6 +1020,7 @@ export function executeAsyncChain(
989
1020
  controlConfig,
990
1021
  turnBudget: params.turnBudget,
991
1022
  toolBudget: params.toolBudget,
1023
+ usageBudget: params.usageBudget,
992
1024
  controlIntercomTarget,
993
1025
  childIntercomTargets,
994
1026
  resultMode,
@@ -1106,6 +1138,7 @@ export function executeAsyncChain(
1106
1138
  asyncDir,
1107
1139
  ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
1108
1140
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1141
+ ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
1109
1142
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1110
1143
  nestedRoute,
1111
1144
  });
@@ -1119,7 +1152,7 @@ export function executeAsyncChain(
1119
1152
 
1120
1153
  return {
1121
1154
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`, ctx.interactive === true) }],
1122
- details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1155
+ details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1123
1156
  };
1124
1157
  }
1125
1158
 
@@ -1151,7 +1184,12 @@ export function executeAsyncSingle(
1151
1184
  nestedRoute,
1152
1185
  } = params;
1153
1186
  const task = params.task ?? "";
1154
- const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
1187
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
1188
+ try {
1189
+ assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
1190
+ } catch (error) {
1191
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1192
+ }
1155
1193
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1156
1194
  const skillNames = params.skills ?? agentConfig.skills ?? [];
1157
1195
  const availableModels = params.availableModels;
@@ -1213,6 +1251,7 @@ export function executeAsyncSingle(
1213
1251
  : params.timeoutMs;
1214
1252
  if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
1215
1253
  const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1254
+ const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1216
1255
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1217
1256
  const structuredOutput = params.structuredOutputSchema
1218
1257
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
@@ -1231,7 +1270,9 @@ export function executeAsyncSingle(
1231
1270
  structuredOutput: Boolean(params.structuredOutputSchema),
1232
1271
  capabilityCeiling,
1233
1272
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1273
+ agentName: agentConfig.name,
1234
1274
  });
1275
+ const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1235
1276
  const launchContractDigest = launchBindingDigest({
1236
1277
  definitionDigest: agentDefinitionDigest(agentConfig),
1237
1278
  task,
@@ -1265,6 +1306,7 @@ export function executeAsyncSingle(
1265
1306
  sourceRunId: id,
1266
1307
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1267
1308
  agent,
1309
+ launchResolvedExtensions,
1268
1310
  ...(sessionFile ? { sessionFile } : {}),
1269
1311
  cwd: runnerCwd,
1270
1312
  ...(model ? { model } : {}),
@@ -1339,6 +1381,7 @@ export function executeAsyncSingle(
1339
1381
  definitionDigest: agentDefinitionDigest(agentConfig),
1340
1382
  launchBindingTask: task,
1341
1383
  launchContractDigest,
1384
+ launchResolvedExtensions,
1342
1385
  effectiveAcceptance: resolvedAcceptance,
1343
1386
  ...(structuredOutput ? { structuredOutput } : {}),
1344
1387
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
@@ -1366,10 +1409,12 @@ export function executeAsyncSingle(
1366
1409
  deadlineAt,
1367
1410
  turnBudget: params.turnBudget,
1368
1411
  toolBudget: params.toolBudget,
1412
+ usageBudget: params.usageBudget,
1369
1413
  controlIntercomTarget,
1370
1414
  childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
1371
1415
  resultMode: "single",
1372
1416
  launchContractDigest,
1417
+ launchResolvedExtensions,
1373
1418
  ...(params.revivalLease ? { revivalLease: params.revivalLease } : {}),
1374
1419
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
1375
1420
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
@@ -1441,8 +1486,10 @@ export function executeAsyncSingle(
1441
1486
  cwd: runnerCwd,
1442
1487
  asyncDir,
1443
1488
  launchContractDigest,
1489
+ launchResolvedExtensions,
1444
1490
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}),
1445
1491
  ...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
1492
+ ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
1446
1493
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
1447
1494
  nestedRoute,
1448
1495
  });
@@ -1450,6 +1497,6 @@ export function executeAsyncSingle(
1450
1497
 
1451
1498
  return {
1452
1499
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
1453
- details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
1500
+ 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: params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) },
1454
1501
  };
1455
1502
  }
@@ -165,6 +165,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
165
165
  const startedFromTail = savedCursor === undefined && stat.size > CONTROL_EVENT_SCAN_WINDOW_BYTES;
166
166
  if (startedFromTail) cursor = stat.size - CONTROL_EVENT_SCAN_WINDOW_BYTES;
167
167
  if (stat.size <= cursor) return;
168
+ const previousByte = Buffer.alloc(1);
169
+ const startsMidLine = cursor > 0
170
+ && fs.readSync(fd, previousByte, 0, 1, cursor - 1) === 1
171
+ && previousByte[0] !== 0x0a;
168
172
  const scanEnd = Math.min(stat.size, cursor + CONTROL_EVENT_SCAN_WINDOW_BYTES);
169
173
  const handleLine = (line: string) => {
170
174
  if (!line.trim()) return;
@@ -217,7 +221,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
217
221
  let lastCompleteCursor = cursor;
218
222
  let lineParts: Buffer[] = [];
219
223
  let lineBytes = 0;
220
- let skippingOversizedLine = startedFromTail;
224
+ let skippingOversizedLine = startedFromTail || startsMidLine;
221
225
  const appendLineSegment = (segment: Buffer) => {
222
226
  if (segment.length === 0 || skippingOversizedLine) return;
223
227
  if (lineBytes + segment.length > MAX_CONTROL_EVENT_LINE_BYTES) {
@@ -290,6 +290,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
290
290
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
291
291
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
292
292
  "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
293
+ "launchResolvedExtensions",
293
294
  ]);
294
295
  for (const field of Object.keys(parsed)) {
295
296
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type ChainCheckpointState } from "../../shared/types.ts";
6
6
  import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
7
7
  import { readStatus } from "../../shared/utils.ts";
8
8
  import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
@@ -17,9 +17,11 @@ interface AsyncRunStepSummary {
17
17
  agent: string;
18
18
  context?: ContextMode;
19
19
  label?: string;
20
+ description?: string;
20
21
  phase?: string;
21
22
  outputName?: string;
22
23
  structured?: boolean;
24
+ checkpoint?: ChainCheckpointState;
23
25
  status: AsyncJobStep["status"];
24
26
  activityState?: ActivityState;
25
27
  lastActivityAt?: number;
@@ -53,6 +55,8 @@ interface AsyncRunStepSummary {
53
55
  review?: AsyncJobStep["review"];
54
56
  effects?: AsyncJobStep["effects"];
55
57
  processTerminal?: AsyncJobStep["processTerminal"];
58
+ launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
59
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
56
60
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
57
61
  capabilityAudit?: SubagentCapabilityAudit;
58
62
  children?: NestedRunSummary[];
@@ -62,7 +66,7 @@ export interface AsyncRunSummary {
62
66
  id: string;
63
67
  asyncDir: string;
64
68
  sessionId?: string;
65
- state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped";
69
+ state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
66
70
  error?: string;
67
71
  activityState?: ActivityState;
68
72
  lastActivityAt?: number;
@@ -90,14 +94,18 @@ export interface AsyncRunSummary {
90
94
  pendingAppends?: number;
91
95
  parallelGroups?: AsyncParallelGroupStatus[];
92
96
  steps: AsyncRunStepSummary[];
97
+ checkpoint?: ChainCheckpointState;
93
98
  sessionDir?: string;
94
99
  outputFile?: string;
95
100
  totalTokens?: TokenUsage;
96
101
  totalCost?: CostSummary;
102
+ usageBudget?: UsageBudgetState;
97
103
  sessionFile?: string;
98
104
  nestedChildren?: NestedRunSummary[];
99
105
  nestedWarnings?: string[];
100
106
  processTerminal?: AsyncStatus["processTerminal"];
107
+ launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
108
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
101
109
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
102
110
  capabilityAudit?: SubagentCapabilityAudit;
103
111
  }
@@ -225,9 +233,11 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
225
233
  agent: step.agent,
226
234
  ...(step.context ? { context: step.context } : {}),
227
235
  ...(step.label ? { label: step.label } : {}),
236
+ ...(step.description ? { description: step.description } : {}),
228
237
  ...(step.phase ? { phase: step.phase } : {}),
229
238
  ...(step.outputName ? { outputName: step.outputName } : {}),
230
239
  ...(step.structured ? { structured: step.structured } : {}),
240
+ ...(step.checkpoint ? { checkpoint: step.checkpoint } : {}),
231
241
  status: step.status,
232
242
  ...(stepActivityState ? { activityState: stepActivityState } : {}),
233
243
  ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
@@ -258,6 +268,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
258
268
  ...(step.acceptance ? { acceptance: step.acceptance } : {}),
259
269
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
260
270
  ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
271
+ ...(step.launchResolvedExtensions ? { launchResolvedExtensions: step.launchResolvedExtensions } : {}),
272
+ ...(step.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: step.runtimeAcknowledgedExtensions } : {}),
261
273
  ...(step.execution ? { execution: step.execution } : {}),
262
274
  ...(step.review ? { review: step.review } : {}),
263
275
  ...(step.effects ? { effects: step.effects } : {}),
@@ -300,16 +312,20 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
300
312
  ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
301
313
  ...(parallelGroups.length ? { parallelGroups } : {}),
302
314
  steps: summarizedSteps,
315
+ ...(status.checkpoint ? { checkpoint: status.checkpoint } : {}),
303
316
  ...(nestedChildren.length ? { nestedChildren } : {}),
304
317
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
305
318
  ...(processTerminal ? { processTerminal } : {}),
306
319
  ...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
320
+ ...(status.launchResolvedExtensions ? { launchResolvedExtensions: status.launchResolvedExtensions } : {}),
321
+ ...(status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {}),
307
322
  ...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
308
323
  ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
309
324
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
310
325
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
311
326
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
312
327
  ...(status.totalCost ? { totalCost: status.totalCost } : {}),
328
+ ...(status.usageBudget ? { usageBudget: status.usageBudget } : {}),
313
329
  ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
314
330
  };
315
331
  }
@@ -5,8 +5,8 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
5
  import { appendJsonl } from "../../shared/artifacts.ts";
6
6
  import type { AsyncParallelGroupStatus, AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
7
7
  import { readStatus } from "../../shared/utils.ts";
8
- import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep } from "../shared/parallel-utils.ts";
9
- import { isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
8
+ import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep, RunnerCheckpointStep } from "../shared/parallel-utils.ts";
9
+ import { isCheckpointRunnerStep, isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
10
10
 
11
11
  const APPEND_REQUESTS_DIR = "append-requests";
12
12
 
@@ -51,7 +51,9 @@ export function countPendingChainAppendRequests(asyncDir: string): number {
51
51
  export function runnerStepOutputNames(steps: RunnerStep[]): string[] {
52
52
  const names: string[] = [];
53
53
  for (const step of steps) {
54
- if (isParallelGroup(step)) {
54
+ if (isCheckpointRunnerStep(step)) {
55
+ continue;
56
+ } else if (isParallelGroup(step)) {
55
57
  names.push(...step.parallel.map((task) => task.outputName).filter((name): name is string => Boolean(name)));
56
58
  } else if (isDynamicRunnerGroup(step)) {
57
59
  if (step.collect.as) names.push(step.collect.as);
@@ -128,9 +130,22 @@ export function consumeChainAppendRequests(asyncDir: string): ChainAppendRequest
128
130
  return requests.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
129
131
  }
130
132
 
133
+ const MAX_STATUS_STEP_DESCRIPTION_CHARS = 160;
134
+
135
+ /** Bounded one-line per-step task description persisted into status.json for fleet display. */
136
+ export function statusStepDescription(task: string | undefined): string | undefined {
137
+ const description = task?.replace(/\s+/g, " ").trim();
138
+ if (!description) return undefined;
139
+ return description.length > MAX_STATUS_STEP_DESCRIPTION_CHARS
140
+ ? `${description.slice(0, MAX_STATUS_STEP_DESCRIPTION_CHARS - 1)}…`
141
+ : description;
142
+ }
143
+
131
144
  function statusStepForTask(task: RunnerSubagentStep): StatusStep {
145
+ const description = statusStepDescription(task.task);
132
146
  return {
133
147
  agent: task.agent,
148
+ ...(description ? { description } : {}),
134
149
  ...(task.context ? { context: task.context } : {}),
135
150
  phase: task.phase,
136
151
  label: task.label,
@@ -147,7 +162,20 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
147
162
  };
148
163
  }
149
164
 
150
- function statusStepsForRunnerStep(step: RunnerStep): StatusStep[] {
165
+ function statusStepForCheckpoint(step: RunnerCheckpointStep, stepIndex: number): StatusStep {
166
+ return {
167
+ agent: `checkpoint:${step.checkpoint}`,
168
+ phase: step.phase,
169
+ label: step.label ?? step.checkpoint,
170
+ status: "pending",
171
+ checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
172
+ recentTools: [],
173
+ recentOutput: [],
174
+ };
175
+ }
176
+
177
+ function statusStepsForRunnerStep(step: RunnerStep, stepIndex: number): StatusStep[] {
178
+ if (isCheckpointRunnerStep(step)) return [statusStepForCheckpoint(step, stepIndex)];
151
179
  if (isParallelGroup(step)) return step.parallel.map(statusStepForTask);
152
180
  if (isDynamicRunnerGroup(step)) {
153
181
  return [{
@@ -239,6 +267,21 @@ function graphNodeForDynamic(step: DynamicRunnerGroup, stepIndex: number): Workf
239
267
 
240
268
  function appendWorkflowNode(graph: WorkflowGraphSnapshot | undefined, step: RunnerStep, stepIndex: number, flatIndex: number): void {
241
269
  if (!graph) return;
270
+ if (isCheckpointRunnerStep(step)) {
271
+ const node: WorkflowGraphNode = {
272
+ id: `step-${stepIndex}`,
273
+ kind: "checkpoint",
274
+ phase: step.phase,
275
+ label: step.label?.trim() || step.checkpoint,
276
+ status: "pending",
277
+ flatIndex,
278
+ stepIndex,
279
+ checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
280
+ };
281
+ graph.nodes.push(node);
282
+ pushPhase(graph, step.phase, node.id);
283
+ return;
284
+ }
242
285
  if (isParallelGroup(step)) {
243
286
  graph.nodes.push(graphNodeForParallel(step, stepIndex, flatIndex, graph));
244
287
  return;
@@ -263,7 +306,7 @@ export function appendRunnerStepsToStatus(input: {
263
306
  for (const step of input.steps) {
264
307
  const stepIndex = input.status.chainStepCount ?? input.status.steps?.length ?? 0;
265
308
  const flatIndex = input.status.steps?.length ?? 0;
266
- const statusSteps = statusStepsForRunnerStep(step);
309
+ const statusSteps = statusStepsForRunnerStep(step, stepIndex);
267
310
  input.status.steps ??= [];
268
311
  input.status.steps.push(...statusSteps);
269
312
  if (isParallelGroup(step)) {