wave-agent-sdk 0.17.1 → 0.17.2
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.
- package/builtin/skills/deep-research/SKILL.md +90 -0
- package/builtin/skills/settings/ENV.md +6 -3
- package/dist/agent.d.ts +28 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +128 -34
- package/dist/constants/goalPrompts.d.ts +2 -0
- package/dist/constants/goalPrompts.d.ts.map +1 -0
- package/dist/constants/goalPrompts.js +10 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +1 -0
- package/dist/managers/aiManager.d.ts +7 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +77 -41
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +10 -2
- package/dist/managers/goalManager.d.ts +43 -0
- package/dist/managers/goalManager.d.ts.map +1 -0
- package/dist/managers/goalManager.js +177 -0
- package/dist/managers/messageManager.d.ts +2 -2
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageQueue.d.ts +10 -0
- package/dist/managers/messageQueue.d.ts.map +1 -1
- package/dist/managers/messageQueue.js +53 -1
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +7 -1
- package/dist/managers/skillManager.d.ts +2 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +19 -9
- package/dist/managers/slashCommandManager.d.ts +6 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +105 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +5 -0
- package/dist/managers/workflowManager.d.ts +65 -0
- package/dist/managers/workflowManager.d.ts.map +1 -0
- package/dist/managers/workflowManager.js +380 -0
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +3 -3
- package/dist/services/aiService.d.ts +23 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +102 -9
- package/dist/services/configurationService.d.ts +1 -1
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +3 -16
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +4 -0
- package/dist/services/session.d.ts +9 -1
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +28 -1
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +49 -7
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +1 -1
- package/dist/tools/taskManagementTools.d.ts.map +1 -1
- package/dist/tools/taskManagementTools.js +103 -157
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +0 -9
- package/dist/tools/workflowTool.d.ts +11 -0
- package/dist/tools/workflowTool.d.ts.map +1 -0
- package/dist/tools/workflowTool.js +190 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/commands.d.ts +4 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/config.d.ts +2 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/core.d.ts +1 -1
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +2 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/messaging.d.ts +2 -2
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/processes.d.ts +6 -2
- package/dist/types/processes.d.ts.map +1 -1
- package/dist/types/workflow.d.ts +2 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +1 -0
- package/dist/utils/cacheControlUtils.d.ts +13 -8
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +73 -102
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +7 -0
- package/dist/utils/markdownParser.d.ts.map +1 -1
- package/dist/utils/markdownParser.js +21 -6
- package/dist/utils/messageOperations.d.ts +2 -2
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/notificationXml.d.ts.map +1 -1
- package/dist/workflow/budgetTracker.d.ts +12 -0
- package/dist/workflow/budgetTracker.d.ts.map +1 -0
- package/dist/workflow/budgetTracker.js +30 -0
- package/dist/workflow/concurrencyLimiter.d.ts +14 -0
- package/dist/workflow/concurrencyLimiter.d.ts.map +1 -0
- package/dist/workflow/concurrencyLimiter.js +39 -0
- package/dist/workflow/journal.d.ts +19 -0
- package/dist/workflow/journal.d.ts.map +1 -0
- package/dist/workflow/journal.js +74 -0
- package/dist/workflow/progressReporter.d.ts +21 -0
- package/dist/workflow/progressReporter.d.ts.map +1 -0
- package/dist/workflow/progressReporter.js +118 -0
- package/dist/workflow/runState.d.ts +16 -0
- package/dist/workflow/runState.d.ts.map +1 -0
- package/dist/workflow/runState.js +57 -0
- package/dist/workflow/scriptRuntime.d.ts +35 -0
- package/dist/workflow/scriptRuntime.d.ts.map +1 -0
- package/dist/workflow/scriptRuntime.js +196 -0
- package/dist/workflow/structuredOutput.d.ts +27 -0
- package/dist/workflow/structuredOutput.d.ts.map +1 -0
- package/dist/workflow/structuredOutput.js +106 -0
- package/dist/workflow/types.d.ts +81 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +1 -0
- package/dist/workflow/workflowApis.d.ts +46 -0
- package/dist/workflow/workflowApis.d.ts.map +1 -0
- package/dist/workflow/workflowApis.js +280 -0
- package/package.json +1 -1
- package/src/agent.ts +144 -34
- package/src/constants/goalPrompts.ts +10 -0
- package/src/constants/tools.ts +1 -0
- package/src/managers/aiManager.ts +91 -47
- package/src/managers/backgroundTaskManager.ts +16 -4
- package/src/managers/goalManager.ts +232 -0
- package/src/managers/messageManager.ts +2 -2
- package/src/managers/messageQueue.ts +59 -1
- package/src/managers/pluginManager.ts +8 -1
- package/src/managers/skillManager.ts +20 -9
- package/src/managers/slashCommandManager.ts +119 -0
- package/src/managers/toolManager.ts +7 -0
- package/src/managers/workflowManager.ts +491 -0
- package/src/prompts/index.ts +4 -2
- package/src/services/aiService.ts +166 -12
- package/src/services/configurationService.ts +2 -22
- package/src/services/hook.ts +5 -0
- package/src/services/session.ts +42 -2
- package/src/tools/bashTool.ts +64 -9
- package/src/tools/readTool.ts +1 -2
- package/src/tools/taskManagementTools.ts +146 -195
- package/src/tools/types.ts +2 -0
- package/src/tools/webFetchTool.ts +0 -12
- package/src/tools/workflowTool.ts +205 -0
- package/src/types/agent.ts +6 -0
- package/src/types/commands.ts +4 -0
- package/src/types/config.ts +2 -2
- package/src/types/core.ts +3 -3
- package/src/types/hooks.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/messaging.ts +2 -2
- package/src/types/processes.ts +10 -2
- package/src/types/workflow.ts +5 -0
- package/src/utils/cacheControlUtils.ts +106 -131
- package/src/utils/containerSetup.ts +9 -0
- package/src/utils/markdownParser.ts +26 -8
- package/src/utils/messageOperations.ts +2 -2
- package/src/utils/notificationXml.ts +6 -1
- package/src/workflow/budgetTracker.ts +34 -0
- package/src/workflow/concurrencyLimiter.ts +47 -0
- package/src/workflow/journal.ts +95 -0
- package/src/workflow/progressReporter.ts +141 -0
- package/src/workflow/runState.ts +65 -0
- package/src/workflow/scriptRuntime.ts +274 -0
- package/src/workflow/structuredOutput.ts +123 -0
- package/src/workflow/types.ts +95 -0
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|