@yycholla/pi-dynamic-workflows 3.3.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 (179) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +325 -0
  3. package/assets/readme/hero.png +0 -0
  4. package/assets/readme/hero.svg +70 -0
  5. package/assets/readme/package-cover.png +0 -0
  6. package/assets/readme/package-cover.svg +66 -0
  7. package/assets/readme/workflow.png +0 -0
  8. package/assets/readme/workflow.svg +77 -0
  9. package/dist/accept-workflow-guidance.d.ts +14 -0
  10. package/dist/accept-workflow-guidance.js +53 -0
  11. package/dist/adversarial-review.d.ts +30 -0
  12. package/dist/adversarial-review.js +107 -0
  13. package/dist/agent-history.d.ts +20 -0
  14. package/dist/agent-history.js +122 -0
  15. package/dist/agent-registry.d.ts +83 -0
  16. package/dist/agent-registry.js +190 -0
  17. package/dist/agent.d.ts +364 -0
  18. package/dist/agent.js +714 -0
  19. package/dist/builtin-commands.d.ts +19 -0
  20. package/dist/builtin-commands.js +251 -0
  21. package/dist/builtin-workflows.d.ts +45 -0
  22. package/dist/builtin-workflows.js +121 -0
  23. package/dist/code-review.d.ts +26 -0
  24. package/dist/code-review.js +181 -0
  25. package/dist/config.d.ts +37 -0
  26. package/dist/config.js +44 -0
  27. package/dist/deep-research.d.ts +30 -0
  28. package/dist/deep-research.js +124 -0
  29. package/dist/display.d.ts +134 -0
  30. package/dist/display.js +248 -0
  31. package/dist/effort-command.d.ts +28 -0
  32. package/dist/effort-command.js +68 -0
  33. package/dist/enums.d.ts +69 -0
  34. package/dist/enums.js +78 -0
  35. package/dist/errors.d.ts +113 -0
  36. package/dist/errors.js +140 -0
  37. package/dist/extension-reload.d.ts +37 -0
  38. package/dist/extension-reload.js +78 -0
  39. package/dist/fs-persistence.d.ts +63 -0
  40. package/dist/fs-persistence.js +102 -0
  41. package/dist/index.d.ts +57 -0
  42. package/dist/index.js +35 -0
  43. package/dist/logger.d.ts +21 -0
  44. package/dist/logger.js +66 -0
  45. package/dist/model-routing.d.ts +30 -0
  46. package/dist/model-routing.js +50 -0
  47. package/dist/model-spec.d.ts +29 -0
  48. package/dist/model-spec.js +252 -0
  49. package/dist/model-tier-config.d.ts +133 -0
  50. package/dist/model-tier-config.js +249 -0
  51. package/dist/run-persistence.d.ts +180 -0
  52. package/dist/run-persistence.js +294 -0
  53. package/dist/saved-commands.d.ts +28 -0
  54. package/dist/saved-commands.js +100 -0
  55. package/dist/shared-store.d.ts +98 -0
  56. package/dist/shared-store.js +212 -0
  57. package/dist/structured-output.d.ts +19 -0
  58. package/dist/structured-output.js +30 -0
  59. package/dist/task-panel.d.ts +61 -0
  60. package/dist/task-panel.js +422 -0
  61. package/dist/usage-limit-scheduler.d.ts +145 -0
  62. package/dist/usage-limit-scheduler.js +368 -0
  63. package/dist/web-tools.d.ts +20 -0
  64. package/dist/web-tools.js +120 -0
  65. package/dist/workflow-authoring-coverage.d.ts +70 -0
  66. package/dist/workflow-authoring-coverage.js +421 -0
  67. package/dist/workflow-authoring-reference.d.ts +20 -0
  68. package/dist/workflow-authoring-reference.js +156 -0
  69. package/dist/workflow-capability-contract.d.ts +131 -0
  70. package/dist/workflow-capability-contract.js +604 -0
  71. package/dist/workflow-commands.d.ts +18 -0
  72. package/dist/workflow-commands.js +260 -0
  73. package/dist/workflow-comprehension.d.ts +133 -0
  74. package/dist/workflow-comprehension.js +1321 -0
  75. package/dist/workflow-context-measurement.d.ts +72 -0
  76. package/dist/workflow-context-measurement.js +213 -0
  77. package/dist/workflow-control-tool.d.ts +30 -0
  78. package/dist/workflow-control-tool.js +176 -0
  79. package/dist/workflow-delivery-choice.d.ts +20 -0
  80. package/dist/workflow-delivery-choice.js +48 -0
  81. package/dist/workflow-editor.d.ts +93 -0
  82. package/dist/workflow-editor.js +363 -0
  83. package/dist/workflow-manager.d.ts +492 -0
  84. package/dist/workflow-manager.js +1124 -0
  85. package/dist/workflow-paths.d.ts +22 -0
  86. package/dist/workflow-paths.js +46 -0
  87. package/dist/workflow-release-gate.d.ts +39 -0
  88. package/dist/workflow-release-gate.js +309 -0
  89. package/dist/workflow-saved.d.ts +38 -0
  90. package/dist/workflow-saved.js +126 -0
  91. package/dist/workflow-settings.d.ts +70 -0
  92. package/dist/workflow-settings.js +131 -0
  93. package/dist/workflow-tool.d.ts +71 -0
  94. package/dist/workflow-tool.js +367 -0
  95. package/dist/workflow-ui.d.ts +182 -0
  96. package/dist/workflow-ui.js +1587 -0
  97. package/dist/workflow.d.ts +333 -0
  98. package/dist/workflow.js +1151 -0
  99. package/dist/workflows-models-command.d.ts +31 -0
  100. package/dist/workflows-models-command.js +156 -0
  101. package/dist/worktree.d.ts +25 -0
  102. package/dist/worktree.js +61 -0
  103. package/extensions/workflow.ts +151 -0
  104. package/package.json +104 -0
  105. package/skills/workflow-authoring/SKILL.md +30 -0
  106. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  107. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  108. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  109. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  110. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  111. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  112. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  113. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  114. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  115. package/skills/workflow-authoring/examples/tournament.js +81 -0
  116. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  117. package/skills/workflow-authoring/references/capabilities.md +41 -0
  118. package/skills/workflow-authoring/references/capability-details.md +357 -0
  119. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  120. package/skills/workflow-authoring/references/debugging.md +27 -0
  121. package/skills/workflow-authoring/references/focused-recipes.md +11 -0
  122. package/skills/workflow-authoring/references/helpers.md +9 -0
  123. package/skills/workflow-authoring/references/lifecycle.md +33 -0
  124. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  125. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  126. package/skills/workflow-authoring/references/registry-ownership.md +15 -0
  127. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  128. package/skills/workflow-authoring/references/review.md +43 -0
  129. package/skills/workflow-authoring/references/runtime.md +27 -0
  130. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  131. package/skills/workflow-authoring/references/versions.md +13 -0
  132. package/skills/workflow-patterns/SKILL.md +51 -0
  133. package/src/accept-workflow-guidance.ts +71 -0
  134. package/src/adversarial-review.ts +120 -0
  135. package/src/agent-history.ts +157 -0
  136. package/src/agent-registry.ts +221 -0
  137. package/src/agent.ts +929 -0
  138. package/src/builtin-commands.ts +286 -0
  139. package/src/builtin-workflows.ts +155 -0
  140. package/src/code-review.ts +183 -0
  141. package/src/config.ts +55 -0
  142. package/src/deep-research.ts +135 -0
  143. package/src/display.ts +367 -0
  144. package/src/effort-command.ts +87 -0
  145. package/src/enums.ts +77 -0
  146. package/src/errors.ts +199 -0
  147. package/src/extension-reload.ts +100 -0
  148. package/src/fs-persistence.ts +124 -0
  149. package/src/index.ts +176 -0
  150. package/src/logger.ts +88 -0
  151. package/src/model-routing.ts +73 -0
  152. package/src/model-spec.ts +309 -0
  153. package/src/model-tier-config.ts +296 -0
  154. package/src/run-persistence.ts +484 -0
  155. package/src/saved-commands.ts +115 -0
  156. package/src/shared-store.ts +228 -0
  157. package/src/structured-output.ts +47 -0
  158. package/src/task-panel.ts +490 -0
  159. package/src/usage-limit-scheduler.ts +432 -0
  160. package/src/web-tools.ts +124 -0
  161. package/src/workflow-authoring-coverage.ts +486 -0
  162. package/src/workflow-authoring-reference.ts +186 -0
  163. package/src/workflow-capability-contract.ts +806 -0
  164. package/src/workflow-commands.ts +287 -0
  165. package/src/workflow-comprehension.ts +1673 -0
  166. package/src/workflow-context-measurement.ts +262 -0
  167. package/src/workflow-control-tool.ts +238 -0
  168. package/src/workflow-delivery-choice.ts +69 -0
  169. package/src/workflow-editor.ts +444 -0
  170. package/src/workflow-manager.ts +1405 -0
  171. package/src/workflow-paths.ts +63 -0
  172. package/src/workflow-release-gate.ts +529 -0
  173. package/src/workflow-saved.ts +180 -0
  174. package/src/workflow-settings.ts +194 -0
  175. package/src/workflow-tool.ts +464 -0
  176. package/src/workflow-ui.ts +1789 -0
  177. package/src/workflow.ts +1615 -0
  178. package/src/workflows-models-command.ts +211 -0
  179. package/src/worktree.ts +76 -0
@@ -0,0 +1,464 @@
1
+ import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { Type } from "typebox";
4
+ import { BUILTIN_WORKFLOW_NAMES, resolveWorkflowInvocation } from "./builtin-workflows.js";
5
+ import {
6
+ createToolUpdateWorkflowDisplay,
7
+ createWorkflowSnapshot,
8
+ fmtCost,
9
+ fmtFull,
10
+ fmtTokenSegment,
11
+ recomputeWorkflowSnapshot,
12
+ renderWorkflowText,
13
+ tokenFigures,
14
+ type WorkflowSnapshot,
15
+ } from "./display.js";
16
+ import { WorkflowError, WorkflowErrorCode } from "./errors.js";
17
+ import { parseWorkflowScript, type WorkflowRunResult } from "./workflow.js";
18
+ import { WorkflowManager } from "./workflow-manager.js";
19
+ import { createWorkflowStorage, type WorkflowStorage } from "./workflow-saved.js";
20
+ import { loadWorkflowSettings } from "./workflow-settings.js";
21
+
22
+ /** The single always-on gate that authorizes workflow use without forcing it. */
23
+ export const WORKFLOW_GATE_GUIDELINE =
24
+ "The `workflow` tool runs multi-agent orchestration — it fans decomposable work out across subagents, and fits tasks shaped like: repo-wide inspection, independent parallel research/checks, multi-perspective review, or fan-out/fan-in synthesis. ONLY call it when the user explicitly opts in — via the workflow trigger word, `/workflows run`, or their own words (e.g. 'run a workflow', 'fan this out', '并行审一遍'). For any other task — even one that would clearly benefit — do not call it; you may briefly offer it (with a rough cost) as an option instead.";
25
+
26
+ const workflowToolSchema = Type.Object({
27
+ script: Type.Optional(
28
+ Type.String({
29
+ description: [
30
+ "Raw JavaScript workflow script, with no Markdown fences. Required unless `name` is given.",
31
+ "First statement: export const meta = { name: 'short_snake_case', description: 'non-empty description' }. Add phases: [{ title: 'Phase' }] only when the workflow has named phases, and declare only phases it will use. With multiple phases, call phase('Exact Title') before each phase's work or set `phase` in the agent options.",
32
+ "Use `await workflow(savedName, childArgs)` to run a saved workflow inline; nesting is limited to one level and shares the parent run's concurrency, agent, and token limits.",
33
+ "Optional quality helpers include verify(), judgePanel(), loopUntilDry(), and completenessCheck().",
34
+ "Optional control helpers include retry() and gate(); budget exposes total, spent(), and remaining(), and phase('Name', { budget: N }) sets a phase token limit.",
35
+ "The optional `agentType` option selects a named user or project definition that can bind tools, a model, and role instructions; use it only when its name and purpose are provided in context. Its bound model overrides `tier`; an explicit `model` overrides both.",
36
+ "Use plain JavaScript only; imports, require(), filesystem modules, Date.now(), Math.random(), and new Date() are unavailable.",
37
+ "Use phase('Name'), agent(prompt, opts), parallel(arrayOfFunctions), pipeline(items, ...stages), log(message), args, cwd, process.cwd(), and budget. The workflow must call agent() at least once.",
38
+ "parallel() requires functions, not promises, and returns results in input order: await parallel(items.map(item => () => agent(...))).",
39
+ "pipeline(items, ...stages) runs stages sequentially for each item while items proceed concurrently; each stage receives (previousValue, originalItem, index).",
40
+ ].join(" "),
41
+ }),
42
+ ),
43
+ name: Type.Optional(
44
+ Type.String({
45
+ description:
46
+ "Run a saved or built-in workflow by name instead of `script`; its args go in `args`. " +
47
+ `Built-ins: ${BUILTIN_WORKFLOW_NAMES.join(", ")} — see the workflow-patterns skill for each one's args. ` +
48
+ "A same-named saved workflow wins. Not combinable with resumeFromRunId.",
49
+ }),
50
+ ),
51
+ args: Type.Optional(
52
+ Type.Any({ description: "Optional JSON value exposed to the workflow script as global `args`." }),
53
+ ),
54
+ background: Type.Optional(
55
+ Type.Boolean({
56
+ description:
57
+ "Run the workflow in the background. Default: true — the tool returns immediately with a run ID, the turn ends so the user isn't blocked, and the result is delivered back into the conversation when it finishes. Set to false only when you need the result inline in this same turn (the call will block until the workflow completes).",
58
+ }),
59
+ ),
60
+ maxAgents: Type.Optional(
61
+ Type.Number({
62
+ description:
63
+ "Maximum number of agents allowed in this run. Default: 1000; this is a safety ceiling, not a target. Set a lower limit for dynamic or exploratory fan-out, and reserve large fan-outs for explicit user intent.",
64
+ }),
65
+ ),
66
+ concurrency: Type.Optional(
67
+ Type.Number({
68
+ description:
69
+ "Maximum concurrent agents for this run. Clamped to the runtime maximum. Use when provider/transport stability matters.",
70
+ }),
71
+ ),
72
+ agentRetries: Type.Optional(
73
+ Type.Number({
74
+ description:
75
+ "Retry attempts for recoverable agent failures such as timeout, connection failure, or empty assistant output. Default 0 unless configured.",
76
+ }),
77
+ ),
78
+ agentTimeoutMs: Type.Optional(
79
+ Type.Number({
80
+ description:
81
+ "Timeout per agent in milliseconds. Omit to use configured `defaultAgentTimeoutMs`; without one, there is no hard timeout. Set only when the user asks to bound time.",
82
+ }),
83
+ ),
84
+ tokenBudget: Type.Optional(
85
+ Type.Number({
86
+ description:
87
+ "Soft pre-call token gate for the whole run. Once recorded spend reaches it, further agent() calls fail; concurrent in-flight work can overshoot. Omit to use configured `defaultTokenBudget`; without one, the run is unlimited. Set it only when the user asks to bound spend.",
88
+ }),
89
+ ),
90
+ resumeFromRunId: Type.Optional(
91
+ Type.String({
92
+ description: [
93
+ "Resume a prior run (this ID) with an edited `script` instead of starting a new run.",
94
+ "Unchanged agent() calls replay from that run's cache; the first changed/new call onward re-runs.",
95
+ "Calls match by position: keep earlier good calls identical and in order. Always background.",
96
+ ].join(" "),
97
+ }),
98
+ ),
99
+ });
100
+
101
+ export type WorkflowToolInput = {
102
+ script?: string;
103
+ name?: string;
104
+ args?: unknown;
105
+ background?: boolean;
106
+ maxAgents?: number;
107
+ concurrency?: number;
108
+ agentRetries?: number;
109
+ agentTimeoutMs?: number;
110
+ tokenBudget?: number;
111
+ resumeFromRunId?: string;
112
+ };
113
+
114
+ export interface WorkflowToolOptions {
115
+ cwd?: string;
116
+ concurrency?: number;
117
+ /** Shared manager so background runs are reachable from the `/workflows` command. */
118
+ manager?: WorkflowManager;
119
+ /** Shared saved-workflow storage. */
120
+ storage?: WorkflowStorage;
121
+ /** Default per-agent timeout for runs created by this tool. null means no hard timeout. */
122
+ defaultAgentTimeoutMs?: number | null;
123
+ /** Default max concurrent agents when no tool-level concurrency is passed. */
124
+ defaultConcurrency?: number;
125
+ /** Default retry attempts after recoverable agent failures. */
126
+ defaultAgentRetries?: number;
127
+ }
128
+
129
+ export function createWorkflowTool(options: WorkflowToolOptions = {}): ToolDefinition<typeof workflowToolSchema, any> {
130
+ const storage = options.storage ?? createWorkflowStorage(options.cwd ?? process.cwd());
131
+ const cwd = options.cwd ?? process.cwd();
132
+ const defaults = resolveWorkflowToolDefaults(options, cwd);
133
+ const manager =
134
+ options.manager ??
135
+ new WorkflowManager({
136
+ cwd: options.cwd,
137
+ concurrency: defaults.concurrency,
138
+ loadSavedWorkflow: (name: string) => storage.load(name)?.script,
139
+ defaultAgentTimeoutMs: defaults.agentTimeoutMs,
140
+ defaultAgentRetries: defaults.agentRetries,
141
+ });
142
+
143
+ return defineTool({
144
+ name: "workflow",
145
+ label: "Workflow",
146
+ description:
147
+ "Run a JavaScript workflow that delegates work to subagents with agent(), optionally composing calls with parallel() and pipeline().",
148
+ promptSnippet:
149
+ "Delegate substantive independent or staged work to subagents with a JavaScript workflow, optionally composing agent calls with parallel(), pipeline(), or both",
150
+ get promptGuidelines() {
151
+ return [WORKFLOW_GATE_GUIDELINE];
152
+ },
153
+ parameters: workflowToolSchema,
154
+ prepareArguments(args) {
155
+ return normalizeWorkflowToolArgs(args);
156
+ },
157
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
158
+ // `name` resolves through the same registry the built-in slash commands
159
+ // and saved-workflow commands use (see builtin-workflows.ts /
160
+ // workflow-saved.ts): a project/user saved workflow of that name wins on
161
+ // a collision, else one of the 5 curated built-in patterns. This lets the
162
+ // model reach a curated pattern by name instead of having to author an
163
+ // equivalent script from scratch (and, for patterns that need it, the
164
+ // right exec context — e.g. deep-research's web tools — travels with it).
165
+ let invocationTools: ToolDefinition[] | undefined;
166
+ let invocationToolset: string | undefined;
167
+ let script: string;
168
+ if (params.name) {
169
+ if (params.resumeFromRunId) {
170
+ throw new Error(
171
+ "workflow: `name` cannot be combined with `resumeFromRunId` — resume with an edited `script` instead.",
172
+ );
173
+ }
174
+ const resolved = resolveWorkflowInvocation(params.name, params.args, { storage, cwd });
175
+ if (!resolved) {
176
+ throw new Error(
177
+ `workflow: no saved or built-in workflow named "${params.name}". Built-in names: ${BUILTIN_WORKFLOW_NAMES.join(", ")}.`,
178
+ );
179
+ }
180
+ script = normalizeWorkflowScript(resolved.script);
181
+ invocationTools = resolved.tools;
182
+ invocationToolset = resolved.toolset;
183
+ } else {
184
+ if (!params.script) throw new Error("workflow requires either `script` or `name`");
185
+ script = normalizeWorkflowScript(params.script);
186
+ }
187
+ const parsed = parseWorkflowScript(script);
188
+
189
+ // Iteration / cached-prefix reuse: resume a prior run with THIS (edited)
190
+ // script instead of creating a brand-new run. Unchanged agent() calls
191
+ // replay from the prior run's journal; the first edited/new call and
192
+ // everything after it re-run live. Always background (the resumed run is
193
+ // detached and its result is delivered back into the conversation).
194
+ if (params.resumeFromRunId) {
195
+ const runId = params.resumeFromRunId;
196
+ const resumed = await manager.resume(runId, { script, args: params.args });
197
+ if (!resumed) {
198
+ throw new Error(resumeFailureText(manager, runId));
199
+ }
200
+ return {
201
+ content: [{ type: "text", text: resumedText(parsed.meta.name, runId) }],
202
+ details: { runId, background: true, resumedFrom: runId },
203
+ };
204
+ }
205
+
206
+ // checkpoint() reaches the human only on a UI-bearing foreground run; a
207
+ // background run is detached, so checkpoint() falls back to its headless
208
+ // default. Map a checkpoint to ctx.ui.confirm (a yes/no gate) when available.
209
+ const uiCtx = ctx as
210
+ | { hasUI?: boolean; ui?: { confirm?(title: string, message: string): Promise<boolean> } }
211
+ | undefined;
212
+ const uiConfirm = uiCtx?.hasUI ? uiCtx.ui?.confirm : undefined;
213
+ const confirm = uiConfirm
214
+ ? (promptText: string) => uiConfirm.call(uiCtx?.ui, "Workflow checkpoint", promptText)
215
+ : undefined;
216
+
217
+ // Background execution is the default: return immediately so the turn ends
218
+ // and the user isn't blocked. The result is delivered back into the
219
+ // conversation when the run finishes (see installResultDelivery). Only an
220
+ // explicit `background: false` blocks for the result inline.
221
+ if (params.background ?? true) {
222
+ const { runId } = manager.startInBackground(script, params.args, {
223
+ maxAgents: params.maxAgents,
224
+ concurrency: params.concurrency,
225
+ agentRetries: params.agentRetries,
226
+ agentTimeoutMs: params.agentTimeoutMs,
227
+ tokenBudget: params.tokenBudget,
228
+ tools: invocationTools,
229
+ toolset: invocationToolset,
230
+ });
231
+ return {
232
+ content: [{ type: "text", text: backgroundStartedText(parsed.meta.name, runId) }],
233
+ details: { runId, background: true },
234
+ };
235
+ }
236
+
237
+ // Synchronous execution (blocking) — but routed through the manager so the
238
+ // run shows up live in the /workflows navigator and the task panel while it
239
+ // runs, then stays in history afterwards. We still block on the result and
240
+ // return it inline, so the model gets the full output in the same turn.
241
+ let snapshot: WorkflowSnapshot = createWorkflowSnapshot(parsed.meta);
242
+ const display = createToolUpdateWorkflowDisplay(onUpdate, undefined, {
243
+ key: "workflow",
244
+ streamToolUpdates: true,
245
+ maxAgents: 4,
246
+ showResultPreviews: false,
247
+ });
248
+
249
+ let result: WorkflowRunResult;
250
+ try {
251
+ result = await manager.runSync(script, params.args, {
252
+ maxAgents: params.maxAgents,
253
+ concurrency: params.concurrency,
254
+ agentRetries: params.agentRetries,
255
+ agentTimeoutMs: params.agentTimeoutMs,
256
+ tokenBudget: params.tokenBudget,
257
+ tools: invocationTools,
258
+ toolset: invocationToolset,
259
+ confirm,
260
+ externalSignal: signal,
261
+ onProgress(live) {
262
+ snapshot = recomputeWorkflowSnapshot(live);
263
+ display.update(snapshot);
264
+ },
265
+ });
266
+ } catch (error) {
267
+ if (signal?.aborted || (error instanceof WorkflowError && error.code === WorkflowErrorCode.WORKFLOW_ABORTED)) {
268
+ for (const agent of snapshot.agents) {
269
+ if (agent.status === "running") {
270
+ agent.status = "skipped";
271
+ agent.error = "aborted";
272
+ }
273
+ }
274
+ snapshot = recomputeWorkflowSnapshot(snapshot);
275
+ display.complete(snapshot);
276
+ throw new Error("Workflow was aborted");
277
+ }
278
+ throw error;
279
+ }
280
+
281
+ if (result.agentCount === 0) {
282
+ throw new Error(
283
+ "workflow scripts must call agent() at least once; this workflow declared phases but did not run any subagents",
284
+ );
285
+ }
286
+
287
+ snapshot.result = result.result;
288
+ snapshot.durationMs = result.durationMs;
289
+ snapshot = recomputeWorkflowSnapshot(snapshot);
290
+ display.complete(snapshot);
291
+
292
+ // Format token usage (include cost when the provider reports it)
293
+ const tokenSegment = fmtTokenSegment(tokenFigures(result.tokenUsage), fmtFull);
294
+ const tokenInfo = tokenSegment
295
+ ? `\n\nToken usage: ${tokenSegment}${result.tokenUsage?.cost ? ` (${fmtCost(result.tokenUsage.cost)})` : ""}`
296
+ : "";
297
+
298
+ const formattedResult =
299
+ result.result !== undefined ? `\n\`\`\`json\n${JSON.stringify(result.result, null, 2)}\n\`\`\`` : "";
300
+
301
+ return {
302
+ content: [
303
+ {
304
+ type: "text",
305
+ text: `Workflow **${result.meta.name}** completed with **${result.agentCount}** agent(s).${tokenInfo}\n\n## Result${formattedResult}\n\n${reviseHint(result.runId)}`,
306
+ },
307
+ ],
308
+ details: {
309
+ ...snapshot,
310
+ meta: result.meta,
311
+ phases: result.phases,
312
+ logs: result.logs,
313
+ result: result.result,
314
+ durationMs: result.durationMs,
315
+ tokenUsage: result.tokenUsage,
316
+ runId: result.runId,
317
+ },
318
+ };
319
+ },
320
+ renderCall(_args, theme) {
321
+ return new Text(theme.fg("toolTitle", theme.bold("workflow")), 0, 0);
322
+ },
323
+ renderResult(result, { isPartial }, theme) {
324
+ const snapshot = result.details as WorkflowSnapshot | undefined;
325
+ if (snapshot?.name) {
326
+ return new Text(renderWorkflowText(snapshot, !isPartial), 0, 0);
327
+ }
328
+ // Fallback: strip markdown syntax so the TUI doesn't display raw asterisks/hashes.
329
+ // The `content` field is for the LLM (where markdown is preserved), but the TUI
330
+ // renderer (Text component) shows text literally — so we strip markdown here.
331
+ const text = result.content?.[0];
332
+ const raw = text?.type === "text" ? text.text : theme.fg("muted", "workflow");
333
+ const clean = raw
334
+ .replace(/\*\*/g, "")
335
+ .replace(/```[a-z]*\n/g, "")
336
+ .replace(/```/g, "")
337
+ .replace(/^##+\s*/gm, "")
338
+ .trim();
339
+ return new Text(clean || theme.fg("muted", "workflow"), 0, 0);
340
+ },
341
+ });
342
+ }
343
+
344
+ function resolveWorkflowToolDefaults(
345
+ options: WorkflowToolOptions,
346
+ cwd: string,
347
+ ): { agentTimeoutMs: number | null; concurrency?: number; agentRetries: number } {
348
+ const settings = loadWorkflowSettings({ cwd });
349
+ return {
350
+ agentTimeoutMs:
351
+ options.defaultAgentTimeoutMs !== undefined
352
+ ? options.defaultAgentTimeoutMs
353
+ : (settings.defaultAgentTimeoutMs ?? null),
354
+ concurrency: options.defaultConcurrency ?? options.concurrency ?? settings.defaultConcurrency,
355
+ agentRetries: options.defaultAgentRetries ?? settings.defaultAgentRetries ?? 0,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * The tool result returned when a workflow starts in the background. It both
361
+ * informs the model and tells it to reassure the user: the run continues on its
362
+ * own and the conversation will resume automatically when it finishes, so the
363
+ * user can just wait here (or go do something else).
364
+ */
365
+ export function backgroundStartedText(name: string, runId: string): string {
366
+ return [
367
+ `Workflow "${name}" started in the background.`,
368
+ `Run ID: ${runId}`,
369
+ "It keeps running on its own. When it finishes, the result is delivered back",
370
+ "here and the conversation continues automatically — the user does not need to",
371
+ "do anything. Tell the user they can simply wait here for it to finish (it will",
372
+ "resume the conversation by itself), or keep chatting / working on other things",
373
+ "in the meantime; either way the result will come back to this conversation.",
374
+ `They can also track or cancel it with /workflows status ${runId} or /workflows stop ${runId}.`,
375
+ reviseHint(runId),
376
+ ].join("\n");
377
+ }
378
+
379
+ /**
380
+ * One-line hint telling the model it can iterate on a finished/running run by
381
+ * resuming it with an edited script instead of re-running the whole workflow.
382
+ * Unchanged agent() calls replay from the journal (cache); only edited/new ones
383
+ * re-run. Omitted when there is no runId to reference.
384
+ */
385
+ export function reviseHint(runId: string | undefined): string {
386
+ if (!runId) return "";
387
+ return `To revise without re-running everything: re-call workflow with resumeFromRunId="${runId}" and an edited script — unchanged agent() calls replay from cache, only edited/new ones re-run.`;
388
+ }
389
+
390
+ /**
391
+ * The tool result returned when the model resumes a run with an edited script.
392
+ * The resumed run is always background, so its result is delivered back later.
393
+ */
394
+ export function resumedText(name: string, runId: string): string {
395
+ return [
396
+ `Workflow "${name}" resumed from run ${runId} with your edited script.`,
397
+ "Unchanged agent() calls replay from that run's journal (cache); the first",
398
+ "edited or newly inserted agent() call — and everything after it — re-runs live.",
399
+ "It runs in the background; the result is delivered back here when it finishes,",
400
+ "and the conversation continues automatically. The user can wait or keep working.",
401
+ `Track or cancel it with /workflows status ${runId} or /workflows stop ${runId}.`,
402
+ ].join("\n");
403
+ }
404
+
405
+ /**
406
+ * Explain why a resumeFromRunId could not be resumed, so the model gets a clear
407
+ * tool error instead of a silent failure. Inspects live + persisted state to
408
+ * name the concrete reason (not found / running / completed / stopped).
409
+ */
410
+ export function resumeFailureText(manager: WorkflowManager, runId: string): string {
411
+ const active = manager.getRun(runId);
412
+ if (active?.status === "running") {
413
+ return `Cannot resume workflow run "${runId}": it is still running. Wait for it to finish (or /workflows stop ${runId}) before resuming with an edited script.`;
414
+ }
415
+ const persisted = manager.getPersistence().load(runId);
416
+ if (!persisted) {
417
+ return `Cannot resume workflow run "${runId}": no run with that ID was found. Use the runId from a prior workflow result, or omit resumeFromRunId to start a new run.`;
418
+ }
419
+ if (persisted.status === "completed") {
420
+ return `Cannot resume workflow run "${runId}": it already completed. Start a new run instead (omit resumeFromRunId).`;
421
+ }
422
+ if (persisted.status === "aborted" || active?.status === "aborted") {
423
+ return `Cannot resume workflow run "${runId}": it was stopped/aborted and is not resumable. Start a new run instead (omit resumeFromRunId).`;
424
+ }
425
+ if (!persisted.script) {
426
+ return `Cannot resume workflow run "${runId}": it has no persisted script to resume. Start a new run instead (omit resumeFromRunId).`;
427
+ }
428
+ return `Cannot resume workflow run "${runId}": it is not currently resumable (it may be busy under another process). Try again shortly, or start a new run.`;
429
+ }
430
+
431
+ function normalizeWorkflowToolArgs(args: unknown): WorkflowToolInput {
432
+ if (!args || typeof args !== "object")
433
+ throw new Error("workflow requires an object argument with a `script` string or a `name`");
434
+ const value = args as Record<string, unknown>;
435
+ // `name` resolves a saved/built-in workflow at execute() time, so `script` is
436
+ // optional here — but if `script` is present at all it must still be a
437
+ // string (same requirement as the script-only path below), so a caller
438
+ // passing a malformed `script` alongside `name` gets a clear error instead
439
+ // of it being silently dropped.
440
+ if (typeof value.name === "string" && value.name.trim()) {
441
+ if (value.script !== undefined && typeof value.script !== "string") {
442
+ throw new Error("workflow's `script` must be a string when provided alongside `name`");
443
+ }
444
+ return {
445
+ ...value,
446
+ name: value.name.trim(),
447
+ script: typeof value.script === "string" ? normalizeWorkflowScript(value.script) : undefined,
448
+ } as WorkflowToolInput;
449
+ }
450
+ if (typeof value.script !== "string") throw new Error("workflow requires either `script` or `name` to be a string");
451
+ return { ...value, script: normalizeWorkflowScript(value.script) } as WorkflowToolInput;
452
+ }
453
+
454
+ function normalizeWorkflowScript(script: string): string {
455
+ let text = script.trim();
456
+ const fence = text.match(/^```(?:js|javascript)?\s*\n([\s\S]*?)\n```$/i);
457
+ if (fence) text = fence[1].trim();
458
+ return text;
459
+ }
460
+
461
+ function _isAbortError(error: unknown): boolean {
462
+ if (!(error instanceof Error)) return false;
463
+ return /\babort(?:ed)?\b/i.test(error.message);
464
+ }