pi-workflow-engine 0.13.0 → 1.1.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 (98) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +152 -416
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
  6. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
  7. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +12 -13
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +131 -117
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
  16. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
  17. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
  18. package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
  19. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
  20. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
  21. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
  22. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +58 -117
  23. package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -80
  24. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
  25. package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
  26. package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
  27. package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
  28. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
  29. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
  30. package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
  31. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
  32. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
  33. package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
  34. package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
  35. package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
  36. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
  37. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
  38. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
  39. package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
  40. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
  41. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
  42. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
  43. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +29 -31
  44. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
  45. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
  46. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
  52. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
  53. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
  54. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
  55. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
  56. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
  57. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
  58. package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
  59. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +17 -4
  60. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
  61. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
  62. package/.pi/extensions/pi-workflow-engine/src/types.ts +30 -0
  63. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
  64. package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
  65. package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
  68. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
  69. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
  70. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
  71. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
  72. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
  73. package/.pi/extensions/pi-workflow-engine/src/usage.ts +41 -34
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
  79. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
  80. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
  81. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
  82. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
  83. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
  84. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
  85. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
  86. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
  87. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
  88. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
  89. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
  90. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
  91. package/README.md +1 -1
  92. package/USAGE.md +79 -20
  93. package/package.json +1 -2
  94. package/skills/workflow-code-review-actions/SKILL.md +12 -23
  95. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
  96. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
  97. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
  98. package/.pi/settings.json +0 -3
@@ -1,26 +1,21 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { Type } from "typebox";
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";
3
+ import { getSelectListTheme, keyText, VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { AutocompleteItem, SelectItem } from "@earendil-works/pi-tui";
6
5
  import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
7
- import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
8
- import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
9
- import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./src/ui/workflow-viewer-layout.ts";
6
+ import type { WorkflowRunState } from "./src/workflow-run-record.ts";
7
+ import { WORKFLOW_TOOL_NAME, type LoadedWorkflow, type WorkflowModule, type WorkflowProgressSource, type WorkflowRef } from "./src/types.ts";
8
+ import { showWorkflowInspector } from "./src/ui/workflow-inspector.ts";
9
+ import { WorkflowPicker } from "./src/ui/workflow-picker.ts";
10
10
  import type { PerfSink } from "./src/perf.ts";
11
- import type { WorkflowUsageSnapshot } from "./src/usage.ts";
12
11
  import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
13
12
  import { sessionKey } from "./src/session-identity.ts";
14
13
  import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
15
14
  import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
15
+ import { isWorkflowResult, renderWorkflowResult, type WorkflowResultHints } from "./src/ui/workflow-result-renderer.ts";
16
+ import { renderWorkflowToolCall, renderWorkflowToolResult } from "./src/ui/workflow-tool-renderer.ts";
16
17
  import {
17
- formatWorkflowDetailLines,
18
- isWorkflowResult,
19
- renderWorkflowResult,
20
- } from "./src/ui/workflow-result-renderer.ts";
21
- import {
22
- parseWorkflowBudgetString,
23
- parseWorkflowIntegerString,
18
+ parseWorkflowInvocation,
24
19
  resolveWorkflowRunOptions,
25
20
  type ResolvedWorkflowRunOptions,
26
21
  WORKFLOW_AGENT_TIMEOUT_MAX_MS,
@@ -36,7 +31,8 @@ import {
36
31
  WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
37
32
  WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
38
33
  } from "./src/options.ts";
39
- import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./src/workflow-execution.ts";
34
+ import { executeWorkflowInvocation, type WorkflowResultEnvelope } from "./src/workflow-execution.ts";
35
+ import { formatWorkflowResultForContext } from "./src/workflow-result-context.ts";
40
36
  import { registerWorkflowModelProfileCommand } from "./src/model-profile-command.ts";
41
37
  import { BackgroundWorkflowCoordinator } from "./src/background-workflows.ts";
42
38
  import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./src/background-workflow-tool.ts";
@@ -44,58 +40,13 @@ import { registerWorkflowRunCommand, WorkflowRunController } from "./src/workflo
44
40
  import { completeCurrentArgument, splitArgumentPrefix } from "./src/command-completions.ts";
45
41
  import { assertSupportedPiVersion } from "./src/pi-compat.ts";
46
42
  import { formatWorkflowInspection, workflowInspectionSnapshot } from "./src/ui/workflow-format.ts";
43
+ import { workflowUsageForPi, type WorkflowUsageSnapshot } from "./src/usage.ts";
44
+ import { unknownErrorMessage } from "./src/unknown-error.ts";
45
+ import { createHostToolBridge } from "./src/host-tools.ts";
47
46
 
48
47
  /** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
49
48
  const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
50
49
 
51
- function summarize(result: unknown): string {
52
- if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
53
- return typeof result === "string" ? result : "Workflow finished.";
54
- }
55
-
56
- function formatMessageContent(
57
- name: string,
58
- result: unknown,
59
- usage?: WorkflowUsageSnapshot,
60
- perf?: WorkflowPerfDetails,
61
- metadata?: WorkflowRunMetadata,
62
- ): string {
63
- const details = formatWorkflowDetailLines({ usage, perf, metadata });
64
- return `## Workflow: ${name}\n\n${formatResultForContext(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
65
- }
66
-
67
- function formatResultForContext(result: unknown): string {
68
- if (!isAdvisoryReport(result)) return summarize(result);
69
-
70
- const lines = [result.summary];
71
- if (result.findings.length > 0) {
72
- lines.push("", "Findings:");
73
- result.findings.forEach((finding, index) => {
74
- const id = `R${String(index + 1).padStart(3, "0")}`;
75
- lines.push(
76
- `\n### ${id}: ${finding.summary}`,
77
- `- Severity: ${finding.severity}`,
78
- `- Confidence: ${finding.confidence}`,
79
- `- Category: ${finding.category}`,
80
- `- Location: ${finding.locations.map(formatFindingLocation).join(", ")}`,
81
- `- Impact: ${finding.impact}`,
82
- `- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
83
- `- Recommendation: ${finding.recommendation}`,
84
- );
85
- });
86
- }
87
- if (result.nextSteps.length > 0) {
88
- lines.push("", "Next steps:", ...result.nextSteps.map((step) => `- ${step}`));
89
- }
90
- return lines.join("\n");
91
- }
92
-
93
- function formatFindingLocation(location: { readonly file: string; readonly line?: number; readonly symbol?: string }): string {
94
- const line = location.line === undefined ? "" : `:${location.line}`;
95
- const symbol = location.symbol === undefined ? "" : ` (${location.symbol})`;
96
- return `${location.file}${line}${symbol}`;
97
- }
98
-
99
50
  type DiscoveryModule = typeof import("./src/discovery.ts");
100
51
  type EngineModule = typeof import("./src/engine.ts");
101
52
  type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
@@ -133,7 +84,7 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
133
84
  }
134
85
 
135
86
  const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
136
- const AUTHOR_TEMP_WORKFLOW_LABEL = "Author temporary one-shot workflow";
87
+ const AUTHOR_TEMP_WORKFLOW_LABEL = "+ author a one-off workflow";
137
88
  const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
138
89
 
139
90
  const WORKFLOW_OPTION_COMPLETIONS = [
@@ -147,7 +98,7 @@ const WORKFLOW_OPTION_COMPLETIONS = [
147
98
  { value: "--parallel-limit=", description: "Set the parallel submission limit" },
148
99
  { value: "--max-agents=", description: "Set the maximum admitted live agents" },
149
100
  { value: "--agent-timeout-ms=", description: "Set the timeout for each agent attempt" },
150
- { value: "--agent-retries=", description: "Set retries for each agent call" },
101
+ { value: "--agent-retries=", description: "Restart an agent after pi's in-session turn retries are exhausted" },
151
102
  { value: "--budget=", description: "Set the workflow output-token budget" },
152
103
  { value: "--resume=", description: "Resume from a retained workflow run ID" },
153
104
  ] as const;
@@ -168,21 +119,22 @@ async function workflowArgumentCompletions(argumentPrefix: string): Promise<Auto
168
119
  return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
169
120
  }
170
121
 
122
+ /** Registered workflows in discovery order (built-ins first), then the author option. */
171
123
  async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
124
+ const workflowItems: SelectItem[] = [...workflows.values()].map((workflow) => ({
125
+ value: workflow.meta.name,
126
+ label: workflow.meta.name,
127
+ description: workflow.meta.description,
128
+ }));
129
+ if (ctx.mode === "tui") {
130
+ const items = [...workflowItems, { value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL }];
131
+ return await ctx.ui.custom<string | undefined>((...[, theme, , done]) => new WorkflowPicker(items, theme, getSelectListTheme(), done));
132
+ }
172
133
  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
- })),
134
+ ...workflowItems.map((item) => ({ value: item.value, display: `${item.label} — ${item.description}` })),
135
+ { value: AUTHOR_TEMP_WORKFLOW_VALUE, display: `${AUTHOR_TEMP_WORKFLOW_LABEL} — ${AUTHOR_TEMP_WORKFLOW_DESCRIPTION}` },
181
136
  ];
182
- const selected = await ctx.ui.select(
183
- "Run workflow",
184
- choices.map((choice) => choice.display),
185
- );
137
+ const selected = await ctx.ui.select("Run a workflow", choices.map((choice) => choice.display));
186
138
  return choices.find((choice) => choice.display === selected)?.value;
187
139
  }
188
140
 
@@ -191,6 +143,7 @@ export interface LastWorkflowInspection {
191
143
  readonly args: string;
192
144
  readonly completedAt: number;
193
145
  readonly snapshot: WorkflowProgressSnapshot;
146
+ readonly state?: () => WorkflowRunState;
194
147
  }
195
148
 
196
149
  export interface ActiveWorkflowInspection {
@@ -198,6 +151,7 @@ export interface ActiveWorkflowInspection {
198
151
  readonly args: string;
199
152
  readonly startedAt: number;
200
153
  readonly snapshot: () => WorkflowProgressSnapshot;
154
+ readonly state?: () => WorkflowRunState;
201
155
  }
202
156
 
203
157
  interface SessionWorkflowInspections {
@@ -206,22 +160,13 @@ interface SessionWorkflowInspections {
206
160
  }
207
161
 
208
162
  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;
214
- }
215
163
 
216
164
  export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
217
165
  if (!ctx.hasUI || ctx.mode !== "tui") {
218
166
  ctx.ui.notify(formatWorkflowInspection(inspection), "info");
219
167
  return;
220
168
  }
221
- await ctx.ui.custom<void>(
222
- (tui, theme, _keybindings, done) => new WorkflowInspector(() => workflowInspectionSnapshot(inspection), tui, theme, () => done(undefined)),
223
- WORKFLOW_VIEWER_OVERLAY_OPTIONS,
224
- );
169
+ await showWorkflowInspector(ctx.ui, () => workflowInspectionSnapshot(inspection), () => ({ state: inspection.state?.() }));
225
170
  }
226
171
 
227
172
  function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
@@ -244,190 +189,7 @@ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionCo
244
189
  }
245
190
 
246
191
  function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
247
- return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
248
- }
249
-
250
- export interface WorkflowInvocation {
251
- name: string;
252
- args: string;
253
- options: WorkflowRunOptions;
254
- refreshDiscovery?: boolean;
255
- optionErrors?: string[];
256
- authorBrief?: string;
257
- }
258
-
259
- export function parseWorkflowInvocation(input: string): WorkflowInvocation {
260
- const trimmed = input.trim();
261
- const space = trimmed.indexOf(" ");
262
- const name = space === -1 ? trimmed : trimmed.slice(0, space);
263
- const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
264
- const { args, options, refreshDiscovery, optionErrors } = parseWorkflowOptions(rest);
265
- const invocation: WorkflowInvocation = { name, args, options };
266
- if (refreshDiscovery) invocation.refreshDiscovery = refreshDiscovery;
267
- if (optionErrors) invocation.optionErrors = optionErrors;
268
- return invocation;
269
- }
270
-
271
- const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
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>";
277
-
278
- function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
279
- const tokens = input.split(/\s+/).filter(Boolean);
280
- const kept: string[] = [];
281
- const options: WorkflowRunOptions = {};
282
- const optionErrors: string[] = [];
283
- let refreshDiscovery = false;
284
- for (let i = 0; i < tokens.length; i++) {
285
- const token = tokens[i];
286
- if (token === "--inspect") {
287
- options.inspect = true;
288
- continue;
289
- }
290
- if (token === "--refresh") {
291
- refreshDiscovery = true;
292
- continue;
293
- }
294
- if (token === "--perf") {
295
- options.perf = true;
296
- continue;
297
- }
298
- if (token === "--resume-edited") {
299
- options.resumeEditedWorkflow = true;
300
- continue;
301
- }
302
- if (token === "--result-viewer" || token === "--review-viewer") {
303
- options.resultViewer = "open";
304
- continue;
305
- }
306
- if (token === "--no-result-viewer" || token === "--no-review-viewer") {
307
- options.resultViewer = "skip";
308
- continue;
309
- }
310
- if (token.startsWith("--concurrency=")) {
311
- options.concurrency = parseNumericOption(token.slice("--concurrency=".length));
312
- continue;
313
- }
314
- if (token === "--concurrency") {
315
- const next = tokens[i + 1];
316
- options.concurrency = parseNumericOption(next);
317
- if (next !== undefined) i++;
318
- continue;
319
- }
320
- if (token.startsWith("--parallel-limit=")) {
321
- options.parallelSubmissionLimit = parseNumericOption(token.slice("--parallel-limit=".length));
322
- continue;
323
- }
324
- if (token === "--parallel-limit") {
325
- const next = tokens[i + 1];
326
- options.parallelSubmissionLimit = parseNumericOption(next);
327
- if (next !== undefined) i++;
328
- continue;
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
- }
378
- if (token.startsWith("--budget=")) {
379
- const parsed = parseBudgetOption(token.slice("--budget=".length));
380
- if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
381
- else options.budget = parsed;
382
- continue;
383
- }
384
- if (token === "--budget") {
385
- const next = tokens[i + 1];
386
- const parsed = next === undefined ? undefined : parseBudgetOption(next);
387
- if (parsed === undefined) {
388
- optionErrors.push(INVALID_BUDGET_OPTION);
389
- } else {
390
- options.budget = parsed;
391
- i++;
392
- }
393
- continue;
394
- }
395
- if (token.startsWith("--resume=")) {
396
- const value = token.slice("--resume=".length).trim();
397
- if (value === "") optionErrors.push(INVALID_RESUME_OPTION);
398
- else options.resumeFromRunId = value;
399
- continue;
400
- }
401
- if (token === "--resume") {
402
- const next = tokens[i + 1];
403
- if (next === undefined || next.startsWith("--")) {
404
- optionErrors.push(INVALID_RESUME_OPTION);
405
- } else {
406
- options.resumeFromRunId = next;
407
- i++;
408
- }
409
- continue;
410
- }
411
- kept.push(token);
412
- }
413
- if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
414
- return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
415
- }
416
-
417
- function parseBudgetOption(value: string): number | undefined {
418
- return parseWorkflowBudgetString(value);
419
- }
420
-
421
- function parseNumericOption(value: string | undefined): number | undefined {
422
- if (value === undefined) return undefined;
423
- const parsed = Number(value);
424
- return Number.isFinite(parsed) ? parsed : undefined;
425
- }
426
-
427
- function compactInlinePreview(script: string | undefined): string {
428
- if (!script) return "";
429
- const compact = script.replace(/\s+/g, " ").trim();
430
- return compact.length > 60 ? `${compact.slice(0, 57)}…` : compact;
192
+ return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot(), state: source.state };
431
193
  }
432
194
 
433
195
  export function buildTemporaryWorkflowAuthorPrompt(brief: string): string {
@@ -436,15 +198,7 @@ export function buildTemporaryWorkflowAuthorPrompt(brief: string): string {
436
198
  User brief:
437
199
  ${brief.trim()}
438
200
 
439
- Use the workflow tool with a script argument, not a saved workflow name.
440
- The script must start with export const meta = { ... } and default-export an async workflow function.
441
- Use the injected Type object for schemas. Do not import anything or use dynamic import().
442
- Set profile to "small", "medium", or "big" on each agent() call; use explicit model/thinkingLevel only for an intentional override.
443
- Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
444
- If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
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.
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}
201
+ Use the workflow tool with a script argument, not a saved workflow name, and follow its guidelines for inline scripts.
448
202
  Do not edit files unless the user explicitly requested edits.`;
449
203
  }
450
204
 
@@ -460,9 +214,17 @@ export type WorkflowToolRequest =
460
214
  | { readonly kind: "inline"; readonly script: string }
461
215
  | { readonly kind: "error"; readonly error: "invalid_workflow_invocation"; readonly message: string };
462
216
 
463
- export interface WorkflowToolErrorResult {
217
+ export interface WorkflowToolErrorResult<TDetails = WorkflowToolErrorDetails> {
464
218
  readonly content: Array<{ readonly type: "text"; readonly text: string }>;
465
- readonly details: { readonly error: "invalid_workflow_invocation" } | { readonly error: "inline_compile_error"; readonly message: string };
219
+ readonly details: TDetails;
220
+ /** The model sees a failed call while the renderer still reads `details`. */
221
+ readonly isError: true;
222
+ }
223
+
224
+ type WorkflowToolErrorDetails = { readonly error: "invalid_workflow_invocation" } | { readonly error: "inline_compile_error"; readonly message: string };
225
+
226
+ function workflowToolError<TDetails>(text: string, details: TDetails): WorkflowToolErrorResult<TDetails> {
227
+ return { content: [{ type: "text", text }], details, isError: true };
466
228
  }
467
229
 
468
230
  const INVALID_WORKFLOW_INVOCATION_MESSAGE = "Provide exactly one workflow name or inline workflow script.";
@@ -477,34 +239,37 @@ export function normalizeWorkflowToolRequest(params: WorkflowToolRequestParams):
477
239
  }
478
240
 
479
241
  export function invalidWorkflowInvocationResult(): WorkflowToolErrorResult {
480
- return { content: [{ type: "text", text: INVALID_WORKFLOW_INVOCATION_MESSAGE }], details: { error: "invalid_workflow_invocation" } };
242
+ return workflowToolError(INVALID_WORKFLOW_INVOCATION_MESSAGE, { error: "invalid_workflow_invocation" });
481
243
  }
482
244
 
483
245
  export function inlineCompileErrorResult(message: string): WorkflowToolErrorResult {
484
- return { content: [{ type: "text", text: `Inline workflow did not compile: ${message}` }], details: { error: "inline_compile_error", message } };
246
+ return workflowToolError(`Inline workflow did not compile: ${message}`, { error: "inline_compile_error", message });
485
247
  }
486
248
 
249
+ export type WorkflowPickerSelection =
250
+ | { readonly kind: "run"; readonly name: string; readonly args: string }
251
+ | { readonly kind: "author"; readonly brief: string };
252
+
487
253
  export async function pickWorkflow(
488
254
  workflows: ReadonlyMap<string, WorkflowModule>,
489
255
  ctx: ExtensionCommandContext,
490
- ): Promise<WorkflowInvocation | undefined> {
491
- const choice = await selectWorkflowValue(workflows, ctx);
492
- if (!choice) return undefined;
256
+ ): Promise<WorkflowPickerSelection | undefined> {
257
+ const name = await selectWorkflowValue(workflows, ctx);
258
+ if (!name) return undefined;
493
259
 
494
- if (choice === AUTHOR_TEMP_WORKFLOW_VALUE || choice === AUTHOR_TEMP_WORKFLOW_LABEL) {
260
+ if (name === AUTHOR_TEMP_WORKFLOW_VALUE) {
495
261
  const brief = await ctx.ui.editor(
496
262
  "Describe temporary workflow",
497
263
  "Goal:\n\nAgents to run:\n- \n\nFinal output should include:\n- summary\n- findings\n- next steps\n",
498
264
  );
499
265
  const trimmed = brief?.trim();
500
- if (!trimmed) return undefined;
501
- return { name: "", args: "", options: {}, authorBrief: trimmed };
266
+ return trimmed ? { kind: "author", brief: trimmed } : undefined;
502
267
  }
503
268
 
504
- const separator = choice.indexOf(" — ");
505
- const name = separator === -1 ? choice : choice.slice(0, separator);
506
- const args = name === "code-review" ? (await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff"))?.trim() ?? "" : "";
507
- return { name, args, options: {} };
269
+ if (name !== "code-review") return { kind: "run", name, args: "" };
270
+ // Escape resolves undefined and cancels; only a submitted blank target means auto-detect.
271
+ const target = await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff");
272
+ return target === undefined ? undefined : { kind: "run", name, args: target.trim() };
508
273
  }
509
274
 
510
275
  export async function sendWorkflowResult(
@@ -513,36 +278,14 @@ export async function sendWorkflowResult(
513
278
  name: string,
514
279
  mod: LoadedWorkflow,
515
280
  args: string,
516
- options: WorkflowRunOptions,
281
+ options: ResolvedWorkflowRunOptions,
517
282
  perfRecorder?: PerfSink,
518
283
  reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
519
284
  ): 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,
541
- ): Promise<void> {
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);
285
+ const envelope = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
286
+ const retained = reviewSessions.remember(ctx, envelope, options);
287
+ publishWorkflowResult(pi, envelope);
288
+ await reviewSessions.present(ctx, retained, options);
546
289
  }
547
290
 
548
291
  async function executeResolvedWorkflow(
@@ -553,7 +296,7 @@ async function executeResolvedWorkflow(
553
296
  args: string,
554
297
  options: ResolvedWorkflowRunOptions,
555
298
  perfRecorder?: PerfSink,
556
- ): Promise<WorkflowExecution> {
299
+ ): Promise<WorkflowResultEnvelope> {
557
300
  const { runResolvedWorkflow } = await loadEngine();
558
301
  let liveInspection: ActiveWorkflowInspection | undefined;
559
302
  const inspections = workflowInspectionState(pi, ctx);
@@ -570,35 +313,25 @@ async function executeResolvedWorkflow(
570
313
  if (source) {
571
314
  liveInspection = bindActiveWorkflowInspection(name, args, source);
572
315
  inspections.active = liveInspection;
573
- latestActiveWorkflowInspection = liveInspection;
574
316
  } else if (inspections.active === liveInspection) {
575
317
  inspections.active = undefined;
576
- if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
577
318
  liveInspection = undefined;
578
319
  }
579
320
  },
580
321
  onProgressSnapshot(snapshot) {
581
- const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
582
- inspections.last = completed;
583
- latestWorkflowInspection = completed;
322
+ // The durable state settles after this final snapshot; keep reading it from the live source.
323
+ inspections.last = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot, state: liveInspection?.state };
584
324
  },
585
325
  });
586
326
  }
587
327
 
588
- function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
589
- const { name } = execution.envelope;
328
+ function publishWorkflowResult(pi: ExtensionAPI, envelope: WorkflowResultEnvelope): void {
590
329
  pi.sendMessage(
591
330
  {
592
331
  customType: "workflow-result",
593
- content: formatMessageContent(
594
- name,
595
- execution.envelope.result,
596
- execution.envelope.usage,
597
- execution.envelope.perf,
598
- execution.metadata,
599
- ),
332
+ content: formatWorkflowResultForContext(envelope),
600
333
  display: true,
601
- details: execution.envelope,
334
+ details: envelope,
602
335
  },
603
336
  { triggerTurn: false },
604
337
  );
@@ -610,7 +343,7 @@ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordina
610
343
  const perfRecorder = await createInvocationPerf(options);
611
344
  return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
612
345
  },
613
- publish: (execution) => sendWorkflowExecution(pi, execution),
346
+ publish: (envelope) => publishWorkflowResult(pi, envelope),
614
347
  });
615
348
  }
616
349
 
@@ -625,12 +358,12 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
625
358
  },
626
359
  async execute(ctx, name, workflow, options) {
627
360
  const perfRecorder = await createInvocationPerf(options);
628
- const execution = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
629
- reviewSessions.remember(ctx, execution, options);
361
+ const envelope = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
362
+ reviewSessions.remember(ctx, envelope, options);
630
363
  },
631
364
  });
632
365
  backgroundWorkflows.onRunSettled((ctx, runId) => workflowRuns.runSettled(ctx, runId));
633
- registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
366
+ const dynamax = registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
634
367
  registerWorkflowModelProfileCommand(pi);
635
368
  registerWorkflowRunCommand(pi, workflowRuns);
636
369
  pi.on("session_start", async (_event, ctx) => {
@@ -658,13 +391,8 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
658
391
 
659
392
  pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
660
393
  const details = message.details;
661
- if (isWorkflowResult(details)) {
662
- return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
663
- runId: details.runId,
664
- resumedFromRunId: details.resumedFromRunId,
665
- }, details.perf);
666
- }
667
- return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
394
+ const view = isWorkflowResult(details) ? details : { name: "workflow", result: details ?? message.content };
395
+ return renderWorkflowResult(view, expanded, theme, workflowResultHints(shortcuts));
668
396
  });
669
397
 
670
398
  pi.registerCommand("workflow:inspector", {
@@ -697,40 +425,54 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
697
425
  description: "Run a multi-agent workflow: /workflow <name> [args]",
698
426
  getArgumentCompletions: workflowArgumentCompletions,
699
427
  handler: async (args: string, ctx: ExtensionCommandContext) => {
700
- const direct = parseWorkflowInvocation(args);
701
- if (direct.optionErrors?.length) {
702
- ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
428
+ const invocation = parseWorkflowInvocation(args);
429
+ if (invocation.optionErrors.length > 0) {
430
+ ctx.ui.notify(`Invalid workflow option: ${invocation.optionErrors.join("; ")}`, "warning");
703
431
  return;
704
432
  }
705
- const directOptions = resolveWorkflowRunOptions(direct.options);
706
- const perfRecorder = await createInvocationPerf(directOptions);
433
+ const options = resolveWorkflowRunOptions(invocation.options);
434
+ const perfRecorder = await createInvocationPerf(options);
707
435
  const { discoverWorkflows } = await loadDiscovery();
708
- const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
436
+ const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: invocation.refreshDiscovery, perf: perfRecorder });
709
437
  const available = [...workflows.keys()].join(", ") || "(none)";
710
- const invocation = direct.name ? direct : ctx.hasUI ? await pickWorkflow(workflows, ctx) : undefined;
711
-
712
- if (!invocation) {
438
+ if (!invocation.name && !ctx.hasUI) {
713
439
  ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
714
440
  return;
715
441
  }
716
-
717
- if (invocation.authorBrief) {
718
- pi.sendUserMessage(buildTemporaryWorkflowAuthorPrompt(invocation.authorBrief));
442
+ const selection: WorkflowPickerSelection | undefined = invocation.name
443
+ ? { kind: "run", name: invocation.name, args: invocation.args }
444
+ : await pickWorkflow(workflows, ctx);
445
+ // Dismissing the picker or one of its follow-up prompts is a deliberate cancel, not a usage error.
446
+ if (!selection) return;
447
+
448
+ if (selection.kind === "author") {
449
+ // A send while the agent streams is rejected, which would leave the one-shot armed for an unrelated prompt.
450
+ await ctx.waitForIdle();
451
+ dynamax.markOneShot(ctx);
452
+ pi.sendUserMessage(buildTemporaryWorkflowAuthorPrompt(selection.brief));
719
453
  return;
720
454
  }
721
455
 
722
- const mod = workflows.get(invocation.name);
456
+ const mod = workflows.get(selection.name);
723
457
  if (!mod) {
724
- ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
458
+ ctx.ui.notify(`Unknown workflow "${selection.name}". Available: ${available}`, "error");
725
459
  return;
726
460
  }
727
461
 
728
- const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
729
- await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
462
+ await sendWorkflowResult(pi, ctx, selection.name, mod, selection.args, options, perfRecorder, reviewSessions);
730
463
  },
731
464
  });
732
465
 
733
- registerWorkflowTool(pi, reviewSessions, backgroundWorkflows);
466
+ registerWorkflowTool(pi, reviewSessions, backgroundWorkflows, shortcuts);
467
+ }
468
+
469
+ /** Result footers advertise the configured triage and inspector shortcuts and pi's current expand key, read at render time. */
470
+ function workflowResultHints(shortcuts: DynamaxShortcuts): WorkflowResultHints {
471
+ return {
472
+ triage: shortcuts.results ?? "/workflow:results",
473
+ inspect: shortcuts.inspector ?? "/workflow:inspector",
474
+ expand: keyText("app.tools.expand") || undefined,
475
+ };
734
476
  }
735
477
 
736
478
  /** Register the host-facing workflow tool independently from command and lifecycle surfaces. */
@@ -738,13 +480,18 @@ function registerWorkflowTool(
738
480
  pi: ExtensionAPI,
739
481
  reviewSessions: ReviewSessionCoordinator,
740
482
  backgroundWorkflows: BackgroundWorkflowCoordinator,
483
+ shortcuts: DynamaxShortcuts,
741
484
  ): void {
742
485
  pi.registerTool({
743
- name: "workflow",
486
+ name: WORKFLOW_TOOL_NAME,
744
487
  label: "Workflow",
745
488
  description:
746
489
  "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.",
747
490
  promptSnippet: "Run an existing named workflow or an inline one-off workflow script",
491
+ // Like codemode itself, an orchestrating tool stays a top-level model action: from a codemode script a
492
+ // run would lose its tool row, hit the script's output cap, and parallel runs would each get their own
493
+ // concurrency cap. It also stays declared under `codemode.mode: "only"`, which hides direct tools.
494
+ exposure: "model-only",
748
495
  promptGuidelines: [
749
496
  "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.",
750
497
  "Use workflow with `name` for existing registered workflows such as code-review, diagnose, refactor-scout, or perf-review.",
@@ -752,9 +499,13 @@ function registerWorkflowTool(
752
499
  "Inline workflow scripts must use the injected `Type` object for schemas and must not contain imports or dynamic import().",
753
500
  "Inline scripts may compose registered workflows in-process via `api.workflow(\"<name>\", args)` (e.g. `await api.workflow(\"code-review\", \"HEAD~3\")`); it returns the sub-workflow's result and nests one level only.",
754
501
  "Subagents receive no skills by default. In inline workflows, pass `skills: [\"skill-name\"]` per `agent()` call when the user asks for a skill or a stage should use one; grant only the needed skills.",
502
+ "Set `profile: \"small\"`, `\"medium\"`, or `\"big\"` on every `api.agent()` call so the user's central model routes apply; use `model`/`thinkingLevel` only for an intentional override.",
755
503
  "Always pass a plain string as the first `api.agent()` argument; build prompts with template strings before calling agent().",
756
504
  "When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
757
505
  "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.",
506
+ "Add \"codemode\" to an inline agent's `tools` when it should batch or filter many tool calls in one script; its scripts can call only that agent's other allowed tools.",
507
+ "`api.classify({ state, questions }, { label })` answers typed choice/score/bool questions with a classifier model and no chat session; use it for cheap gates such as dedup or triage, and handle its rejection when no classifier is configured.",
508
+ "Host MCP tools reach subagents only in synchronous workflow tool runs: name them in `tools` (e.g. `mcp__server__tool`), or use `toolHints: [\"external-search\"]` to pick up MCP web research tools.",
758
509
  "`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
510
  ADAPTIVE_WORKFLOW_GUIDANCE,
760
511
  "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.",
@@ -780,7 +531,7 @@ function registerWorkflowTool(
780
531
  ),
781
532
  agentRetries: Type.Optional(
782
533
  Type.Integer({
783
- description: `Retries per agent for classified transient provider failures; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
534
+ description: `Whole-agent restarts after a transient provider failure that pi's in-session turn retry (the user's pi retry settings) could not recover; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
784
535
  }),
785
536
  ),
786
537
  autoResumeOnUsageLimit: Type.Optional(
@@ -815,43 +566,20 @@ function registerWorkflowTool(
815
566
  background: Type.Optional(Type.Boolean({ description: "Return a durable run ID immediately and deliver completion to this conversation later" })),
816
567
  }),
817
568
  renderCall(args, theme) {
818
- const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
819
- const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
820
- if (args.name?.trim()) {
821
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${background}${suffix}`, 0, 0);
822
- }
823
- const preview = compactInlinePreview(args.script);
824
- const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
825
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${background}${suffix}${previewSuffix}`, 0, 0);
569
+ return renderWorkflowToolCall(args, theme);
826
570
  },
827
- renderResult(result, { expanded, isPartial }, theme) {
828
- if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
829
- const details = result.details;
830
- if (isWorkflowResult(details)) {
831
- return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
832
- runId: details.runId,
833
- resumedFromRunId: details.resumedFromRunId,
834
- }, details.perf);
835
- }
836
- const first = result.content[0];
837
- const text = first?.type === "text" ? first.text : "Workflow finished.";
838
- return new Text(theme.fg("muted", text), 0, 0);
571
+ renderResult(result, options, theme, context) {
572
+ return renderWorkflowToolResult(result, options, theme, context.args, workflowResultHints(shortcuts));
839
573
  },
840
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
574
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
841
575
  const request = normalizeWorkflowToolRequest(params);
842
576
  if (request.kind === "error") return invalidWorkflowInvocationResult();
843
577
  const resumeFromRunId = params.resumeFromRunId?.trim();
844
578
  if (params.resumeFromRunId !== undefined && resumeFromRunId === "") {
845
- return {
846
- content: [{ type: "text", text: "resumeFromRunId must be non-empty." }],
847
- details: { error: "invalid_resume_from_run_id" },
848
- };
579
+ return workflowToolError("resumeFromRunId must be non-empty.", { error: "invalid_resume_from_run_id" });
849
580
  }
850
581
  if (params.resumeEditedWorkflow && !resumeFromRunId) {
851
- return {
852
- content: [{ type: "text", text: "resumeEditedWorkflow requires resumeFromRunId." }],
853
- details: { error: "invalid_edited_workflow_resume" },
854
- };
582
+ return workflowToolError("resumeEditedWorkflow requires resumeFromRunId.", { error: "invalid_edited_workflow_resume" });
855
583
  }
856
584
  if (params.background) {
857
585
  const unavailable = backgroundUnavailableResult(ctx.mode);
@@ -859,7 +587,6 @@ function registerWorkflowTool(
859
587
  }
860
588
 
861
589
  const runOptions = resolveWorkflowRunOptions({
862
- inspect: ctx.hasUI && ctx.mode === "tui",
863
590
  concurrency: params.concurrency,
864
591
  parallelSubmissionLimit: params.parallelSubmissionLimit,
865
592
  maxAgents: params.maxAgents,
@@ -884,10 +611,7 @@ function registerWorkflowTool(
884
611
  const named = workflows.get(request.name);
885
612
  if (!named) {
886
613
  const available = [...workflows.keys()].join(", ") || "(none)";
887
- return {
888
- content: [{ type: "text", text: `Unknown workflow "${request.name}". Available: ${available}` }],
889
- details: { error: "unknown_workflow", available },
890
- };
614
+ return workflowToolError(`Unknown workflow "${request.name}". Available: ${available}`, { error: "unknown_workflow", available });
891
615
  }
892
616
  mod = named;
893
617
  resultName = request.name;
@@ -910,7 +634,7 @@ function registerWorkflowTool(
910
634
  name: resultName,
911
635
  options: runOptions,
912
636
  async execute(backgroundCtx, backgroundOptions) {
913
- const execution = await executeResolvedWorkflow(
637
+ const envelope = await executeResolvedWorkflow(
914
638
  pi,
915
639
  backgroundCtx,
916
640
  resultName,
@@ -919,24 +643,36 @@ function registerWorkflowTool(
919
643
  backgroundOptions,
920
644
  perfRecorder,
921
645
  );
922
- reviewSessions.remember(ctx, execution, backgroundOptions);
646
+ reviewSessions.remember(ctx, envelope, backgroundOptions);
923
647
  },
924
648
  });
925
649
  }
926
- const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
927
- reviewSessions.remember(ctx, execution, runOptions);
650
+ // Marks the tool row as running (rendered `● running <name>`) until the envelope replaces it.
651
+ onUpdate?.({ content: [{ type: "text", text: `Running workflow ${resultName}.` }], details: { state: "running", name: resultName } });
652
+ // Only this synchronous path may bridge host MCP tools: executeTool ends with the tool call.
653
+ const hostTools = createHostToolBridge(() => pi.getAllTools(), ctx);
654
+ // Keep the latest usage: a failed run still reports what its agents spent instead of dropping it with the error.
655
+ let spent: WorkflowUsageSnapshot | undefined;
656
+ const onUsageSnapshot = (snapshot: WorkflowUsageSnapshot) => {
657
+ spent = snapshot;
658
+ return runOptions.onUsageSnapshot?.(snapshot);
659
+ };
660
+ let envelope: WorkflowResultEnvelope;
661
+ try {
662
+ envelope = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, { ...runOptions, hostTools, onUsageSnapshot }, perfRecorder);
663
+ } catch (error) {
664
+ const message = unknownErrorMessage(error);
665
+ return {
666
+ ...workflowToolError(`Workflow ${resultName} failed: ${message}`, { error: "workflow_failed", name: resultName, message }),
667
+ usage: workflowUsageForPi(spent),
668
+ };
669
+ }
670
+ reviewSessions.remember(ctx, envelope, runOptions);
928
671
  return {
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,
672
+ content: [{ type: "text", text: formatWorkflowResultForContext(envelope) }],
673
+ details: envelope,
674
+ // pi counts tool-result usage toward the session cost; subagent spend was invisible there before.
675
+ usage: workflowUsageForPi(envelope.usage),
940
676
  };
941
677
  },
942
678
  });