pi-subagents 0.65.0 → 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 (104) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +17 -3
  5. package/docs/configuration.md +34 -0
  6. package/docs/extension-api.md +94 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +11 -5
  10. package/docs/workflows.md +22 -7
  11. package/package.json +4 -1
  12. package/runner-server-preload.mjs +13 -0
  13. package/skills/pi-subagents/SKILL.md +2 -1
  14. package/skills/pi-subagents/references/execution-controls.md +19 -2
  15. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  16. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  17. package/src/agents/advertised-agent-prompt.ts +63 -0
  18. package/src/agents/agent-management.ts +14 -1
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +8 -0
  21. package/src/api/preflight.ts +5 -1
  22. package/src/api/shared-types.ts +1 -1
  23. package/src/api/workflow-resources.ts +6 -0
  24. package/src/extension/config.ts +4 -2
  25. package/src/extension/index.ts +71 -4
  26. package/src/extension/public-execution.ts +0 -1
  27. package/src/extension/rpc.ts +4 -21
  28. package/src/extension/schemas.ts +9 -7
  29. package/src/extension/tool-description.ts +10 -5
  30. package/src/integrations/pi-web-session-liveness.ts +73 -0
  31. package/src/intercom/native-supervisor-channel.ts +102 -88
  32. package/src/intercom/supervisor-ui.ts +3 -2
  33. package/src/missions/workflow-state.ts +37 -16
  34. package/src/runs/background/active-async-capacity.ts +18 -18
  35. package/src/runs/background/async-execution.ts +8 -1
  36. package/src/runs/background/async-job-tracker.ts +35 -3
  37. package/src/runs/background/async-resume.ts +3 -1
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/async-status-snapshot.ts +10 -12
  40. package/src/runs/background/async-status.ts +17 -9
  41. package/src/runs/background/auto-drain.ts +40 -29
  42. package/src/runs/background/chain-root-attachment.ts +8 -0
  43. package/src/runs/background/control-channel.ts +78 -44
  44. package/src/runs/background/notify.ts +88 -12
  45. package/src/runs/background/owned-process-tree.ts +6 -6
  46. package/src/runs/background/process-terminal.ts +23 -23
  47. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  48. package/src/runs/background/run-child-session.ts +62 -33
  49. package/src/runs/background/run-status.ts +75 -5
  50. package/src/runs/background/runner-aliases.ts +46 -8
  51. package/src/runs/background/runner-child-launch.ts +86 -0
  52. package/src/runs/background/stale-run-reconciler.ts +3 -1
  53. package/src/runs/background/subagent-runner.ts +430 -208
  54. package/src/runs/background/subagent-wait.ts +3 -0
  55. package/src/runs/background/wait-completions.ts +4 -0
  56. package/src/runs/foreground/async-steering-action.ts +19 -0
  57. package/src/runs/foreground/execution.ts +116 -26
  58. package/src/runs/foreground/foreground-history.ts +3 -1
  59. package/src/runs/foreground/prompt-audit.ts +9 -5
  60. package/src/runs/foreground/subagent-executor.ts +531 -227
  61. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  62. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  63. package/src/runs/shared/acceptance.ts +16 -3
  64. package/src/runs/shared/agent-contract.ts +1 -1
  65. package/src/runs/shared/async-status-projection.ts +47 -47
  66. package/src/runs/shared/child-hooks.ts +151 -2
  67. package/src/runs/shared/child-launch.ts +18 -13
  68. package/src/runs/shared/child-session.ts +55 -24
  69. package/src/runs/shared/child-tool-plan.ts +2 -2
  70. package/src/runs/shared/completion-evidence.ts +2 -2
  71. package/src/runs/shared/completion-guard.ts +1 -0
  72. package/src/runs/shared/host-step-status.ts +11 -11
  73. package/src/runs/shared/llm-intent-arbiter.ts +30 -20
  74. package/src/runs/shared/model-exclusions.ts +2 -1
  75. package/src/runs/shared/model-fallback.ts +41 -8
  76. package/src/runs/shared/nested-events.ts +8 -8
  77. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  78. package/src/runs/shared/parallel-handoff.ts +57 -12
  79. package/src/runs/shared/parallel-utils.ts +3 -2
  80. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  81. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  82. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  83. package/src/runs/shared/run-fanout-budget.ts +8 -8
  84. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  85. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  86. package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
  87. package/src/runs/shared/worktree-setup-command.ts +190 -0
  88. package/src/runs/shared/worktree.ts +403 -210
  89. package/src/shared/model-response-aliases.ts +13 -0
  90. package/src/shared/types.ts +89 -60
  91. package/src/shared/utils.ts +10 -2
  92. package/src/shared/watch-strategy.ts +2 -0
  93. package/src/shared/workflow-child-permit.ts +18 -13
  94. package/src/tui/fleet-status.ts +1 -1
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +44 -15
  97. package/src/workflows/chat-progress.ts +3 -3
  98. package/src/workflows/scripted-workflow.ts +70 -16
  99. package/src/workflows/workflow-checklist.ts +15 -18
  100. package/src/workflows/workflow-child-summary.ts +57 -8
  101. package/src/workflows/workflow-preflight.ts +19 -19
  102. package/src/workflows/workflow-receipt.ts +3 -3
  103. package/src/workflows/workflow-resources.ts +96 -21
  104. package/src/workflows/workflow-settlement.ts +3 -0
package/src/tui/render.ts CHANGED
@@ -1284,7 +1284,7 @@ function workflowChecklistItemPriority(state: WorkflowChecklistState): number {
1284
1284
  return 6;
1285
1285
  }
1286
1286
 
1287
- function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame?: number): string {
1287
+ function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame: number | undefined, includeError: boolean): string {
1288
1288
  const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter(Boolean).join(" · ");
1289
1289
  const context = contextModeBadge(theme, item.context);
1290
1290
  const state = item.state === "complete" ? "" : ` ${theme.fg("dim", `· ${workflowChecklistStateLabel(item.state)}`)}`;
@@ -1294,15 +1294,26 @@ function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, in
1294
1294
  !item.currentTool && item.durationMs !== undefined ? formatDuration(item.durationMs) : undefined,
1295
1295
  item.toolCount !== undefined ? `${item.toolCount} tools` : undefined,
1296
1296
  item.outputName ? `out:${item.outputName}` : undefined,
1297
- item.error ? `error:${oneLine(item.error)}` : undefined,
1297
+ includeError && item.error ? `error:${oneLine(item.error)}` : undefined,
1298
1298
  ].filter(Boolean).join(" · ");
1299
1299
  return `${indent}${workflowChecklistGlyph(item, theme, frame)} ${theme.bold(identity || item.key)}${context}${state}${details ? ` ${theme.fg("dim", `· ${details}`)}` : ""}`;
1300
1300
  }
1301
1301
 
1302
1302
  const COLLAPSED_WORKFLOW_PHASE_LIMIT = 4;
1303
1303
 
1304
- function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | undefined, theme: Theme, indent: string, expanded: boolean, frame?: number, includeSummary = true, limitPhases = !expanded, includeBottleneckOutput = expanded): string[] {
1304
+ interface WorkflowChecklistWidgetOptions {
1305
+ includeSummary?: boolean;
1306
+ limitPhases?: boolean;
1307
+ includeBottleneckOutput?: boolean;
1308
+ includeItemErrors?: boolean;
1309
+ }
1310
+
1311
+ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | undefined, theme: Theme, indent: string, expanded: boolean, frame?: number, options: WorkflowChecklistWidgetOptions = {}): string[] {
1305
1312
  if (!checklist?.total) return [];
1313
+ const includeSummary = options.includeSummary ?? true;
1314
+ const limitPhases = options.limitPhases ?? !expanded;
1315
+ const includeBottleneckOutput = options.includeBottleneckOutput ?? expanded;
1316
+ const includeItemErrors = options.includeItemErrors ?? true;
1306
1317
  const lines = includeSummary ? [`${indent}${theme.fg("dim", `Checklist ${formatWorkflowChecklistSummary(checklist)}`)}`] : [];
1307
1318
  const phases = !limitPhases || checklist.phases.length <= COLLAPSED_WORKFLOW_PHASE_LIMIT
1308
1319
  ? checklist.phases
@@ -1326,11 +1337,11 @@ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | u
1326
1337
  lines.push(`${indent}${glyph} ${theme.bold(formatWorkflowChecklistPhase(phase))}`);
1327
1338
  if (expanded) {
1328
1339
  for (const item of [...phase.items].sort((left, right) => workflowChecklistItemPriority(left.state) - workflowChecklistItemPriority(right.state))) {
1329
- lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame));
1340
+ lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame, includeItemErrors || item.kind === "host"));
1330
1341
  }
1331
1342
  }
1332
1343
  }
1333
- const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput });
1344
+ const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput, includeError: !expanded });
1334
1345
  if (bottleneck) {
1335
1346
  const tone = checklist.bottleneck?.state === "blocked" || checklist.bottleneck?.state === "failed" ? "error" : checklist.bottleneck?.state === "running" ? "accent" : "warning";
1336
1347
  lines.push(`${indent}${theme.fg(tone, `bottleneck · ${bottleneck}`)}`);
@@ -2371,7 +2382,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
2371
2382
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
2372
2383
  const lines: string[] = [
2373
2384
  ...(expanded ? workflowPreflightLines(job) : []),
2374
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2385
+ ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
2375
2386
  ];
2376
2387
  const group = activeParallelWidgetGroup(job);
2377
2388
  if (group) {
@@ -2711,16 +2722,29 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
2711
2722
  return rendered.lines;
2712
2723
  }
2713
2724
 
2714
- function buildWidgetComponent(jobs: AsyncJobState[], isExpanded: () => boolean): (_tui: unknown, theme: Theme) => Component {
2715
- return (_tui, theme) => {
2725
+ const asyncWidgetUpdates = new WeakMap<ExtensionContext["ui"], (jobs: AsyncJobState[]) => void>();
2726
+
2727
+ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"]): (tui: { requestRender(): void }, theme: Theme) => Component {
2728
+ return (tui, theme) => {
2716
2729
  const container = new Container();
2717
2730
  let cachedRenderWidth: number | undefined;
2718
2731
  let cachedFrame: number | undefined;
2719
2732
  let cachedExpanded: boolean | undefined;
2720
2733
  let cachedLines: string[] | undefined;
2734
+ const update = (nextJobs: AsyncJobState[]): void => {
2735
+ jobs = nextJobs;
2736
+ cachedLines = undefined;
2737
+ tui.requestRender();
2738
+ };
2739
+ asyncWidgetUpdates.set(ui, update);
2740
+ const component = Object.assign(container, {
2741
+ dispose(): void {
2742
+ if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
2743
+ },
2744
+ });
2721
2745
  container.render = (renderWidth: number): string[] => {
2722
2746
  const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
2723
- const expanded = isExpanded();
2747
+ const expanded = ui.getToolsExpanded?.() ?? false;
2724
2748
  if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded) return cachedLines;
2725
2749
  const width = Math.max(0, renderWidth - 2);
2726
2750
  const projectionFor = workflowWidgetProjectionLookup();
@@ -2735,7 +2759,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], isExpanded: () => boolean):
2735
2759
  cachedLines = fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
2736
2760
  return cachedLines;
2737
2761
  };
2738
- return container;
2762
+ return component;
2739
2763
  };
2740
2764
  }
2741
2765
 
@@ -2767,7 +2791,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
2767
2791
  : [
2768
2792
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
2769
2793
  ...widgetLaneDetailLines(job, theme, projection),
2770
- ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
2794
+ ...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
2771
2795
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
2772
2796
  ];
2773
2797
  items.push([
@@ -2830,6 +2854,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
2830
2854
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
2831
2855
  if (jobs.length === 0) {
2832
2856
  resetWidgetLayoutSession();
2857
+ asyncWidgetUpdates.delete(ctx.ui);
2833
2858
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
2834
2859
  return;
2835
2860
  }
@@ -2838,7 +2863,11 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
2838
2863
  ctx.ui.setWidget(WIDGET_KEY, encodeAsyncStatusSnapshotWidget(jobs));
2839
2864
  return;
2840
2865
  }
2841
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, () => ctx.ui.getToolsExpanded?.() ?? false));
2866
+ // Pi replaces a widget by deleting and reinserting its key. Update the mounted
2867
+ // component instead so progress cannot move it past other extensions' widgets.
2868
+ const update = asyncWidgetUpdates.get(ctx.ui);
2869
+ if (update) update(jobs);
2870
+ else ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui));
2842
2871
  }
2843
2872
 
2844
2873
  function renderSingleCompact(
@@ -2991,7 +3020,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
2991
3020
  if (d.preflight) c.addChild(new Text(truncLine(theme.fg("dim", formatWorkflowPreflightPlanSummary(d.preflight, { indent: rowIndent })), width), 0, 0));
2992
3021
  if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
2993
3022
  const checklist = projectWorkflowChecklist({ hostSteps: workflow?.receipt?.hostSteps, preflight: d.preflight, trace: workflow?.trace, now: Date.now() });
2994
- for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame, true, !expanded, expanded)) {
3023
+ for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame, { limitPhases: !expanded, includeBottleneckOutput: expanded })) {
2995
3024
  c.addChild(new Text(truncLine(line, width), 0, 0));
2996
3025
  }
2997
3026
  if (rows.length === 0) {
@@ -3073,7 +3102,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
3073
3102
  const detailIndent = mainWindowIndent(layout, 2);
3074
3103
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
3075
3104
  if (checklistPrimary) {
3076
- for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, false)) {
3105
+ for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, { includeSummary: false })) {
3077
3106
  c.addChild(new Text(truncLine(line, width), 0, 0));
3078
3107
  }
3079
3108
  if (hasRunning || workflowChecklist.running > 0) c.addChild(new Text(truncLine(theme.fg("accent", `${rowIndent}${liveDetailHintText()}`), width), 0, 0));
@@ -3424,7 +3453,7 @@ export function renderSubagentResult(
3424
3453
  c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
3425
3454
  }
3426
3455
  const workflowChecklist = foregroundWorkflowChecklist(d);
3427
- for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame)) {
3456
+ for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame, { includeItemErrors: false })) {
3428
3457
  c.addChild(new Text(fit(line), 0, 0));
3429
3458
  }
3430
3459
 
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import type { Details, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
4
+ import type { Details, WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
5
5
  import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
6
6
 
7
7
  export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "live-card"] as const;
@@ -103,14 +103,14 @@ export interface WorkflowChatProgressRow {
103
103
  runId?: string;
104
104
  durationMs?: number;
105
105
  error?: string;
106
- preflight?: WorkflowPreflightLaneV1;
106
+ preflight?: WorkflowPreflightLane;
107
107
  }
108
108
 
109
109
  function cleanLabel(value: unknown): string | undefined {
110
110
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
111
111
  }
112
112
 
113
- export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workflow"]>["trace"], preflight?: WorkflowPreflightV1): WorkflowChatProgressRow[] {
113
+ export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workflow"]>["trace"], preflight?: WorkflowPreflight): WorkflowChatProgressRow[] {
114
114
  const rows = new Map<string, WorkflowChatProgressRow>();
115
115
  for (const entry of trace) {
116
116
  if (entry.operation !== "run") continue;
@@ -5,10 +5,12 @@ import { Worker } from "node:worker_threads";
5
5
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
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
- import type { AcceptanceRecoveryMetadata, HostStepNodeV1, SingleResult } from "../shared/types.ts";
8
+ import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
9
+ import type { AcceptanceRecoveryMetadata, HostStepNode, SingleResult } from "../shared/types.ts";
9
10
  import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
10
11
 
11
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.";
12
14
  function validGitRef(ref: unknown): ref is string {
13
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;
14
16
  if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
@@ -74,6 +76,12 @@ function stableRunJson(value) {
74
76
  return JSON.stringify(value) ?? "undefined";
75
77
  }
76
78
 
79
+ function canonicalRunParams(params) {
80
+ if (params.gate === undefined || params.acceptance !== false) return params;
81
+ const { acceptance: _acceptance, ...withoutAcceptance } = params;
82
+ return withoutAcceptance;
83
+ }
84
+
77
85
  function isDirectWorkflowScriptPromiseHandlerCall() {
78
86
  const stack = new Error().stack;
79
87
  if (typeof stack !== "string") return false;
@@ -445,7 +453,7 @@ function validateLaneSpecs(laneSpecs) {
445
453
  validateLaneStageBounds(validationParams, stageLabel);
446
454
  validateRunCall(generatedKey, validationParams, stageLabel, validationFingerprints);
447
455
  const existingFingerprint = runFingerprints.get(generatedKey);
448
- if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(params))) {
456
+ if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(canonicalRunParams(params)))) {
449
457
  throw new Error("runs.lanes generated child key '" + generatedKey + "' is already used with incompatible launch params.");
450
458
  }
451
459
  stages.push({ key: stageKey, generatedKey, resume, params });
@@ -614,6 +622,19 @@ function validateLaneMetadata(value, label, workflowKey) {
614
622
  }
615
623
  }
616
624
 
625
+ function describeGateAcceptanceConflict(gate, acceptance) {
626
+ const render = (value) => {
627
+ let encoded;
628
+ try {
629
+ encoded = JSON.stringify(value) ?? String(value);
630
+ } catch {
631
+ encoded = String(value);
632
+ }
633
+ return encoded.length > 120 ? encoded.slice(0, 120) + "..." : encoded;
634
+ };
635
+ return " Both fields were present: gate=" + render(gate) + " acceptance=" + render(acceptance) + ".";
636
+ }
637
+
617
638
  function validateRunCall(key, params, label, fingerprints) {
618
639
  if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
619
640
  if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
@@ -624,10 +645,10 @@ function validateRunCall(key, params, label, fingerprints) {
624
645
  }
625
646
  if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
626
647
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
627
- 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.");
628
649
  validateLaneMetadata(params.lane, label + " lane", key);
629
650
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
630
- if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
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));
631
652
  if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
632
653
  if (params.extensionBindings !== undefined && params.resume !== undefined) throw new Error(label + " extensionBindings is not supported with retained resume; resume uses the original retained child binding.");
633
654
  if (params.resume !== undefined && typeof params.resume !== "string") {
@@ -644,7 +665,7 @@ function validateRunCall(key, params, label, fingerprints) {
644
665
  if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) throw new Error(label + " resume requires a non-empty task follow-up.");
645
666
  validateExtensionBindings(params.extensionBindings, label);
646
667
  assertJsonValue(params, label + " params");
647
- const fingerprint = stableRunJson(params);
668
+ const fingerprint = stableRunJson(canonicalRunParams(params));
648
669
  const existing = fingerprints.get(key);
649
670
  if (existing !== undefined && existing !== fingerprint) throw new Error("Duplicate workflow key '" + key + "' used with incompatible launch params.");
650
671
  fingerprints.set(key, fingerprint);
@@ -1090,11 +1111,11 @@ export interface RunWorkflowScriptOptions {
1090
1111
  globalConcurrencyLimit?: number;
1091
1112
  admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
1092
1113
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
1093
- resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
1114
+ resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
1094
1115
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
1095
1116
  steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
1096
1117
  host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
1097
- onHostStep?: (hostStep: HostStepNodeV1) => void;
1118
+ onHostStep?: (hostStep: HostStepNode) => void;
1098
1119
  state?: {
1099
1120
  get: (key: string) => unknown | Promise<unknown>;
1100
1121
  set: (key: string, value: unknown) => void | Promise<void>;
@@ -1370,6 +1391,12 @@ function stableJson(value: unknown): string {
1370
1391
  return JSON.stringify(value) ?? "undefined";
1371
1392
  }
1372
1393
 
1394
+ function canonicalRunParams(params: Record<string, unknown>): Record<string, unknown> {
1395
+ if (params.gate === undefined || params.acceptance !== false) return params;
1396
+ const { acceptance: _acceptance, ...withoutAcceptance } = params;
1397
+ return withoutAcceptance;
1398
+ }
1399
+
1373
1400
  function validateKey(value: unknown, owner = "runs.run"): string {
1374
1401
  if (typeof value !== "string" || !KEY_PATTERN.test(value)) {
1375
1402
  throw new Error(`${owner} key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
@@ -1476,7 +1503,7 @@ function definitelyNonJson(node: AstNode, normalizeUndefined = false): string |
1476
1503
  if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
1477
1504
  const values = new Map<string, AstNode>();
1478
1505
  for (const property of node.properties) {
1479
- 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;
1480
1507
  const key = staticPropertyKey(property);
1481
1508
  if (key === undefined) return undefined;
1482
1509
  values.set(key, property.value);
@@ -1505,6 +1532,26 @@ function directObjectPropertyValue(node: AstNode, name: string): AstNode | undef
1505
1532
  return value;
1506
1533
  }
1507
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
+
1508
1555
  function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
1509
1556
  const args = Array.isArray(call.arguments) ? call.arguments : [];
1510
1557
  const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
@@ -1546,6 +1593,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1546
1593
  const key = literalString(keyNode);
1547
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) });
1548
1595
  if (astNode(args[1])) {
1596
+ errors.push(...validateStaticBaseRef(args[1], "runs.run"));
1549
1597
  const message = definitelyNonJson(args[1]);
1550
1598
  if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
1551
1599
  }
@@ -1555,6 +1603,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1555
1603
  const args = Array.isArray(node.arguments) ? node.arguments : [];
1556
1604
  if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
1557
1605
  for (const item of args[0].elements) if (astNode(item)) {
1606
+ errors.push(...validateStaticBaseRef(item, "runs.all item"));
1558
1607
  const message = definitelyNonJson(item);
1559
1608
  if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
1560
1609
  }
@@ -1725,7 +1774,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1725
1774
  console.error("Workflow onLanePlan callback failed:", error);
1726
1775
  }
1727
1776
  };
1728
- const hostStepChanged = (hostStep: HostStepNodeV1) => {
1777
+ const hostStepChanged = (hostStep: HostStepNode) => {
1729
1778
  try {
1730
1779
  options.onHostStep?.(hostStep);
1731
1780
  } catch (error) {
@@ -2021,7 +2070,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2021
2070
  if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
2022
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.`)));
2023
2072
  const startedAt = Date.now();
2024
- const startedStep: HostStepNodeV1 = {
2073
+ const startedStep: HostStepNode = {
2025
2074
  version: 1,
2026
2075
  kind: "host-step",
2027
2076
  monitorKind: "command",
@@ -2082,7 +2131,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2082
2131
  }
2083
2132
  return result;
2084
2133
  });
2085
- const fingerprint = stableJson(params);
2134
+ const fingerprint = stableJson(canonicalRunParams(params));
2086
2135
  const existing = launches.get(key);
2087
2136
  if (existing) {
2088
2137
  if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
@@ -2104,13 +2153,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2104
2153
  return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
2105
2154
  }
2106
2155
  if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
2107
- 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}`)));
2108
2157
  }
2109
2158
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
2110
2159
  return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
2111
2160
  }
2112
- if (params.gate !== undefined && params.acceptance !== undefined) {
2113
- return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.`)));
2161
+ if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) {
2162
+ return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.` + describeGateAcceptanceConflict(params.gate, params.acceptance))));
2114
2163
  }
2115
2164
  if (params.gate !== undefined && params.resume !== undefined) {
2116
2165
  return respond(Promise.reject(new Error(`runs.run('${key}') gate is not supported with retained resume.`)));
@@ -2157,10 +2206,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2157
2206
  const childStopController = new AbortController();
2158
2207
  childStopControllers.set(key, childStopController);
2159
2208
  const childSignal = combinedAbortSignal([childController.signal, childStopController.signal]);
2160
- const resolvedResumeValue = resumeReference
2209
+ const resumeInput = resumeReference ?? (typeof params.resume === "string" && options.resolveResume ? params.resume : undefined);
2210
+ const resolvedResumeValue = resumeInput
2161
2211
  ? await Promise.resolve().then(() => {
2162
2212
  if (!options.resolveResume) throw new Error("Keyed workflow receipt resume is unavailable in this host.");
2163
- return options.resolveResume(resumeReference, childSignal);
2213
+ return options.resolveResume(resumeInput, childSignal, typeof params.index === "number" ? params.index : undefined);
2164
2214
  })
2165
2215
  : undefined;
2166
2216
  const resolvedResume = typeof resolvedResumeValue === "string"
@@ -2176,6 +2226,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2176
2226
  : [];
2177
2227
  resolvedResumeLineage = [...new Set(lineage.length ? lineage : [resolvedResumeId!])];
2178
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
+ }
2179
2233
  }
2180
2234
  const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
2181
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)
@@ -402,11 +398,12 @@ export function formatWorkflowChecklistPhase(phase: WorkflowChecklistPhase): str
402
398
  return counts.length ? `${phase.label} ${counts.join(" · ")}` : phase.label;
403
399
  }
404
400
 
405
- export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean } = {}): string | undefined {
401
+ export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean; includeError?: boolean } = {}): string | undefined {
406
402
  if (!item) return undefined;
407
403
  const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
408
404
  const includeOutput = options.includeOutput ?? true;
409
- const details = [item.context ? `(${item.context})` : undefined, item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDurationText(item.durationMs)}` : ""}` : undefined, !item.currentTool && item.currentPath ? item.currentPath : undefined, !item.currentTool && item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, includeOutput && item.outputName ? `out:${item.outputName}` : undefined, item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
405
+ const includeError = options.includeError ?? true;
406
+ const details = [item.context ? `(${item.context})` : undefined, item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDurationText(item.durationMs)}` : ""}` : undefined, !item.currentTool && item.currentPath ? item.currentPath : undefined, !item.currentTool && item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, includeOutput && item.outputName ? `out:${item.outputName}` : undefined, includeError && item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
410
407
  return [identity, ...details].join(" · ");
411
408
  }
412
409
 
@@ -433,7 +430,7 @@ export function formatWorkflowChecklistText(projection: WorkflowChecklistProject
433
430
  lines.push(`${indent} ${marker} ${formatWorkflowChecklistItem(item)}`);
434
431
  }
435
432
  }
436
- const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck);
433
+ const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck, { includeError: options.includeItems === false });
437
434
  if (bottleneck) lines.push(`${indent} bottleneck · ${bottleneck}`);
438
435
  return lines;
439
436
  }
@@ -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.");