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
@@ -76,6 +76,12 @@ export class AIManager {
76
76
  private originalWorkdir: string;
77
77
  private consecutiveCompactionFailures: number = 0;
78
78
  private readonly maxTurns?: number;
79
+ /** Override tool_choice for this AI manager (e.g. for structured output) */
80
+ public toolChoiceOverride?:
81
+ | "auto"
82
+ | "none"
83
+ | "required"
84
+ | { type: "function"; function: { name: string } };
79
85
 
80
86
  // Service overrides
81
87
  constructor(
@@ -613,12 +619,7 @@ export class AIManager {
613
619
  if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
614
620
  }
615
621
 
616
- // 2. Working directory
617
- contextParts.push(
618
- `\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`,
619
- );
620
-
621
- // 3. Plan mode context
622
+ // 2. Plan mode context
622
623
  const currentMode = this.permissionManager?.getCurrentEffectiveMode(
623
624
  this.getModelConfig().permissionMode,
624
625
  );
@@ -894,6 +895,7 @@ export class AIManager {
894
895
  filteredToolPlugins,
895
896
  {
896
897
  workdir: this.getWorkdir(),
898
+ originalWorkdir: this.getOriginalWorkdir(),
897
899
  memory: combinedMemory,
898
900
  language: this.getLanguage(),
899
901
  isSubagent: !!this.subagentType,
@@ -902,6 +904,7 @@ export class AIManager {
902
904
  },
903
905
  ), // Pass custom system prompt
904
906
  maxTokens: maxTokens, // Pass max tokens override
907
+ toolChoice: this.toolChoiceOverride, // Pass tool_choice override
905
908
  };
906
909
 
907
910
  // Add streaming callbacks only if streaming is enabled
@@ -943,35 +946,13 @@ export class AIManager {
943
946
  };
944
947
  }
945
948
 
946
- startLLMRequestSpan(model || this.getModelConfig().model);
947
- const apiStartTime = Date.now();
948
- let ttftMs: number | undefined;
949
-
950
- // If streaming, track TTFT via callback
951
- if (this.stream && callAgentOptions.onContentUpdate) {
952
- const originalOnContentUpdate = callAgentOptions.onContentUpdate;
953
- let firstTokenReceived = false;
954
- callAgentOptions.onContentUpdate = (content: string) => {
955
- if (!firstTokenReceived) {
956
- ttftMs = Date.now() - apiStartTime;
957
- firstTokenReceived = true;
958
- }
959
- originalOnContentUpdate(content);
960
- };
961
- }
949
+ startLLMRequestSpan(model || this.getModelConfig().model || "");
962
950
 
963
951
  const result = await aiService.callAgent(callAgentOptions);
964
- const ttltMs = Date.now() - apiStartTime;
965
952
 
966
953
  // End LLM span with usage data
967
954
  endLLMRequestSpan({
968
- model: model || this.getModelConfig().model,
969
- inputTokens: result.usage?.prompt_tokens,
970
- outputTokens: result.usage?.completion_tokens,
971
- cacheReadTokens: result.usage?.cache_read_input_tokens,
972
- cacheCreationTokens: result.usage?.cache_creation_input_tokens,
973
- ttftMs,
974
- ttltMs,
955
+ model: model || this.getModelConfig().model || "",
975
956
  success: true,
976
957
  hasToolCall: !!(result.tool_calls && result.tool_calls.length > 0),
977
958
  });
@@ -1232,7 +1213,7 @@ export class AIManager {
1232
1213
  (toolResult.error ? `Error: ${toolResult.error}` : "");
1233
1214
  if (jsonRecovered) {
1234
1215
  toolResultContent +=
1235
- "\n\n⚠️ Tool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
1216
+ "\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
1236
1217
  }
1237
1218
 
1238
1219
  // Update message state - tool execution completed
@@ -1407,7 +1388,7 @@ export class AIManager {
1407
1388
  } catch (error) {
1408
1389
  // End LLM span with error
1409
1390
  endLLMRequestSpan({
1410
- model: model || this.getModelConfig().model,
1391
+ model: model || this.getModelConfig().model || "",
1411
1392
  success: false,
1412
1393
  error: error instanceof Error ? error.message : String(error),
1413
1394
  });
@@ -1480,23 +1461,85 @@ export class AIManager {
1480
1461
  }
1481
1462
  }
1482
1463
 
1483
- const shouldContinue = await this.executeStopHooks();
1464
+ // Goal evaluation supersedes Stop hooks when active
1465
+ const goalManager = this.container.has("GoalManager")
1466
+ ? this.container.get<import("./goalManager.js").GoalManager>(
1467
+ "GoalManager",
1468
+ )
1469
+ : undefined;
1470
+
1471
+ let goalContinuing = false;
1472
+
1473
+ if (goalManager?.isGoalActive() && !this.subagentType) {
1474
+ // 1. Increment turn count and check circuit breakers
1475
+ goalManager.incrementTurnCount();
1476
+ const circuitBreaker = goalManager.checkCircuitBreakers();
1477
+
1478
+ if (circuitBreaker) {
1479
+ goalManager.clearGoal();
1480
+ logger?.info(`[Goal] ${circuitBreaker}`);
1481
+ this.messageManager.addUserMessage({
1482
+ content: `<system-reminder>${circuitBreaker}</system-reminder>`,
1483
+ isMeta: true,
1484
+ });
1485
+ // Fall through to normal Stop hooks on the final turn
1486
+ } else {
1487
+ // 2. Evaluate goal
1488
+ const evaluation = await goalManager.evaluateGoal(
1489
+ abortController.signal,
1490
+ );
1484
1491
 
1485
- // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
1486
- // restart the AI conversation cycle
1487
- if (shouldContinue) {
1488
- logger?.info(
1489
- `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
1490
- );
1492
+ if (evaluation.isMet) {
1493
+ goalManager.clearGoal();
1494
+ logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
1495
+ this.messageManager.addUserMessage({
1496
+ content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
1497
+ isMeta: true,
1498
+ });
1499
+ // Fall through to normal Stop hooks on the final turn
1500
+ } else {
1501
+ const goal = goalManager.getGoal()!;
1502
+ goal.lastReason = evaluation.reason;
1503
+ logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
1504
+ this.messageManager.addUserMessage({
1505
+ content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
1506
+ isMeta: true,
1507
+ });
1508
+ // Keep loading state active to prevent UI flicker
1509
+ this.setIsLoading(true);
1510
+ goalContinuing = true;
1511
+ await this.sendAIMessage({
1512
+ recursionDepth: 0,
1513
+ model,
1514
+ allowedRules,
1515
+ maxTokens,
1516
+ });
1517
+ }
1518
+ }
1519
+ }
1491
1520
 
1492
- // Restart the conversation to let AI fix the issues
1493
- // Use recursionDepth = 0 to set loading false again for continuation
1494
- await this.sendAIMessage({
1495
- recursionDepth: 0,
1496
- model,
1497
- allowedRules,
1498
- maxTokens,
1499
- });
1521
+ // Skip Stop hooks when goal evaluator is continuing the conversation
1522
+ if (goalContinuing) {
1523
+ // Goal evaluator supersedes Stop hooks
1524
+ } else {
1525
+ const shouldContinue = await this.executeStopHooks();
1526
+
1527
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
1528
+ // restart the AI conversation cycle
1529
+ if (shouldContinue) {
1530
+ logger?.info(
1531
+ `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
1532
+ );
1533
+
1534
+ // Restart the conversation to let AI fix the issues
1535
+ // Use recursionDepth = 0 to set loading false again for continuation
1536
+ await this.sendAIMessage({
1537
+ recursionDepth: 0,
1538
+ model,
1539
+ allowedRules,
1540
+ maxTokens,
1541
+ });
1542
+ }
1500
1543
  }
1501
1544
  }
1502
1545
  }
@@ -1688,6 +1731,7 @@ export class AIManager {
1688
1731
  toolInput,
1689
1732
  toolResponse,
1690
1733
  subagentType: this.subagentType, // Include subagent type in hook context
1734
+ planFilePath: this.permissionManager?.getPlanFilePath(),
1691
1735
  env:
1692
1736
  this.container.get<Record<string, string>>("MergedEnv") ||
1693
1737
  (process.env as Record<string, string>),
@@ -96,8 +96,14 @@ export class BackgroundTaskManager {
96
96
  if (child.pid && !child.killed) {
97
97
  try {
98
98
  process.kill(-child.pid, "SIGKILL");
99
- } catch (error) {
100
- logger.error("Failed to force kill process:", error);
99
+ } catch (error: unknown) {
100
+ // ESRCH means the process already exited — not an error
101
+ if (
102
+ !(error instanceof Error) ||
103
+ (error as NodeJS.ErrnoException).code !== "ESRCH"
104
+ ) {
105
+ logger.error("Failed to force kill process:", error);
106
+ }
101
107
  }
102
108
  }
103
109
  }, 1000);
@@ -267,8 +273,14 @@ export class BackgroundTaskManager {
267
273
  if (child.pid && !child.killed) {
268
274
  try {
269
275
  process.kill(-child.pid, "SIGKILL");
270
- } catch (error) {
271
- logger.error("Failed to force kill process:", error);
276
+ } catch (error: unknown) {
277
+ // ESRCH means the process already exited — not an error
278
+ if (
279
+ !(error instanceof Error) ||
280
+ (error as NodeJS.ErrnoException).code !== "ESRCH"
281
+ ) {
282
+ logger.error("Failed to force kill process:", error);
283
+ }
272
284
  }
273
285
  }
274
286
  }, 1000);
@@ -0,0 +1,232 @@
1
+ import { Container } from "../utils/container.js";
2
+ import type { MessageManager } from "./messageManager.js";
3
+ import type { AIManager } from "./aiManager.js";
4
+ import type { Usage } from "../types/index.js";
5
+ import { evaluateGoal as aiEvaluateGoal } from "../services/aiService.js";
6
+ import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
7
+ import { logger } from "../utils/globalLogger.js";
8
+
9
+ export interface GoalState {
10
+ condition: string;
11
+ startedAt: number;
12
+ turnCount: number;
13
+ tokenBaseline: number;
14
+ lastReason?: string;
15
+ consecutiveEvalFailures: number;
16
+ }
17
+
18
+ const MAX_GOAL_TURNS = 50;
19
+ const MAX_GOAL_DURATION_MS = 30 * 60 * 1000; // 30 minutes
20
+ const MAX_CONSECUTIVE_EVAL_FAILURES = 3;
21
+ const MAX_CONDITION_LENGTH = 4000;
22
+
23
+ export class GoalManager {
24
+ private state: GoalState | null = null;
25
+ private onGoalStateChange?: (
26
+ active: boolean,
27
+ condition?: string,
28
+ elapsed?: string,
29
+ ) => void;
30
+ private onGoalEvaluating?: (evaluating: boolean) => void;
31
+
32
+ constructor(private container: Container) {}
33
+
34
+ private get messageManager(): MessageManager {
35
+ return this.container.get<MessageManager>("MessageManager")!;
36
+ }
37
+
38
+ private get aiManager(): AIManager {
39
+ return this.container.get<AIManager>("AIManager")!;
40
+ }
41
+
42
+ public setOnGoalStateChange(
43
+ callback: (active: boolean, condition?: string, elapsed?: string) => void,
44
+ ): void {
45
+ this.onGoalStateChange = callback;
46
+ }
47
+
48
+ public setOnGoalEvaluating(callback: (evaluating: boolean) => void): void {
49
+ this.onGoalEvaluating = callback;
50
+ }
51
+
52
+ public setGoal(condition: string): void {
53
+ if (condition.length > MAX_CONDITION_LENGTH) {
54
+ throw new Error(
55
+ `Goal condition exceeds maximum length of ${MAX_CONDITION_LENGTH} characters`,
56
+ );
57
+ }
58
+
59
+ const totalTokens = this.messageManager.getLatestTotalTokens?.() ?? 0;
60
+
61
+ this.state = {
62
+ condition,
63
+ startedAt: Date.now(),
64
+ turnCount: 0,
65
+ tokenBaseline: totalTokens,
66
+ consecutiveEvalFailures: 0,
67
+ };
68
+
69
+ this.onGoalStateChange?.(true, condition, "0m");
70
+ logger?.info(`[Goal] Set goal: ${condition}`);
71
+ }
72
+
73
+ public clearGoal(): void {
74
+ if (this.state) {
75
+ logger?.info(`[Goal] Cleared goal: ${this.state.condition}`);
76
+ this.state = null;
77
+ this.onGoalStateChange?.(false);
78
+ }
79
+ }
80
+
81
+ public getGoal(): GoalState | null {
82
+ return this.state;
83
+ }
84
+
85
+ public isGoalActive(): boolean {
86
+ return this.state !== null;
87
+ }
88
+
89
+ public incrementTurnCount(): void {
90
+ if (this.state) {
91
+ this.state.turnCount++;
92
+ }
93
+ }
94
+
95
+ public getStatusString(): string {
96
+ if (!this.state) return "No active goal";
97
+ const elapsed = this.formatElapsed(Date.now() - this.state.startedAt);
98
+ let status = `Goal: ${this.state.condition}\nElapsed: ${elapsed}\nTurns: ${this.state.turnCount}`;
99
+ if (this.state.lastReason) {
100
+ status += `\nLast evaluation: ${this.state.lastReason}`;
101
+ }
102
+ return status;
103
+ }
104
+
105
+ /**
106
+ * Check circuit breakers. Returns a clear reason if goal should be force-cleared, null otherwise.
107
+ */
108
+ public checkCircuitBreakers(): string | null {
109
+ if (!this.state) return null;
110
+
111
+ if (this.state.turnCount >= MAX_GOAL_TURNS) {
112
+ return `Goal cancelled: maximum turns (${MAX_GOAL_TURNS}) exceeded`;
113
+ }
114
+
115
+ if (Date.now() - this.state.startedAt >= MAX_GOAL_DURATION_MS) {
116
+ return `Goal cancelled: time limit (${MAX_GOAL_DURATION_MS / 60000} minutes) exceeded`;
117
+ }
118
+
119
+ if (this.state.consecutiveEvalFailures >= MAX_CONSECUTIVE_EVAL_FAILURES) {
120
+ return `Goal cancelled: ${MAX_CONSECUTIVE_EVAL_FAILURES} consecutive evaluation failures`;
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Evaluate whether the goal has been met using the fast model.
128
+ */
129
+ public async evaluateGoal(abortSignal?: AbortSignal): Promise<{
130
+ isMet: boolean;
131
+ reason: string;
132
+ }> {
133
+ if (!this.state) {
134
+ return { isMet: false, reason: "No active goal" };
135
+ }
136
+
137
+ try {
138
+ const messages = this.messageManager.getMessages();
139
+ const apiMessages = convertMessagesForAPI(messages);
140
+ const gatewayConfig = this.aiManager.getGatewayConfig();
141
+ const modelConfig = this.aiManager.getModelConfig();
142
+ const fastModel = modelConfig.fastModel || modelConfig.model;
143
+ if (!fastModel) {
144
+ return {
145
+ isMet: false,
146
+ reason: "No model configured for goal evaluation",
147
+ };
148
+ }
149
+
150
+ this.onGoalEvaluating?.(true);
151
+ const result = await aiEvaluateGoal({
152
+ gatewayConfig,
153
+ modelConfig,
154
+ model: fastModel,
155
+ goalCondition: this.state.condition,
156
+ messages: apiMessages,
157
+ abortSignal,
158
+ });
159
+
160
+ // Track evaluation tokens separately
161
+ if (result.usage) {
162
+ const usage: Usage = {
163
+ ...result.usage,
164
+ operation_type: "goal_evaluation",
165
+ model: fastModel,
166
+ };
167
+ this.messageManager.addUsage(usage);
168
+ }
169
+
170
+ // Reset failure counter on success
171
+ this.state.consecutiveEvalFailures = 0;
172
+
173
+ this.onGoalEvaluating?.(false);
174
+
175
+ // Parse the response
176
+ return this.parseEvaluationResponse(result.content);
177
+ } catch (error) {
178
+ this.onGoalEvaluating?.(false);
179
+ this.state.consecutiveEvalFailures++;
180
+ logger?.warn(
181
+ `[Goal] Evaluation failed (${this.state.consecutiveEvalFailures}/${MAX_CONSECUTIVE_EVAL_FAILURES}): ${(error as Error).message}`,
182
+ );
183
+ return {
184
+ isMet: false,
185
+ reason: `Evaluation failed: ${(error as Error).message}`,
186
+ };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Parse the evaluation response from the fast model.
192
+ */
193
+ private parseEvaluationResponse(content: string): {
194
+ isMet: boolean;
195
+ reason: string;
196
+ } {
197
+ // Try direct JSON parse
198
+ try {
199
+ const parsed = JSON.parse(content);
200
+ if (typeof parsed.met === "boolean") {
201
+ return {
202
+ isMet: parsed.met,
203
+ reason: parsed.reason || "No reason provided",
204
+ };
205
+ }
206
+ } catch {
207
+ // Fall through to regex
208
+ }
209
+
210
+ // Try regex extraction
211
+ const metMatch = content.match(/"met"\s*:\s*(true|false)/);
212
+ const reasonMatch = content.match(/"reason"\s*:\s*"([^"]*)"/);
213
+ if (metMatch) {
214
+ return {
215
+ isMet: metMatch[1] === "true",
216
+ reason: reasonMatch?.[1] || "No reason provided",
217
+ };
218
+ }
219
+
220
+ // Default: not met
221
+ return { isMet: false, reason: "Could not parse evaluation response" };
222
+ }
223
+
224
+ private formatElapsed(ms: number): string {
225
+ const minutes = Math.floor(ms / 60000);
226
+ if (minutes < 1) return "<1m";
227
+ if (minutes < 60) return `${minutes}m`;
228
+ const hours = Math.floor(minutes / 60);
229
+ const remainingMin = minutes % 60;
230
+ return `${hours}h${remainingMin}m`;
231
+ }
232
+ }
@@ -62,8 +62,8 @@ export interface MessageManagerCallbacks {
62
62
  // Notification callback
63
63
  onNotificationMessageAdded?: (params: {
64
64
  taskId: string;
65
- taskType: "shell" | "agent";
66
- status: "completed" | "failed" | "killed";
65
+ taskType: "shell" | "agent" | "workflow";
66
+ status: "completed" | "failed" | "killed" | "aborted";
67
67
  summary: string;
68
68
  }) => void;
69
69
  }
@@ -1,16 +1,42 @@
1
+ export type QueueState = "idle" | "dispatching" | "running";
2
+
1
3
  export interface QueuedMessage {
4
+ id?: string;
2
5
  type?: "message" | "bang";
3
6
  content: string;
4
7
  images?: Array<{ path: string; mimeType: string }>;
5
8
  longTextMap?: Record<string, string>;
9
+ editable?: boolean; // default true
6
10
  }
7
11
 
8
12
  export class MessageQueue {
9
13
  private queue: QueuedMessage[] = [];
14
+ private nextId = 0;
15
+ private _state: QueueState = "idle";
10
16
  onMessageEnqueued?: () => void;
11
17
 
18
+ get state(): QueueState {
19
+ return this._state;
20
+ }
21
+
22
+ transitionTo(newState: QueueState): boolean {
23
+ const valid: Record<QueueState, QueueState[]> = {
24
+ idle: ["dispatching"],
25
+ dispatching: ["running", "idle"],
26
+ running: ["idle"],
27
+ };
28
+ if (!valid[this._state].includes(newState)) return false;
29
+ this._state = newState;
30
+ return true;
31
+ }
32
+
12
33
  enqueue(message: QueuedMessage): void {
13
- this.queue.push(message);
34
+ const msg: QueuedMessage = {
35
+ ...message,
36
+ id: message.id || `mq-${this.nextId++}`,
37
+ editable: message.editable ?? true,
38
+ };
39
+ this.queue.push(msg);
14
40
  this.onMessageEnqueued?.();
15
41
  }
16
42
 
@@ -20,6 +46,7 @@ export class MessageQueue {
20
46
 
21
47
  clear(): void {
22
48
  this.queue = [];
49
+ this._state = "idle";
23
50
  }
24
51
 
25
52
  hasPending(): boolean {
@@ -38,4 +65,35 @@ export class MessageQueue {
38
65
  this.onMessageEnqueued?.();
39
66
  return true;
40
67
  }
68
+
69
+ removeById(id: string): boolean {
70
+ const index = this.queue.findIndex((m) => m.id === id);
71
+ if (index === -1) return false;
72
+ this.queue.splice(index, 1);
73
+ this.onMessageEnqueued?.();
74
+ return true;
75
+ }
76
+
77
+ popLastEditable(): QueuedMessage | null {
78
+ for (let i = this.queue.length - 1; i >= 0; i--) {
79
+ if (this.queue[i].editable !== false) {
80
+ return this.queue.splice(i, 1)[0] ?? null;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ popAllEditable(): QueuedMessage[] {
87
+ const editable: QueuedMessage[] = [];
88
+ const remaining: QueuedMessage[] = [];
89
+ for (const msg of this.queue) {
90
+ if (msg.editable !== false) {
91
+ editable.push(msg);
92
+ } else {
93
+ remaining.push(msg);
94
+ }
95
+ }
96
+ this.queue = remaining;
97
+ return editable;
98
+ }
41
99
  }
@@ -138,7 +138,14 @@ export class PluginManager {
138
138
  continue;
139
139
  }
140
140
  } catch {
141
- // Manifest read failed (marketplace not cloned yet?) fall through to installPlugin
141
+ // Manifest read failed marketplace may not be cloned yet, try to clone/update it
142
+ try {
143
+ await marketplaceService.updateMarketplace(marketplaceName);
144
+ } catch (updateError) {
145
+ logger?.warn(
146
+ `Failed to clone/update marketplace ${marketplaceName}: ${updateError instanceof Error ? updateError.message : String(updateError)}`,
147
+ );
148
+ }
142
149
  }
143
150
 
144
151
  logger?.info(`Auto-installing missing plugin: ${pluginId}`);
@@ -34,6 +34,8 @@ export class SkillManager extends EventEmitter {
34
34
 
35
35
  private skillMetadata = new Map<string, SkillMetadata>();
36
36
  private skillContent = new Map<string, Skill>();
37
+ private pluginSkillMetadata = new Map<string, SkillMetadata>();
38
+ private pluginSkillContent = new Map<string, Skill>();
37
39
  private initialized = false;
38
40
  private fileWatcher: FileWatcherService | null = null;
39
41
  private watchEnabled: boolean;
@@ -77,7 +79,7 @@ export class SkillManager extends EventEmitter {
77
79
  * Refresh skills by re-discovering them
78
80
  */
79
81
  private async refreshSkills(): Promise<void> {
80
- // Clear existing data before discovery
82
+ // Clear only discovered skills (builtin/personal/project), preserve plugin skills
81
83
  this.skillMetadata.clear();
82
84
  this.skillContent.clear();
83
85
 
@@ -94,6 +96,14 @@ export class SkillManager extends EventEmitter {
94
96
  this.skillMetadata.set(name, skill);
95
97
  });
96
98
 
99
+ // Restore plugin skills
100
+ this.pluginSkillMetadata.forEach((metadata, name) => {
101
+ this.skillMetadata.set(name, metadata);
102
+ });
103
+ this.pluginSkillContent.forEach((skill, name) => {
104
+ this.skillContent.set(name, skill);
105
+ });
106
+
97
107
  // Log any discovery errors
98
108
  if (discovery.errors.length > 0) {
99
109
  logger?.warn(`Found ${discovery.errors.length} skill discovery errors`);
@@ -382,7 +392,7 @@ export class SkillManager extends EventEmitter {
382
392
  } catch (error) {
383
393
  logger?.error(`Failed to execute skill '${skill_name}':`, error);
384
394
  return {
385
- content: `❌ **Error executing skill**: ${error instanceof Error ? error.message : String(error)}`,
395
+ content: `**Error executing skill**: ${error instanceof Error ? error.message : String(error)}`,
386
396
  };
387
397
  }
388
398
  }
@@ -404,14 +414,14 @@ export class SkillManager extends EventEmitter {
404
414
 
405
415
  if (!skill) {
406
416
  return {
407
- content: `❌ **Skill not found**: "${skill_name}"\n\nAvailable skills:\n${this.formatAvailableSkills()}`,
417
+ content: `**Skill not found**: "${skill_name}"\n\nAvailable skills:\n${this.formatAvailableSkills()}`,
408
418
  };
409
419
  }
410
420
 
411
421
  if (!skill.isValid) {
412
422
  const errorMsg = formatSkillError(skill.skillPath, skill.errors);
413
423
  return {
414
- content: `❌ **Skill validation failed**:\n\n\`\`\`\n${errorMsg}\n\`\`\``,
424
+ content: `**Skill validation failed**:\n\n\`\`\`\n${errorMsg}\n\`\`\``,
415
425
  };
416
426
  }
417
427
 
@@ -420,7 +430,7 @@ export class SkillManager extends EventEmitter {
420
430
  } catch (error) {
421
431
  logger?.error(`Failed to prepare skill '${skill_name}':`, error);
422
432
  return {
423
- content: `❌ **Error preparing skill**: ${error instanceof Error ? error.message : String(error)}`,
433
+ content: `**Error preparing skill**: ${error instanceof Error ? error.message : String(error)}`,
424
434
  };
425
435
  }
426
436
  }
@@ -429,9 +439,7 @@ export class SkillManager extends EventEmitter {
429
439
  * Prepare skill content with arguments but without bash execution
430
440
  */
431
441
  private prepareSkillContent(skill: Skill, argsString: string): string {
432
- const header = `🧠 **${skill.name}** (${skill.type} skill)\n\n`;
433
- const description = `*${skill.description}*\n\n`;
434
- const skillPath = `📁 Skill location: \`${skill.skillPath}\`\n\n`;
442
+ const skillPath = `Base directory for this skill: ${skill.skillPath}\n\n`;
435
443
 
436
444
  // Extract content after frontmatter
437
445
  const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
@@ -451,7 +459,7 @@ export class SkillManager extends EventEmitter {
451
459
  );
452
460
  }
453
461
 
454
- return header + description + skillPath + mainContent;
462
+ return skillPath + mainContent;
455
463
  }
456
464
 
457
465
  /**
@@ -508,6 +516,9 @@ export class SkillManager extends EventEmitter {
508
516
 
509
517
  this.skillMetadata.set(namespacedName, metadata);
510
518
  this.skillContent.set(namespacedName, skill);
519
+ // Also store in plugin-specific maps so they survive refreshSkills()
520
+ this.pluginSkillMetadata.set(namespacedName, metadata);
521
+ this.pluginSkillContent.set(namespacedName, skill);
511
522
  }
512
523
  logger?.debug(
513
524
  `Registered ${skills.length} plugin skills from ${pluginName}. Total skills: ${this.skillMetadata.size}`,