@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,492 @@
1
+ /**
2
+ * Workflow manager for background execution, pause/resume, and run management.
3
+ */
4
+ import { EventEmitter } from "node:events";
5
+ import type { ModelRegistry, ToolDefinition } from "@earendil-works/pi-coding-agent";
6
+ import type { WorkflowAgent } from "./agent.js";
7
+ import { type WorkflowAgentSnapshot, type WorkflowSnapshot } from "./display.js";
8
+ import { WorkflowError } from "./errors.js";
9
+ import { type PersistedRunState, type RunLease, type RunPersistence, type RunStatus } from "./run-persistence.js";
10
+ import { type JournalEntry, type WorkflowRunResult } from "./workflow.js";
11
+ export interface ManagedRun {
12
+ runId: string;
13
+ status: RunStatus;
14
+ snapshot: WorkflowSnapshot;
15
+ result?: WorkflowRunResult;
16
+ error?: WorkflowError;
17
+ controller: AbortController;
18
+ startedAt: Date;
19
+ /** The real script, kept so the run can be resumed. */
20
+ script: string;
21
+ args?: unknown;
22
+ /** Accumulated agent results for resume (deterministic call index -> result). */
23
+ journal: JournalEntry[];
24
+ /** Cross-process execution lease for this run, when it is actively executing. */
25
+ lease?: RunLease;
26
+ /**
27
+ * True when the run was started in the background (or resumed) and the caller is
28
+ * not awaiting its result inline. Only background runs deliver their result back
29
+ * into the conversation; a foreground sync run already returns it as the tool
30
+ * result, so re-delivering would duplicate it.
31
+ */
32
+ background: boolean;
33
+ /**
34
+ * Auto-resume eligibility for this run (see ExecOptions.autoResume). Set once
35
+ * at creation and carried through resume() so it survives pause/resume cycles.
36
+ * Undefined means eligible (default-on); false opts out.
37
+ */
38
+ autoResume?: boolean;
39
+ /**
40
+ * The run's resolved hard token budget (per-run value, else the manager
41
+ * default), fixed at run start and carried through resume() — a resumed run
42
+ * must keep the budget it started with, not re-resolve against the current
43
+ * default (an explicit `null` opt-out would otherwise regain a budget).
44
+ */
45
+ tokenBudget?: number | null;
46
+ /**
47
+ * Named toolset tag for this run (see WorkflowManagerOptions.toolsets).
48
+ * ToolDefinitions are functions and can't be persisted, so the tag is what
49
+ * survives on disk — resume() re-resolves it so e.g. a resumed
50
+ * `/deep-research` run keeps its web tools instead of silently degrading to
51
+ * the default coding tools.
52
+ */
53
+ toolset?: string;
54
+ /**
55
+ * Real per-agent start/end timestamps, captured at onAgentStart/onAgentEnd
56
+ * (never fabricated), keyed by the agent's snapshot id. A running agent has
57
+ * an entry with no endedAt; persistRun() reads from here instead of stamping
58
+ * every agent with the run's startedAt / "now".
59
+ */
60
+ agentTimestamps: Map<number, {
61
+ startedAt: string;
62
+ endedAt?: string;
63
+ }>;
64
+ /**
65
+ * Live snapshot-agent lookup keyed by the agent CALL's unique id (see
66
+ * WorkflowRunOptions.onAgentStart/onAgentEnd/onAgentHistory's `id` field in
67
+ * workflow.ts — unique per call, never per label). onAgentEnd/onAgentHistory
68
+ * must resolve the snapshot entry to update through this map, never by
69
+ * scanning managed.snapshot.agents for a label match: two concurrent agents
70
+ * routinely share a label (e.g. parallel()'s default `"${phase} agent N"`
71
+ * labeling, or an author-supplied label reused across a fan-out), and a
72
+ * label+status scan would update whichever same-label entry it happens to
73
+ * find first — misattributing one agent's end/history event to a different,
74
+ * still-running sibling.
75
+ */
76
+ agentsById: Map<string, WorkflowAgentSnapshot>;
77
+ /**
78
+ * The run's cap on total agents (per-run value, else left undefined so
79
+ * runWorkflow applies its own MAX_AGENTS_PER_RUN default), fixed at run
80
+ * start/resume and carried through resume() — mirrors ManagedRun.tokenBudget
81
+ * exactly: a resumed run must keep the cap it started with, not silently
82
+ * regain the (much larger) default because ExecOptions.maxAgents isn't
83
+ * threaded through resume()'s executeRun() call.
84
+ */
85
+ maxAgents?: number;
86
+ /**
87
+ * The run's resolved per-agent timeout (per-run value, else the manager
88
+ * default at the time), fixed at run start/resume — same rationale as
89
+ * tokenBudget/maxAgents: resume() must not re-resolve against the manager's
90
+ * CURRENT defaultAgentTimeoutMs.
91
+ */
92
+ agentTimeoutMs?: number | null;
93
+ /**
94
+ * The run's resolved concurrency (per-run value, else the manager's
95
+ * concurrency at the time), fixed at run start/resume for the same reason
96
+ * as tokenBudget.
97
+ */
98
+ concurrency?: number;
99
+ /**
100
+ * The run's resolved agent-retry count (per-run value, else the manager
101
+ * default at the time), fixed at run start/resume for the same reason as
102
+ * tokenBudget.
103
+ */
104
+ agentRetries?: number;
105
+ }
106
+ /** Per-execution options shared by sync, background, and resume runs. */
107
+ export interface ExecOptions {
108
+ /**
109
+ * Replay these journaled agent/checkpoint results for the unchanged prefix
110
+ * (resume), keyed by `${runId}:${index}` — see
111
+ * WorkflowRunOptions.resumeJournal in workflow.ts.
112
+ */
113
+ resumeJournal?: Map<string, JournalEntry>;
114
+ /** Cap on total agents for this run. */
115
+ maxAgents?: number;
116
+ /** Per-agent timeout in milliseconds. null/omitted means no hard timeout. */
117
+ agentTimeoutMs?: number | null;
118
+ /** Host signal (e.g. tool/Esc) that should abort this run when fired. */
119
+ externalSignal?: AbortSignal;
120
+ /** Called with the live snapshot on every progress event. */
121
+ onProgress?: (snapshot: WorkflowSnapshot) => void;
122
+ /** Hard token budget for this run; once spent reaches it, agent() throws. */
123
+ tokenBudget?: number | null;
124
+ /**
125
+ * Tool set for this run's subagents, replacing the default coding tools —
126
+ * e.g. built-in `/deep-research` appends web tools. Omit for the default.
127
+ * Not persistable (functions): pair with `toolset` so a resumed run can
128
+ * re-resolve the same tools.
129
+ */
130
+ tools?: ToolDefinition[];
131
+ /**
132
+ * Named toolset tag, resolved via WorkflowManagerOptions.toolsets. Persisted
133
+ * with the run and re-resolved on resume(). When both `tools` and `toolset`
134
+ * are given, `tools` wins for this execution and `toolset` is what resumes use.
135
+ */
136
+ toolset?: string;
137
+ /** Max concurrent agents for this execution. */
138
+ concurrency?: number;
139
+ /** Retry attempts after recoverable agent failures for this execution. */
140
+ agentRetries?: number;
141
+ /** Resolve a checkpoint() question with a human reply (only for UI-bearing runs). */
142
+ confirm?: (promptText: string, options: unknown) => Promise<unknown>;
143
+ /**
144
+ * Whether this run is eligible for auto-resume when it pauses on a provider
145
+ * usage limit. Default-on: omit or pass true to stay eligible, pass false to
146
+ * opt out. Persisted on the run so a cold-start UsageLimitScheduler respects
147
+ * it too. See usage-limit-scheduler.ts.
148
+ */
149
+ autoResume?: boolean;
150
+ /**
151
+ * Seed for the execution's cumulative token counters — passed through to
152
+ * runWorkflow's WorkflowRunOptions.initialTokenUsage. Only resume() sets
153
+ * this (from the persisted run's tokenUsage-at-pause), so the resumed
154
+ * execution's fresh SharedRuntime starts counting from the already-spent
155
+ * total instead of zero (see A2 in workflow-manager's resume()).
156
+ */
157
+ initialTokenUsage?: {
158
+ input: number;
159
+ output: number;
160
+ total: number;
161
+ cost: number;
162
+ cacheRead: number;
163
+ cacheWrite: number;
164
+ };
165
+ }
166
+ export interface WorkflowManagerOptions {
167
+ cwd?: string;
168
+ concurrency?: number;
169
+ /** Resolve a saved-workflow name to its script, enabling nested `workflow('name')`. */
170
+ loadSavedWorkflow?: (name: string) => string | undefined;
171
+ /** Inject a custom agent runner (tests); defaults to a real subagent session. */
172
+ agent?: Pick<WorkflowAgent, "run">;
173
+ /** The session's main model (provider/id), for auto-tiering explore agents. */
174
+ mainModel?: string;
175
+ /**
176
+ * The host Pi session's model registry. When provided, workflow subagents
177
+ * resolve models against the same registry as the main session, including
178
+ * extension-registered providers such as ollama-cloud.
179
+ */
180
+ modelRegistry?: ModelRegistry;
181
+ /** The pi session id to tag runs with (see setSessionId). */
182
+ sessionId?: string;
183
+ /** Default per-agent timeout when a run does not pass agentTimeoutMs. null means no hard timeout. */
184
+ defaultAgentTimeoutMs?: number | null;
185
+ /** Default retry attempts after recoverable agent failures. */
186
+ defaultAgentRetries?: number;
187
+ /** Default hard token budget when a run does not pass tokenBudget. null/omitted means no budget. */
188
+ defaultTokenBudget?: number | null;
189
+ /**
190
+ * Named toolsets resolvable by ExecOptions.toolset — e.g.
191
+ * `{ "web-research": () => [...createCodingTools(cwd), ...createWebTools()] }`.
192
+ * Called lazily per execution (including on resume). An unknown tag resolves
193
+ * to the default coding tools.
194
+ */
195
+ toolsets?: Record<string, () => ToolDefinition[]>;
196
+ /**
197
+ * Extra tool NAMES to deny in every subagent session, on top of the always-on
198
+ * `workflow`/`workflow_control` defaults (see DEFAULT_EXCLUDED_SUBAGENT_TOOLS).
199
+ * Host wiring passes settings.excludeSubagentTools here so users can also block
200
+ * other recursive-orchestration tools (#107).
201
+ */
202
+ excludeSubagentTools?: string[];
203
+ /**
204
+ * Persist each subagent transcript as a real pi session file under the
205
+ * standard sessions directory. Default false (in-memory, discarded).
206
+ */
207
+ persistAgentSessions?: boolean;
208
+ /**
209
+ * How many terminal (completed/failed/aborted) runs to retain full
210
+ * in-memory state for before the oldest is evicted from `runs` (see the
211
+ * class-level doc comment on that field). Defaults to
212
+ * DEFAULT_MAX_TERMINAL_RUNS_IN_MEMORY; exposed mainly for tests that want
213
+ * to observe eviction without creating dozens of runs.
214
+ */
215
+ maxTerminalRunsInMemory?: number;
216
+ }
217
+ /** Options that a fresh extension generation may safely refresh on a live
218
+ * manager handed across `/reload`. Execution identity (`cwd`, persistence,
219
+ * injected agent, and in-memory runs) is intentionally excluded. */
220
+ export type WorkflowManagerReloadOptions = Pick<WorkflowManagerOptions, "concurrency" | "loadSavedWorkflow" | "defaultAgentTimeoutMs" | "defaultAgentRetries" | "defaultTokenBudget" | "toolsets" | "excludeSubagentTools" | "persistAgentSessions">;
221
+ export declare class WorkflowManager extends EventEmitter {
222
+ /**
223
+ * Lifecycle contract for `runs`:
224
+ *
225
+ * - An entry is added when a run starts (startInBackground/runSync) or is
226
+ * resumed (resume()), always with a live AbortController and (usually)
227
+ * an active RunLease.
228
+ * - While status is "running" or "paused", the entry is NEVER evicted —
229
+ * its execution could still settle (a pending executeRun() promise) or
230
+ * it is mid-usage-limit-checkpoint/manually-paused and still considered
231
+ * "the current state of this run" by callers. Eviction only ever
232
+ * considers an entry AFTER executeRun() has fully settled it to
233
+ * "completed" | "failed" | "aborted" (see IN_MEMORY_TERMINAL_STATUSES)
234
+ * and persisted + released its lease — i.e. strictly after the same
235
+ * isCurrent()-gated persistRun()/releaseRunLease() calls in
236
+ * executeRun()'s success/catch tails.
237
+ * - Once terminal, an entry becomes eviction-ELIGIBLE (recordTerminalRun())
238
+ * but is not necessarily evicted immediately: up to
239
+ * maxTerminalRunsInMemory terminal entries are kept, oldest evicted
240
+ * first, so a `getRun()` call immediately after completion (e.g. the
241
+ * "complete" event's own synchronous listeners — task-panel's result
242
+ * delivery, `/workflows watch`) still sees the live object. Once
243
+ * evicted, the entry is simply removed from `runs`; nothing else reads
244
+ * or writes it again.
245
+ * - Every caller of getRun()/getSnapshot() must treat "undefined"/null as
246
+ * "no live in-memory copy right now" and fall back to listRuns() (backed
247
+ * by run-persistence.ts, which is what's authoritative for a run once
248
+ * the in-memory copy is gone) — this mirrors how those callers already
249
+ * treat any run this process never had in memory (e.g. one started by a
250
+ * different process and only ever seen via listRuns()). resume() never
251
+ * depends on `runs` for a run's state either: it always reloads from
252
+ * persistence, so an evicted runId resumes exactly like one from a
253
+ * prior process.
254
+ * - isCurrent(managed) composes with eviction the same way it composes
255
+ * with resume()/deleteRun() replacing or removing an entry: eviction
256
+ * removes the map entry outright, so a stale execution's later settle
257
+ * (isCurrent() check) sees `this.runs.get(runId) !== managed` (in fact
258
+ * undefined) and correctly no-ops, exactly as it would after
259
+ * resume()/deleteRun().
260
+ */
261
+ private runs;
262
+ /**
263
+ * FIFO of runIds that reached IN_MEMORY_TERMINAL_STATUSES, oldest first —
264
+ * the eviction order for `runs` (see its doc comment). A runId can appear
265
+ * more than once (e.g. resumed after eviction, then terminates again);
266
+ * evicting is idempotent (recordTerminalRun() re-checks the CURRENT status
267
+ * of the current map entry for that id before deleting), so duplicates
268
+ * are harmless.
269
+ */
270
+ private terminalRunQueue;
271
+ private maxTerminalRunsInMemory;
272
+ private persistence;
273
+ private cwd;
274
+ private concurrency;
275
+ private loadSavedWorkflow?;
276
+ private agent?;
277
+ /** The session's main model (provider/id), for auto-tiering explore agents. */
278
+ private mainModel?;
279
+ /** The host Pi session's model registry, shared with subagents. */
280
+ private modelRegistry?;
281
+ /** The current pi session id; runs are stamped with it and listRuns() filters by it. */
282
+ private sessionId?;
283
+ private defaultAgentTimeoutMs;
284
+ private defaultAgentRetries;
285
+ private defaultTokenBudget;
286
+ private toolsets?;
287
+ private excludeSubagentTools?;
288
+ private persistAgentSessions;
289
+ constructor(options?: WorkflowManagerOptions);
290
+ /** Bind the manager to the current pi session, so new runs are tagged with it and
291
+ * the navigator/task-panel show only this session's runs (set on session_start). */
292
+ setSessionId(id: string | undefined): void;
293
+ /**
294
+ * On startup, any persisted run still marked "running" belongs to a process
295
+ * that died mid-run (this fresh manager has it nowhere in memory). Reconcile it
296
+ * to "paused" — never "failed" — so its journal is preserved and resume() can
297
+ * replay the completed prefix and finish the rest.
298
+ */
299
+ private recoverStaleRuns;
300
+ /**
301
+ * Refresh host configuration after Pi reloads the extension while retaining
302
+ * this manager's live runs, controllers, leases, and event listeners.
303
+ * Existing executions keep the options they captured at start; subsequent
304
+ * runs and resumes use these refreshed defaults.
305
+ */
306
+ reconfigureAfterReload(options: WorkflowManagerReloadOptions): void;
307
+ /** Set the session's main model (provider/id). Used to auto-tier explore agents. */
308
+ setMainModel(spec: string | undefined): void;
309
+ /** Set the host session's model registry so subagents resolve models consistently. */
310
+ setModelRegistry(registry: ModelRegistry): void;
311
+ /**
312
+ * Expose the host session's model registry to integrations sharing this
313
+ * manager. Workflow execution reads the same registry internally.
314
+ */
315
+ getModelRegistry(): ModelRegistry | undefined;
316
+ /**
317
+ * Start a workflow in the background.
318
+ * Returns immediately with a run ID; the workflow executes asynchronously.
319
+ */
320
+ startInBackground(script: string, args?: unknown, exec?: ExecOptions): {
321
+ runId: string;
322
+ promise: Promise<WorkflowRunResult>;
323
+ };
324
+ /**
325
+ * Execute a workflow synchronously (blocking) while still tracking it like a
326
+ * background run, so the `/workflows` navigator and the live task panel see it.
327
+ * `onProgress` fires on every progress event with the current snapshot, letting
328
+ * a caller (e.g. the workflow tool) drive its own inline display.
329
+ */
330
+ runSync(script: string, args?: unknown, exec?: ExecOptions): Promise<WorkflowRunResult>;
331
+ /** Build a fresh managed run with an empty snapshot. */
332
+ private createManaged;
333
+ private executeRun;
334
+ /**
335
+ * True when `managed` is still the live, current entry for its runId in
336
+ * `this.runs` — false once resume() has replaced it with a new ManagedRun
337
+ * object for the same runId, or deleteRun() has removed it entirely. A
338
+ * superseded ManagedRun's async completion (executeRun's promise settling
339
+ * well after something else already took over or tore down that runId)
340
+ * must not write to disk or touch lease state on the newer execution's
341
+ * behalf — see writeRunToDisk() and executeRun()'s post-await persist calls.
342
+ */
343
+ private isCurrent;
344
+ /**
345
+ * Emit an event on behalf of `managed`, but only while it's still the
346
+ * current entry for its runId (see isCurrent()) — mirrors the disk/lease
347
+ * guard for the observer-facing side of the same problem. A superseded
348
+ * execution's progress/terminal events (log, phase, agentStart/End,
349
+ * tokenUsage, complete, error, paused) are not just stale-but-harmless:
350
+ * "complete" in particular can drive background result delivery into the
351
+ * conversation, so letting a deleted/superseded run's stale settle still
352
+ * fire it would deliver a result for a run that, from the caller's POV, no
353
+ * longer exists (or has since been superseded by a newer execution whose
354
+ * own events already tell the true story). No event in this set has a
355
+ * legitimate reason to still reach listeners once superseded — unlike
356
+ * disk writes there's no "expected race, harmless no-op" nuance here, it's
357
+ * simply wrong to notify twice (or for a run that's gone). Events emitted
358
+ * directly by pause()/stop()/resume()/deleteRun() themselves are NOT routed
359
+ * through this helper — those methods own the transition and ARE current
360
+ * at the moment they fire, same precedent as their persist/lease calls.
361
+ */
362
+ private emitLive;
363
+ /**
364
+ * Mark `runId` as eviction-eligible now that its execution has genuinely
365
+ * settled to a terminal status (completed/failed/aborted — see
366
+ * IN_MEMORY_TERMINAL_STATUSES), and evict the oldest eligible entries
367
+ * beyond maxTerminalRunsInMemory. Callers must only invoke this after the
368
+ * same isCurrent()-gated persistRun()/releaseRunLease() sequence executeRun()
369
+ * already uses (see the `runs` field doc comment for the full contract) —
370
+ * this method itself re-validates the CURRENT entry's status before
371
+ * deleting anything, so it never evicts a run that isn't (or is no longer)
372
+ * genuinely terminal, including one resumed back to "running" after being
373
+ * queued here but before its turn to be evicted came up.
374
+ */
375
+ private recordTerminalRun;
376
+ /**
377
+ * Additively fold one agent-call's token cost into the run-wide persisted
378
+ * aggregate (managed.snapshot.tokenUsage), seeded (on resume) from the
379
+ * persisted total-at-pause — see A2. Shared by onAgentEnd (a completed or
380
+ * finally-failed agent call) and onRetrySpend (a failed attempt that WILL
381
+ * be retried, whose cost recordTokens() already folded into
382
+ * shared.spent/tokenUsage in workflow.ts, but which onAgentEnd never sees —
383
+ * see WorkflowRunOptions.onRetrySpend for why that needs its own channel).
384
+ */
385
+ private accumulateTokenUsage;
386
+ private releaseRunLease;
387
+ /** Trailing-edge throttle window for high-frequency progress persists (see schedulePersist). */
388
+ private static readonly PERSIST_THROTTLE_MS;
389
+ /** Pending trailing-edge persist timers for high-frequency progress events, keyed by runId. */
390
+ private persistTimers;
391
+ /**
392
+ * Coalesce rapid progress persists (currently: onAgentJournal, which fires
393
+ * once per completed agent and can burst under concurrency) to at most one
394
+ * disk write per PERSIST_THROTTLE_MS (trailing edge) instead of one write
395
+ * per tick — persistRun() does a full JSON.stringify of the run plus up to
396
+ * 3 sync writes, so firing it once per agent in a long run is O(N^2).
397
+ *
398
+ * Lifecycle-critical writes (status transitions, run end, pause/resume/stop)
399
+ * must NOT use this — call persistRun() directly, which flushes (and cancels)
400
+ * any pending timer first so a stale trailing write can never fire after, and
401
+ * resurrect, a terminal state.
402
+ */
403
+ private schedulePersist;
404
+ /**
405
+ * Persist immediately and synchronously. Cancels any pending throttled write
406
+ * for this run first, so the write that lands is always the caller's current
407
+ * (final) state — never superseded by a stale deferred write. Use this for
408
+ * every lifecycle-critical persist: run start, status transitions, run end,
409
+ * pause()/resume()/stop().
410
+ */
411
+ private persistRun;
412
+ private writeRunToDisk;
413
+ /**
414
+ * Pause a running workflow.
415
+ */
416
+ pause(runId: string): boolean;
417
+ /**
418
+ * Resume an interrupted run: replay journaled results for the unchanged prefix
419
+ * and run the rest live. Returns false if there is nothing resumable.
420
+ *
421
+ * `opts.script` lets the orchestrating model resume with an EDITED script
422
+ * (cached-prefix reuse / iteration): unchanged agent() calls whose content
423
+ * hash still matches the journal entry at their positional callIndex replay
424
+ * from cache, while the first changed or newly inserted call — and everything
425
+ * after it — re-runs live. When `opts.script` is omitted, resume behaves
426
+ * exactly as before and uses the persisted script (auto-resume, TUI resume);
427
+ * this keeps the existing single-arg `resume(runId)` callers (e.g. the
428
+ * UsageLimitScheduler) unchanged. `opts.args` overrides the persisted args
429
+ * only when provided; otherwise the persisted args are kept.
430
+ */
431
+ resume(runId: string, opts?: {
432
+ script?: string;
433
+ args?: unknown;
434
+ }): Promise<boolean>;
435
+ /**
436
+ * Stop a running workflow.
437
+ *
438
+ * Fast path: the run is live in this process (`this.runs`) — abort its
439
+ * controller and persist "aborted" as before. Fallback: the run is not in
440
+ * memory but is persisted as "running" or "paused" — e.g. it belongs to a
441
+ * prior pi session that this process's recoverStaleRuns() flipped to
442
+ * "paused" on disk without repopulating this.runs (see workflow-control-tool's
443
+ * findRun(), which resolves candidates from disk via listRuns()). There is no
444
+ * live controller to abort in that case — the run simply isn't executing in
445
+ * this process — so mark it aborted on disk directly, mirroring resume()'s
446
+ * persisted-fallback lease handling.
447
+ */
448
+ stop(runId: string): boolean;
449
+ /**
450
+ * Get status of a specific run.
451
+ */
452
+ getRun(runId: string): ManagedRun | undefined;
453
+ /**
454
+ * List all runs (active + persisted).
455
+ */
456
+ /**
457
+ * Runs for the navigator/task panel. Once bound to a session (setSessionId), only
458
+ * that session's runs are returned — runs from other sessions stay on disk and
459
+ * reappear when you switch back. Unbound (tests/legacy) returns everything.
460
+ */
461
+ listRuns(): PersistedRunState[];
462
+ /** All persisted runs regardless of session (used by cross-session recovery). */
463
+ listAllRuns(): PersistedRunState[];
464
+ /**
465
+ * Get snapshot of a run.
466
+ */
467
+ getSnapshot(runId: string): WorkflowSnapshot | null;
468
+ /**
469
+ * Delete a persisted run.
470
+ *
471
+ * If `runId` is still live in this process (running or paused-in-memory),
472
+ * abort its controller FIRST, before any teardown below — a live run left
473
+ * un-aborted would otherwise keep executing in the background indefinitely
474
+ * (burning API calls/tokens/holding a worktree) after its record is gone.
475
+ * Aborting first, while `managed` is still `this.runs.get(runId)`, costs
476
+ * nothing extra: the abort signal is fire-and-forget (cooperative — the
477
+ * execution winds down on its own schedule), so the exact instant we flip
478
+ * `this.runs`/release the lease/delete files relative to it doesn't matter
479
+ * for correctness. What DOES matter is that once this method returns, the
480
+ * aborted execution's eventual settle (executeRun's success/catch path,
481
+ * asynchronously, possibly much later) must be a harmless no-op rather than
482
+ * a resurrection — that's what isCurrent() guarantees: `this.runs.delete()`
483
+ * below means executeRun's later persistRun()/releaseRunLease() calls on
484
+ * this same `managed` object find `this.runs.get(runId) !== managed` (in
485
+ * fact `undefined`, since the entry is gone) and skip writing/releasing.
486
+ */
487
+ deleteRun(runId: string): boolean;
488
+ /**
489
+ * Get the persistence layer (for saving workflows).
490
+ */
491
+ getPersistence(): RunPersistence;
492
+ }