pi-subagents 0.65.1 → 0.66.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 (85) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +16 -2
  5. package/docs/configuration.md +18 -0
  6. package/docs/extension-api.md +91 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +3 -3
  10. package/docs/workflows.md +14 -7
  11. package/package.json +2 -1
  12. package/skills/pi-subagents/references/execution-controls.md +14 -1
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  14. package/src/agents/advertised-agent-prompt.ts +63 -0
  15. package/src/agents/agent-management.ts +14 -1
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +8 -0
  18. package/src/api/shared-types.ts +1 -1
  19. package/src/api/workflow-resources.ts +6 -0
  20. package/src/extension/index.ts +40 -2
  21. package/src/extension/public-execution.ts +0 -1
  22. package/src/extension/rpc.ts +4 -21
  23. package/src/extension/schemas.ts +8 -6
  24. package/src/extension/tool-description.ts +6 -5
  25. package/src/intercom/native-supervisor-channel.ts +82 -54
  26. package/src/runs/background/active-async-capacity.ts +18 -18
  27. package/src/runs/background/async-job-tracker.ts +35 -3
  28. package/src/runs/background/async-status-snapshot.ts +10 -12
  29. package/src/runs/background/async-status.ts +17 -9
  30. package/src/runs/background/auto-drain.ts +40 -29
  31. package/src/runs/background/chain-root-attachment.ts +8 -0
  32. package/src/runs/background/control-channel.ts +78 -44
  33. package/src/runs/background/notify.ts +86 -12
  34. package/src/runs/background/owned-process-tree.ts +6 -6
  35. package/src/runs/background/process-terminal.ts +23 -23
  36. package/src/runs/background/run-child-session.ts +60 -32
  37. package/src/runs/background/run-status.ts +75 -5
  38. package/src/runs/background/runner-aliases.ts +18 -7
  39. package/src/runs/background/runner-child-launch.ts +86 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -1
  41. package/src/runs/background/subagent-runner.ts +412 -209
  42. package/src/runs/background/subagent-wait.ts +3 -0
  43. package/src/runs/background/wait-completions.ts +4 -0
  44. package/src/runs/foreground/async-steering-action.ts +19 -0
  45. package/src/runs/foreground/execution.ts +101 -25
  46. package/src/runs/foreground/subagent-executor.ts +474 -197
  47. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  48. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  49. package/src/runs/shared/acceptance.ts +2 -2
  50. package/src/runs/shared/agent-contract.ts +1 -1
  51. package/src/runs/shared/async-status-projection.ts +47 -47
  52. package/src/runs/shared/child-hooks.ts +151 -2
  53. package/src/runs/shared/child-launch.ts +18 -13
  54. package/src/runs/shared/child-session.ts +42 -6
  55. package/src/runs/shared/child-tool-plan.ts +2 -2
  56. package/src/runs/shared/completion-evidence.ts +2 -2
  57. package/src/runs/shared/completion-guard.ts +1 -0
  58. package/src/runs/shared/host-step-status.ts +11 -11
  59. package/src/runs/shared/llm-intent-arbiter.ts +10 -9
  60. package/src/runs/shared/model-fallback.ts +10 -6
  61. package/src/runs/shared/nested-events.ts +5 -5
  62. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  63. package/src/runs/shared/parallel-handoff.ts +57 -12
  64. package/src/runs/shared/parallel-utils.ts +2 -2
  65. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  66. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  67. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  68. package/src/runs/shared/run-fanout-budget.ts +8 -8
  69. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  70. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  71. package/src/runs/shared/worktree-setup-command.ts +190 -0
  72. package/src/runs/shared/worktree.ts +332 -204
  73. package/src/shared/types.ts +81 -60
  74. package/src/shared/utils.ts +7 -2
  75. package/src/shared/workflow-child-permit.ts +18 -13
  76. package/src/tui/fleet.ts +11 -5
  77. package/src/tui/render.ts +23 -5
  78. package/src/workflows/chat-progress.ts +3 -3
  79. package/src/workflows/scripted-workflow.ts +38 -10
  80. package/src/workflows/workflow-checklist.ts +11 -15
  81. package/src/workflows/workflow-child-summary.ts +57 -8
  82. package/src/workflows/workflow-preflight.ts +19 -19
  83. package/src/workflows/workflow-receipt.ts +3 -3
  84. package/src/workflows/workflow-resources.ts +96 -21
  85. package/src/workflows/workflow-settlement.ts +3 -0
@@ -6,10 +6,11 @@ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/para
6
6
  import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
7
7
  import { classifyTaskMutationIntent } from "../runs/shared/task-intent.ts";
8
8
  import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
9
- import type { AcceptanceRecoveryMetadata, HostStepNodeV1, SingleResult } from "../shared/types.ts";
9
+ import type { AcceptanceRecoveryMetadata, HostStepNode, SingleResult } from "../shared/types.ts";
10
10
  import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
11
11
 
12
12
  const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13
+ const BASE_REF_VALIDATION_ERROR = "baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.";
13
14
  function validGitRef(ref: unknown): ref is string {
14
15
  if (typeof ref !== "string" || !ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
15
16
  if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
@@ -644,7 +645,7 @@ function validateRunCall(key, params, label, fingerprints) {
644
645
  }
645
646
  if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
646
647
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
647
- if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref.");
648
+ if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.");
648
649
  validateLaneMetadata(params.lane, label + " lane", key);
649
650
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
650
651
  if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(params.gate, params.acceptance));
@@ -1110,11 +1111,11 @@ export interface RunWorkflowScriptOptions {
1110
1111
  globalConcurrencyLimit?: number;
1111
1112
  admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
1112
1113
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
1113
- resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
1114
+ resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
1114
1115
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
1115
1116
  steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
1116
1117
  host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
1117
- onHostStep?: (hostStep: HostStepNodeV1) => void;
1118
+ onHostStep?: (hostStep: HostStepNode) => void;
1118
1119
  state?: {
1119
1120
  get: (key: string) => unknown | Promise<unknown>;
1120
1121
  set: (key: string, value: unknown) => void | Promise<void>;
@@ -1502,7 +1503,7 @@ function definitelyNonJson(node: AstNode, normalizeUndefined = false): string |
1502
1503
  if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
1503
1504
  const values = new Map<string, AstNode>();
1504
1505
  for (const property of node.properties) {
1505
- if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) return undefined;
1506
+ if (!astNode(property) || property.type !== "Property" || !astNode(property.value) || property.kind !== "init") return undefined;
1506
1507
  const key = staticPropertyKey(property);
1507
1508
  if (key === undefined) return undefined;
1508
1509
  values.set(key, property.value);
@@ -1531,6 +1532,26 @@ function directObjectPropertyValue(node: AstNode, name: string): AstNode | undef
1531
1532
  return value;
1532
1533
  }
1533
1534
 
1535
+ function validateStaticBaseRef(params: AstNode, owner: string): WorkflowScriptValidationError[] {
1536
+ if (params.type !== "ObjectExpression" || !Array.isArray(params.properties)) return [];
1537
+ // Inspect the final definition only. A later spread or unknown key may overwrite it.
1538
+ for (let index = params.properties.length - 1; index >= 0; index--) {
1539
+ const property = params.properties[index];
1540
+ if (!astNode(property) || property.type !== "Property") return [];
1541
+ const key = staticPropertyKey(property);
1542
+ if (key === undefined) return [];
1543
+ if (key !== "baseRef") continue;
1544
+ if (property.kind !== "init" || !astNode(property.value)) return [];
1545
+ const valueNode = property.value;
1546
+ const value = literalString(valueNode);
1547
+ if ((value !== undefined || valueNode.type === "Literal") && !validGitRef(value)) {
1548
+ return [{ message: `${owner} ${BASE_REF_VALIDATION_ERROR}`, ...nodeLocation(valueNode) }];
1549
+ }
1550
+ return [];
1551
+ }
1552
+ return [];
1553
+ }
1554
+
1534
1555
  function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
1535
1556
  const args = Array.isArray(call.arguments) ? call.arguments : [];
1536
1557
  const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
@@ -1572,6 +1593,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1572
1593
  const key = literalString(keyNode);
1573
1594
  if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
1574
1595
  if (astNode(args[1])) {
1596
+ errors.push(...validateStaticBaseRef(args[1], "runs.run"));
1575
1597
  const message = definitelyNonJson(args[1]);
1576
1598
  if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
1577
1599
  }
@@ -1581,6 +1603,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1581
1603
  const args = Array.isArray(node.arguments) ? node.arguments : [];
1582
1604
  if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
1583
1605
  for (const item of args[0].elements) if (astNode(item)) {
1606
+ errors.push(...validateStaticBaseRef(item, "runs.all item"));
1584
1607
  const message = definitelyNonJson(item);
1585
1608
  if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
1586
1609
  }
@@ -1751,7 +1774,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1751
1774
  console.error("Workflow onLanePlan callback failed:", error);
1752
1775
  }
1753
1776
  };
1754
- const hostStepChanged = (hostStep: HostStepNodeV1) => {
1777
+ const hostStepChanged = (hostStep: HostStepNode) => {
1755
1778
  try {
1756
1779
  options.onHostStep?.(hostStep);
1757
1780
  } catch (error) {
@@ -2047,7 +2070,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2047
2070
  if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
2048
2071
  if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
2049
2072
  const startedAt = Date.now();
2050
- const startedStep: HostStepNodeV1 = {
2073
+ const startedStep: HostStepNode = {
2051
2074
  version: 1,
2052
2075
  kind: "host-step",
2053
2076
  monitorKind: "command",
@@ -2130,7 +2153,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2130
2153
  return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
2131
2154
  }
2132
2155
  if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
2133
- return respond(Promise.reject(new Error(`runs.run('${key}') baseRef must be a valid Git ref.`)));
2156
+ return respond(Promise.reject(new Error(`runs.run('${key}') ${BASE_REF_VALIDATION_ERROR}`)));
2134
2157
  }
2135
2158
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
2136
2159
  return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
@@ -2183,10 +2206,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2183
2206
  const childStopController = new AbortController();
2184
2207
  childStopControllers.set(key, childStopController);
2185
2208
  const childSignal = combinedAbortSignal([childController.signal, childStopController.signal]);
2186
- const resolvedResumeValue = resumeReference
2209
+ const resumeInput = resumeReference ?? (typeof params.resume === "string" && options.resolveResume ? params.resume : undefined);
2210
+ const resolvedResumeValue = resumeInput
2187
2211
  ? await Promise.resolve().then(() => {
2188
2212
  if (!options.resolveResume) throw new Error("Keyed workflow receipt resume is unavailable in this host.");
2189
- return options.resolveResume(resumeReference, childSignal);
2213
+ return options.resolveResume(resumeInput, childSignal, typeof params.index === "number" ? params.index : undefined);
2190
2214
  })
2191
2215
  : undefined;
2192
2216
  const resolvedResume = typeof resolvedResumeValue === "string"
@@ -2202,6 +2226,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2202
2226
  : [];
2203
2227
  resolvedResumeLineage = [...new Set(lineage.length ? lineage : [resolvedResumeId!])];
2204
2228
  if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
2229
+ if (typeof resumeInput === "string") {
2230
+ const predecessor = [...children.values()].find((child) => child.runId === resolvedResumeId);
2231
+ if (predecessor?.continuation && predecessor.continuation.runIds.at(-1) === resolvedResumeId) resolvedResumeLineage = predecessor.continuation.runIds;
2232
+ }
2205
2233
  }
2206
2234
  const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
2207
2235
  await launchSemaphore.acquire();
@@ -1,6 +1,6 @@
1
- import type { AsyncJobStep, HostStepNodeV1, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
1
+ import type { AsyncJobStep, HostStepNode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
2
2
  import { sanitizeDisplayText } from "../shared/display-text.ts";
3
- import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
3
+ import { workflowPreflightLaneForRuntimeKey as laneFor } from "./workflow-preflight.ts";
4
4
 
5
5
  export type WorkflowChecklistState = "complete" | "running" | "queued" | "blocked" | "failed" | "paused" | "stopped";
6
6
 
@@ -62,12 +62,12 @@ export interface WorkflowChecklistItem {
62
62
  toolCount?: number;
63
63
  outputName?: string;
64
64
  error?: string;
65
- preflight?: WorkflowPreflightLaneV1;
65
+ preflight?: WorkflowPreflightLane;
66
66
  kind?: "child" | "host";
67
- monitorKind?: HostStepNodeV1["monitorKind"];
67
+ monitorKind?: HostStepNode["monitorKind"];
68
68
  provider?: string;
69
69
  role?: string;
70
- verdict?: HostStepNodeV1["verdict"];
70
+ verdict?: HostStepNode["verdict"];
71
71
  target?: string;
72
72
  reasonCode?: string;
73
73
  stale?: boolean;
@@ -106,8 +106,8 @@ export interface WorkflowChecklistProjection {
106
106
  export interface WorkflowChecklistInput {
107
107
  graph?: WorkflowGraphSnapshot;
108
108
  steps?: readonly WorkflowChecklistStep[] | readonly AsyncJobStep[];
109
- hostSteps?: readonly HostStepNodeV1[];
110
- preflight?: WorkflowPreflightV1;
109
+ hostSteps?: readonly HostStepNode[];
110
+ preflight?: WorkflowPreflight;
111
111
  trace?: readonly WorkflowChecklistTraceEntry[];
112
112
  now?: number;
113
113
  }
@@ -190,15 +190,11 @@ function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" |
190
190
  return end === undefined ? undefined : Math.max(0, end - startedAt);
191
191
  }
192
192
 
193
- function laneFor(preflight: WorkflowPreflightV1 | undefined, key: string, preferredKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined {
194
- return workflowPreflightLaneForRuntimeKey(preflight, key, preferredKeys);
195
- }
196
-
197
193
  function stepKey(step: WorkflowChecklistStep): string | undefined {
198
194
  return step.key ?? step.workflowKey ?? step.runId;
199
195
  }
200
196
 
201
- function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?: WorkflowPreflightLaneV1): WorkflowChecklistItem {
197
+ function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?: WorkflowPreflightLane): WorkflowChecklistItem {
202
198
  const state = checklistState(step);
203
199
  return {
204
200
  key: keyText(key, `step-${index + 1}`),
@@ -219,7 +215,7 @@ function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key
219
215
  };
220
216
  }
221
217
 
222
- function hostItem(host: HostStepNodeV1, phase: string, key = host.id): WorkflowChecklistItem {
218
+ function hostItem(host: HostStepNode, phase: string, key = host.id): WorkflowChecklistItem {
223
219
  const state = checklistState({ status: host.state, verdict: host.verdict, stale: host.freshness?.stale });
224
220
  return {
225
221
  key: keyText(key, "host-step"),
@@ -264,7 +260,7 @@ function traceSources(trace: readonly WorkflowChecklistTraceEntry[] | undefined)
264
260
  return [...latest.values()];
265
261
  }
266
262
 
267
- function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
263
+ function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLane | undefined): WorkflowChecklistItem {
268
264
  const phase = keyText(preflight?.key ?? entry.generatedLaneKey ?? entry.phase, "Workflow");
269
265
  const item = stepItem({ key: entry.key, label: entry.label, phase, agent: entry.agent, status: entry.state === "started" ? "running" : entry.state, durationMs: entry.durationMs, error: entry.error }, index, phase, entry.key, entry.label ?? entry.key, preflight);
270
266
  if (entry.operation === "host") item.kind = "host";
@@ -284,7 +280,7 @@ function add(phases: Map<string, WorkflowChecklistPhase>, phase: string, item: W
284
280
  phaseFor(phases, phase).items.push(item);
285
281
  }
286
282
 
287
- function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
283
+ function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight: WorkflowPreflightLane | undefined): WorkflowChecklistItem {
288
284
  const state = checklistState(step);
289
285
  const nodeState = checklistState({ status: node.status, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined });
290
286
  const status = TERMINAL_STATES.has(nodeState) && !TERMINAL_STATES.has(state)
@@ -1,9 +1,42 @@
1
- import type { AsyncStatus, WorkflowChildSummaryV1 } from "../shared/types.ts";
1
+ import type { AgentProgress, AsyncStatus, WorkflowChildActivity, WorkflowChildSummary } from "../shared/types.ts";
2
2
  import type { WorkflowScriptChildResult, WorkflowScriptTraceEntry } from "./scripted-workflow.ts";
3
3
 
4
4
  const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
5
  const TERMINAL_STATES = new Set(["completed", "failed", "paused", "stopped", "rejected", "detached"]);
6
6
  const MAX_REQUIRED_ID_BYTES = 4_096;
7
+ const ACTIVITY_COUNTERS = ["currentToolStartedAt", "lastActivityAt", "durationMs", "toolCount", "turnCount", "tokens", "inputTokens", "outputTokens"] as const;
8
+ type WorkflowChildLiveProgress = WorkflowChildActivity & Pick<AgentProgress, "agent" | "sessionName" | "model" | "thinking">;
9
+
10
+ export function workflowChildProgress(progress: AgentProgress): WorkflowChildLiveProgress {
11
+ return {
12
+ ...workflowChildActivity(progress),
13
+ agent: bounded(progress.agent, 256) ?? "",
14
+ ...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
15
+ ...(bounded(progress.model, 256) ? { model: progress.model } : {}),
16
+ ...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
17
+ };
18
+ }
19
+
20
+ /** Fixed field count and a 256 UTF-8 byte tool name keep JSON activity below 2 KiB, including escaping. */
21
+ export function workflowChildActivity(progress: WorkflowChildActivity): WorkflowChildActivity {
22
+ const activity: WorkflowChildActivity = {};
23
+ if (bounded(progress.currentTool, 256)) activity.currentTool = progress.currentTool;
24
+ for (const field of ACTIVITY_COUNTERS) {
25
+ const value = progress[field];
26
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) activity[field] = value;
27
+ }
28
+ return activity;
29
+ }
30
+
31
+ function parseActivity(value: unknown): WorkflowChildActivity | undefined {
32
+ if (value === undefined) return undefined;
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren child activity is invalid.");
34
+ const input = value as Record<string, unknown>;
35
+ for (const [key, value] of Object.entries(input)) {
36
+ if (key === "currentTool" ? !bounded(value, 256) : !ACTIVITY_COUNTERS.includes(key as typeof ACTIVITY_COUNTERS[number]) || typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error("workflowChildren child activity is invalid.");
37
+ }
38
+ return { ...input } as WorkflowChildActivity;
39
+ }
7
40
 
8
41
  function bounded(value: unknown, maxBytes: number): string | undefined {
9
42
  if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) return undefined;
@@ -19,13 +52,15 @@ function requiredId(value: string, label: string): string {
19
52
  export function workflowChildSummary(input: {
20
53
  parentToolCallId: string;
21
54
  workflowRunId: string;
22
- workflowState: WorkflowChildSummaryV1["workflowState"];
55
+ workflowState: WorkflowChildSummary["workflowState"];
23
56
  inventoryComplete: boolean;
24
57
  trace?: WorkflowScriptTraceEntry[];
25
58
  children?: WorkflowScriptChildResult[];
26
59
  steps?: NonNullable<AsyncStatus["steps"]>;
27
- }): WorkflowChildSummaryV1 {
28
- const rows = new Map<string, WorkflowChildSummaryV1["children"][number]>();
60
+ /** In-memory snapshots from synchronous child callbacks, keyed by workflow key. */
61
+ progress?: ReadonlyMap<string, WorkflowChildLiveProgress>;
62
+ }): WorkflowChildSummary {
63
+ const rows = new Map<string, WorkflowChildSummary["children"][number]>();
29
64
  for (const entry of input.trace ?? []) {
30
65
  if (entry.operation !== "run" || !KEY_PATTERN.test(entry.key)) continue;
31
66
  const previous = rows.get(entry.key);
@@ -83,6 +118,18 @@ export function workflowChildSummary(input: {
83
118
  if (!TERMINAL_STATES.has(row.state)) rows.set(key, { ...row, state: input.workflowState === "stopped" ? "stopped" : "failed" });
84
119
  }
85
120
  }
121
+ for (const [key, progress] of input.progress ?? []) {
122
+ const row = rows.get(key);
123
+ if (!row || row.state !== "running") continue;
124
+ rows.set(key, {
125
+ ...row,
126
+ ...(bounded(progress.agent, 256) ? { agent: progress.agent } : {}),
127
+ ...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
128
+ ...(bounded(progress.model, 256) ? { model: progress.model } : {}),
129
+ ...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
130
+ activity: workflowChildActivity(progress),
131
+ });
132
+ }
86
133
  return {
87
134
  version: 1,
88
135
  parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"),
@@ -93,7 +140,7 @@ export function workflowChildSummary(input: {
93
140
  };
94
141
  }
95
142
 
96
- export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV1 | undefined {
143
+ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummary | undefined {
97
144
  if (value === undefined) return undefined;
98
145
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren must be an object.");
99
146
  const input = value as Record<string, unknown>;
@@ -102,17 +149,19 @@ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV
102
149
  if (input.version !== 1 || typeof input.inventoryComplete !== "boolean" || !Array.isArray(input.children)) throw new Error("workflowChildren is invalid.");
103
150
  const workflowState = input.workflowState;
104
151
  if (workflowState !== "queued" && workflowState !== "running" && workflowState !== "completed" && workflowState !== "failed" && workflowState !== "paused" && workflowState !== "stopped") throw new Error("workflowChildren.workflowState is invalid.");
105
- const children = input.children.map((row): WorkflowChildSummaryV1["children"][number] => {
152
+ const children = input.children.map((row): WorkflowChildSummary["children"][number] => {
106
153
  if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("workflowChildren child row is invalid.");
107
154
  const child = row as Record<string, unknown>;
108
- if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
155
+ if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state", "activity"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
109
156
  if (typeof child.childId !== "string" || !KEY_PATTERN.test(child.childId)) throw new Error("workflowChildren childId is invalid.");
110
157
  const state = child.state;
111
158
  if (state !== "pending" && state !== "running" && state !== "completed" && state !== "failed" && state !== "paused" && state !== "stopped" && state !== "rejected" && state !== "detached") throw new Error("workflowChildren child state is invalid.");
112
159
  for (const [field, maxBytes] of [["runId", 256], ["agent", 256], ["sessionName", 256], ["model", 256], ["thinking", 32]] as const) {
113
160
  if (child[field] !== undefined && bounded(child[field], maxBytes) === undefined) throw new Error(`workflowChildren child ${field} is invalid.`);
114
161
  }
115
- return { childId: child.childId, state, ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.sessionName ? { sessionName: child.sessionName as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
162
+ const activity = parseActivity(child.activity);
163
+ if (activity && state !== "running") throw new Error("workflowChildren child activity requires running state.");
164
+ return { childId: child.childId, state, ...(activity ? { activity } : {}), ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.sessionName ? { sessionName: child.sessionName as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
116
165
  });
117
166
  if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
118
167
  if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
@@ -1,4 +1,4 @@
1
- import type { WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
1
+ import type { WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
2
2
 
3
3
  export const WORKFLOW_PREFLIGHT_VERSION = 1 as const;
4
4
  export const WORKFLOW_PREFLIGHT_MAX_LANES = 64;
@@ -11,7 +11,7 @@ export const WORKFLOW_PREFLIGHT_MAX_WARNINGS = 16;
11
11
  const WORKFLOW_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
12
  const PREFLIGHT_FIELDS = new Set(["version", "coverage", "lanes"]);
13
13
  const LANE_FIELDS = new Set(["key", "mode", "decision", "claims", "expectedOutput", "independence"]);
14
- const PREFLIGHT_MODES = new Set<WorkflowPreflightLaneV1["mode"]>(["mutation", "review", "scout", "gate"]);
14
+ const PREFLIGHT_MODES = new Set<WorkflowPreflightLane["mode"]>(["mutation", "review", "scout", "gate"]);
15
15
 
16
16
  type WorkflowTraceLike = {
17
17
  operation: string;
@@ -23,7 +23,7 @@ type WorkflowTraceLike = {
23
23
 
24
24
  export interface WorkflowPreflightValidationResult {
25
25
  ok: boolean;
26
- preflight?: WorkflowPreflightV1;
26
+ preflight?: WorkflowPreflight;
27
27
  error?: string;
28
28
  }
29
29
 
@@ -60,18 +60,18 @@ function normalizeDisplayString(value: unknown, path: string): string {
60
60
  return normalized;
61
61
  }
62
62
 
63
- function normalizeLane(value: unknown, index: number): WorkflowPreflightLaneV1 {
63
+ function normalizeLane(value: unknown, index: number): WorkflowPreflightLane {
64
64
  const path = `preflight.lanes[${index}]`;
65
65
  assertPlainObject(value, path);
66
66
  assertKnownFields(value, LANE_FIELDS, path);
67
67
  const key = normalizeDisplayString(value.key, `${path}.key`);
68
68
  if (!WORKFLOW_KEY_PATTERN.test(key)) throw new Error(`${path}.key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
69
- let mode: WorkflowPreflightLaneV1["mode"];
69
+ let mode: WorkflowPreflightLane["mode"];
70
70
  if (value.mode !== undefined) {
71
- if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as WorkflowPreflightLaneV1["mode"])) {
71
+ if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as WorkflowPreflightLane["mode"])) {
72
72
  throw new Error(`${path}.mode must be one of: mutation, review, scout, gate.`);
73
73
  }
74
- mode = value.mode as WorkflowPreflightLaneV1["mode"];
74
+ mode = value.mode as WorkflowPreflightLane["mode"];
75
75
  }
76
76
  let claims: string[] | undefined;
77
77
  if (value.claims !== undefined) {
@@ -97,7 +97,7 @@ function normalizeLane(value: unknown, index: number): WorkflowPreflightLaneV1 {
97
97
  * independent of workflowScript parsing: callers may describe dynamic fanout
98
98
  * without making the metadata a second execution graph.
99
99
  */
100
- export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflightV1 | undefined {
100
+ export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflight | undefined {
101
101
  if (input === undefined) return undefined;
102
102
  assertPlainObject(input, "preflight");
103
103
  assertKnownFields(input, PREFLIGHT_FIELDS, "preflight");
@@ -115,7 +115,7 @@ export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflightV1
115
115
  if (seenKeys.has(lane.key)) throw new Error(`preflight.lanes contains duplicate key '${lane.key}'.`);
116
116
  seenKeys.add(lane.key);
117
117
  }
118
- const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies WorkflowPreflightV1;
118
+ const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies WorkflowPreflight;
119
119
  const depth = 1 + (lanes.length > 0 ? 1 : 0) + (lanes.some((lane) => lane.claims !== undefined) ? 1 : 0);
120
120
  if (depth > WORKFLOW_PREFLIGHT_MAX_DEPTH) throw new Error(`preflight exceeds the maximum nesting depth of ${WORKFLOW_PREFLIGHT_MAX_DEPTH}.`);
121
121
  const bytes = Buffer.byteLength(JSON.stringify(normalized), "utf8");
@@ -138,7 +138,7 @@ function warningLimit(messages: string[]): string[] {
138
138
  return [...messages.slice(0, WORKFLOW_PREFLIGHT_MAX_WARNINGS - 1), `Preflight advisory: ${omitted} additional mismatch warning(s) omitted.`];
139
139
  }
140
140
 
141
- function declaredKeys(preflight: WorkflowPreflightV1): Set<string> {
141
+ function declaredKeys(preflight: WorkflowPreflight): Set<string> {
142
142
  return new Set(preflight.lanes.map((lane) => lane.key));
143
143
  }
144
144
 
@@ -152,15 +152,15 @@ export function workflowKeyMatchesPreflightLane(key: string, laneKey: string, ge
152
152
 
153
153
  /** Select the most specific advisory lane without allowing declaration order to override an exact runtime key. */
154
154
  export function workflowPreflightLaneForRuntimeKey(
155
- preflight: WorkflowPreflightV1 | undefined,
155
+ preflight: WorkflowPreflight | undefined,
156
156
  key: string,
157
157
  preferredKeys: readonly (string | undefined)[] = [],
158
- ): WorkflowPreflightLaneV1 | undefined {
158
+ ): WorkflowPreflightLane | undefined {
159
159
  const lanes = preflight?.lanes;
160
160
  if (!lanes) return undefined;
161
161
  const exact = lanes.find((lane) => lane.key === key);
162
162
  if (exact) return exact;
163
- let closestRoot: WorkflowPreflightLaneV1 | undefined;
163
+ let closestRoot: WorkflowPreflightLane | undefined;
164
164
  for (const lane of lanes) {
165
165
  if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
166
166
  }
@@ -203,7 +203,7 @@ function unlaunchedWarning(key: string): string {
203
203
 
204
204
  /** Return bounded advisory mismatch warnings; these never reject a child launch. */
205
205
  export function workflowPreflightWarnings(
206
- preflight: WorkflowPreflightV1 | undefined,
206
+ preflight: WorkflowPreflight | undefined,
207
207
  trace: readonly WorkflowTraceLike[],
208
208
  options: { settled?: boolean } = {},
209
209
  ): string[] {
@@ -218,7 +218,7 @@ export function workflowPreflightWarnings(
218
218
  }
219
219
 
220
220
  /** Attach the first undeclared-key warning to its first trace row for status/debug views. */
221
- export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight: WorkflowPreflightV1 | undefined): T[] {
221
+ export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight: WorkflowPreflight | undefined): T[] {
222
222
  if (!preflight) return [...trace];
223
223
  const declared = declaredKeys(preflight);
224
224
  const warned = new Set<string>();
@@ -235,14 +235,14 @@ function laneCell(value: string | undefined): string {
235
235
 
236
236
  const WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH = 96;
237
237
 
238
- function planLaneLabel(lane: WorkflowPreflightLaneV1): string {
238
+ function planLaneLabel(lane: WorkflowPreflightLane): string {
239
239
  const value = lane.decision?.trim() || lane.key;
240
240
  if (value.length <= WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH) return value;
241
241
  return `${value.slice(0, WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH - 1).trimEnd()}…`;
242
242
  }
243
243
 
244
244
  /** Render the detailed bounded table reserved for tool output and expanded/debug views. */
245
- export function formatWorkflowPreflight(preflight: WorkflowPreflightV1 | undefined, options: { indent?: string } = {}): string {
245
+ export function formatWorkflowPreflight(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
246
246
  if (!preflight) return "";
247
247
  const indent = options.indent ?? "";
248
248
  const count = preflight.lanes.length;
@@ -263,7 +263,7 @@ export function formatWorkflowPreflight(preflight: WorkflowPreflightV1 | undefin
263
263
  return lines.join("\n");
264
264
  }
265
265
 
266
- export function formatWorkflowPreflightSummary(preflight: WorkflowPreflightV1 | undefined): string {
266
+ export function formatWorkflowPreflightSummary(preflight: WorkflowPreflight | undefined): string {
267
267
  if (!preflight) return "";
268
268
  const keys = preflight.lanes.slice(0, 4).map((lane) => lane.key).join(", ");
269
269
  const remainder = preflight.lanes.length > 4 ? `, +${preflight.lanes.length - 4}` : "";
@@ -271,7 +271,7 @@ export function formatWorkflowPreflightSummary(preflight: WorkflowPreflightV1 |
271
271
  }
272
272
 
273
273
  /** Render the operator-facing one-line plan shown in routine status views. */
274
- export function formatWorkflowPreflightPlanSummary(preflight: WorkflowPreflightV1 | undefined, options: { indent?: string } = {}): string {
274
+ export function formatWorkflowPreflightPlanSummary(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
275
275
  if (!preflight) return "";
276
276
  const indent = options.indent ?? "";
277
277
  const count = preflight.lanes.length;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
4
- import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenanceV1, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
4
+ import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenance, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
5
5
  import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
6
6
  import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
7
7
  import { HOST_STEP_MAX_COUNT, assertUniqueHostStepIds, parseHostStepNode } from "../runs/shared/host-step-status.ts";
@@ -39,7 +39,7 @@ export function buildWorkflowReceipt(input: {
39
39
  children: WorkflowScriptChildResult[];
40
40
  hostSteps?: WorkflowReceipt["hostSteps"];
41
41
  workflowChildren?: WorkflowReceipt["workflowChildren"];
42
- resource?: WorkflowResourceProvenanceV1;
42
+ resource?: WorkflowResourceProvenance;
43
43
  terminalOutcome?: WorkflowTerminalOutcome;
44
44
  createdAt?: number;
45
45
  }): WorkflowReceipt {
@@ -262,7 +262,7 @@ function parseWorkflowResolution(value: unknown, source: string): WorkflowTermin
262
262
  return value;
263
263
  }
264
264
 
265
- function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenanceV1 | undefined {
265
+ function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenance | undefined {
266
266
  if (value === undefined) return undefined;
267
267
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': resource must be an object.`);
268
268
  const resource = value as Record<string, unknown>;