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
|
@@ -25,6 +25,7 @@ import type { SkillMetadata } from "../types/skills.js";
|
|
|
25
25
|
import type { SubagentManager } from "./subagentManager.js";
|
|
26
26
|
import type { MemoryService } from "../services/memory.js";
|
|
27
27
|
import type { HookManager } from "./hookManager.js";
|
|
28
|
+
import type { GoalManager } from "./goalManager.js";
|
|
28
29
|
|
|
29
30
|
import { logger } from "../utils/globalLogger.js";
|
|
30
31
|
|
|
@@ -91,15 +92,23 @@ export class SlashCommandManager {
|
|
|
91
92
|
return this.container.get<HookManager>("HookManager");
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
private get goalManager(): GoalManager | undefined {
|
|
96
|
+
return this.container.get<GoalManager>("GoalManager");
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
private initializeBuiltinCommands(): void {
|
|
95
100
|
// Register built-in clear command
|
|
96
101
|
this.registerCommand({
|
|
97
102
|
id: "clear",
|
|
98
103
|
name: "clear",
|
|
99
104
|
description: "Clear conversation history and reset session",
|
|
105
|
+
immediate: true,
|
|
100
106
|
handler: async () => {
|
|
101
107
|
this.aiManager.abortAIMessage();
|
|
102
108
|
|
|
109
|
+
// Clear any active goal
|
|
110
|
+
this.goalManager?.clearGoal();
|
|
111
|
+
|
|
103
112
|
// Capture old session info before clearing
|
|
104
113
|
const oldSessionId = this.messageManager.getSessionId();
|
|
105
114
|
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
@@ -164,6 +173,7 @@ export class SlashCommandManager {
|
|
|
164
173
|
id: "compact",
|
|
165
174
|
name: "compact",
|
|
166
175
|
description: "Compact conversation history to reduce context usage",
|
|
176
|
+
immediate: true,
|
|
167
177
|
handler: async (args?: string, signal?: AbortSignal) => {
|
|
168
178
|
this.aiManager.abortAIMessage();
|
|
169
179
|
|
|
@@ -175,6 +185,103 @@ export class SlashCommandManager {
|
|
|
175
185
|
});
|
|
176
186
|
},
|
|
177
187
|
});
|
|
188
|
+
|
|
189
|
+
// Register built-in goal command
|
|
190
|
+
this.registerCommand({
|
|
191
|
+
id: "goal",
|
|
192
|
+
name: "goal",
|
|
193
|
+
description: "Set, check, or clear an autonomous goal for the session",
|
|
194
|
+
immediate: (args?: string) => {
|
|
195
|
+
const trimmed = args?.trim() ?? "";
|
|
196
|
+
return (
|
|
197
|
+
!trimmed ||
|
|
198
|
+
["clear", "stop", "off", "reset", "none", "cancel"].includes(trimmed)
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
handler: async (args?: string) => {
|
|
202
|
+
const goalManager = this.goalManager;
|
|
203
|
+
if (!goalManager) {
|
|
204
|
+
this.messageManager.addUserMessage({
|
|
205
|
+
content: "Goal manager is not available",
|
|
206
|
+
isMeta: true,
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const trimmed = args?.trim() ?? "";
|
|
212
|
+
|
|
213
|
+
// Clear aliases
|
|
214
|
+
if (
|
|
215
|
+
["clear", "stop", "off", "reset", "none", "cancel"].includes(trimmed)
|
|
216
|
+
) {
|
|
217
|
+
if (goalManager.isGoalActive()) {
|
|
218
|
+
goalManager.clearGoal();
|
|
219
|
+
this.messageManager.addUserMessage({
|
|
220
|
+
content: "<system-reminder>Goal cleared.</system-reminder>",
|
|
221
|
+
isMeta: true,
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
this.messageManager.addUserMessage({
|
|
225
|
+
content:
|
|
226
|
+
"<system-reminder>No active goal to clear.</system-reminder>",
|
|
227
|
+
isMeta: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Show status
|
|
234
|
+
if (!trimmed) {
|
|
235
|
+
if (goalManager.isGoalActive()) {
|
|
236
|
+
this.messageManager.addUserMessage({
|
|
237
|
+
content: `<system-reminder>${goalManager.getStatusString()}</system-reminder>`,
|
|
238
|
+
isMeta: true,
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
this.messageManager.addUserMessage({
|
|
242
|
+
content:
|
|
243
|
+
"<system-reminder>No active goal. Use /goal <condition> to set one.</system-reminder>",
|
|
244
|
+
isMeta: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check plan mode
|
|
251
|
+
const permissionMode = this.container.has("PermissionMode")
|
|
252
|
+
? this.container.get<
|
|
253
|
+
import("../types/permissions.js").PermissionMode
|
|
254
|
+
>("PermissionMode")
|
|
255
|
+
: undefined;
|
|
256
|
+
if (permissionMode === "plan") {
|
|
257
|
+
this.messageManager.addUserMessage({
|
|
258
|
+
content:
|
|
259
|
+
"<system-reminder>Cannot set a goal in plan mode. Exit plan mode first.</system-reminder>",
|
|
260
|
+
isMeta: true,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Set goal
|
|
266
|
+
try {
|
|
267
|
+
goalManager.setGoal(trimmed);
|
|
268
|
+
this.messageManager.addUserMessage({
|
|
269
|
+
content: `<system-reminder>Goal set: ${trimmed}. The agent will work autonomously until this goal is achieved.</system-reminder>`,
|
|
270
|
+
isMeta: true,
|
|
271
|
+
});
|
|
272
|
+
// Add the goal as a user directive to start working
|
|
273
|
+
this.messageManager.addUserMessage({
|
|
274
|
+
content: trimmed,
|
|
275
|
+
});
|
|
276
|
+
this.aiManager.sendAIMessage();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.messageManager.addUserMessage({
|
|
279
|
+
content: `<system-reminder>Failed to set goal: ${(error as Error).message}</system-reminder>`,
|
|
280
|
+
isMeta: true,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
178
285
|
}
|
|
179
286
|
|
|
180
287
|
/**
|
|
@@ -574,6 +681,18 @@ export class SlashCommandManager {
|
|
|
574
681
|
return this.commands.has(commandId);
|
|
575
682
|
}
|
|
576
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Check if a slash command should bypass the message queue when AI is busy.
|
|
686
|
+
* Returns true for commands marked as immediate (boolean or function).
|
|
687
|
+
*/
|
|
688
|
+
public isImmediateCommand(input: string): boolean {
|
|
689
|
+
const { command: commandId, args } = parseSlashCommandInput(input);
|
|
690
|
+
const command = this.commands.get(commandId);
|
|
691
|
+
if (!command?.immediate) return false;
|
|
692
|
+
if (typeof command.immediate === "boolean") return command.immediate;
|
|
693
|
+
return command.immediate(args);
|
|
694
|
+
}
|
|
695
|
+
|
|
577
696
|
/**
|
|
578
697
|
* Get custom command details
|
|
579
698
|
*/
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "../tools/taskManagementTools.js";
|
|
26
26
|
import { enterWorktreeTool } from "../tools/enterWorktreeTool.js";
|
|
27
27
|
import { exitWorktreeTool } from "../tools/exitWorktreeTool.js";
|
|
28
|
+
import { workflowTool } from "../tools/workflowTool.js";
|
|
28
29
|
import { McpManager } from "./mcpManager.js";
|
|
29
30
|
import { PermissionManager } from "./permissionManager.js";
|
|
30
31
|
import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
@@ -132,6 +133,7 @@ class ToolManager {
|
|
|
132
133
|
webFetchTool,
|
|
133
134
|
enterWorktreeTool,
|
|
134
135
|
exitWorktreeTool,
|
|
136
|
+
workflowTool,
|
|
135
137
|
];
|
|
136
138
|
|
|
137
139
|
for (const tool of builtInTools) {
|
|
@@ -246,6 +248,11 @@ class ToolManager {
|
|
|
246
248
|
"HookManager",
|
|
247
249
|
)
|
|
248
250
|
: undefined,
|
|
251
|
+
workflowManager: this.container.has("WorkflowManager")
|
|
252
|
+
? this.container.get<import("./workflowManager.js").WorkflowManager>(
|
|
253
|
+
"WorkflowManager",
|
|
254
|
+
)
|
|
255
|
+
: undefined,
|
|
249
256
|
sessionId: context.sessionId,
|
|
250
257
|
toolCallId: context.toolCallId,
|
|
251
258
|
};
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { Container } from "../utils/container.js";
|
|
6
|
+
import { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
7
|
+
import { NotificationQueue } from "./notificationQueue.js";
|
|
8
|
+
import { SubagentManager } from "./subagentManager.js";
|
|
9
|
+
import { taskNotificationToXml } from "../utils/notificationXml.js";
|
|
10
|
+
import { ConcurrencyLimiter } from "../workflow/concurrencyLimiter.js";
|
|
11
|
+
import { BudgetTracker } from "../workflow/budgetTracker.js";
|
|
12
|
+
import { ProgressReporter } from "../workflow/progressReporter.js";
|
|
13
|
+
import { Journal } from "../workflow/journal.js";
|
|
14
|
+
import { createWorkflowApis } from "../workflow/workflowApis.js";
|
|
15
|
+
import {
|
|
16
|
+
validateScript,
|
|
17
|
+
parseScript,
|
|
18
|
+
executeScript,
|
|
19
|
+
} from "../workflow/scriptRuntime.js";
|
|
20
|
+
import type { WorkflowRun } from "../workflow/types.js";
|
|
21
|
+
import { RunStateStore } from "../workflow/runState.js";
|
|
22
|
+
import { logger } from "../utils/globalLogger.js";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_CONCURRENCY = () =>
|
|
25
|
+
Math.max(1, Math.min(16, os.cpus().length - 2));
|
|
26
|
+
|
|
27
|
+
export class WorkflowManager {
|
|
28
|
+
private runs = new Map<string, WorkflowRun>();
|
|
29
|
+
private abortControllers = new Map<string, AbortController>();
|
|
30
|
+
private agentControllers = new Map<string, Map<number, AbortController>>();
|
|
31
|
+
private container: Container;
|
|
32
|
+
private runStateStore: RunStateStore | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(container: Container) {
|
|
35
|
+
this.container = container;
|
|
36
|
+
// Initialize RunStateStore lazily (sessionDir depends on MessageManager)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private get stateStore(): RunStateStore {
|
|
40
|
+
if (!this.runStateStore) {
|
|
41
|
+
this.runStateStore = new RunStateStore(
|
|
42
|
+
path.join(this.sessionDir, "workflows"),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return this.runStateStore;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private get backgroundTaskManager(): BackgroundTaskManager {
|
|
49
|
+
return this.container.get<BackgroundTaskManager>("BackgroundTaskManager")!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private get notificationQueue(): NotificationQueue {
|
|
53
|
+
return this.container.get<NotificationQueue>("NotificationQueue")!;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private get subagentManager(): SubagentManager {
|
|
57
|
+
return this.container.get<SubagentManager>("SubagentManager")!;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private get workdir(): string {
|
|
61
|
+
return this.container.get<string>("workdir") || process.cwd();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private get sessionDir(): string {
|
|
65
|
+
const messageManager =
|
|
66
|
+
this.container.get<import("./messageManager.js").MessageManager>(
|
|
67
|
+
"MessageManager",
|
|
68
|
+
);
|
|
69
|
+
return (
|
|
70
|
+
messageManager?.getSessionDir() ||
|
|
71
|
+
path.join(os.homedir(), ".wave", "sessions")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a new workflow run from a script string or file path.
|
|
77
|
+
* Persists the script to the session directory.
|
|
78
|
+
*/
|
|
79
|
+
async createRun(
|
|
80
|
+
script: string,
|
|
81
|
+
args?: unknown,
|
|
82
|
+
opts?: { budget?: number | null; resumeFromRunId?: string },
|
|
83
|
+
): Promise<WorkflowRun> {
|
|
84
|
+
// Validate script
|
|
85
|
+
const validation = validateScript(script);
|
|
86
|
+
if (!validation.valid) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Script validation failed:\n${validation.errors.join("\n")}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parse meta for the run object
|
|
93
|
+
const { meta } = parseScript(script);
|
|
94
|
+
|
|
95
|
+
// Validate resumeFromRunId if provided
|
|
96
|
+
if (opts?.resumeFromRunId) {
|
|
97
|
+
const prevRun = this.runs.get(opts.resumeFromRunId);
|
|
98
|
+
if (!prevRun) {
|
|
99
|
+
throw new Error(`Cannot resume: run ${opts.resumeFromRunId} not found`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Generate run ID and persist script
|
|
104
|
+
const runId = `wf_${randomUUID().slice(0, 8)}`;
|
|
105
|
+
const runDir = path.join(this.sessionDir, "workflows", runId);
|
|
106
|
+
await fs.promises.mkdir(path.join(runDir, "agents"), { recursive: true });
|
|
107
|
+
const scriptPath = path.join(runDir, "script.js");
|
|
108
|
+
await fs.promises.writeFile(scriptPath, script, "utf-8");
|
|
109
|
+
|
|
110
|
+
const run: WorkflowRun = {
|
|
111
|
+
runId,
|
|
112
|
+
meta,
|
|
113
|
+
status: "running",
|
|
114
|
+
scriptPath,
|
|
115
|
+
args,
|
|
116
|
+
startTime: Date.now(),
|
|
117
|
+
phases: [],
|
|
118
|
+
totalAgents: 0,
|
|
119
|
+
totalTokens: 0,
|
|
120
|
+
resumeFromRunId: opts?.resumeFromRunId,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.runs.set(runId, run);
|
|
124
|
+
|
|
125
|
+
// Persist run state
|
|
126
|
+
this.stateStore.save(run).catch((err) => {
|
|
127
|
+
logger.warn(
|
|
128
|
+
`[Workflow] Failed to persist run state: ${err instanceof Error ? err.message : String(err)}`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return run;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Start executing a workflow run in the background.
|
|
137
|
+
* Returns immediately; the workflow runs asynchronously.
|
|
138
|
+
*/
|
|
139
|
+
async startRun(
|
|
140
|
+
runId: string,
|
|
141
|
+
opts?: { retryAgentIndex?: number },
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const run = this.runs.get(runId);
|
|
144
|
+
if (!run) throw new Error(`Workflow run ${runId} not found`);
|
|
145
|
+
if (run.status !== "running")
|
|
146
|
+
throw new Error(`Workflow run ${runId} is not in running state`);
|
|
147
|
+
|
|
148
|
+
const abortController = new AbortController();
|
|
149
|
+
this.abortControllers.set(runId, abortController);
|
|
150
|
+
const runAgentControllers = new Map<number, AbortController>();
|
|
151
|
+
this.agentControllers.set(runId, runAgentControllers);
|
|
152
|
+
|
|
153
|
+
// Read script from file
|
|
154
|
+
const script = await fs.promises.readFile(run.scriptPath, "utf-8");
|
|
155
|
+
|
|
156
|
+
// Set up infrastructure
|
|
157
|
+
const concurrencyLimiter = new ConcurrencyLimiter(DEFAULT_CONCURRENCY());
|
|
158
|
+
const budgetTracker = new BudgetTracker(
|
|
159
|
+
run.args &&
|
|
160
|
+
typeof run.args === "object" &&
|
|
161
|
+
"budget" in (run.args as Record<string, unknown>)
|
|
162
|
+
? ((run.args as Record<string, unknown>).budget as number | null)
|
|
163
|
+
: null,
|
|
164
|
+
);
|
|
165
|
+
const progressReporter = new ProgressReporter(run.meta, runId);
|
|
166
|
+
|
|
167
|
+
// Forward progress events via onProgress callback
|
|
168
|
+
const onProgress = (
|
|
169
|
+
event: import("../workflow/types.js").WorkflowProgressEvent,
|
|
170
|
+
) => {
|
|
171
|
+
logger.debug(
|
|
172
|
+
`[Workflow:${runId}] progress: ${event.type} phase=${event.phaseIndex} agent=${event.agentIndex}`,
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
progressReporter.onEvent(onProgress);
|
|
176
|
+
|
|
177
|
+
// Journal — load previous journal if resuming
|
|
178
|
+
let journal: Journal;
|
|
179
|
+
let initialAgentCount = 0;
|
|
180
|
+
if (run.resumeFromRunId || opts?.retryAgentIndex !== undefined) {
|
|
181
|
+
// Try new-format path first, fall back to old format
|
|
182
|
+
const sourceRunId = run.resumeFromRunId || runId;
|
|
183
|
+
const newJournalPath = path.join(
|
|
184
|
+
this.sessionDir,
|
|
185
|
+
"workflows",
|
|
186
|
+
sourceRunId,
|
|
187
|
+
"journal.jsonl",
|
|
188
|
+
);
|
|
189
|
+
const oldJournalPath = path.join(
|
|
190
|
+
this.sessionDir,
|
|
191
|
+
"workflows",
|
|
192
|
+
`journal-${sourceRunId}.jsonl`,
|
|
193
|
+
);
|
|
194
|
+
const journalPath = (await fs.promises
|
|
195
|
+
.access(newJournalPath)
|
|
196
|
+
.then(() => true)
|
|
197
|
+
.catch(() => false))
|
|
198
|
+
? newJournalPath
|
|
199
|
+
: oldJournalPath;
|
|
200
|
+
journal = await Journal.load(journalPath);
|
|
201
|
+
|
|
202
|
+
// When retrying a specific agent, remove its failed entry
|
|
203
|
+
if (opts?.retryAgentIndex !== undefined) {
|
|
204
|
+
journal.removeFailedEntry(opts.retryAgentIndex);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Count existing agent entries to offset the counter
|
|
208
|
+
initialAgentCount = journal.agentEntryCount;
|
|
209
|
+
// Re-open for appending (load() doesn't open the write stream)
|
|
210
|
+
await journal.init();
|
|
211
|
+
logger.info(
|
|
212
|
+
`[Workflow] Resuming from ${sourceRunId} with ${initialAgentCount} cached agent results`,
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
const journalPath = path.join(
|
|
216
|
+
this.sessionDir,
|
|
217
|
+
"workflows",
|
|
218
|
+
runId,
|
|
219
|
+
"journal.jsonl",
|
|
220
|
+
);
|
|
221
|
+
journal = new Journal(journalPath);
|
|
222
|
+
await journal.init();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Register as background task
|
|
226
|
+
const taskId = this.backgroundTaskManager.generateId();
|
|
227
|
+
this.backgroundTaskManager.addTask({
|
|
228
|
+
id: taskId,
|
|
229
|
+
type: "workflow",
|
|
230
|
+
status: "running",
|
|
231
|
+
startTime: Date.now(),
|
|
232
|
+
stdout: "",
|
|
233
|
+
stderr: "",
|
|
234
|
+
description: `Workflow: ${run.meta.name}`,
|
|
235
|
+
runId,
|
|
236
|
+
onStop: () => {
|
|
237
|
+
abortController.abort();
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Create workflow APIs
|
|
242
|
+
const apis = createWorkflowApis({
|
|
243
|
+
subagentManager: this.subagentManager,
|
|
244
|
+
concurrencyLimiter,
|
|
245
|
+
budgetTracker,
|
|
246
|
+
progressReporter,
|
|
247
|
+
journal,
|
|
248
|
+
abortSignal: abortController.signal,
|
|
249
|
+
args: run.args,
|
|
250
|
+
initialAgentCount,
|
|
251
|
+
sessionDir: this.sessionDir,
|
|
252
|
+
runDir: path.join(this.sessionDir, "workflows", runId),
|
|
253
|
+
agentControllers: runAgentControllers,
|
|
254
|
+
onProgress: (event) => {
|
|
255
|
+
logger.debug(
|
|
256
|
+
`[Workflow:${runId}] progress: ${event.type} phase=${event.phaseIndex} agent=${event.agentIndex}`,
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
onLog: (message: string) => {
|
|
260
|
+
logger.info(`[Workflow:${runId}] ${message}`);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Execute in background
|
|
265
|
+
run.completionPromise = (async () => {
|
|
266
|
+
try {
|
|
267
|
+
const { result } = await executeScript(
|
|
268
|
+
script,
|
|
269
|
+
apis,
|
|
270
|
+
abortController.signal,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Update run state
|
|
274
|
+
run.status = "completed";
|
|
275
|
+
run.endTime = Date.now();
|
|
276
|
+
run.result = result;
|
|
277
|
+
run.phases = progressReporter.getPhaseStates();
|
|
278
|
+
run.totalAgents = progressReporter.totalAgents;
|
|
279
|
+
run.totalTokens = progressReporter.totalTokens;
|
|
280
|
+
|
|
281
|
+
// Persist final state
|
|
282
|
+
this.stateStore.save(run).catch(() => {});
|
|
283
|
+
|
|
284
|
+
// Close journal
|
|
285
|
+
await journal.close();
|
|
286
|
+
|
|
287
|
+
// Update background task status
|
|
288
|
+
const task = this.backgroundTaskManager.getTask(taskId);
|
|
289
|
+
if (task) {
|
|
290
|
+
task.status = "completed";
|
|
291
|
+
task.endTime = Date.now();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Enqueue completion notification
|
|
295
|
+
const journalPath = journal.filePath;
|
|
296
|
+
this.notificationQueue.enqueue(
|
|
297
|
+
taskNotificationToXml({
|
|
298
|
+
type: "task_notification",
|
|
299
|
+
taskId: runId,
|
|
300
|
+
taskType: "workflow",
|
|
301
|
+
status: "completed",
|
|
302
|
+
summary: `Workflow "${run.meta.name}" completed — ${run.totalAgents} agents, ${(run.totalTokens / 1000).toFixed(1)}k tokens`,
|
|
303
|
+
...(journalPath && { outputFile: journalPath }),
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
logger.info(
|
|
308
|
+
`[Workflow] Run ${runId} completed: ${run.totalAgents} agents, ${run.totalTokens} tokens`,
|
|
309
|
+
);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
// Only update if stopRun hasn't already set the status
|
|
312
|
+
if (run.status === "running") {
|
|
313
|
+
if (abortController.signal.aborted) {
|
|
314
|
+
run.status = "aborted";
|
|
315
|
+
} else {
|
|
316
|
+
run.status = "failed";
|
|
317
|
+
run.error = error instanceof Error ? error.message : String(error);
|
|
318
|
+
}
|
|
319
|
+
run.endTime = Date.now();
|
|
320
|
+
}
|
|
321
|
+
run.phases = progressReporter.getPhaseStates();
|
|
322
|
+
run.totalAgents = progressReporter.totalAgents;
|
|
323
|
+
run.totalTokens = progressReporter.totalTokens;
|
|
324
|
+
|
|
325
|
+
// Persist failure/abort state
|
|
326
|
+
this.stateStore.save(run).catch(() => {});
|
|
327
|
+
|
|
328
|
+
await journal.close();
|
|
329
|
+
|
|
330
|
+
// Update background task status
|
|
331
|
+
const task = this.backgroundTaskManager.getTask(taskId);
|
|
332
|
+
if (task) {
|
|
333
|
+
task.status = abortController.signal.aborted ? "killed" : "failed";
|
|
334
|
+
task.stderr = run.error || "";
|
|
335
|
+
task.endTime = Date.now();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.notificationQueue.enqueue(
|
|
339
|
+
taskNotificationToXml({
|
|
340
|
+
type: "task_notification",
|
|
341
|
+
taskId: runId,
|
|
342
|
+
taskType: "workflow",
|
|
343
|
+
status: run.status === "aborted" ? "aborted" : "failed",
|
|
344
|
+
summary: `Workflow "${run.meta.name}" ${run.status}${run.error ? `: ${run.error}` : ""}`,
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
logger.warn(
|
|
349
|
+
`[Workflow] Run ${runId} ${run.status}: ${run.error || "aborted"}`,
|
|
350
|
+
);
|
|
351
|
+
} finally {
|
|
352
|
+
this.abortControllers.delete(runId);
|
|
353
|
+
this.agentControllers.delete(runId);
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
|
|
357
|
+
// Don't await — let it run in background
|
|
358
|
+
run.completionPromise.catch(() => {}); // Prevent unhandled rejection
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Resume a workflow run from its journal.
|
|
363
|
+
*/
|
|
364
|
+
async resumeRun(runId: string): Promise<void> {
|
|
365
|
+
const run = this.runs.get(runId);
|
|
366
|
+
if (!run) throw new Error(`Workflow run ${runId} not found`);
|
|
367
|
+
|
|
368
|
+
run.status = "running";
|
|
369
|
+
await this.startRun(runId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Stop a running workflow.
|
|
374
|
+
*/
|
|
375
|
+
stopRun(runId: string): void {
|
|
376
|
+
const controller = this.abortControllers.get(runId);
|
|
377
|
+
if (controller) {
|
|
378
|
+
controller.abort();
|
|
379
|
+
}
|
|
380
|
+
const run = this.runs.get(runId);
|
|
381
|
+
if (run && run.status === "running") {
|
|
382
|
+
run.status = "aborted";
|
|
383
|
+
run.endTime = Date.now();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Skip a specific agent in a running workflow.
|
|
389
|
+
* Aborts the agent's controller and lets the workflow continue.
|
|
390
|
+
*/
|
|
391
|
+
skipAgent(runId: string, agentIndex: number): void {
|
|
392
|
+
const run = this.runs.get(runId);
|
|
393
|
+
if (!run || run.status !== "running") return;
|
|
394
|
+
|
|
395
|
+
const agentController = this.agentControllers.get(runId)?.get(agentIndex);
|
|
396
|
+
if (agentController) {
|
|
397
|
+
agentController.abort();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Retry a specific agent by removing its journal entry and resuming.
|
|
403
|
+
*/
|
|
404
|
+
async retryAgent(runId: string, agentIndex: number): Promise<void> {
|
|
405
|
+
const run = this.runs.get(runId);
|
|
406
|
+
if (!run) throw new Error(`Workflow run ${runId} not found`);
|
|
407
|
+
|
|
408
|
+
// Stop the current execution
|
|
409
|
+
this.stopRun(runId);
|
|
410
|
+
|
|
411
|
+
// The journal will have cached results; when we resume,
|
|
412
|
+
// the agent_failed entry for this index will cause getCachedResult
|
|
413
|
+
// to return undefined, forcing re-execution
|
|
414
|
+
run.status = "running";
|
|
415
|
+
run.error = undefined;
|
|
416
|
+
run.failedAgentIndex = undefined;
|
|
417
|
+
run.failedAgentError = undefined;
|
|
418
|
+
await this.startRun(runId, { retryAgentIndex: agentIndex });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Kill a running workflow — aborts all agent controllers plus the run controller.
|
|
423
|
+
*/
|
|
424
|
+
killRun(runId: string): void {
|
|
425
|
+
// Abort all per-agent controllers
|
|
426
|
+
const agentControllers = this.agentControllers.get(runId);
|
|
427
|
+
if (agentControllers) {
|
|
428
|
+
for (const controller of agentControllers.values()) {
|
|
429
|
+
controller.abort();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Abort the run controller
|
|
434
|
+
const controller = this.abortControllers.get(runId);
|
|
435
|
+
if (controller) {
|
|
436
|
+
controller.abort();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const run = this.runs.get(runId);
|
|
440
|
+
if (run && run.status === "running") {
|
|
441
|
+
run.status = "aborted";
|
|
442
|
+
run.endTime = Date.now();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* List all workflow runs (includes in-memory and persisted).
|
|
448
|
+
*/
|
|
449
|
+
async listRuns(): Promise<WorkflowRun[]> {
|
|
450
|
+
// Load persisted runs that aren't already in memory
|
|
451
|
+
const persistedIds = await this.stateStore.listRuns();
|
|
452
|
+
for (const runId of persistedIds) {
|
|
453
|
+
if (!this.runs.has(runId)) {
|
|
454
|
+
const run = await this.stateStore.load(runId);
|
|
455
|
+
if (run) {
|
|
456
|
+
this.runs.set(runId, run);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return Array.from(this.runs.values());
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get a specific workflow run.
|
|
465
|
+
*/
|
|
466
|
+
getRun(runId: string): WorkflowRun | undefined {
|
|
467
|
+
return this.runs.get(runId);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Clean up all running workflows.
|
|
472
|
+
*/
|
|
473
|
+
cleanup(): void {
|
|
474
|
+
for (const controller of this.abortControllers.values()) {
|
|
475
|
+
controller.abort();
|
|
476
|
+
}
|
|
477
|
+
this.abortControllers.clear();
|
|
478
|
+
for (const agentMap of this.agentControllers.values()) {
|
|
479
|
+
for (const controller of agentMap.values()) {
|
|
480
|
+
controller.abort();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.agentControllers.clear();
|
|
484
|
+
for (const run of this.runs.values()) {
|
|
485
|
+
if (run.status === "running") {
|
|
486
|
+
run.status = "aborted";
|
|
487
|
+
run.endTime = Date.now();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|