wave-agent-sdk 0.17.1 → 0.17.3

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 (173) hide show
  1. package/builtin/skills/deep-research/SKILL.md +90 -0
  2. package/builtin/skills/settings/ENV.md +6 -3
  3. package/dist/agent.d.ts +28 -1
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +128 -34
  6. package/dist/constants/goalPrompts.d.ts +2 -0
  7. package/dist/constants/goalPrompts.d.ts.map +1 -0
  8. package/dist/constants/goalPrompts.js +10 -0
  9. package/dist/constants/tools.d.ts +1 -0
  10. package/dist/constants/tools.d.ts.map +1 -1
  11. package/dist/constants/tools.js +1 -0
  12. package/dist/managers/aiManager.d.ts +7 -0
  13. package/dist/managers/aiManager.d.ts.map +1 -1
  14. package/dist/managers/aiManager.js +77 -41
  15. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  16. package/dist/managers/backgroundTaskManager.js +10 -2
  17. package/dist/managers/goalManager.d.ts +43 -0
  18. package/dist/managers/goalManager.d.ts.map +1 -0
  19. package/dist/managers/goalManager.js +177 -0
  20. package/dist/managers/messageManager.d.ts +2 -2
  21. package/dist/managers/messageManager.d.ts.map +1 -1
  22. package/dist/managers/messageQueue.d.ts +10 -0
  23. package/dist/managers/messageQueue.d.ts.map +1 -1
  24. package/dist/managers/messageQueue.js +53 -1
  25. package/dist/managers/pluginManager.d.ts.map +1 -1
  26. package/dist/managers/pluginManager.js +7 -1
  27. package/dist/managers/skillManager.d.ts +2 -0
  28. package/dist/managers/skillManager.d.ts.map +1 -1
  29. package/dist/managers/skillManager.js +19 -9
  30. package/dist/managers/slashCommandManager.d.ts +6 -0
  31. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  32. package/dist/managers/slashCommandManager.js +105 -0
  33. package/dist/managers/toolManager.d.ts.map +1 -1
  34. package/dist/managers/toolManager.js +5 -0
  35. package/dist/managers/workflowManager.d.ts +65 -0
  36. package/dist/managers/workflowManager.d.ts.map +1 -0
  37. package/dist/managers/workflowManager.js +380 -0
  38. package/dist/prompts/index.d.ts +2 -1
  39. package/dist/prompts/index.d.ts.map +1 -1
  40. package/dist/prompts/index.js +3 -3
  41. package/dist/services/MarketplaceService.d.ts +2 -2
  42. package/dist/services/MarketplaceService.d.ts.map +1 -1
  43. package/dist/services/MarketplaceService.js +11 -32
  44. package/dist/services/aiService.d.ts +23 -0
  45. package/dist/services/aiService.d.ts.map +1 -1
  46. package/dist/services/aiService.js +102 -9
  47. package/dist/services/configurationService.d.ts +1 -1
  48. package/dist/services/configurationService.d.ts.map +1 -1
  49. package/dist/services/configurationService.js +3 -16
  50. package/dist/services/hook.d.ts.map +1 -1
  51. package/dist/services/hook.js +4 -0
  52. package/dist/services/session.d.ts +9 -1
  53. package/dist/services/session.d.ts.map +1 -1
  54. package/dist/services/session.js +28 -1
  55. package/dist/tools/bashTool.d.ts.map +1 -1
  56. package/dist/tools/bashTool.js +49 -7
  57. package/dist/tools/readTool.d.ts.map +1 -1
  58. package/dist/tools/readTool.js +1 -1
  59. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  60. package/dist/tools/taskManagementTools.js +103 -157
  61. package/dist/tools/types.d.ts +2 -0
  62. package/dist/tools/types.d.ts.map +1 -1
  63. package/dist/tools/webFetchTool.d.ts.map +1 -1
  64. package/dist/tools/webFetchTool.js +0 -9
  65. package/dist/tools/workflowTool.d.ts +11 -0
  66. package/dist/tools/workflowTool.d.ts.map +1 -0
  67. package/dist/tools/workflowTool.js +190 -0
  68. package/dist/types/agent.d.ts +2 -0
  69. package/dist/types/agent.d.ts.map +1 -1
  70. package/dist/types/commands.d.ts +4 -0
  71. package/dist/types/commands.d.ts.map +1 -1
  72. package/dist/types/config.d.ts +2 -2
  73. package/dist/types/config.d.ts.map +1 -1
  74. package/dist/types/core.d.ts +1 -1
  75. package/dist/types/core.d.ts.map +1 -1
  76. package/dist/types/hooks.d.ts +2 -0
  77. package/dist/types/hooks.d.ts.map +1 -1
  78. package/dist/types/index.d.ts +1 -0
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/types/index.js +1 -0
  81. package/dist/types/messaging.d.ts +2 -2
  82. package/dist/types/messaging.d.ts.map +1 -1
  83. package/dist/types/processes.d.ts +6 -2
  84. package/dist/types/processes.d.ts.map +1 -1
  85. package/dist/types/workflow.d.ts +2 -0
  86. package/dist/types/workflow.d.ts.map +1 -0
  87. package/dist/types/workflow.js +1 -0
  88. package/dist/utils/cacheControlUtils.d.ts +13 -8
  89. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  90. package/dist/utils/cacheControlUtils.js +73 -102
  91. package/dist/utils/containerSetup.d.ts.map +1 -1
  92. package/dist/utils/containerSetup.js +7 -0
  93. package/dist/utils/markdownParser.d.ts.map +1 -1
  94. package/dist/utils/markdownParser.js +21 -6
  95. package/dist/utils/messageOperations.d.ts +2 -2
  96. package/dist/utils/messageOperations.d.ts.map +1 -1
  97. package/dist/utils/notificationXml.d.ts.map +1 -1
  98. package/dist/workflow/budgetTracker.d.ts +12 -0
  99. package/dist/workflow/budgetTracker.d.ts.map +1 -0
  100. package/dist/workflow/budgetTracker.js +30 -0
  101. package/dist/workflow/concurrencyLimiter.d.ts +14 -0
  102. package/dist/workflow/concurrencyLimiter.d.ts.map +1 -0
  103. package/dist/workflow/concurrencyLimiter.js +39 -0
  104. package/dist/workflow/journal.d.ts +19 -0
  105. package/dist/workflow/journal.d.ts.map +1 -0
  106. package/dist/workflow/journal.js +74 -0
  107. package/dist/workflow/progressReporter.d.ts +21 -0
  108. package/dist/workflow/progressReporter.d.ts.map +1 -0
  109. package/dist/workflow/progressReporter.js +118 -0
  110. package/dist/workflow/runState.d.ts +16 -0
  111. package/dist/workflow/runState.d.ts.map +1 -0
  112. package/dist/workflow/runState.js +57 -0
  113. package/dist/workflow/scriptRuntime.d.ts +35 -0
  114. package/dist/workflow/scriptRuntime.d.ts.map +1 -0
  115. package/dist/workflow/scriptRuntime.js +196 -0
  116. package/dist/workflow/structuredOutput.d.ts +27 -0
  117. package/dist/workflow/structuredOutput.d.ts.map +1 -0
  118. package/dist/workflow/structuredOutput.js +106 -0
  119. package/dist/workflow/types.d.ts +81 -0
  120. package/dist/workflow/types.d.ts.map +1 -0
  121. package/dist/workflow/types.js +1 -0
  122. package/dist/workflow/workflowApis.d.ts +46 -0
  123. package/dist/workflow/workflowApis.d.ts.map +1 -0
  124. package/dist/workflow/workflowApis.js +280 -0
  125. package/package.json +1 -1
  126. package/src/agent.ts +144 -34
  127. package/src/constants/goalPrompts.ts +10 -0
  128. package/src/constants/tools.ts +1 -0
  129. package/src/managers/aiManager.ts +91 -47
  130. package/src/managers/backgroundTaskManager.ts +16 -4
  131. package/src/managers/goalManager.ts +232 -0
  132. package/src/managers/messageManager.ts +2 -2
  133. package/src/managers/messageQueue.ts +59 -1
  134. package/src/managers/pluginManager.ts +8 -1
  135. package/src/managers/skillManager.ts +20 -9
  136. package/src/managers/slashCommandManager.ts +119 -0
  137. package/src/managers/toolManager.ts +7 -0
  138. package/src/managers/workflowManager.ts +491 -0
  139. package/src/prompts/index.ts +4 -2
  140. package/src/services/MarketplaceService.ts +14 -38
  141. package/src/services/aiService.ts +166 -12
  142. package/src/services/configurationService.ts +2 -22
  143. package/src/services/hook.ts +5 -0
  144. package/src/services/session.ts +42 -2
  145. package/src/tools/bashTool.ts +64 -9
  146. package/src/tools/readTool.ts +1 -2
  147. package/src/tools/taskManagementTools.ts +146 -195
  148. package/src/tools/types.ts +2 -0
  149. package/src/tools/webFetchTool.ts +0 -12
  150. package/src/tools/workflowTool.ts +205 -0
  151. package/src/types/agent.ts +6 -0
  152. package/src/types/commands.ts +4 -0
  153. package/src/types/config.ts +2 -2
  154. package/src/types/core.ts +3 -3
  155. package/src/types/hooks.ts +2 -0
  156. package/src/types/index.ts +1 -0
  157. package/src/types/messaging.ts +2 -2
  158. package/src/types/processes.ts +10 -2
  159. package/src/types/workflow.ts +5 -0
  160. package/src/utils/cacheControlUtils.ts +106 -131
  161. package/src/utils/containerSetup.ts +9 -0
  162. package/src/utils/markdownParser.ts +26 -8
  163. package/src/utils/messageOperations.ts +2 -2
  164. package/src/utils/notificationXml.ts +6 -1
  165. package/src/workflow/budgetTracker.ts +34 -0
  166. package/src/workflow/concurrencyLimiter.ts +47 -0
  167. package/src/workflow/journal.ts +95 -0
  168. package/src/workflow/progressReporter.ts +141 -0
  169. package/src/workflow/runState.ts +65 -0
  170. package/src/workflow/scriptRuntime.ts +274 -0
  171. package/src/workflow/structuredOutput.ts +123 -0
  172. package/src/workflow/types.ts +95 -0
  173. package/src/workflow/workflowApis.ts +412 -0
@@ -8,6 +8,7 @@ import {
8
8
  SKILL_BASH_MAX_OUTPUT_CHARS,
9
9
  PREVIEW_SIZE_BYTES,
10
10
  } from "../constants/toolLimits.js";
11
+ import { logger } from "./globalLogger.js";
11
12
 
12
13
  const execAsync = promisify(exec);
13
14
 
@@ -222,6 +223,18 @@ export function truncateOutput(output: string): string {
222
223
  return `${preview}\n\n[Output truncated (${output.length} chars). Full output saved to: ${tempFile}]`;
223
224
  }
224
225
 
226
+ /**
227
+ * Format a bash command result for inclusion in skill content.
228
+ * Failed commands (non-zero exit code) are wrapped with an error indicator.
229
+ */
230
+ function formatBashResult(result: BashCommandResult): string {
231
+ const output = truncateOutput(result.output);
232
+ if (result.exitCode !== 0) {
233
+ return `<error>Command failed (exit code ${result.exitCode}): ${output}</error>`;
234
+ }
235
+ return output;
236
+ }
237
+
225
238
  /**
226
239
  * Replace bash command placeholders with their outputs.
227
240
  * Uses function replacer to avoid $$, $&, $' corruption in shell output.
@@ -238,7 +251,7 @@ export function replaceBashCommandsWithOutput(
238
251
  processedContent = processedContent.replace(BLOCK_BASH_REGEX, () => {
239
252
  if (commandIndex < results.length) {
240
253
  const result = results[commandIndex++];
241
- return truncateOutput(result.output);
254
+ return formatBashResult(result);
242
255
  }
243
256
  return "";
244
257
  });
@@ -247,7 +260,7 @@ export function replaceBashCommandsWithOutput(
247
260
  processedContent = processedContent.replace(INLINE_BASH_REGEX, () => {
248
261
  if (commandIndex < results.length) {
249
262
  const result = results[commandIndex++];
250
- return truncateOutput(result.output);
263
+ return formatBashResult(result);
251
264
  }
252
265
  return "";
253
266
  });
@@ -283,14 +296,19 @@ export async function executeBashCommands(
283
296
  message?: string;
284
297
  code?: number;
285
298
  };
299
+ const errorOutput = (
300
+ (execError.stdout || "") +
301
+ (execError.stderr || "") +
302
+ (execError.message || "")
303
+ ).trim();
304
+ const exitCode = execError.code || 1;
305
+ logger?.warn(
306
+ `[Skill bash] Command failed (exit code ${exitCode}): ${command}\n${errorOutput}`,
307
+ );
286
308
  results.push({
287
309
  command,
288
- output: (
289
- (execError.stdout || "") +
290
- (execError.stderr || "") +
291
- (execError.message || "")
292
- ).trim(),
293
- exitCode: execError.code || 1,
310
+ output: errorOutput,
311
+ exitCode,
294
312
  });
295
313
  }
296
314
  }
@@ -597,8 +597,8 @@ export function getMessageContent(message: Message): string {
597
597
  export interface AddNotificationMessageParams {
598
598
  messages: Message[];
599
599
  taskId: string;
600
- taskType: "shell" | "agent";
601
- status: "completed" | "failed" | "killed";
600
+ taskType: "shell" | "agent" | "workflow";
601
+ status: "completed" | "failed" | "killed" | "aborted";
602
602
  summary: string;
603
603
  outputFile?: string;
604
604
  }
@@ -24,11 +24,16 @@ export function parseTaskNotificationXml(
24
24
  ): TaskNotificationBlock | null {
25
25
  try {
26
26
  const taskId = extractTag(xml, "task-id");
27
- const taskType = extractTag(xml, "task-type") as "shell" | "agent" | null;
27
+ const taskType = extractTag(xml, "task-type") as
28
+ | "shell"
29
+ | "agent"
30
+ | "workflow"
31
+ | null;
28
32
  const status = extractTag(xml, "status") as
29
33
  | "completed"
30
34
  | "failed"
31
35
  | "killed"
36
+ | "aborted"
32
37
  | null;
33
38
  const summary = extractTag(xml, "summary");
34
39
 
@@ -0,0 +1,34 @@
1
+ export class BudgetTracker {
2
+ private totalSpent = 0;
3
+
4
+ constructor(private _total: number | null = null) {}
5
+
6
+ addUsage(tokens: number): void {
7
+ this.totalSpent += tokens;
8
+ }
9
+
10
+ spent(): number {
11
+ return this.totalSpent;
12
+ }
13
+
14
+ remaining(): number {
15
+ if (this._total === null) return Infinity;
16
+ return Math.max(0, this._total - this.totalSpent);
17
+ }
18
+
19
+ isExceeded(): boolean {
20
+ return this._total !== null && this.totalSpent >= this._total;
21
+ }
22
+
23
+ get total(): number | null {
24
+ return this._total;
25
+ }
26
+
27
+ toBudgetInfo(): import("./types.js").BudgetInfo {
28
+ return {
29
+ total: this._total,
30
+ spent: () => this.totalSpent,
31
+ remaining: () => this.remaining(),
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,47 @@
1
+ export class ConcurrencyLimiter {
2
+ private running = 0;
3
+ private queue: Array<() => void> = [];
4
+ private readonly maxConcurrency: number;
5
+ private activeSet = new Set<Promise<unknown>>();
6
+
7
+ constructor(maxConcurrency: number) {
8
+ this.maxConcurrency = Math.max(1, maxConcurrency);
9
+ }
10
+
11
+ async acquire(): Promise<void> {
12
+ if (this.running < this.maxConcurrency) {
13
+ this.running++;
14
+ return;
15
+ }
16
+ return new Promise<void>((resolve) => {
17
+ this.queue.push(resolve);
18
+ });
19
+ }
20
+
21
+ release(): void {
22
+ this.running--;
23
+ if (this.queue.length > 0) {
24
+ this.running++;
25
+ const next = this.queue.shift()!;
26
+ next();
27
+ }
28
+ }
29
+
30
+ track<T>(promise: Promise<T>): Promise<T> {
31
+ this.activeSet.add(promise);
32
+ promise.finally(() => this.activeSet.delete(promise));
33
+ return promise;
34
+ }
35
+
36
+ async drain(): Promise<void> {
37
+ await Promise.allSettled(this.activeSet);
38
+ }
39
+
40
+ get activeCount(): number {
41
+ return this.running;
42
+ }
43
+
44
+ get pendingCount(): number {
45
+ return this.queue.length;
46
+ }
47
+ }
@@ -0,0 +1,95 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { JournalLine } from "./types.js";
4
+
5
+ export class Journal {
6
+ private entries: JournalLine[] = [];
7
+ private stream: fs.WriteStream | null = null;
8
+
9
+ constructor(public readonly filePath: string) {}
10
+
11
+ async init(): Promise<void> {
12
+ // Ensure directory exists
13
+ await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
14
+ // Create append-only write stream
15
+ this.stream = fs.createWriteStream(this.filePath, { flags: "a" });
16
+ }
17
+
18
+ append(entry: JournalLine): void {
19
+ this.entries.push(entry);
20
+ if (this.stream && !this.stream.destroyed && !this.stream.writableEnded) {
21
+ this.stream.write(JSON.stringify(entry) + "\n");
22
+ }
23
+ }
24
+
25
+ appendLog(message: string): void {
26
+ this.append({ type: "log", message });
27
+ }
28
+
29
+ getCachedResult(agentIndex: number): unknown | undefined {
30
+ const agentEntries = this.entries.filter(
31
+ (e): e is import("./types.js").JournalEntry => !("type" in e),
32
+ );
33
+ // Check if this agent was marked as failed
34
+ const failedEntry = this.entries.find(
35
+ (e): e is import("./types.js").AgentFailedEntry =>
36
+ "type" in e && e.type === "agent_failed" && e.agentIndex === agentIndex,
37
+ );
38
+ if (failedEntry) {
39
+ return undefined; // Failed agents don't have cached results
40
+ }
41
+ if (agentIndex < agentEntries.length) {
42
+ return agentEntries[agentIndex].result;
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ get length(): number {
48
+ return this.entries.length;
49
+ }
50
+
51
+ /** Count of agent entries (excluding log entries) */
52
+ get agentEntryCount(): number {
53
+ return this.entries.filter(
54
+ (e): e is import("./types.js").JournalEntry => !("type" in e),
55
+ ).length;
56
+ }
57
+
58
+ /** Remove the agent_failed entry for a given agent index (for retry support) */
59
+ removeFailedEntry(agentIndex: number): void {
60
+ this.entries = this.entries.filter(
61
+ (e) =>
62
+ !(
63
+ "type" in e &&
64
+ e.type === "agent_failed" &&
65
+ e.agentIndex === agentIndex
66
+ ),
67
+ );
68
+ }
69
+
70
+ async close(): Promise<void> {
71
+ const s = this.stream;
72
+ this.stream = null;
73
+ if (s) {
74
+ await new Promise<void>((resolve) => {
75
+ s.end(() => resolve());
76
+ });
77
+ }
78
+ }
79
+
80
+ static async load(filePath: string): Promise<Journal> {
81
+ const journal = new Journal(filePath);
82
+ try {
83
+ const content = await fs.promises.readFile(filePath, "utf-8");
84
+ for (const line of content.split("\n")) {
85
+ const trimmed = line.trim();
86
+ if (trimmed) {
87
+ journal.entries.push(JSON.parse(trimmed));
88
+ }
89
+ }
90
+ } catch {
91
+ // File doesn't exist yet — empty journal
92
+ }
93
+ return journal;
94
+ }
95
+ }
@@ -0,0 +1,141 @@
1
+ import type {
2
+ WorkflowPhaseState,
3
+ WorkflowMeta,
4
+ WorkflowProgressEvent,
5
+ } from "./types.js";
6
+
7
+ export class ProgressReporter {
8
+ private phases: WorkflowPhaseState[] = [];
9
+ private currentPhaseIndex = -1;
10
+ private agentCounter = 0;
11
+ private listeners: Array<(event: WorkflowProgressEvent) => void> = [];
12
+ private runId: string;
13
+
14
+ constructor(
15
+ private meta: WorkflowMeta,
16
+ runId?: string,
17
+ ) {
18
+ this.runId = runId || "";
19
+ // Pre-initialize phases from meta so they appear even if the script
20
+ // doesn't call phase() explicitly
21
+ if (meta.phases?.length) {
22
+ for (const p of meta.phases) {
23
+ this.phases.push({
24
+ title: p.title,
25
+ agentCount: 0,
26
+ tokens: 0,
27
+ elapsed: 0,
28
+ startTime: Date.now(),
29
+ });
30
+ }
31
+ // Default to the first phase so agentStarted/agentCompleted
32
+ // track into it even without an explicit phase() call
33
+ this.currentPhaseIndex = 0;
34
+ }
35
+ }
36
+
37
+ setPhase(title: string): void {
38
+ // Emit phase_completed for the previous phase
39
+ if (this.currentPhaseIndex >= 0) {
40
+ this.emit({
41
+ type: "phase_completed",
42
+ phaseIndex: this.currentPhaseIndex,
43
+ });
44
+ }
45
+
46
+ const existing = this.phases.findIndex((p) => p.title === title);
47
+ if (existing >= 0) {
48
+ this.currentPhaseIndex = existing;
49
+ } else {
50
+ this.phases.push({
51
+ title,
52
+ agentCount: 0,
53
+ tokens: 0,
54
+ elapsed: 0,
55
+ startTime: Date.now(),
56
+ });
57
+ this.currentPhaseIndex = this.phases.length - 1;
58
+ }
59
+
60
+ this.emit({
61
+ type: "phase_started",
62
+ phaseIndex: this.currentPhaseIndex,
63
+ });
64
+ }
65
+
66
+ agentStarted(): void {
67
+ this.agentCounter++;
68
+ if (this.currentPhaseIndex >= 0) {
69
+ this.phases[this.currentPhaseIndex].agentCount++;
70
+ }
71
+ this.emit({
72
+ type: "agent_started",
73
+ phaseIndex: this.currentPhaseIndex,
74
+ agentIndex: this.agentCounter - 1,
75
+ });
76
+ }
77
+
78
+ agentCompleted(tokens: number): void {
79
+ if (this.currentPhaseIndex >= 0) {
80
+ this.phases[this.currentPhaseIndex].tokens += tokens;
81
+ this.phases[this.currentPhaseIndex].elapsed =
82
+ Date.now() - this.phases[this.currentPhaseIndex].startTime;
83
+ }
84
+ this.emit({
85
+ type: "agent_completed",
86
+ phaseIndex: this.currentPhaseIndex,
87
+ agentIndex: this.agentCounter - 1,
88
+ });
89
+ }
90
+
91
+ agentFailed(agentIndex: number): void {
92
+ this.emit({
93
+ type: "agent_failed",
94
+ phaseIndex: this.currentPhaseIndex,
95
+ agentIndex,
96
+ });
97
+ }
98
+
99
+ formatSummary(): string {
100
+ const phaseInfo =
101
+ this.currentPhaseIndex >= 0
102
+ ? `Phase ${this.currentPhaseIndex + 1}/${this.phases.length}: ${this.phases[this.currentPhaseIndex].title}`
103
+ : "Initializing";
104
+ const totalTokens = this.phases.reduce((sum, p) => sum + p.tokens, 0);
105
+ const elapsed = this.phases.reduce((sum, p) => sum + p.elapsed, 0);
106
+ return `${phaseInfo} | ${this.agentCounter} agents | ${(totalTokens / 1000).toFixed(1)}k tokens | ${Math.round(elapsed / 1000)}s`;
107
+ }
108
+
109
+ getPhaseStates(): WorkflowPhaseState[] {
110
+ return [...this.phases];
111
+ }
112
+
113
+ get totalAgents(): number {
114
+ return this.agentCounter;
115
+ }
116
+
117
+ get totalTokens(): number {
118
+ return this.phases.reduce((sum, p) => sum + p.tokens, 0);
119
+ }
120
+
121
+ onEvent(listener: (event: WorkflowProgressEvent) => void): void {
122
+ this.listeners.push(listener);
123
+ }
124
+
125
+ private emit(
126
+ event: Omit<WorkflowProgressEvent, "runId" | "timestamp">,
127
+ ): void {
128
+ const fullEvent: WorkflowProgressEvent = {
129
+ ...event,
130
+ runId: this.runId,
131
+ timestamp: Date.now(),
132
+ };
133
+ for (const listener of this.listeners) {
134
+ try {
135
+ listener(fullEvent);
136
+ } catch {
137
+ // Swallow listener errors
138
+ }
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,65 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { WorkflowRun } from "./types.js";
4
+
5
+ /**
6
+ * Persists WorkflowRun state to <runDir>/run-state.json.
7
+ * Enables run recovery across process restarts.
8
+ */
9
+ export class RunStateStore {
10
+ constructor(private baseDir: string) {}
11
+
12
+ /** Persist a run's state to disk */
13
+ async save(run: WorkflowRun): Promise<void> {
14
+ const runDir = path.join(this.baseDir, run.runId);
15
+ await fs.promises.mkdir(runDir, { recursive: true });
16
+ const statePath = path.join(runDir, "run-state.json");
17
+ // Omit non-serializable fields
18
+ const { completionPromise, ...serializable } = run;
19
+ void completionPromise;
20
+ await fs.promises.writeFile(
21
+ statePath,
22
+ JSON.stringify(serializable, null, 2),
23
+ "utf-8",
24
+ );
25
+ }
26
+
27
+ /** Load a single run's state from disk */
28
+ async load(runId: string): Promise<WorkflowRun | null> {
29
+ const statePath = path.join(this.baseDir, runId, "run-state.json");
30
+ try {
31
+ const content = await fs.promises.readFile(statePath, "utf-8");
32
+ return JSON.parse(content) as WorkflowRun;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /** List all persisted run IDs */
39
+ async listRuns(): Promise<string[]> {
40
+ try {
41
+ const entries = await fs.promises.readdir(this.baseDir, {
42
+ withFileTypes: true,
43
+ });
44
+ const runIds: string[] = [];
45
+ for (const entry of entries) {
46
+ if (entry.isDirectory() && entry.name.startsWith("wf_")) {
47
+ const statePath = path.join(
48
+ this.baseDir,
49
+ entry.name,
50
+ "run-state.json",
51
+ );
52
+ try {
53
+ await fs.promises.access(statePath);
54
+ runIds.push(entry.name);
55
+ } catch {
56
+ // Directory exists but no run-state.json — skip
57
+ }
58
+ }
59
+ }
60
+ return runIds;
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+ }