wave-agent-sdk 0.17.0 → 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.
Files changed (169) 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/aiService.d.ts +23 -0
  42. package/dist/services/aiService.d.ts.map +1 -1
  43. package/dist/services/aiService.js +102 -9
  44. package/dist/services/configurationService.d.ts +1 -1
  45. package/dist/services/configurationService.d.ts.map +1 -1
  46. package/dist/services/configurationService.js +3 -16
  47. package/dist/services/hook.d.ts.map +1 -1
  48. package/dist/services/hook.js +4 -0
  49. package/dist/services/session.d.ts +9 -1
  50. package/dist/services/session.d.ts.map +1 -1
  51. package/dist/services/session.js +28 -1
  52. package/dist/tools/bashTool.d.ts.map +1 -1
  53. package/dist/tools/bashTool.js +49 -7
  54. package/dist/tools/readTool.d.ts.map +1 -1
  55. package/dist/tools/readTool.js +1 -1
  56. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  57. package/dist/tools/taskManagementTools.js +103 -157
  58. package/dist/tools/types.d.ts +2 -0
  59. package/dist/tools/types.d.ts.map +1 -1
  60. package/dist/tools/webFetchTool.d.ts.map +1 -1
  61. package/dist/tools/webFetchTool.js +0 -9
  62. package/dist/tools/workflowTool.d.ts +11 -0
  63. package/dist/tools/workflowTool.d.ts.map +1 -0
  64. package/dist/tools/workflowTool.js +190 -0
  65. package/dist/types/agent.d.ts +2 -0
  66. package/dist/types/agent.d.ts.map +1 -1
  67. package/dist/types/commands.d.ts +4 -0
  68. package/dist/types/commands.d.ts.map +1 -1
  69. package/dist/types/config.d.ts +2 -2
  70. package/dist/types/config.d.ts.map +1 -1
  71. package/dist/types/core.d.ts +1 -1
  72. package/dist/types/core.d.ts.map +1 -1
  73. package/dist/types/hooks.d.ts +2 -0
  74. package/dist/types/hooks.d.ts.map +1 -1
  75. package/dist/types/index.d.ts +1 -0
  76. package/dist/types/index.d.ts.map +1 -1
  77. package/dist/types/index.js +1 -0
  78. package/dist/types/messaging.d.ts +2 -2
  79. package/dist/types/messaging.d.ts.map +1 -1
  80. package/dist/types/processes.d.ts +6 -2
  81. package/dist/types/processes.d.ts.map +1 -1
  82. package/dist/types/workflow.d.ts +2 -0
  83. package/dist/types/workflow.d.ts.map +1 -0
  84. package/dist/types/workflow.js +1 -0
  85. package/dist/utils/cacheControlUtils.d.ts +13 -8
  86. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  87. package/dist/utils/cacheControlUtils.js +73 -102
  88. package/dist/utils/containerSetup.d.ts.map +1 -1
  89. package/dist/utils/containerSetup.js +7 -0
  90. package/dist/utils/markdownParser.d.ts.map +1 -1
  91. package/dist/utils/markdownParser.js +21 -6
  92. package/dist/utils/messageOperations.d.ts +2 -2
  93. package/dist/utils/messageOperations.d.ts.map +1 -1
  94. package/dist/utils/notificationXml.d.ts.map +1 -1
  95. package/dist/workflow/budgetTracker.d.ts +12 -0
  96. package/dist/workflow/budgetTracker.d.ts.map +1 -0
  97. package/dist/workflow/budgetTracker.js +30 -0
  98. package/dist/workflow/concurrencyLimiter.d.ts +14 -0
  99. package/dist/workflow/concurrencyLimiter.d.ts.map +1 -0
  100. package/dist/workflow/concurrencyLimiter.js +39 -0
  101. package/dist/workflow/journal.d.ts +19 -0
  102. package/dist/workflow/journal.d.ts.map +1 -0
  103. package/dist/workflow/journal.js +74 -0
  104. package/dist/workflow/progressReporter.d.ts +21 -0
  105. package/dist/workflow/progressReporter.d.ts.map +1 -0
  106. package/dist/workflow/progressReporter.js +118 -0
  107. package/dist/workflow/runState.d.ts +16 -0
  108. package/dist/workflow/runState.d.ts.map +1 -0
  109. package/dist/workflow/runState.js +57 -0
  110. package/dist/workflow/scriptRuntime.d.ts +35 -0
  111. package/dist/workflow/scriptRuntime.d.ts.map +1 -0
  112. package/dist/workflow/scriptRuntime.js +196 -0
  113. package/dist/workflow/structuredOutput.d.ts +27 -0
  114. package/dist/workflow/structuredOutput.d.ts.map +1 -0
  115. package/dist/workflow/structuredOutput.js +106 -0
  116. package/dist/workflow/types.d.ts +81 -0
  117. package/dist/workflow/types.d.ts.map +1 -0
  118. package/dist/workflow/types.js +1 -0
  119. package/dist/workflow/workflowApis.d.ts +46 -0
  120. package/dist/workflow/workflowApis.d.ts.map +1 -0
  121. package/dist/workflow/workflowApis.js +280 -0
  122. package/package.json +1 -1
  123. package/src/agent.ts +144 -34
  124. package/src/constants/goalPrompts.ts +10 -0
  125. package/src/constants/tools.ts +1 -0
  126. package/src/managers/aiManager.ts +91 -47
  127. package/src/managers/backgroundTaskManager.ts +16 -4
  128. package/src/managers/goalManager.ts +232 -0
  129. package/src/managers/messageManager.ts +2 -2
  130. package/src/managers/messageQueue.ts +59 -1
  131. package/src/managers/pluginManager.ts +8 -1
  132. package/src/managers/skillManager.ts +20 -9
  133. package/src/managers/slashCommandManager.ts +119 -0
  134. package/src/managers/toolManager.ts +7 -0
  135. package/src/managers/workflowManager.ts +491 -0
  136. package/src/prompts/index.ts +4 -2
  137. package/src/services/aiService.ts +166 -12
  138. package/src/services/configurationService.ts +2 -22
  139. package/src/services/hook.ts +5 -0
  140. package/src/services/session.ts +42 -2
  141. package/src/tools/bashTool.ts +64 -9
  142. package/src/tools/readTool.ts +1 -2
  143. package/src/tools/taskManagementTools.ts +146 -195
  144. package/src/tools/types.ts +2 -0
  145. package/src/tools/webFetchTool.ts +0 -12
  146. package/src/tools/workflowTool.ts +205 -0
  147. package/src/types/agent.ts +6 -0
  148. package/src/types/commands.ts +4 -0
  149. package/src/types/config.ts +2 -2
  150. package/src/types/core.ts +3 -3
  151. package/src/types/hooks.ts +2 -0
  152. package/src/types/index.ts +1 -0
  153. package/src/types/messaging.ts +2 -2
  154. package/src/types/processes.ts +10 -2
  155. package/src/types/workflow.ts +5 -0
  156. package/src/utils/cacheControlUtils.ts +106 -131
  157. package/src/utils/containerSetup.ts +9 -0
  158. package/src/utils/markdownParser.ts +26 -8
  159. package/src/utils/messageOperations.ts +2 -2
  160. package/src/utils/notificationXml.ts +6 -1
  161. package/src/workflow/budgetTracker.ts +34 -0
  162. package/src/workflow/concurrencyLimiter.ts +47 -0
  163. package/src/workflow/journal.ts +95 -0
  164. package/src/workflow/progressReporter.ts +141 -0
  165. package/src/workflow/runState.ts +65 -0
  166. package/src/workflow/scriptRuntime.ts +274 -0
  167. package/src/workflow/structuredOutput.ts +123 -0
  168. package/src/workflow/types.ts +95 -0
  169. package/src/workflow/workflowApis.ts +412 -0
@@ -0,0 +1,280 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { createStructuredOutputPrompt, createStructuredOutputTool, extractStructuredResult, } from "./structuredOutput.js";
4
+ import { AGENT_TOOL_NAME, WORKFLOW_TOOL_NAME } from "../constants/tools.js";
5
+ import { logger } from "../utils/globalLogger.js";
6
+ /**
7
+ * Attempt to parse args if it was passed as a JSON string.
8
+ * LLMs may serialize array/object args as a string (e.g. "[3,8,15,42,7]")
9
+ * which would cause `for...of` to iterate character-by-character.
10
+ */
11
+ function parseArgsIfNeeded(args) {
12
+ if (typeof args === "string") {
13
+ try {
14
+ const parsed = JSON.parse(args);
15
+ // Only accept parsed arrays/objects, not primitives like "42" → 42
16
+ if (typeof parsed === "object" && parsed !== null) {
17
+ return parsed;
18
+ }
19
+ }
20
+ catch {
21
+ // Not valid JSON — use as-is (string)
22
+ }
23
+ }
24
+ return args;
25
+ }
26
+ const MAX_TOTAL_AGENTS = 1000;
27
+ const MAX_ITEMS_PER_CALL = 4096;
28
+ export function createWorkflowApis(ctx) {
29
+ let agentCounter = ctx.initialAgentCount ?? 0;
30
+ const agent = async (prompt, opts) => {
31
+ const index = agentCounter++;
32
+ // Check agent limit
33
+ if (index >= MAX_TOTAL_AGENTS) {
34
+ throw new Error(`Workflow exceeded maximum agent count of ${MAX_TOTAL_AGENTS}`);
35
+ }
36
+ // Check abort
37
+ if (ctx.abortSignal.aborted) {
38
+ return null;
39
+ }
40
+ // Check budget
41
+ if (ctx.budgetTracker.isExceeded()) {
42
+ throw new Error("Workflow token budget exceeded");
43
+ }
44
+ // Check journal for cached result (resume)
45
+ const cached = ctx.journal.getCachedResult(index);
46
+ if (cached !== undefined) {
47
+ logger.debug(`[Workflow] agent(${index}): using cached result`);
48
+ return cached;
49
+ }
50
+ // Acquire concurrency slot
51
+ await ctx.concurrencyLimiter.acquire();
52
+ // Create per-agent abort controller linked to the run's signal
53
+ const agentController = new AbortController();
54
+ ctx.agentControllers.set(index, agentController);
55
+ // If the run is already aborted, abort this agent immediately
56
+ if (ctx.abortSignal.aborted) {
57
+ agentController.abort();
58
+ }
59
+ // Propagate run abort to agent abort
60
+ const onRunAbort = () => agentController.abort();
61
+ ctx.abortSignal.addEventListener("abort", onRunAbort);
62
+ try {
63
+ // Resolve subagent type
64
+ const subagentType = opts?.agentType || "general-purpose";
65
+ let configuration = await ctx.subagentManager.findSubagent(subagentType);
66
+ if (!configuration) {
67
+ logger.warn(`[Workflow] agent(${index}): subagent type "${subagentType}" not found, falling back to general-purpose`);
68
+ configuration =
69
+ await ctx.subagentManager.findSubagent("general-purpose");
70
+ if (!configuration) {
71
+ throw new Error(`No subagent type available for agent call`);
72
+ }
73
+ }
74
+ // Build the effective prompt
75
+ let effectivePrompt = prompt;
76
+ if (opts?.schema) {
77
+ effectivePrompt += createStructuredOutputPrompt(opts.schema);
78
+ }
79
+ // Set phase if specified
80
+ if (opts?.phase) {
81
+ ctx.progressReporter.setPhase(opts.phase);
82
+ }
83
+ ctx.progressReporter.agentStarted();
84
+ // Create subagent instance
85
+ const instance = await ctx.subagentManager.createInstance(configuration, {
86
+ description: opts?.label || `workflow-agent-${index}`,
87
+ prompt: effectivePrompt,
88
+ subagent_type: subagentType,
89
+ model: opts?.model,
90
+ });
91
+ // Capture subagent linkage info
92
+ const subagentId = instance.subagentId;
93
+ const transcriptPath = instance.messageManager.getTranscriptPath();
94
+ // If schema provided, register StructuredOutput tool on the subagent's tool manager
95
+ // and force the model to call it via tool_choice
96
+ if (opts?.schema) {
97
+ const structuredTool = createStructuredOutputTool(opts.schema);
98
+ instance.toolManager.register(structuredTool);
99
+ instance.aiManager.toolChoiceOverride = {
100
+ type: "function",
101
+ function: { name: "StructuredOutput" },
102
+ };
103
+ }
104
+ // Deny Agent and Workflow tools in workflow subagents
105
+ // (prevent infinite recursion)
106
+ instance.permissionManager.addTemporaryRules([
107
+ `${AGENT_TOOL_NAME}:deny`,
108
+ `${WORKFLOW_TOOL_NAME}:deny`,
109
+ ]);
110
+ // Execute agent
111
+ const result = await ctx.subagentManager.executeAgent(instance, effectivePrompt, agentController.signal, false);
112
+ // Track token usage — sum from assistant messages' usage field
113
+ // (getUsages() is empty because onUsageAdded targets the parent agent)
114
+ const messages = instance.messageManager.getMessages();
115
+ const tokens = messages.reduce((sum, msg) => {
116
+ if (msg.role !== "assistant" || !msg.usage)
117
+ return sum;
118
+ const u = msg.usage;
119
+ return (sum +
120
+ (u.total_tokens || 0) +
121
+ (u.cache_read_input_tokens || 0) +
122
+ (u.cache_creation_input_tokens || 0));
123
+ }, 0);
124
+ ctx.budgetTracker.addUsage(tokens);
125
+ ctx.progressReporter.agentCompleted(tokens);
126
+ // Extract structured result if schema was provided
127
+ let finalResult;
128
+ if (opts?.schema) {
129
+ const messages = instance.messageManager.getMessages();
130
+ // Build the format extractStructuredResult expects:
131
+ // Look for StructuredOutput tool calls in ToolBlocks
132
+ const structuredToolBlocks = messages
133
+ .filter((m) => m.role === "assistant")
134
+ .flatMap((m) => m.blocks)
135
+ .filter((b) => b.type === "tool" &&
136
+ b.name === "StructuredOutput" &&
137
+ b.stage === "end");
138
+ if (structuredToolBlocks.length > 0) {
139
+ // Parse the StructuredOutput tool call parameters
140
+ const lastBlock = structuredToolBlocks[structuredToolBlocks.length - 1];
141
+ try {
142
+ const parsed = JSON.parse(lastBlock.parameters || "{}");
143
+ finalResult = parsed;
144
+ }
145
+ catch {
146
+ // Parameters parse failed, try result
147
+ try {
148
+ const parsed = JSON.parse(lastBlock.result || "null");
149
+ finalResult = parsed;
150
+ }
151
+ catch {
152
+ finalResult = result;
153
+ }
154
+ }
155
+ }
156
+ else {
157
+ // Fallback: try extractStructuredResult with message content
158
+ finalResult = extractStructuredResult(messages.map((m) => {
159
+ const textBlock = m.blocks.find((b) => b.type === "text");
160
+ return {
161
+ role: m.role,
162
+ content: textBlock?.content,
163
+ tool_calls: m.tool_calls,
164
+ };
165
+ }), opts.schema);
166
+ if (finalResult === null) {
167
+ // Schema enforcement failed — return the raw text
168
+ finalResult = result;
169
+ }
170
+ }
171
+ }
172
+ else {
173
+ finalResult = result;
174
+ }
175
+ // Append to journal
176
+ ctx.journal.append({
177
+ agentIndex: index,
178
+ prompt,
179
+ opts: { ...opts },
180
+ result: finalResult,
181
+ tokens,
182
+ subagentId,
183
+ transcriptPath,
184
+ });
185
+ // Write agent metadata sidecar
186
+ try {
187
+ const agentMeta = {
188
+ agentType: subagentType,
189
+ subagentId,
190
+ transcriptPath,
191
+ label: opts?.label,
192
+ phase: opts?.phase,
193
+ };
194
+ const metaPath = path.join(ctx.runDir, "agents", `${index}.meta.json`);
195
+ await fs.promises.writeFile(metaPath, JSON.stringify(agentMeta, null, 2), "utf-8");
196
+ }
197
+ catch (metaErr) {
198
+ logger.warn(`[Workflow] Failed to write agent meta for ${index}: ${metaErr instanceof Error ? metaErr.message : String(metaErr)}`);
199
+ }
200
+ // Cleanup
201
+ ctx.subagentManager.cleanupInstance(instance.subagentId);
202
+ return finalResult;
203
+ }
204
+ catch (error) {
205
+ // Agent errors are logged but don't crash the workflow
206
+ // Return null so the caller can filter with .filter(Boolean)
207
+ const errorMsg = error instanceof Error ? error.message : String(error);
208
+ logger.warn(`[Workflow] agent(${index}) failed: ${errorMsg}`);
209
+ // Write agent_failed entry to journal for skip/retry tracking
210
+ ctx.journal.append({
211
+ type: "agent_failed",
212
+ agentIndex: index,
213
+ error: errorMsg,
214
+ });
215
+ // Emit progress event for agent failure
216
+ ctx.progressReporter.agentFailed(index);
217
+ return null;
218
+ }
219
+ finally {
220
+ ctx.agentControllers.delete(index);
221
+ ctx.abortSignal.removeEventListener("abort", onRunAbort);
222
+ ctx.concurrencyLimiter.release();
223
+ }
224
+ };
225
+ const parallel = async (thunks) => {
226
+ if (thunks.length > MAX_ITEMS_PER_CALL) {
227
+ throw new Error(`parallel() accepts at most ${MAX_ITEMS_PER_CALL} items, got ${thunks.length}`);
228
+ }
229
+ // Note: thunks typically call agent() which acquires its own slot,
230
+ // so parallel does NOT acquire a slot per thunk to avoid deadlock.
231
+ const results = await Promise.allSettled(thunks.map(async (thunk) => {
232
+ try {
233
+ return await thunk();
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ }));
239
+ // Convert rejected promises to null
240
+ return results.map((r) => (r.status === "fulfilled" ? r.value : null));
241
+ };
242
+ const pipeline = async (items, ...stages) => {
243
+ if (items.length > MAX_ITEMS_PER_CALL) {
244
+ throw new Error(`pipeline() accepts at most ${MAX_ITEMS_PER_CALL} items, got ${items.length}`);
245
+ }
246
+ // Run each item through all stages, items are independent.
247
+ // Note: agent() inside stages acquires its own concurrency slot,
248
+ // so pipeline does NOT acquire a slot per item to avoid deadlock.
249
+ const results = await Promise.allSettled(items.map(async (item, index) => {
250
+ try {
251
+ let result = undefined;
252
+ for (const stage of stages) {
253
+ result = await stage(result, item, index);
254
+ }
255
+ return result;
256
+ }
257
+ catch (error) {
258
+ logger.warn(`[Workflow] pipeline item ${index} failed: ${error instanceof Error ? error.message : String(error)}`);
259
+ return null;
260
+ }
261
+ }));
262
+ return results.map((r) => (r.status === "fulfilled" ? r.value : null));
263
+ };
264
+ const phase = (title) => {
265
+ ctx.progressReporter.setPhase(title);
266
+ };
267
+ const log = (message) => {
268
+ ctx.journal.appendLog(message);
269
+ ctx.onLog?.(message);
270
+ };
271
+ return {
272
+ agent,
273
+ parallel,
274
+ pipeline,
275
+ phase,
276
+ log,
277
+ args: parseArgsIfNeeded(ctx.args),
278
+ budget: ctx.budgetTracker.toBudgetInfo(),
279
+ };
280
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.ts CHANGED
@@ -8,6 +8,7 @@ import { McpManager } from "./managers/mcpManager.js";
8
8
  import { LspManager } from "./managers/lspManager.js";
9
9
  import { BangManager } from "./managers/bangManager.js";
10
10
  import { CronManager } from "./managers/cronManager.js";
11
+ import { GoalManager } from "./managers/goalManager.js";
11
12
  import { BackgroundTaskManager } from "./managers/backgroundTaskManager.js";
12
13
  import { NotificationQueue } from "./managers/notificationQueue.js";
13
14
  import { MessageQueue, type QueuedMessage } from "./managers/messageQueue.js";
@@ -71,6 +72,7 @@ export class Agent {
71
72
  private pluginManager: PluginManager; // Add plugin manager instance
72
73
  private skillManager: SkillManager; // Add skill manager instance
73
74
  private cronManager: CronManager; // Add cron manager instance
75
+ private goalManager: GoalManager; // Add goal manager instance
74
76
  private hookManager: HookManager; // Add hooks manager instance
75
77
  private reversionManager: ReversionManager;
76
78
  private notificationQueue: NotificationQueue; // Add notification queue instance
@@ -202,6 +204,7 @@ export class Agent {
202
204
  this.pluginManager = this.container.get("PluginManager")!;
203
205
  this.bangManager = this.container.get("BangManager")!;
204
206
  this.cronManager = this.container.get("CronManager")!;
207
+ this.goalManager = this.container.get("GoalManager")!;
205
208
  this.notificationQueue = this.container.get("NotificationQueue")!;
206
209
  this.messageQueue = this.container.get("MessageQueue")!;
207
210
 
@@ -229,39 +232,33 @@ export class Agent {
229
232
  });
230
233
 
231
234
  // Wire up message queue to process when agent becomes idle
232
- this.messageQueue.onMessageEnqueued = () => {
233
- // If the AI is NOT loading and command is not running, trigger dequeue
234
- if (!this.aiManager.isLoading && !this.isCommandRunning) {
235
- this.processQueuedMessage().catch((error) => {
236
- this.logger?.error("Failed to process queued message:", error);
237
- });
238
- }
239
- };
235
+ this.messageQueue.onMessageEnqueued = () => this.tryDequeue();
240
236
 
241
237
  // Wire up AI loading changes to process queue when AI becomes idle
242
238
  this.aiManager.onLoadingChange = (loading: boolean) => {
243
- if (!loading && !this.isCommandRunning) {
244
- this.processQueuedMessage().catch((error) => {
245
- this.logger?.error("Failed to process queued message:", error);
246
- });
247
- }
239
+ if (!loading) this.tryDequeue();
248
240
  };
249
241
 
250
242
  // Wire up bang manager callback for command running changes
251
243
  this.bangManager.onCommandRunningChange = (running: boolean) => {
252
244
  this.options.callbacks?.onCommandRunningChange?.(running);
253
- // When command stops and AI is idle, process queue
254
- if (!running && !this.aiManager.isLoading) {
255
- this.processQueuedMessage().catch((error) => {
256
- this.logger?.error("Failed to process queued message:", error);
257
- });
258
- }
245
+ if (!running) this.tryDequeue();
259
246
  };
260
247
 
261
248
  // Set initial permission mode if provided
262
249
  if (options.permissionMode) {
263
250
  this.setPermissionMode(options.permissionMode);
264
251
  }
252
+
253
+ // Wire up goal state change callback
254
+ this.goalManager.setOnGoalStateChange((active, condition, elapsed) => {
255
+ this.options.callbacks?.onGoalStateChange?.(active, condition, elapsed);
256
+ });
257
+
258
+ // Wire up goal evaluating callback
259
+ this.goalManager.setOnGoalEvaluating((evaluating) => {
260
+ this.options.callbacks?.onGoalEvaluating?.(evaluating);
261
+ });
265
262
  }
266
263
 
267
264
  // Public getter methods
@@ -343,6 +340,16 @@ export class Agent {
343
340
  return this.messageQueue.getQueue();
344
341
  }
345
342
 
343
+ /** Get goal status string */
344
+ public get goalStatus(): string {
345
+ return this.goalManager.getStatusString();
346
+ }
347
+
348
+ /** Check if a goal is active */
349
+ public get isGoalActive(): boolean {
350
+ return this.goalManager.isGoalActive();
351
+ }
352
+
346
353
  /**
347
354
  * Remove a queued message by index
348
355
  * @param index - The index of the message to remove
@@ -356,14 +363,60 @@ export class Agent {
356
363
  return removed;
357
364
  }
358
365
 
366
+ /**
367
+ * Recall the last editable queued message (for inline editing)
368
+ * @returns The recalled message, or null if no editable message exists
369
+ */
370
+ public recallQueuedMessage(): QueuedMessage | null {
371
+ const msg = this.messageQueue.popLastEditable();
372
+ if (msg) {
373
+ this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
374
+ }
375
+ return msg;
376
+ }
377
+
378
+ /**
379
+ * Remove a queued message by its ID
380
+ * @param id - The ID of the message to remove
381
+ * @returns true if the message was removed, false if not found
382
+ */
383
+ public removeQueuedMessageById(id: string): boolean {
384
+ const removed = this.messageQueue.removeById(id);
385
+ if (removed) {
386
+ this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
387
+ }
388
+ return removed;
389
+ }
390
+
391
+ /**
392
+ * Unified dequeue trigger — checks state machine before processing.
393
+ * Called from onMessageEnqueued, onLoadingChange(false), and
394
+ * onCommandRunningChange(false).
395
+ */
396
+ private tryDequeue(): void {
397
+ if (this.messageQueue.state !== "idle") return;
398
+ if (!this.messageQueue.hasPending()) return;
399
+ if (this.aiManager.isLoading || this.isCommandRunning) return;
400
+
401
+ this.messageQueue.transitionTo("dispatching");
402
+ this.processQueuedMessage()
403
+ .catch((error) => {
404
+ this.logger?.error("Failed to process queued message:", error);
405
+ })
406
+ .finally(() => {
407
+ this.messageQueue.transitionTo("idle");
408
+ this.tryDequeue(); // Re-check after processing
409
+ });
410
+ }
411
+
359
412
  /**
360
413
  * Process the next queued message when the agent becomes idle.
361
- * Dequeues the next message and handles it based on type.
362
414
  */
363
415
  private async processQueuedMessage(): Promise<void> {
364
416
  const next = this.messageQueue.dequeue();
365
417
  if (!next) return;
366
418
 
419
+ this.messageQueue.transitionTo("running");
367
420
  this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
368
421
 
369
422
  if (next.type === "bang") {
@@ -414,6 +467,40 @@ export class Agent {
414
467
  return this.backgroundTaskManager.stopTask(id);
415
468
  }
416
469
 
470
+ /** Get all workflow runs */
471
+ public async getWorkflowRuns(): Promise<
472
+ import("./workflow/types.js").WorkflowRun[]
473
+ > {
474
+ if (!this.container.has("WorkflowManager")) return [];
475
+ return this.container
476
+ .get<
477
+ import("./managers/workflowManager.js").WorkflowManager
478
+ >("WorkflowManager")!
479
+ .listRuns();
480
+ }
481
+
482
+ /** Get a specific workflow run by ID */
483
+ public getWorkflowRun(
484
+ runId: string,
485
+ ): import("./workflow/types.js").WorkflowRun | undefined {
486
+ if (!this.container.has("WorkflowManager")) return undefined;
487
+ return this.container
488
+ .get<
489
+ import("./managers/workflowManager.js").WorkflowManager
490
+ >("WorkflowManager")!
491
+ .getRun(runId);
492
+ }
493
+
494
+ /** Stop a workflow run */
495
+ public stopWorkflowRun(runId: string): void {
496
+ if (!this.container.has("WorkflowManager")) return;
497
+ this.container
498
+ .get<
499
+ import("./managers/workflowManager.js").WorkflowManager
500
+ >("WorkflowManager")!
501
+ .stopRun(runId);
502
+ }
503
+
417
504
  /**
418
505
  * Static async factory method for creating Agent instances
419
506
  *
@@ -518,12 +605,18 @@ export class Agent {
518
605
  });
519
606
 
520
607
  // Log session_start event
521
- const modelConfig = this.getModelConfig();
522
- logOTelEvent("session_start", {
523
- sessionId: this.messageManager.getSessionId(),
524
- model: modelConfig.model,
525
- workdir: this.workdir,
526
- }).catch(() => {}); // Non-blocking
608
+ try {
609
+ const modelConfig = this.getModelConfig();
610
+ if (modelConfig.model) {
611
+ logOTelEvent("session_start", {
612
+ sessionId: this.messageManager.getSessionId(),
613
+ model: modelConfig.model,
614
+ workdir: this.workdir,
615
+ }).catch(() => {}); // Non-blocking
616
+ }
617
+ } catch {
618
+ // Model not configured yet - will be available after SSO login
619
+ }
527
620
  }
528
621
 
529
622
  /**
@@ -597,10 +690,13 @@ export class Agent {
597
690
 
598
691
  /** Unified interrupt method, interrupts both AI messages and command execution */
599
692
  public abortMessage(): void {
600
- // Clear queue first to prevent processQueuedMessage from dequeuing
601
- // when abortAIMessage triggers onLoadingChange(false)
602
- this.messageQueue.clear();
603
- this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
693
+ if (this.aiManager.isLoading || this.isCommandRunning) {
694
+ // Clear queue first to prevent processQueuedMessage from dequeuing
695
+ // when abortAIMessage triggers onLoadingChange(false)
696
+ this.messageQueue.clear();
697
+ this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
698
+ }
699
+ this.messageQueue.transitionTo("idle"); // Reset state on abort
604
700
  this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
605
701
  this.abortBashCommand();
606
702
  this.abortSlashCommand();
@@ -796,11 +892,25 @@ export class Agent {
796
892
  content: string,
797
893
  images?: Array<{ path: string; mimeType: string }>,
798
894
  ): Promise<void> {
799
- // If the agent is busy, enqueue the message
895
+ // If the agent is busy, enqueue the message — unless it's an immediate
896
+ // slash command (e.g., /goal clear, /clear, /compact) that should execute
897
+ // right away even while AI is processing
800
898
  if (this.aiManager.isLoading || this.isCommandRunning) {
801
- this.messageQueue.enqueue({ content, images });
802
- this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
803
- return;
899
+ const trimmed = content.trim();
900
+ const isImmediate =
901
+ trimmed.startsWith("/") &&
902
+ trimmed !== "/" &&
903
+ this.slashCommandManager.isImmediateCommand(trimmed);
904
+
905
+ if (!isImmediate) {
906
+ this.messageQueue.enqueue({
907
+ content,
908
+ images,
909
+ });
910
+ this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
911
+ return;
912
+ }
913
+ // Immediate slash command: fall through to InteractionService
804
914
  }
805
915
 
806
916
  await InteractionService.sendMessage(
@@ -0,0 +1,10 @@
1
+ export const GOAL_EVALUATION_SYSTEM_PROMPT = `You are a goal evaluator. Given a goal condition and a conversation, determine whether the goal has been achieved.
2
+
3
+ Rules:
4
+ - Judge ONLY based on what the conversation shows — tool outputs, test results, file contents, etc.
5
+ - Do NOT assume work is done without evidence in the conversation.
6
+ - Be strict and conservative: when uncertain, return met: false.
7
+ - Be concise: your reason should be 1-2 sentences.
8
+
9
+ Respond with JSON only, no other text:
10
+ {"met": boolean, "reason": "short explanation"}`;
@@ -21,3 +21,4 @@ export const CRON_LIST_TOOL_NAME = "CronList";
21
21
  export const WEB_FETCH_TOOL_NAME = "WebFetch";
22
22
  export const ENTER_WORKTREE_TOOL_NAME = "EnterWorktree";
23
23
  export const EXIT_WORKTREE_TOOL_NAME = "ExitWorktree";
24
+ export const WORKFLOW_TOOL_NAME = "Workflow";