pi-workflow-engine 0.11.0 → 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 (73) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +255 -97
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
  4. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
  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 +49 -45
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
  9. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
  11. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
  12. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
  13. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  14. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  15. package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
  16. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
  17. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  18. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
  19. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
  20. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
  21. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
  22. package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
  23. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  24. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
  25. package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
  26. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  27. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  28. package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
  29. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  30. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  31. package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
  32. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  33. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  34. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  35. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +74 -38
  36. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
  37. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
  38. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
  39. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
  40. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
  41. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
  42. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  43. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  44. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  45. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  46. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
  47. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
  48. package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
  49. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  50. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  51. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  52. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +2 -1
  53. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  54. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  55. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  56. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
  57. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
  58. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  59. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  60. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  61. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  62. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  63. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  64. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
  65. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
  66. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
  67. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
  68. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
  69. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
  70. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  71. package/README.md +72 -141
  72. package/USAGE.md +211 -17
  73. package/package.json +1 -1
@@ -1,14 +1,16 @@
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";
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
5
  import { isAdvisoryReport } from "./src/advisory-schema.ts";
6
- import type { WorkflowProgressSnapshot } from "./src/progress.ts";
6
+ import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
7
7
  import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
8
8
  import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
9
+ import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./src/ui/workflow-viewer-layout.ts";
9
10
  import type { PerfSink } from "./src/perf.ts";
10
11
  import type { WorkflowUsageSnapshot } from "./src/usage.ts";
11
- import { ADAPTIVE_WORKFLOW_GUIDANCE, dynamaxSessionKey, registerDynamax } from "./src/dynamax.ts";
12
+ import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
13
+ import { sessionKey } from "./src/session-identity.ts";
12
14
  import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
13
15
  import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
14
16
  import {
@@ -18,20 +20,36 @@ import {
18
20
  } from "./src/ui/workflow-result-renderer.ts";
19
21
  import {
20
22
  parseWorkflowBudgetString,
23
+ parseWorkflowIntegerString,
21
24
  resolveWorkflowRunOptions,
22
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,
23
30
  WORKFLOW_BUDGET_MAX,
24
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,
25
38
  } from "./src/options.ts";
26
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";
27
47
 
28
48
  /** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
29
49
  const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
30
50
 
31
51
  function summarize(result: unknown): string {
32
- if (result && typeof result === "object" && typeof (result as { summary?: unknown }).summary === "string") {
33
- return (result as { summary: string }).summary;
34
- }
52
+ if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
35
53
  return typeof result === "string" ? result : "Workflow finished.";
36
54
  }
37
55
 
@@ -117,77 +135,55 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
117
135
  const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
118
136
  const AUTHOR_TEMP_WORKFLOW_LABEL = "Author temporary one-shot workflow";
119
137
  const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
120
- const WORKFLOW_PICKER_MAX_VISIBLE = 8;
121
- const WORKFLOW_PICKER_LAYOUT = { minPrimaryColumnWidth: 18, maxPrimaryColumnWidth: 22 };
122
-
123
- class WorkflowPickerComponent implements Component {
124
- private readonly list: SelectList;
125
-
126
- constructor(
127
- items: SelectItem[],
128
- private readonly tui: TUI,
129
- private readonly theme: Theme,
130
- done: (value: string | undefined) => void,
131
- ) {
132
- this.list = new SelectList(items, Math.min(WORKFLOW_PICKER_MAX_VISIBLE, items.length), selectListTheme(theme), WORKFLOW_PICKER_LAYOUT);
133
- this.list.onSelect = (item) => done(item.value);
134
- this.list.onCancel = () => done(undefined);
135
- this.list.onSelectionChange = () => this.tui.requestRender();
136
- }
137
-
138
- render(width: number): string[] {
139
- const safeWidth = Math.max(20, width);
140
- const rule = this.theme.fg("borderMuted", "-".repeat(safeWidth));
141
- const lines = [
142
- rule,
143
- this.theme.fg("accent", this.theme.bold("Run workflow")),
144
- "",
145
- ...this.list.render(safeWidth),
146
- "",
147
- this.theme.fg("muted", "up/down navigate enter select escape cancel"),
148
- rule,
149
- ];
150
- return lines.map((line) => truncateToWidth(line, safeWidth));
151
- }
152
-
153
- handleInput(data: string): void {
154
- this.list.handleInput(data);
155
- this.tui.requestRender();
156
- }
157
138
 
158
- invalidate(): void {
159
- 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
+ );
160
167
  }
161
- }
162
-
163
- function selectListTheme(theme: Theme): SelectListTheme {
164
- return {
165
- selectedPrefix: (text) => theme.fg("accent", text),
166
- selectedText: (text) => theme.fg("accent", text),
167
- description: (text) => theme.fg("muted", text),
168
- scrollInfo: (text) => theme.fg("muted", text),
169
- noMatch: (text) => theme.fg("muted", text),
170
- };
171
- }
172
-
173
- function workflowPickerItems(workflows: ReadonlyMap<string, WorkflowModule>): SelectItem[] {
174
- return [
175
- { value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL, description: AUTHOR_TEMP_WORKFLOW_DESCRIPTION },
176
- ...[...workflows.values()].map((mod) => ({ value: mod.meta.name, label: mod.meta.name, description: mod.meta.description })),
177
- ];
168
+ return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
178
169
  }
179
170
 
180
171
  async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
181
- const items = workflowPickerItems(workflows);
182
- if (typeof ctx.ui.custom === "function") {
183
- return await ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => new WorkflowPickerComponent(items, tui, theme, done));
184
- }
185
-
186
- 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(
187
183
  "Run workflow",
188
- items.map((item) => item.label),
184
+ choices.map((choice) => choice.display),
189
185
  );
190
- return items.find((item) => item.label === choice)?.value ?? choice;
186
+ return choices.find((choice) => choice.display === selected)?.value;
191
187
  }
192
188
 
193
189
  export interface LastWorkflowInspection {
@@ -217,20 +213,20 @@ export function getLastWorkflowInspection(): LastWorkflowInspection | undefined
217
213
  return latestWorkflowInspection;
218
214
  }
219
215
 
220
- export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
221
- return latestActiveWorkflowInspection;
222
- }
223
-
224
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
+ }
225
221
  await ctx.ui.custom<void>(
226
- (tui, theme, _keybindings, done) => new WorkflowInspector(snapshotGetter(inspection), tui, theme, () => done(undefined)),
227
- { 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,
228
224
  );
229
225
  }
230
226
 
231
227
  function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
232
228
  const sessions = workflowInspections.get(pi) ?? new Map<string, SessionWorkflowInspections>();
233
- const key = dynamaxSessionKey(ctx);
229
+ const key = sessionKey(ctx);
234
230
  const state = sessions.get(key) ?? {};
235
231
  sessions.set(key, state);
236
232
  workflowInspections.set(pi, sessions);
@@ -238,10 +234,6 @@ function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): Sessi
238
234
  }
239
235
 
240
236
  async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
241
- if (!ctx.hasUI) {
242
- ctx.ui.notify("Workflow inspector requires the TUI", "warning");
243
- return;
244
- }
245
237
  const state = workflowInspectionState(pi, ctx);
246
238
  const inspection = state.active ?? state.last;
247
239
  if (!inspection) {
@@ -251,11 +243,6 @@ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionCo
251
243
  await openWorkflowInspector(ctx, inspection);
252
244
  }
253
245
 
254
- function snapshotGetter(inspection: LastWorkflowInspection | ActiveWorkflowInspection): () => WorkflowProgressSnapshot {
255
- const snapshot = inspection.snapshot;
256
- return typeof snapshot === "function" ? snapshot : () => snapshot;
257
- }
258
-
259
246
  function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
260
247
  return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
261
248
  }
@@ -283,6 +270,10 @@ export function parseWorkflowInvocation(input: string): WorkflowInvocation {
283
270
 
284
271
  const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
285
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>";
286
277
 
287
278
  function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
288
279
  const tokens = input.split(/\s+/).filter(Boolean);
@@ -304,6 +295,10 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
304
295
  options.perf = true;
305
296
  continue;
306
297
  }
298
+ if (token === "--resume-edited") {
299
+ options.resumeEditedWorkflow = true;
300
+ continue;
301
+ }
307
302
  if (token === "--result-viewer" || token === "--review-viewer") {
308
303
  options.resultViewer = "open";
309
304
  continue;
@@ -332,6 +327,54 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
332
327
  if (next !== undefined) i++;
333
328
  continue;
334
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
+ }
335
378
  if (token.startsWith("--budget=")) {
336
379
  const parsed = parseBudgetOption(token.slice("--budget=".length));
337
380
  if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
@@ -367,6 +410,7 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
367
410
  }
368
411
  kept.push(token);
369
412
  }
413
+ if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
370
414
  return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
371
415
  }
372
416
 
@@ -395,7 +439,7 @@ ${brief.trim()}
395
439
  Use the workflow tool with a script argument, not a saved workflow name.
396
440
  The script must start with export const meta = { ... } and default-export an async workflow function.
397
441
  Use the injected Type object for schemas. Do not import anything or use dynamic import().
398
- 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.
399
443
  Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
400
444
  If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
401
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.
@@ -408,6 +452,7 @@ export interface WorkflowToolRequestParams {
408
452
  readonly name?: string;
409
453
  readonly script?: string;
410
454
  readonly resumeFromRunId?: string;
455
+ readonly background?: boolean;
411
456
  }
412
457
 
413
458
  export type WorkflowToolRequest =
@@ -570,10 +615,35 @@ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordina
570
615
  }
571
616
 
572
617
  export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
618
+ assertSupportedPiVersion(VERSION);
573
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));
574
633
  registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
575
- pi.on("session_shutdown", (_event, ctx) => {
576
- const key = dynamaxSessionKey(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);
577
647
  workflowInspections.get(pi)?.delete(key);
578
648
  reviewSessions.dispose(ctx);
579
649
  });
@@ -598,11 +668,13 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
598
668
  });
599
669
 
600
670
  pi.registerCommand("workflow:inspector", {
601
- 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),
602
673
  handler: async (args: string, ctx: ExtensionCommandContext) => {
603
674
  const trimmed = args.trim();
604
675
  if (trimmed && trimmed !== "last") {
605
- 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");
606
678
  return;
607
679
  }
608
680
  await openAvailableWorkflowInspector(pi, ctx);
@@ -623,6 +695,7 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
623
695
  // /workflow <name> [args] — user-invoked.
624
696
  pi.registerCommand("workflow", {
625
697
  description: "Run a multi-agent workflow: /workflow <name> [args]",
698
+ getArgumentCompletions: workflowArgumentCompletions,
626
699
  handler: async (args: string, ctx: ExtensionCommandContext) => {
627
700
  const direct = parseWorkflowInvocation(args);
628
701
  if (direct.optionErrors?.length) {
@@ -657,12 +730,20 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
657
730
  },
658
731
  });
659
732
 
660
- // 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 {
661
742
  pi.registerTool({
662
743
  name: "workflow",
663
744
  label: "Workflow",
664
745
  description:
665
- "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.",
666
747
  promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
667
748
  promptGuidelines: [
668
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.",
@@ -676,6 +757,9 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
676
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.",
677
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.",
678
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.",
679
763
  "Every workflow tool call must provide exactly one of `name` or `script`, never both.",
680
764
  ],
681
765
  parameters: Type.Object({
@@ -684,6 +768,38 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
684
768
  args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
685
769
  concurrency: Type.Optional(Type.Number({ description: "Optional per-run agent concurrency cap" })),
686
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
+ ),
687
803
  budget: Type.Optional(
688
804
  Type.Integer({
689
805
  minimum: WORKFLOW_BUDGET_MIN,
@@ -693,15 +809,20 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
693
809
  ),
694
810
  perf: Type.Optional(Type.Boolean({ description: "Include workflow performance timing aggregates in the result details" })),
695
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" })),
696
816
  }),
697
817
  renderCall(args, theme) {
698
818
  const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
819
+ const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
699
820
  if (args.name?.trim()) {
700
- 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);
701
822
  }
702
823
  const preview = compactInlinePreview(args.script);
703
824
  const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
704
- 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);
705
826
  },
706
827
  renderResult(result, { expanded, isPartial }, theme) {
707
828
  if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
@@ -726,14 +847,31 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
726
847
  details: { error: "invalid_resume_from_run_id" },
727
848
  };
728
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
+ }
729
860
 
730
861
  const runOptions = resolveWorkflowRunOptions({
731
- inspect: ctx.hasUI,
862
+ inspect: ctx.hasUI && ctx.mode === "tui",
732
863
  concurrency: params.concurrency,
733
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,
734
871
  budget: params.budget,
735
872
  perf: params.perf,
736
873
  resumeFromRunId,
874
+ resumeEditedWorkflow: params.resumeEditedWorkflow,
737
875
  signal,
738
876
  });
739
877
  const perfRecorder = await createInvocationPerf(runOptions);
@@ -765,6 +903,26 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
765
903
  }
766
904
 
767
905
  const resultArgs = params.args ?? "";
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
+ }
768
926
  const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
769
927
  reviewSessions.remember(ctx, execution, runOptions);
770
928
  return {
@@ -59,12 +59,8 @@ export const AdvisoryReportWithStatsSchema = Type.Object({
59
59
  stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
60
60
  });
61
61
 
62
- export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
63
- export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
64
- export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
65
62
  export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
66
63
  export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
67
- export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
68
64
  export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
69
65
  export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
70
66
  export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
@@ -37,8 +37,9 @@ export async function executeAgentAttempt(input: {
37
37
  readonly label: string;
38
38
  readonly rowId: number;
39
39
  readonly tags: AgentRunTags;
40
+ readonly admitLiveAgent: () => void;
40
41
  }): Promise<AgentAttemptResult> {
41
- const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags } = input;
42
+ const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags, admitLiveAgent } = input;
42
43
  let repositoryBefore: RepositoryResumeContext | undefined;
43
44
  let evidence: AgentReplayEvidence | undefined;
44
45
  if (isReplayEnabled(replay)) {
@@ -104,6 +105,7 @@ export async function executeAgentAttempt(input: {
104
105
  }
105
106
 
106
107
  assertWorkflowBudgetAvailable(rc.budget);
108
+ admitLiveAgent();
107
109
  const rawResult = await promptAgentSession({
108
110
  rc,
109
111
  handle,
@@ -0,0 +1,35 @@
1
+ import { throwIfAborted } from "./cancellation.ts";
2
+
3
+ export class WorkflowAgentLimitError extends Error {
4
+ override readonly name = "WorkflowAgentLimitError";
5
+ readonly code = "WORKFLOW_AGENT_LIMIT_EXCEEDED";
6
+
7
+ constructor(readonly maxAgents: number) {
8
+ super(`Workflow live-agent limit of ${maxAgents} has been reached; no new model request was started.`);
9
+ }
10
+ }
11
+
12
+ export class WorkflowAgentTimeoutError extends Error {
13
+ override readonly name = "WorkflowAgentTimeoutError";
14
+ readonly code = "WORKFLOW_AGENT_TIMEOUT";
15
+
16
+ constructor(
17
+ readonly label: string,
18
+ readonly timeoutMs: number,
19
+ ) {
20
+ super(`Workflow agent "${label}" exceeded its ${timeoutMs}ms duration limit.`);
21
+ }
22
+ }
23
+
24
+ /** Shared run-level admission counter. Cache hits do not call admit(). */
25
+ export class WorkflowAgentLimiter {
26
+ private admitted = 0;
27
+
28
+ constructor(readonly maxAgents: number) {}
29
+
30
+ admit(signal: AbortSignal | undefined): void {
31
+ throwIfAborted(signal);
32
+ if (this.admitted >= this.maxAgents) throw new WorkflowAgentLimitError(this.maxAgents);
33
+ this.admitted++;
34
+ }
35
+ }