pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -94,7 +94,7 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
94
94
  }));
95
95
  }
96
96
 
97
- function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): WorkflowReceipt | undefined {
97
+ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): { receipt: WorkflowReceipt; path: string } | undefined {
98
98
  const receiptRoot = path.dirname(asyncDir);
99
99
  const receiptPath = workflowReceiptPath(receiptRoot, status.runId);
100
100
  if (!fs.existsSync(receiptPath)) return undefined;
@@ -155,8 +155,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
155
155
  delete next.workflowResolution;
156
156
  delete next.recovery;
157
157
  }
158
- writeWorkflowReceipt(asyncDir, next);
159
- return next;
158
+ return { receipt: next, path: writeWorkflowReceipt(asyncDir, next) };
160
159
  }
161
160
 
162
161
  function appendDetachedWorkflowEvent(asyncDir: string, event: Record<string, unknown>): void {
@@ -209,10 +208,13 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
209
208
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
210
209
  }
211
210
  let receipt: WorkflowReceipt | undefined;
211
+ let receiptPath: string | undefined;
212
212
  let receiptError: string | undefined;
213
213
  const resolution = classifyWorkflowSettlement(next, input.result.interrupted);
214
214
  try {
215
- receipt = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
215
+ const publication = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
216
+ receipt = publication?.receipt;
217
+ receiptPath = publication?.path;
216
218
  } catch (error) {
217
219
  receiptError = `Failed to reconcile async workflow receipt: ${error instanceof Error ? error.message : String(error)}`;
218
220
  }
@@ -241,9 +243,10 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
241
243
  cwd: next.cwd,
242
244
  sessionId: next.sessionId ?? (typeof existing?.sessionId === "string" ? existing.sessionId : undefined),
243
245
  completionOwnerId: next.completionOwnerId,
246
+ ...(next.scheduleOrigin ? { scheduleOrigin: next.scheduleOrigin } : {}),
244
247
  },
245
248
  receipt,
246
- receiptPath: path.join(asyncDir, "workflow-receipt.json"),
249
+ receiptPath,
247
250
  receiptPersistenceError: receiptError,
248
251
  resolution,
249
252
  terminalOutcome: receipt?.terminalOutcome,
@@ -283,6 +286,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
283
286
  agent: "workflow",
284
287
  success: plan.status.state === "complete",
285
288
  state: plan.status.state,
289
+ ...(plan.publicResult.workflowReceipt ? { workflowReceipt: plan.publicResult.workflowReceipt } : {}),
286
290
  ...(resolution ? { workflowResolution: resolution } : {}),
287
291
  ...(plan.receipt?.terminalOutcome ? { terminalOutcome: plan.receipt.terminalOutcome } : {}),
288
292
  recovery: plan.recovery,
@@ -291,6 +295,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
291
295
  results,
292
296
  sessionId: plan.status.sessionId,
293
297
  completionOwnerId: plan.status.completionOwnerId,
298
+ ...(plan.status.scheduleOrigin ? { scheduleOrigin: plan.status.scheduleOrigin } : {}),
294
299
  timestamp: Date.now(),
295
300
  triggerTurn: true,
296
301
  });
@@ -3,8 +3,10 @@ import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
5
5
  import { readStatus } from "../../shared/utils.ts";
6
- import { steeringReceipt } from "../background/steering.ts";
7
- import type { SteerDeliveryMode } from "../background/control-channel.ts";
6
+ import { steeringReceipt, waitForSteeringAction } from "../background/steering.ts";
7
+ import { requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
8
+ import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
9
+ import { workflowAsyncChildSteeringGuidance } from "../shared/workflow-async-child-guidance.ts";
8
10
 
9
11
  export interface WorkflowForegroundSteeringTarget {
10
12
  control: ForegroundRunControl;
@@ -66,6 +68,58 @@ function managementError(message: string): AgentToolResult<Details> {
66
68
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
67
69
  }
68
70
 
71
+ /** Local controllers remain authoritative; recorded foreign ownership permits enqueue, not takeover or proof of liveness. */
72
+ export async function steerWorkflowRun(input: {
73
+ state: SubagentState;
74
+ runId: string;
75
+ asyncDir: string;
76
+ message: string;
77
+ mode?: SteerDeliveryMode;
78
+ index?: number;
79
+ signal?: AbortSignal;
80
+ ackTimeoutMs?: number;
81
+ }): Promise<AgentToolResult<Details>> {
82
+ const { state, runId, asyncDir } = input;
83
+ const status = readStatus(asyncDir);
84
+ if (!status || status.mode !== "workflow" || status.runId !== runId || path.basename(asyncDir) !== runId) {
85
+ return managementError(`Workflow '${runId}' does not match its resolved run directory.`);
86
+ }
87
+ if (state.workflowControllers?.has(runId)) {
88
+ const route = resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: runId, asyncDirRoot: path.dirname(asyncDir) });
89
+ if (!route.ok) return managementError([route.message, ...workflowAsyncChildSteeringGuidance(status, state)].join("\n"));
90
+ return steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, index: input.index });
91
+ }
92
+ if (!state.currentSessionId) return managementError("Workflow steering requires an active parent session.");
93
+ if (status.sessionId !== state.currentSessionId) return managementError(`Workflow '${runId}' was not found in the active session.`);
94
+ if (status.state !== "running" && status.state !== "queued") return managementError(`Workflow '${runId}' is not running or queued and cannot be steered.`);
95
+ if (typeof status.completionOwnerId !== "string" || !status.completionOwnerId.trim()
96
+ || status.completionOwnerId === (state.completionOwnerId ?? currentCompletionOwnerId())
97
+ || [...state.foregroundControls.values()].some((control) => control.parentWorkflowRunId === runId)) {
98
+ return managementError(`Workflow '${runId}' has no live foreground child and is not a recorded foreign workflow.`);
99
+ }
100
+ if (input.index !== undefined) {
101
+ const step = status.steps?.[input.index];
102
+ if (!Number.isInteger(input.index) || input.index < 0 || !step) return managementError(`Workflow '${runId}' index ${input.index} is out of range or invalid.`);
103
+ if (step.status !== "running" && step.status !== "pending") return managementError(`Workflow '${runId}' child ${input.index} is ${step.status} and cannot be steered.`);
104
+ if (typeof step.workflowKey !== "string" || !step.workflowKey.trim()) return managementError(`Workflow '${runId}' child ${input.index} has no projected workflow key.`);
105
+ }
106
+ const requestId = randomUUID();
107
+ try {
108
+ requestAsyncSteer(asyncDir, { id: requestId, message: input.message, mode: input.mode, ...(input.index !== undefined ? { targetIndex: input.index } : {}), source: "steer-action" });
109
+ } catch (error) {
110
+ return managementError(`Failed to queue steering for workflow ${runId}: ${error instanceof Error ? error.message : String(error)}`);
111
+ }
112
+ // Only the owner writes the ledger. No acknowledgment leaves an honest, unaddressed queued receipt.
113
+ const steering = await waitForSteeringAction({ asyncDir, sourceRunId: runId, requestId, timeoutMs: input.ackTimeoutMs ?? 3_000, signal: input.signal })
114
+ ?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: runId, targets: input.index === undefined ? [] : [{ index: input.index, state: "pending" as const }] };
115
+ const failed = steering.state === "failed" || steering.state === "partial";
116
+ return {
117
+ content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${failed ? steering.state : steering.deliveryStatus === "delivered" ? "delivered" : "queued"} for workflow ${runId} (request ${requestId}).`) }],
118
+ ...(failed ? { isError: true } : {}),
119
+ details: { mode: "management", results: [], steering },
120
+ };
121
+ }
122
+
69
123
  /**
70
124
  * Steer a live workflow-owned foreground child through its in-process session.
71
125
  * `steer` (and `auto`) interrupt the child at its next safe point; `follow_up`
@@ -91,6 +145,7 @@ export async function steerWorkflowForegroundTarget(input: {
91
145
 
92
146
  const message = input.message.trim();
93
147
  const requestId = randomUUID();
148
+
94
149
  const outcome = await child.steer({ message, ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}) });
95
150
  const target = outcome.state === "delivered"
96
151
  ? { index, state: "delivered" as const, deliveredAt: Date.now() }
@@ -24,7 +24,7 @@ import type {
24
24
  ChildWatchdogProgress,
25
25
  } from "../../shared/types.ts";
26
26
  import { unresolvedChildWatchdogBlockers } from "../../watchdog/child-status.ts";
27
- import { isAgentContractV1 } from "./agent-contract.ts";
27
+ import { isAgentContract } from "./agent-contract.ts";
28
28
  import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.ts";
29
29
 
30
30
  const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
@@ -105,11 +105,14 @@ function inferLevel(input: {
105
105
  const inferredReadOnly = readOnlyTask || ((readOnlyAgent || input.acceptanceRole === "read-only") && !taskMayWrite);
106
106
  const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
107
107
  const dynamicResolvesReadOnly = inferredReadOnly && !writeTask;
108
- const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
108
+ const riskyKeywordPattern = /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/;
109
+ const keywordRiskReadOnly = input.acceptanceRole === undefined
110
+ ? intent.kind === "read-only" && !riskyKeywordPattern.test(task)
111
+ : inferredReadOnly;
109
112
  const risky = Boolean(input.async && writeTask)
110
113
  || (Boolean(input.dynamic) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
111
114
  || (Boolean(input.dynamicGroup) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
112
- || (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
115
+ || (!keywordRiskReadOnly && riskyKeywordPattern.test(task));
113
116
 
114
117
  if (risky) {
115
118
  reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
@@ -435,7 +438,7 @@ export function resolveEffectiveAcceptance(input: {
435
438
  }): ResolvedAcceptanceConfig {
436
439
  const explicit = normalizeAcceptanceInput(input.explicit);
437
440
  const explicitLevel = normalizeLevel(explicit.level);
438
- if (isAgentContractV1(input.agentContract)) {
441
+ if (isAgentContract(input.agentContract)) {
439
442
  const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto"
440
443
  ? "none"
441
444
  : explicitLevel;
@@ -1,6 +1,6 @@
1
1
  import type { AgentContract, EffectsProjection, ExecutionProjection, ReviewProjection, SingleResult } from "../../shared/types.ts";
2
2
 
3
- export function isAgentContractV1(contract: AgentContract | undefined): boolean {
3
+ export function isAgentContract(contract: AgentContract | undefined): boolean {
4
4
  return contract?.version === 1;
5
5
  }
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
2
2
  import { formatModelThinking } from "../../shared/formatters.ts";
3
- import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
3
+ import type { AsyncJobState, AsyncJobStep, HostStepFreshness, HostStepMonitorKind, HostStepNode, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLane, WorkflowPreflight } from "../../shared/types.ts";
4
4
  import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
5
5
  import { workflowPreflightLaneForRuntimeKey } from "../../workflows/workflow-preflight.ts";
6
6
  import { workflowGraphStageNodes } from "./workflow-graph.ts";
@@ -32,7 +32,7 @@ function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshot
32
32
  return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
33
33
  }
34
34
 
35
- export interface AsyncStatusSnapshotActivityV1 {
35
+ export interface AsyncStatusSnapshotActivity {
36
36
  state?: string;
37
37
  currentTool?: string;
38
38
  lastActivityAt?: number;
@@ -41,7 +41,7 @@ export interface AsyncStatusSnapshotActivityV1 {
41
41
  toolCount?: number;
42
42
  }
43
43
 
44
- export interface AsyncStatusSnapshotHostStepV1 {
44
+ export interface AsyncStatusSnapshotHostStep {
45
45
  kind: HostStepMonitorKind;
46
46
  provider?: string;
47
47
  role?: string;
@@ -54,7 +54,7 @@ export interface AsyncStatusSnapshotHostStepV1 {
54
54
  report?: string;
55
55
  }
56
56
 
57
- export interface AsyncStatusSnapshotNodeV1 {
57
+ export interface AsyncStatusSnapshotNode {
58
58
  id: string;
59
59
  kind: AsyncStatusSnapshotKind | "host-step";
60
60
  label: string;
@@ -62,12 +62,12 @@ export interface AsyncStatusSnapshotNodeV1 {
62
62
  startedAt?: number;
63
63
  updatedAt?: number;
64
64
  endedAt?: number;
65
- activity?: AsyncStatusSnapshotActivityV1;
66
- hostStep?: AsyncStatusSnapshotHostStepV1;
67
- children?: AsyncStatusSnapshotNodeV1[];
65
+ activity?: AsyncStatusSnapshotActivity;
66
+ hostStep?: AsyncStatusSnapshotHostStep;
67
+ children?: AsyncStatusSnapshotNode[];
68
68
  }
69
69
 
70
- export interface AsyncStatusSnapshotCapsV1 {
70
+ export interface AsyncStatusSnapshotCaps {
71
71
  maxRuns: number;
72
72
  maxChildrenPerNode: number;
73
73
  maxDepth: number;
@@ -75,19 +75,19 @@ export interface AsyncStatusSnapshotCapsV1 {
75
75
  maxSerializedBytes: number;
76
76
  }
77
77
 
78
- export interface AsyncStatusSnapshotOmittedV1 {
78
+ export interface AsyncStatusSnapshotOmitted {
79
79
  runs: number;
80
80
  children: number;
81
81
  byteLimitExceeded: boolean;
82
82
  }
83
83
 
84
- export interface AsyncStatusSnapshotV1 {
84
+ export interface AsyncStatusSnapshot {
85
85
  kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
86
86
  version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
87
87
  generatedAt: number;
88
- caps: AsyncStatusSnapshotCapsV1;
89
- omitted: AsyncStatusSnapshotOmittedV1;
90
- runs: AsyncStatusSnapshotNodeV1[];
88
+ caps: AsyncStatusSnapshotCaps;
89
+ omitted: AsyncStatusSnapshotOmitted;
90
+ runs: AsyncStatusSnapshotNode[];
91
91
  }
92
92
 
93
93
  export interface AsyncStatusSnapshotOptions {
@@ -108,6 +108,8 @@ export interface AsyncStatusWorkflowRow {
108
108
  modelThinking?: string;
109
109
  activity?: string;
110
110
  startedAt?: number;
111
+ endedAt?: number;
112
+ durationMs?: number;
111
113
  tokens?: number;
112
114
  window?: number;
113
115
  overflow?: number;
@@ -117,19 +119,19 @@ export interface AsyncStatusWorkflowRow {
117
119
  reasonCode?: string;
118
120
  detail?: string;
119
121
  target?: string;
120
- freshness?: HostStepFreshnessV1;
122
+ freshness?: HostStepFreshness;
121
123
  reportPath?: string;
122
- preflight?: WorkflowPreflightLaneV1;
124
+ preflight?: WorkflowPreflightLane;
123
125
  }
124
126
 
125
127
  interface ProjectionContext {
126
- caps: AsyncStatusSnapshotCapsV1;
127
- omitted: AsyncStatusSnapshotOmittedV1;
128
+ caps: AsyncStatusSnapshotCaps;
129
+ omitted: AsyncStatusSnapshotOmitted;
128
130
  }
129
131
 
130
- function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | undefined): HostStepNodeV1[] {
132
+ function validHostStepList(source: readonly HostStepNode[] | WorkflowGraphSnapshot | undefined): HostStepNode[] {
131
133
  if (source && "nodes" in source) return validHostStepNodes(source);
132
- const hostSteps: HostStepNodeV1[] = [];
134
+ const hostSteps: HostStepNode[] = [];
133
135
  for (const [index, value] of (source ?? []).slice(0, HOST_STEP_MAX_COUNT).entries()) {
134
136
  try {
135
137
  hostSteps.push(parseHostStepNode(value, `hostSteps[${index}]`));
@@ -140,7 +142,7 @@ function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnap
140
142
  return hostSteps;
141
143
  }
142
144
 
143
- function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCapsV1 {
145
+ function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCaps {
144
146
  return {
145
147
  maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
146
148
  maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
@@ -199,13 +201,13 @@ function activityFor(source: {
199
201
  currentToolStartedAt?: unknown;
200
202
  turnCount?: unknown;
201
203
  toolCount?: unknown;
202
- }, ctx: ProjectionContext): AsyncStatusSnapshotActivityV1 | undefined {
204
+ }, ctx: ProjectionContext): AsyncStatusSnapshotActivity | undefined {
203
205
  const currentTool = publicOptionalText(source.currentTool, ctx.caps.maxStringLength);
204
206
  const lastActivityAt = publicTime(source.lastActivityAt);
205
207
  const currentToolStartedAt = publicTime(source.currentToolStartedAt);
206
208
  const turnCount = publicCount(source.turnCount);
207
209
  const toolCount = publicCount(source.toolCount);
208
- const activity: AsyncStatusSnapshotActivityV1 = {
210
+ const activity: AsyncStatusSnapshotActivity = {
209
211
  ...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
210
212
  ...(currentTool ? { currentTool } : {}),
211
213
  ...(lastActivityAt !== undefined ? { lastActivityAt } : {}),
@@ -216,19 +218,19 @@ function activityFor(source: {
216
218
  return Object.keys(activity).length ? activity : undefined;
217
219
  }
218
220
 
219
- function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: ProjectionContext): void {
221
+ function appendBoundedChildren(children: AsyncStatusSnapshotNode[], source: readonly AsyncStatusSnapshotNode[], ctx: ProjectionContext): void {
220
222
  const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
221
223
  children.push(...source.slice(0, remaining));
222
224
  ctx.omitted.children += Math.max(0, source.length - remaining);
223
225
  }
224
226
 
225
- function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
227
+ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
226
228
  const state = normalizeState(step.status);
227
229
  const startedAt = publicTime(step.startedAt);
228
230
  const endedAt = publicTime(step.endedAt);
229
231
  const updatedAt = endedAt ?? publicTime(step.lastActivityAt) ?? startedAt;
230
232
  const activity = activityFor(step, ctx);
231
- const node: AsyncStatusSnapshotNodeV1 = {
233
+ const node: AsyncStatusSnapshotNode = {
232
234
  id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
233
235
  kind: "step",
234
236
  label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
@@ -240,7 +242,7 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
240
242
  };
241
243
  if (depth < ctx.caps.maxDepth && step.children?.length) {
242
244
  const nested = step.children.map((child, childIndex) => projectNestedRun(child, childIndex, depth + 1, ctx));
243
- const bounded: AsyncStatusSnapshotNodeV1[] = [];
245
+ const bounded: AsyncStatusSnapshotNode[] = [];
244
246
  appendBoundedChildren(bounded, nested, ctx);
245
247
  if (bounded.length) node.children = bounded;
246
248
  } else if (step.children?.length) {
@@ -249,7 +251,7 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
249
251
  return node;
250
252
  }
251
253
 
252
- function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
254
+ function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
253
255
  const step: AsyncJobStep = {
254
256
  agent: node.agent ?? node.label,
255
257
  status: workflowGraphStepStatus(node.status),
@@ -259,13 +261,13 @@ function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number],
259
261
  return projectStep(step, node.flatIndex ?? index, depth, ctx);
260
262
  }
261
263
 
262
- function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
264
+ function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
263
265
  const state = normalizeState(child.state);
264
266
  const startedAt = publicTime(child.startedAt);
265
267
  const endedAt = publicTime(child.endedAt);
266
268
  const updatedAt = publicTime(child.lastUpdate) ?? endedAt ?? publicTime(child.lastActivityAt) ?? startedAt;
267
269
  const activity = activityFor(child, ctx);
268
- const node: AsyncStatusSnapshotNodeV1 = {
270
+ const node: AsyncStatusSnapshotNode = {
269
271
  id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
270
272
  kind: kindForMode(child.mode),
271
273
  label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
@@ -278,7 +280,7 @@ function projectNestedRun(child: NestedRunSummary, index: number, depth: number,
278
280
  if (depth < ctx.caps.maxDepth) {
279
281
  const nestedSteps = child.steps?.map((step, stepIndex) => projectStep(step, stepIndex, depth + 1, ctx)) ?? [];
280
282
  const nestedChildren = child.children?.map((nested, childIndex) => projectNestedRun(nested, childIndex, depth + 1, ctx)) ?? [];
281
- const bounded: AsyncStatusSnapshotNodeV1[] = [];
283
+ const bounded: AsyncStatusSnapshotNode[] = [];
282
284
  appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
283
285
  if (bounded.length) node.children = bounded;
284
286
  } else {
@@ -295,11 +297,11 @@ function hostStepSnapshotState(state: HostStepState, verdict: HostStepVerdict |
295
297
  return verdict === undefined || verdict === "inconclusive" ? "partial" : "complete";
296
298
  }
297
299
 
298
- function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
300
+ function projectHostStep(hostStep: HostStepNode, ctx: ProjectionContext): AsyncStatusSnapshotNode {
299
301
  const state = hostStepSnapshotState(hostStep.state, hostStep.verdict);
300
302
  const detail = publicOptionalText(hostStep.detail, ctx.caps.maxStringLength);
301
303
  const report = publicOptionalText(hostStepReportName(hostStep.reportPath), ctx.caps.maxStringLength);
302
- const hostMetadata: AsyncStatusSnapshotHostStepV1 = {
304
+ const hostMetadata: AsyncStatusSnapshotHostStep = {
303
305
  kind: hostStep.monitorKind,
304
306
  state: hostStep.state,
305
307
  ...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", ctx.caps.maxStringLength) } : {}),
@@ -322,12 +324,12 @@ function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): Asyn
322
324
  };
323
325
  }
324
326
 
325
- function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
327
+ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNode {
326
328
  const state = normalizeState(job.status);
327
329
  const startedAt = publicTime(job.startedAt);
328
330
  const updatedAt = publicTime(job.updatedAt) ?? startedAt;
329
331
  const activity = activityFor(job, ctx);
330
- const node: AsyncStatusSnapshotNodeV1 = {
332
+ const node: AsyncStatusSnapshotNode = {
331
333
  id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
332
334
  kind: kindForMode(job.mode),
333
335
  label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
@@ -349,7 +351,7 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
349
351
  const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
350
352
  const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
351
353
  const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
352
- const bounded: AsyncStatusSnapshotNodeV1[] = [];
354
+ const bounded: AsyncStatusSnapshotNode[] = [];
353
355
  bounded.push(...retainedOrdinaryChildren, ...retainedHostSteps);
354
356
  ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
355
357
  if (bounded.length) node.children = bounded;
@@ -362,11 +364,11 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
362
364
  return node;
363
365
  }
364
366
 
365
- function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
367
+ function snapshotBytes(snapshot: AsyncStatusSnapshot): number {
366
368
  return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
367
369
  }
368
370
 
369
- function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
371
+ function enforceByteLimit(snapshot: AsyncStatusSnapshot): void {
370
372
  if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
371
373
  snapshot.omitted.byteLimitExceeded = true;
372
374
  const runs = snapshot.runs;
@@ -401,7 +403,7 @@ function workflowStepName(step: AsyncJobStep, index: number): string {
401
403
  return `${phase}${key}${label} (${step.agent})`;
402
404
  }
403
405
 
404
- function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
406
+ function hostStepRow(hostStep: HostStepNode): AsyncStatusWorkflowRow {
405
407
  const freshness = hostStep.freshness
406
408
  ? {
407
409
  expectedRef: publicText(hostStep.freshness.expectedRef, "ref", HOST_STEP_MAX_REF_CHARS),
@@ -425,11 +427,11 @@ function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
425
427
  }
426
428
 
427
429
 
428
- function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowPreflightV1 {
430
+ function isWorkflowPreflight(value: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight | undefined): value is WorkflowPreflight {
429
431
  return value !== undefined && !Array.isArray(value) && "lanes" in value;
430
432
  }
431
433
 
432
- function isWorkflowGraph(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowGraphSnapshot {
434
+ function isWorkflowGraph(value: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight | undefined): value is WorkflowGraphSnapshot {
433
435
  return value !== undefined && !Array.isArray(value) && "nodes" in value;
434
436
  }
435
437
 
@@ -465,7 +467,7 @@ function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): str
465
467
  return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
466
468
  }
467
469
 
468
- function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
470
+ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLane): AsyncStatusWorkflowRow {
469
471
  return {
470
472
  name: workflowGraphRowName(node),
471
473
  state: workflowGraphRowState(node.status),
@@ -476,14 +478,14 @@ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], p
476
478
  /** Project authoritative workflow facts into compact rows, annotated by preflight hints. */
477
479
  export function projectAsyncWorkflowRows(
478
480
  steps: readonly AsyncJobStep[] | undefined,
479
- hostStepsOrPreflight?: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1,
480
- preflightOverride?: WorkflowPreflightV1,
481
+ hostStepsOrPreflight?: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight,
482
+ preflightOverride?: WorkflowPreflight,
481
483
  ): AsyncStatusWorkflowRow[] {
482
484
  const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
483
485
  const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
484
486
  const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
485
487
  const loaded = steps ?? [];
486
- const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined =>
488
+ const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []): WorkflowPreflightLane | undefined =>
487
489
  workflowPreflightLaneForRuntimeKey(preflight, key, groupKeys);
488
490
  if (graph) {
489
491
  const loadedIndexesByKey = new Map<string, number[]>();
@@ -526,7 +528,7 @@ export function projectAsyncWorkflowRows(
526
528
  ];
527
529
  }
528
530
 
529
- function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
531
+ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLane): AsyncStatusWorkflowRow {
530
532
  const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
531
533
  const activity = workflowStepActivity(step);
532
534
  return {
@@ -536,6 +538,8 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
536
538
  ...(modelThinking ? { modelThinking } : {}),
537
539
  ...(activity ? { activity } : {}),
538
540
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
541
+ ...(step.endedAt !== undefined ? { endedAt: step.endedAt } : {}),
542
+ ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
539
543
  ...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
540
544
  ...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
541
545
  ...(preflight ? { preflight } : {}),
@@ -543,7 +547,7 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
543
547
  }
544
548
 
545
549
  /** Project already-loaded async status facts into the bounded public snapshot shape. */
546
- export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
550
+ export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
547
551
  const caps = resolveCaps(options);
548
552
  const ctx: ProjectionContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
549
553
  const sorted = [...jobs].sort((left, right) => {
@@ -552,7 +556,7 @@ export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, option
552
556
  return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
553
557
  });
554
558
  ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
555
- const snapshot: AsyncStatusSnapshotV1 = {
559
+ const snapshot: AsyncStatusSnapshot = {
556
560
  kind: ASYNC_STATUS_SNAPSHOT_KIND,
557
561
  version: ASYNC_STATUS_SNAPSHOT_VERSION,
558
562
  generatedAt: options.generatedAt ?? Date.now(),
@@ -29,6 +29,8 @@ export interface SubagentCapabilityAudit {
29
29
  effectiveMcpTools: string[];
30
30
  agentAllowed: boolean;
31
31
  agentRestrictionSources?: string[];
32
+ /** Builtin tools declared but unavailable on the host runtime. */
33
+ unavailableHostBuiltins?: string[];
32
34
  }
33
35
 
34
36
  export interface RegisterSubagentCapabilityCeilingOptions {