pi-workflow-engine 0.10.3 → 0.12.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 (88) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +418 -213
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
  4. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
  6. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
  7. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
  9. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
  12. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
  13. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
  14. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  16. package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
  17. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
  18. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  19. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
  20. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
  21. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
  22. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
  23. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
  24. package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
  25. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  26. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
  27. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
  28. package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
  29. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
  30. package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
  31. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  32. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  33. package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
  34. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  35. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
  36. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  37. package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
  38. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  39. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
  40. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  41. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  42. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
  43. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
  52. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
  53. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
  54. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
  55. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  56. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  57. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  58. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  59. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
  60. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
  61. package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
  62. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  68. package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
  69. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  86. package/README.md +82 -207
  87. package/USAGE.md +295 -21
  88. package/package.json +9 -9
@@ -1,32 +1,55 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { Type } from "typebox";
3
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
4
- import { SelectList, Text, truncateToWidth, type Component, type SelectItem, type SelectListTheme, type TUI } from "@earendil-works/pi-tui";
5
- import type { WorkflowProgressSnapshot } from "./src/progress.ts";
6
- import type { WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
3
+ import { VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
5
+ import { isAdvisoryReport } from "./src/advisory-schema.ts";
6
+ import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
7
+ import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
7
8
  import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
8
- import type { PerfSink, PerfSnapshot } from "./src/perf.ts";
9
+ import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./src/ui/workflow-viewer-layout.ts";
10
+ import type { PerfSink } from "./src/perf.ts";
9
11
  import type { WorkflowUsageSnapshot } from "./src/usage.ts";
10
- import { registerDynamax } from "./src/dynamax.ts";
11
- import { handleReviewViewerAction } from "./src/review/review-actions.ts";
12
- import { decideReviewResultsPresentation, extensionContextMode, maybeShowReviewResultsViewer } from "./src/review/review-results-flow.ts";
12
+ import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
13
+ import { sessionKey } from "./src/session-identity.ts";
14
+ import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
15
+ import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
13
16
  import {
14
17
  formatWorkflowDetailLines,
15
- isAdvisoryReport,
16
18
  isWorkflowResult,
17
19
  renderWorkflowResult,
18
- type WorkflowPerfDetails,
19
- type WorkflowResultEnvelope,
20
20
  } from "./src/ui/workflow-result-renderer.ts";
21
- import { parseWorkflowBudgetString, WORKFLOW_BUDGET_MAX, WORKFLOW_BUDGET_MIN } from "./src/options.ts";
21
+ import {
22
+ parseWorkflowBudgetString,
23
+ parseWorkflowIntegerString,
24
+ resolveWorkflowRunOptions,
25
+ type ResolvedWorkflowRunOptions,
26
+ WORKFLOW_AGENT_TIMEOUT_MAX_MS,
27
+ WORKFLOW_AGENT_TIMEOUT_MIN_MS,
28
+ WORKFLOW_AGENT_RETRIES_MAX,
29
+ WORKFLOW_AGENT_RETRIES_MIN,
30
+ WORKFLOW_BUDGET_MAX,
31
+ WORKFLOW_BUDGET_MIN,
32
+ WORKFLOW_MAX_AGENTS_MAX,
33
+ WORKFLOW_MAX_AGENTS_MIN,
34
+ WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
35
+ WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
36
+ WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
37
+ WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
38
+ } from "./src/options.ts";
39
+ import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./src/workflow-execution.ts";
40
+ import { registerWorkflowModelProfileCommand } from "./src/model-profile-command.ts";
41
+ import { BackgroundWorkflowCoordinator } from "./src/background-workflows.ts";
42
+ import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./src/background-workflow-tool.ts";
43
+ import { registerWorkflowRunCommand, WorkflowRunController } from "./src/workflow-run-controller.ts";
44
+ import { completeCurrentArgument, splitArgumentPrefix } from "./src/command-completions.ts";
45
+ import { assertSupportedPiVersion } from "./src/pi-compat.ts";
46
+ import { formatWorkflowInspection, workflowInspectionSnapshot } from "./src/ui/workflow-format.ts";
22
47
 
23
48
  /** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
24
49
  const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
25
50
 
26
51
  function summarize(result: unknown): string {
27
- if (result && typeof result === "object" && typeof (result as { summary?: unknown }).summary === "string") {
28
- return (result as { summary: string }).summary;
29
- }
52
+ if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
30
53
  return typeof result === "string" ? result : "Workflow finished.";
31
54
  }
32
55
 
@@ -73,21 +96,6 @@ function formatFindingLocation(location: { readonly file: string; readonly line?
73
96
  return `${location.file}${line}${symbol}`;
74
97
  }
75
98
 
76
- function workflowEnvelope(
77
- name: string,
78
- result: unknown,
79
- usage?: WorkflowUsageSnapshot,
80
- perf?: WorkflowPerfDetails,
81
- metadata?: WorkflowRunMetadata,
82
- ): WorkflowResultEnvelope {
83
- return { name, result, completedAt: Date.now(), usage, perf, runId: metadata?.runId, resumedFromRunId: metadata?.resumedFromRunId };
84
- }
85
-
86
- function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
87
- if (!snapshot?.enabled) return undefined;
88
- return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
89
- }
90
-
91
99
  type DiscoveryModule = typeof import("./src/discovery.ts");
92
100
  type EngineModule = typeof import("./src/engine.ts");
93
101
  type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
@@ -104,9 +112,8 @@ async function loadInlineWorkflow(): Promise<InlineWorkflowModule> {
104
112
  return await import("./src/inline-workflow.ts");
105
113
  }
106
114
 
107
- async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSink | undefined> {
108
- const enabled = options.perf ?? process.env.PI_WORKFLOW_PERF === "1";
109
- if (!enabled) return undefined;
115
+ async function createInvocationPerf(options: ResolvedWorkflowRunOptions): Promise<PerfSink | undefined> {
116
+ if (!options.perf) return undefined;
110
117
  const { createPerfRecorder } = await import("./src/perf.ts");
111
118
  return createPerfRecorder(true);
112
119
  }
@@ -114,7 +121,7 @@ async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSi
114
121
  /**
115
122
  * Resolve an `api.workflow()` reference to a registered workflow module. Throws on an unknown name.
116
123
  */
117
- export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<WorkflowModule> {
124
+ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<LoadedWorkflow> {
118
125
  const { discoverWorkflows } = await loadDiscovery();
119
126
  const workflows = await discoverWorkflows(EXTENSION_DIR, { perf });
120
127
  const mod = workflows.get(ref);
@@ -128,77 +135,55 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
128
135
  const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
129
136
  const AUTHOR_TEMP_WORKFLOW_LABEL = "Author temporary one-shot workflow";
130
137
  const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
131
- const WORKFLOW_PICKER_MAX_VISIBLE = 8;
132
- const WORKFLOW_PICKER_LAYOUT = { minPrimaryColumnWidth: 18, maxPrimaryColumnWidth: 22 };
133
-
134
- class WorkflowPickerComponent implements Component {
135
- private readonly list: SelectList;
136
-
137
- constructor(
138
- items: SelectItem[],
139
- private readonly tui: TUI,
140
- private readonly theme: Theme,
141
- done: (value: string | undefined) => void,
142
- ) {
143
- this.list = new SelectList(items, Math.min(WORKFLOW_PICKER_MAX_VISIBLE, items.length), selectListTheme(theme), WORKFLOW_PICKER_LAYOUT);
144
- this.list.onSelect = (item) => done(item.value);
145
- this.list.onCancel = () => done(undefined);
146
- this.list.onSelectionChange = () => this.tui.requestRender();
147
- }
148
-
149
- render(width: number): string[] {
150
- const safeWidth = Math.max(20, width);
151
- const rule = this.theme.fg("borderMuted", "-".repeat(safeWidth));
152
- const lines = [
153
- rule,
154
- this.theme.fg("accent", this.theme.bold("Run workflow")),
155
- "",
156
- ...this.list.render(safeWidth),
157
- "",
158
- this.theme.fg("muted", "up/down navigate enter select escape cancel"),
159
- rule,
160
- ];
161
- return lines.map((line) => truncateToWidth(line, safeWidth));
162
- }
163
-
164
- handleInput(data: string): void {
165
- this.list.handleInput(data);
166
- this.tui.requestRender();
167
- }
168
138
 
169
- invalidate(): void {
170
- this.list.invalidate();
139
+ const WORKFLOW_OPTION_COMPLETIONS = [
140
+ { value: "--inspect", description: "Open the live workflow inspector" },
141
+ { value: "--refresh", description: "Refresh dynamic workflow discovery" },
142
+ { value: "--perf", description: "Collect workflow performance metrics" },
143
+ { value: "--result-viewer", description: "Open supported result viewers" },
144
+ { value: "--no-result-viewer", description: "Skip supported result viewers" },
145
+ { value: "--resume-edited", description: "Allow resume after workflow source edits" },
146
+ { value: "--concurrency=", description: "Set the global subagent concurrency cap" },
147
+ { value: "--parallel-limit=", description: "Set the parallel submission limit" },
148
+ { value: "--max-agents=", description: "Set the maximum admitted live agents" },
149
+ { value: "--agent-timeout-ms=", description: "Set the timeout for each agent attempt" },
150
+ { value: "--agent-retries=", description: "Set retries for each agent call" },
151
+ { value: "--budget=", description: "Set the workflow output-token budget" },
152
+ { value: "--resume=", description: "Resume from a retained workflow run ID" },
153
+ ] as const;
154
+
155
+ async function workflowArgumentCompletions(argumentPrefix: string): Promise<AutocompleteItem[] | null> {
156
+ const parts = splitArgumentPrefix(argumentPrefix);
157
+ if (parts.completed.length === 0) {
158
+ const { discoverWorkflows } = await loadDiscovery();
159
+ const workflows = await discoverWorkflows(EXTENSION_DIR);
160
+ return completeCurrentArgument(
161
+ argumentPrefix,
162
+ [...workflows.values()].map((workflow) => ({
163
+ value: workflow.meta.name,
164
+ description: workflow.meta.description,
165
+ })),
166
+ );
171
167
  }
172
- }
173
-
174
- function selectListTheme(theme: Theme): SelectListTheme {
175
- return {
176
- selectedPrefix: (text) => theme.fg("accent", text),
177
- selectedText: (text) => theme.fg("accent", text),
178
- description: (text) => theme.fg("muted", text),
179
- scrollInfo: (text) => theme.fg("muted", text),
180
- noMatch: (text) => theme.fg("muted", text),
181
- };
182
- }
183
-
184
- function workflowPickerItems(workflows: ReadonlyMap<string, WorkflowModule>): SelectItem[] {
185
- return [
186
- { value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL, description: AUTHOR_TEMP_WORKFLOW_DESCRIPTION },
187
- ...[...workflows.values()].map((mod) => ({ value: mod.meta.name, label: mod.meta.name, description: mod.meta.description })),
188
- ];
168
+ return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
189
169
  }
190
170
 
191
171
  async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
192
- const items = workflowPickerItems(workflows);
193
- if (typeof ctx.ui.custom === "function") {
194
- return await ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => new WorkflowPickerComponent(items, tui, theme, done));
195
- }
196
-
197
- const choice = await ctx.ui.select(
172
+ const choices = [
173
+ {
174
+ value: AUTHOR_TEMP_WORKFLOW_VALUE,
175
+ display: `${AUTHOR_TEMP_WORKFLOW_LABEL} — ${AUTHOR_TEMP_WORKFLOW_DESCRIPTION}`,
176
+ },
177
+ ...[...workflows.values()].map((workflow) => ({
178
+ value: workflow.meta.name,
179
+ display: `${workflow.meta.name} — ${workflow.meta.description}`,
180
+ })),
181
+ ];
182
+ const selected = await ctx.ui.select(
198
183
  "Run workflow",
199
- items.map((item) => item.label),
184
+ choices.map((choice) => choice.display),
200
185
  );
201
- return items.find((item) => item.label === choice)?.value ?? choice;
186
+ return choices.find((choice) => choice.display === selected)?.value;
202
187
  }
203
188
 
204
189
  export interface LastWorkflowInspection {
@@ -215,30 +200,42 @@ export interface ActiveWorkflowInspection {
215
200
  readonly snapshot: () => WorkflowProgressSnapshot;
216
201
  }
217
202
 
218
- let lastWorkflowInspection: LastWorkflowInspection | undefined;
219
- let activeWorkflowInspection: ActiveWorkflowInspection | undefined;
220
-
221
- export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
222
- return lastWorkflowInspection;
203
+ interface SessionWorkflowInspections {
204
+ last?: LastWorkflowInspection;
205
+ active?: ActiveWorkflowInspection;
223
206
  }
224
207
 
225
- export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
226
- return activeWorkflowInspection;
208
+ const workflowInspections = new WeakMap<ExtensionAPI, Map<string, SessionWorkflowInspections>>();
209
+ let latestWorkflowInspection: LastWorkflowInspection | undefined;
210
+ let latestActiveWorkflowInspection: ActiveWorkflowInspection | undefined;
211
+
212
+ export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
213
+ return latestWorkflowInspection;
227
214
  }
228
215
 
229
216
  export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
217
+ if (!ctx.hasUI || ctx.mode !== "tui") {
218
+ ctx.ui.notify(formatWorkflowInspection(inspection), "info");
219
+ return;
220
+ }
230
221
  await ctx.ui.custom<void>(
231
- (tui, theme, _keybindings, done) => new WorkflowInspector(snapshotGetter(inspection), tui, theme, () => done(undefined)),
232
- { overlay: true, overlayOptions: { anchor: "right-center", width: "60%", maxHeight: "80%", margin: 1 } },
222
+ (tui, theme, _keybindings, done) => new WorkflowInspector(() => workflowInspectionSnapshot(inspection), tui, theme, () => done(undefined)),
223
+ WORKFLOW_VIEWER_OVERLAY_OPTIONS,
233
224
  );
234
225
  }
235
226
 
236
- async function openAvailableWorkflowInspector(ctx: ExtensionContext): Promise<void> {
237
- if (!ctx.hasUI) {
238
- ctx.ui.notify("Workflow inspector requires the TUI", "warning");
239
- return;
240
- }
241
- const inspection = activeWorkflowInspection ?? lastWorkflowInspection;
227
+ function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
228
+ const sessions = workflowInspections.get(pi) ?? new Map<string, SessionWorkflowInspections>();
229
+ const key = sessionKey(ctx);
230
+ const state = sessions.get(key) ?? {};
231
+ sessions.set(key, state);
232
+ workflowInspections.set(pi, sessions);
233
+ return state;
234
+ }
235
+
236
+ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
237
+ const state = workflowInspectionState(pi, ctx);
238
+ const inspection = state.active ?? state.last;
242
239
  if (!inspection) {
243
240
  ctx.ui.notify("No workflow inspector is available yet", "warning");
244
241
  return;
@@ -246,11 +243,6 @@ async function openAvailableWorkflowInspector(ctx: ExtensionContext): Promise<vo
246
243
  await openWorkflowInspector(ctx, inspection);
247
244
  }
248
245
 
249
- function snapshotGetter(inspection: LastWorkflowInspection | ActiveWorkflowInspection): () => WorkflowProgressSnapshot {
250
- const snapshot = inspection.snapshot;
251
- return typeof snapshot === "function" ? snapshot : () => snapshot;
252
- }
253
-
254
246
  function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
255
247
  return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
256
248
  }
@@ -278,6 +270,10 @@ export function parseWorkflowInvocation(input: string): WorkflowInvocation {
278
270
 
279
271
  const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
280
272
  const INVALID_RESUME_OPTION = "--resume requires a workflow run id";
273
+ const INVALID_MAX_AGENTS_OPTION = "--max-agents requires an integer";
274
+ const INVALID_AGENT_TIMEOUT_OPTION = "--agent-timeout-ms requires an integer";
275
+ const INVALID_AGENT_RETRIES_OPTION = "--agent-retries requires an integer";
276
+ const INVALID_EDITED_RESUME_OPTION = "--resume-edited requires --resume <run-id>";
281
277
 
282
278
  function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
283
279
  const tokens = input.split(/\s+/).filter(Boolean);
@@ -299,6 +295,10 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
299
295
  options.perf = true;
300
296
  continue;
301
297
  }
298
+ if (token === "--resume-edited") {
299
+ options.resumeEditedWorkflow = true;
300
+ continue;
301
+ }
302
302
  if (token === "--result-viewer" || token === "--review-viewer") {
303
303
  options.resultViewer = "open";
304
304
  continue;
@@ -327,6 +327,54 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
327
327
  if (next !== undefined) i++;
328
328
  continue;
329
329
  }
330
+ if (token.startsWith("--max-agents=")) {
331
+ const parsed = parseWorkflowIntegerString(token.slice("--max-agents=".length));
332
+ if (parsed === undefined) optionErrors.push(INVALID_MAX_AGENTS_OPTION);
333
+ else options.maxAgents = parsed;
334
+ continue;
335
+ }
336
+ if (token === "--max-agents") {
337
+ const next = tokens[i + 1];
338
+ const parsed = parseWorkflowIntegerString(next);
339
+ if (parsed === undefined) optionErrors.push(INVALID_MAX_AGENTS_OPTION);
340
+ else {
341
+ options.maxAgents = parsed;
342
+ i++;
343
+ }
344
+ continue;
345
+ }
346
+ if (token.startsWith("--agent-timeout-ms=")) {
347
+ const parsed = parseWorkflowIntegerString(token.slice("--agent-timeout-ms=".length));
348
+ if (parsed === undefined) optionErrors.push(INVALID_AGENT_TIMEOUT_OPTION);
349
+ else options.agentTimeoutMs = parsed;
350
+ continue;
351
+ }
352
+ if (token === "--agent-timeout-ms") {
353
+ const next = tokens[i + 1];
354
+ const parsed = parseWorkflowIntegerString(next);
355
+ if (parsed === undefined) optionErrors.push(INVALID_AGENT_TIMEOUT_OPTION);
356
+ else {
357
+ options.agentTimeoutMs = parsed;
358
+ i++;
359
+ }
360
+ continue;
361
+ }
362
+ if (token.startsWith("--agent-retries=")) {
363
+ const parsed = parseWorkflowIntegerString(token.slice("--agent-retries=".length));
364
+ if (parsed === undefined) optionErrors.push(INVALID_AGENT_RETRIES_OPTION);
365
+ else options.agentRetries = parsed;
366
+ continue;
367
+ }
368
+ if (token === "--agent-retries") {
369
+ const next = tokens[i + 1];
370
+ const parsed = parseWorkflowIntegerString(next);
371
+ if (parsed === undefined) optionErrors.push(INVALID_AGENT_RETRIES_OPTION);
372
+ else {
373
+ options.agentRetries = parsed;
374
+ i++;
375
+ }
376
+ continue;
377
+ }
330
378
  if (token.startsWith("--budget=")) {
331
379
  const parsed = parseBudgetOption(token.slice("--budget=".length));
332
380
  if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
@@ -362,6 +410,7 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
362
410
  }
363
411
  kept.push(token);
364
412
  }
413
+ if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
365
414
  return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
366
415
  }
367
416
 
@@ -390,11 +439,12 @@ ${brief.trim()}
390
439
  Use the workflow tool with a script argument, not a saved workflow name.
391
440
  The script must start with export const meta = { ... } and default-export an async workflow function.
392
441
  Use the injected Type object for schemas. Do not import anything or use dynamic import().
393
- Set thinkingLevel explicitly on each agent() call.
442
+ Set profile to "small", "medium", or "big" on each agent() call; use explicit model/thinkingLevel only for an intentional override.
394
443
  Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
395
444
  If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
396
445
  When the run is budgeted, guard expensive loops with \`while (api.budget.total && api.budget.remaining() > N) { ... }\`; api.agent() throws once the budget is spent.
397
446
  Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
447
+ ${ADAPTIVE_WORKFLOW_GUIDANCE}
398
448
  Do not edit files unless the user explicitly requested edits.`;
399
449
  }
400
450
 
@@ -402,6 +452,7 @@ export interface WorkflowToolRequestParams {
402
452
  readonly name?: string;
403
453
  readonly script?: string;
404
454
  readonly resumeFromRunId?: string;
455
+ readonly background?: boolean;
405
456
  }
406
457
 
407
458
  export type WorkflowToolRequest =
@@ -458,76 +509,152 @@ export async function pickWorkflow(
458
509
 
459
510
  export async function sendWorkflowResult(
460
511
  pi: ExtensionAPI,
461
- ctx: ExtensionCommandContext,
512
+ ctx: ExtensionContext,
462
513
  name: string,
463
- mod: WorkflowModule,
514
+ mod: LoadedWorkflow,
464
515
  args: string,
465
516
  options: WorkflowRunOptions,
466
517
  perfRecorder?: PerfSink,
518
+ reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
519
+ ): Promise<void> {
520
+ await sendResolvedWorkflowResult(
521
+ pi,
522
+ ctx,
523
+ name,
524
+ mod,
525
+ args,
526
+ resolveWorkflowRunOptions(options),
527
+ perfRecorder,
528
+ reviewSessions,
529
+ );
530
+ }
531
+
532
+ async function sendResolvedWorkflowResult(
533
+ pi: ExtensionAPI,
534
+ ctx: ExtensionContext,
535
+ name: string,
536
+ mod: LoadedWorkflow,
537
+ args: string,
538
+ options: ResolvedWorkflowRunOptions,
539
+ perfRecorder: PerfSink | undefined,
540
+ reviewSessions: ReviewSessionCoordinator,
467
541
  ): Promise<void> {
468
- const { runWorkflow } = await loadEngine();
469
- let perfSnapshot: PerfSnapshot | undefined;
470
- let usageSnapshot: WorkflowUsageSnapshot | undefined;
471
- let runMetadata: WorkflowRunMetadata | undefined;
542
+ const execution = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
543
+ reviewSessions.remember(ctx, execution, options);
544
+ sendWorkflowExecution(pi, execution);
545
+ await reviewSessions.present(ctx, execution, options);
546
+ }
547
+
548
+ async function executeResolvedWorkflow(
549
+ pi: ExtensionAPI,
550
+ ctx: ExtensionContext,
551
+ name: string,
552
+ mod: LoadedWorkflow,
553
+ args: string,
554
+ options: ResolvedWorkflowRunOptions,
555
+ perfRecorder?: PerfSink,
556
+ ): Promise<WorkflowExecution> {
557
+ const { runResolvedWorkflow } = await loadEngine();
472
558
  let liveInspection: ActiveWorkflowInspection | undefined;
473
- const result = await runWorkflow(ctx, mod, args, {
474
- ...options,
475
- perf: options.perf ?? perfRecorder !== undefined,
559
+ const inspections = workflowInspectionState(pi, ctx);
560
+ return await executeWorkflowInvocation({
561
+ ctx,
562
+ name,
563
+ mod,
564
+ args,
565
+ options,
476
566
  perfRecorder,
567
+ runResolvedWorkflow,
477
568
  resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
478
569
  onProgressSource(source) {
479
570
  if (source) {
480
571
  liveInspection = bindActiveWorkflowInspection(name, args, source);
481
- activeWorkflowInspection = liveInspection;
482
- } else if (activeWorkflowInspection === liveInspection) {
483
- activeWorkflowInspection = undefined;
572
+ inspections.active = liveInspection;
573
+ latestActiveWorkflowInspection = liveInspection;
574
+ } else if (inspections.active === liveInspection) {
575
+ inspections.active = undefined;
576
+ if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
484
577
  liveInspection = undefined;
485
578
  }
486
- options.onProgressSource?.(source);
487
- },
488
- onPerfSnapshot(snapshot) {
489
- perfSnapshot = snapshot;
490
- options.onPerfSnapshot?.(snapshot);
491
- },
492
- onUsageSnapshot(snapshot) {
493
- usageSnapshot = snapshot;
494
- options.onUsageSnapshot?.(snapshot);
495
- },
496
- onRunMetadata(metadata) {
497
- runMetadata = metadata;
498
- options.onRunMetadata?.(metadata);
499
579
  },
500
580
  onProgressSnapshot(snapshot) {
501
- lastWorkflowInspection = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
502
- options.onProgressSnapshot?.(snapshot);
581
+ const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
582
+ inspections.last = completed;
583
+ latestWorkflowInspection = completed;
503
584
  },
504
585
  });
505
- const perf = compactPerfSnapshot(perfSnapshot);
506
- const reviewDecision = decideReviewResultsPresentation({
507
- workflowName: name,
508
- result,
509
- mode: extensionContextMode(ctx),
510
- hasUI: ctx.hasUI,
511
- resultViewer: options.resultViewer,
512
- invocationKind: "command",
513
- });
514
- const reviewAction = await maybeShowReviewResultsViewer(ctx, reviewDecision);
515
- if (reviewDecision.kind !== "send") {
516
- await handleReviewViewerAction(pi, ctx, reviewAction, reviewDecision.issues, reviewDecision.report.reviewContext);
517
- }
586
+ }
587
+
588
+ function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
589
+ const { name } = execution.envelope;
518
590
  pi.sendMessage(
519
591
  {
520
592
  customType: "workflow-result",
521
- content: formatMessageContent(name, result, usageSnapshot, perf, runMetadata),
593
+ content: formatMessageContent(
594
+ name,
595
+ execution.envelope.result,
596
+ execution.envelope.usage,
597
+ execution.envelope.perf,
598
+ execution.metadata,
599
+ ),
522
600
  display: true,
523
- details: workflowEnvelope(name, result, usageSnapshot, perf, runMetadata),
601
+ details: execution.envelope,
524
602
  },
525
603
  { triggerTurn: false },
526
604
  );
527
605
  }
528
606
 
529
- export default function workflowEngine(pi: ExtensionAPI): void {
530
- registerDynamax(pi, undefined, { openInspector: openAvailableWorkflowInspector });
607
+ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordinator {
608
+ return new ReviewSessionCoordinator(pi, {
609
+ async runFollowUp(ctx, workflow, options) {
610
+ const perfRecorder = await createInvocationPerf(options);
611
+ return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
612
+ },
613
+ publish: (execution) => sendWorkflowExecution(pi, execution),
614
+ });
615
+ }
616
+
617
+ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
618
+ assertSupportedPiVersion(VERSION);
619
+ const reviewSessions = createReviewSessionCoordinator(pi);
620
+ const backgroundWorkflows = new BackgroundWorkflowCoordinator(pi);
621
+ const workflowRuns = new WorkflowRunController(backgroundWorkflows, {
622
+ async resolveWorkflow(name) {
623
+ const { discoverWorkflows } = await loadDiscovery();
624
+ return (await discoverWorkflows(EXTENSION_DIR)).get(name);
625
+ },
626
+ async execute(ctx, name, workflow, options) {
627
+ const perfRecorder = await createInvocationPerf(options);
628
+ const execution = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
629
+ reviewSessions.remember(ctx, execution, options);
630
+ },
631
+ });
632
+ backgroundWorkflows.onRunSettled((ctx, runId) => workflowRuns.runSettled(ctx, runId));
633
+ registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
634
+ registerWorkflowModelProfileCommand(pi);
635
+ registerWorkflowRunCommand(pi, workflowRuns);
636
+ pi.on("session_start", async (_event, ctx) => {
637
+ await backgroundWorkflows.sessionStarted(ctx);
638
+ await workflowRuns.sessionStarted(ctx);
639
+ });
640
+ pi.on("agent_settled", async (_event, ctx) => {
641
+ await backgroundWorkflows.agentSettled(ctx);
642
+ });
643
+ pi.on("session_shutdown", async (_event, ctx) => {
644
+ workflowRuns.sessionShutdown(ctx);
645
+ await backgroundWorkflows.sessionShutdown(ctx);
646
+ const key = sessionKey(ctx);
647
+ workflowInspections.get(pi)?.delete(key);
648
+ reviewSessions.dispose(ctx);
649
+ });
650
+ if (shortcuts.results) {
651
+ pi.registerShortcut(shortcuts.results, {
652
+ description: "Open last code-review results",
653
+ handler: async (ctx) => {
654
+ await reviewSessions.reopen(ctx);
655
+ },
656
+ });
657
+ }
531
658
 
532
659
  pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
533
660
  const details = message.details;
@@ -541,27 +668,42 @@ export default function workflowEngine(pi: ExtensionAPI): void {
541
668
  });
542
669
 
543
670
  pi.registerCommand("workflow:inspector", {
544
- description: "Open the current or last workflow inspector",
671
+ description: "Open the current, last, or a retained run inspector",
672
+ getArgumentCompletions: (argumentPrefix) => workflowRuns.inspectorArgumentCompletions(argumentPrefix),
545
673
  handler: async (args: string, ctx: ExtensionCommandContext) => {
546
674
  const trimmed = args.trim();
547
675
  if (trimmed && trimmed !== "last") {
548
- ctx.ui.notify("Usage: /workflow:inspector [last]", "warning");
676
+ if (await workflowRuns.inspectStoredRun(ctx, trimmed)) return;
677
+ ctx.ui.notify(`Workflow run ${trimmed} was not found.`, "warning");
678
+ return;
679
+ }
680
+ await openAvailableWorkflowInspector(pi, ctx);
681
+ },
682
+ });
683
+
684
+ pi.registerCommand("workflow:results", {
685
+ description: "Reopen the last code-review findings viewer",
686
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
687
+ if (args.trim()) {
688
+ ctx.ui.notify("Usage: /workflow:results", "warning");
549
689
  return;
550
690
  }
551
- await openAvailableWorkflowInspector(ctx);
691
+ await reviewSessions.reopen(ctx);
552
692
  },
553
693
  });
554
694
 
555
695
  // /workflow <name> [args] — user-invoked.
556
696
  pi.registerCommand("workflow", {
557
697
  description: "Run a multi-agent workflow: /workflow <name> [args]",
698
+ getArgumentCompletions: workflowArgumentCompletions,
558
699
  handler: async (args: string, ctx: ExtensionCommandContext) => {
559
700
  const direct = parseWorkflowInvocation(args);
560
701
  if (direct.optionErrors?.length) {
561
702
  ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
562
703
  return;
563
704
  }
564
- const perfRecorder = await createInvocationPerf(direct.options);
705
+ const directOptions = resolveWorkflowRunOptions(direct.options);
706
+ const perfRecorder = await createInvocationPerf(directOptions);
565
707
  const { discoverWorkflows } = await loadDiscovery();
566
708
  const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
567
709
  const available = [...workflows.keys()].join(", ") || "(none)";
@@ -583,16 +725,25 @@ export default function workflowEngine(pi: ExtensionAPI): void {
583
725
  return;
584
726
  }
585
727
 
586
- await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options, perfRecorder);
728
+ const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
729
+ await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
587
730
  },
588
731
  });
589
732
 
590
- // workflow tool — lets the host agent fan out mid-conversation.
733
+ registerWorkflowTool(pi, reviewSessions, backgroundWorkflows);
734
+ }
735
+
736
+ /** Register the host-facing workflow tool independently from command and lifecycle surfaces. */
737
+ function registerWorkflowTool(
738
+ pi: ExtensionAPI,
739
+ reviewSessions: ReviewSessionCoordinator,
740
+ backgroundWorkflows: BackgroundWorkflowCoordinator,
741
+ ): void {
591
742
  pi.registerTool({
592
743
  name: "workflow",
593
744
  label: "Workflow",
594
745
  description:
595
- "ONLY call workflow when the user opted into multi-agent orchestration via the literal token `dynamax`, sticky `/workflow:dynamax on`, an explicit request to run or author a workflow, or a command/skill instruction. Runs either a registered named workflow or an inline one-off workflow script (fan-out → verify → synthesize) and returns its structured result.",
746
+ "ONLY call workflow when the user opted into multi-agent orchestration via the literal token `dynamax`, sticky `/workflow:dynamax on`, an explicit request to run or author a workflow, or a command/skill instruction. Runs either a registered named workflow or an inline one-off workflow script (fan-out → verify → synthesize), synchronously by default or explicitly in the background.",
596
747
  promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
597
748
  promptGuidelines: [
598
749
  "Use workflow only when the user opted into workflow orchestration via `dynamax`, `/workflow:dynamax on`, an explicit request to run/author a workflow, or a command/skill instruction.",
@@ -605,6 +756,10 @@ export default function workflowEngine(pi: ExtensionAPI): void {
605
756
  "When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
606
757
  "If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
607
758
  "`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
759
+ ADAPTIVE_WORKFLOW_GUIDANCE,
760
+ "Set background: true only when the user explicitly wants the workflow to continue after this tool call; the tool returns a durable run ID and completion is delivered later.",
761
+ "Set autoResumeOnUsageLimit: true only for an explicitly backgrounded workflow when the user wants bounded automatic continuation after a recognized provider usage window.",
762
+ "Set resumeEditedWorkflow: true only with resumeFromRunId when the user explicitly accepts reusing behaviorally identical calls after workflow source edits.",
608
763
  "Every workflow tool call must provide exactly one of `name` or `script`, never both.",
609
764
  ],
610
765
  parameters: Type.Object({
@@ -613,6 +768,38 @@ export default function workflowEngine(pi: ExtensionAPI): void {
613
768
  args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
614
769
  concurrency: Type.Optional(Type.Number({ description: "Optional per-run agent concurrency cap" })),
615
770
  parallelSubmissionLimit: Type.Optional(Type.Number({ description: "Optional limit for eagerly submitted parallel thunks" })),
771
+ maxAgents: Type.Optional(
772
+ Type.Integer({
773
+ description: `Maximum live model calls started across this run; replay hits are excluded; clamped to ${WORKFLOW_MAX_AGENTS_MIN}-${WORKFLOW_MAX_AGENTS_MAX}`,
774
+ }),
775
+ ),
776
+ agentTimeoutMs: Type.Optional(
777
+ Type.Integer({
778
+ description: `Maximum live duration per agent in milliseconds; clamped to ${WORKFLOW_AGENT_TIMEOUT_MIN_MS}-${WORKFLOW_AGENT_TIMEOUT_MAX_MS}`,
779
+ }),
780
+ ),
781
+ agentRetries: Type.Optional(
782
+ Type.Integer({
783
+ description: `Retries per agent for classified transient provider failures; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
784
+ }),
785
+ ),
786
+ autoResumeOnUsageLimit: Type.Optional(
787
+ Type.Boolean({ description: "For a background run, opt into bounded automatic resume after a recognized provider usage limit" }),
788
+ ),
789
+ usageLimitMaxAttempts: Type.Optional(
790
+ Type.Integer({
791
+ minimum: WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
792
+ maximum: WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
793
+ description: "Maximum total run attempts in one automatic provider-limit resume chain",
794
+ }),
795
+ ),
796
+ usageLimitMaxDelayMs: Type.Optional(
797
+ Type.Integer({
798
+ minimum: WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
799
+ maximum: WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
800
+ description: "Maximum delay accepted from a provider reset hint before automatic resume",
801
+ }),
802
+ ),
616
803
  budget: Type.Optional(
617
804
  Type.Integer({
618
805
  minimum: WORKFLOW_BUDGET_MIN,
@@ -622,15 +809,20 @@ export default function workflowEngine(pi: ExtensionAPI): void {
622
809
  ),
623
810
  perf: Type.Optional(Type.Boolean({ description: "Include workflow performance timing aggregates in the result details" })),
624
811
  resumeFromRunId: Type.Optional(Type.String({ minLength: 1, description: "Workflow run id to resume from by replaying matching completed agent results" })),
812
+ resumeEditedWorkflow: Type.Optional(
813
+ Type.Boolean({ description: "With resumeFromRunId, ignore only workflow-source fingerprint changes while retaining all other replay checks" }),
814
+ ),
815
+ background: Type.Optional(Type.Boolean({ description: "Return a durable run ID immediately and deliver completion to this conversation later" })),
625
816
  }),
626
817
  renderCall(args, theme) {
627
818
  const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
819
+ const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
628
820
  if (args.name?.trim()) {
629
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${suffix}`, 0, 0);
821
+ return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${background}${suffix}`, 0, 0);
630
822
  }
631
823
  const preview = compactInlinePreview(args.script);
632
824
  const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
633
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${suffix}${previewSuffix}`, 0, 0);
825
+ return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${background}${suffix}${previewSuffix}`, 0, 0);
634
826
  },
635
827
  renderResult(result, { expanded, isPartial }, theme) {
636
828
  if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
@@ -655,18 +847,35 @@ export default function workflowEngine(pi: ExtensionAPI): void {
655
847
  details: { error: "invalid_resume_from_run_id" },
656
848
  };
657
849
  }
850
+ if (params.resumeEditedWorkflow && !resumeFromRunId) {
851
+ return {
852
+ content: [{ type: "text", text: "resumeEditedWorkflow requires resumeFromRunId." }],
853
+ details: { error: "invalid_edited_workflow_resume" },
854
+ };
855
+ }
856
+ if (params.background) {
857
+ const unavailable = backgroundUnavailableResult(ctx.mode);
858
+ if (unavailable) return unavailable;
859
+ }
658
860
 
659
- const runOptions: WorkflowRunOptions = {
660
- inspect: ctx.hasUI,
861
+ const runOptions = resolveWorkflowRunOptions({
862
+ inspect: ctx.hasUI && ctx.mode === "tui",
661
863
  concurrency: params.concurrency,
662
864
  parallelSubmissionLimit: params.parallelSubmissionLimit,
865
+ maxAgents: params.maxAgents,
866
+ agentTimeoutMs: params.agentTimeoutMs,
867
+ agentRetries: params.agentRetries,
868
+ autoResumeOnUsageLimit: params.autoResumeOnUsageLimit,
869
+ usageLimitMaxAttempts: params.usageLimitMaxAttempts,
870
+ usageLimitMaxDelayMs: params.usageLimitMaxDelayMs,
663
871
  budget: params.budget,
664
872
  perf: params.perf,
665
873
  resumeFromRunId,
874
+ resumeEditedWorkflow: params.resumeEditedWorkflow,
666
875
  signal,
667
- };
876
+ });
668
877
  const perfRecorder = await createInvocationPerf(runOptions);
669
- let mod: WorkflowModule;
878
+ let mod: LoadedWorkflow;
670
879
  let resultName: string;
671
880
 
672
881
  if (request.kind === "named") {
@@ -693,45 +902,41 @@ export default function workflowEngine(pi: ExtensionAPI): void {
693
902
  resultName = mod.meta.name;
694
903
  }
695
904
 
696
- const { runWorkflow } = await loadEngine();
697
- let perfSnapshot: PerfSnapshot | undefined;
698
- let usageSnapshot: WorkflowUsageSnapshot | undefined;
699
- let runMetadata: WorkflowRunMetadata | undefined;
700
- let liveInspection: ActiveWorkflowInspection | undefined;
701
905
  const resultArgs = params.args ?? "";
702
- const result = await runWorkflow(ctx, mod, resultArgs, {
703
- ...runOptions,
704
- perf: runOptions.perf ?? perfRecorder !== undefined,
705
- perfRecorder,
706
- resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
707
- onProgressSource(source) {
708
- if (source) {
709
- liveInspection = bindActiveWorkflowInspection(resultName, resultArgs, source);
710
- activeWorkflowInspection = liveInspection;
711
- } else if (activeWorkflowInspection === liveInspection) {
712
- activeWorkflowInspection = undefined;
713
- liveInspection = undefined;
714
- }
715
- },
716
- onPerfSnapshot: (snapshot) => {
717
- perfSnapshot = snapshot;
718
- },
719
- onUsageSnapshot: (snapshot) => {
720
- usageSnapshot = snapshot;
721
- },
722
- onRunMetadata: (metadata) => {
723
- runMetadata = metadata;
724
- },
725
- onProgressSnapshot: (snapshot) => {
726
- // Record the run so /workflow:inspector can reopen it — tool-invoked (dynamax) runs were
727
- // previously uninspectable, unlike the /workflow command path.
728
- lastWorkflowInspection = { name: resultName, args: resultArgs, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
729
- },
730
- });
731
- const perf = compactPerfSnapshot(perfSnapshot);
906
+ if (params.background) {
907
+ return await startBackgroundWorkflowTool({
908
+ coordinator: backgroundWorkflows,
909
+ ctx,
910
+ name: resultName,
911
+ options: runOptions,
912
+ async execute(backgroundCtx, backgroundOptions) {
913
+ const execution = await executeResolvedWorkflow(
914
+ pi,
915
+ backgroundCtx,
916
+ resultName,
917
+ mod,
918
+ resultArgs,
919
+ backgroundOptions,
920
+ perfRecorder,
921
+ );
922
+ reviewSessions.remember(ctx, execution, backgroundOptions);
923
+ },
924
+ });
925
+ }
926
+ const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
927
+ reviewSessions.remember(ctx, execution, runOptions);
732
928
  return {
733
- content: [{ type: "text", text: formatMessageContent(resultName, result, usageSnapshot, perf, runMetadata) }],
734
- details: workflowEnvelope(resultName, result, usageSnapshot, perf, runMetadata),
929
+ content: [{
930
+ type: "text",
931
+ text: formatMessageContent(
932
+ resultName,
933
+ execution.envelope.result,
934
+ execution.envelope.usage,
935
+ execution.envelope.perf,
936
+ execution.metadata,
937
+ ),
938
+ }],
939
+ details: execution.envelope,
735
940
  };
736
941
  },
737
942
  });