wave-agent-sdk 0.14.2 → 0.14.4

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 (45) hide show
  1. package/builtin/skills/settings/SKILL.md +1 -1
  2. package/builtin/skills/settings/SKILLS.md +3 -0
  3. package/builtin/skills/settings/SUBAGENTS.md +21 -2
  4. package/dist/managers/aiManager.d.ts +3 -0
  5. package/dist/managers/aiManager.d.ts.map +1 -1
  6. package/dist/managers/aiManager.js +107 -82
  7. package/dist/managers/forkedAgentManager.d.ts +1 -0
  8. package/dist/managers/forkedAgentManager.d.ts.map +1 -1
  9. package/dist/managers/forkedAgentManager.js +1 -0
  10. package/dist/managers/pluginManager.d.ts +1 -0
  11. package/dist/managers/pluginManager.d.ts.map +1 -1
  12. package/dist/managers/pluginManager.js +7 -0
  13. package/dist/managers/subagentManager.d.ts +6 -0
  14. package/dist/managers/subagentManager.d.ts.map +1 -1
  15. package/dist/managers/subagentManager.js +36 -0
  16. package/dist/services/autoMemoryService.d.ts.map +1 -1
  17. package/dist/services/autoMemoryService.js +1 -0
  18. package/dist/services/pluginLoader.d.ts +5 -0
  19. package/dist/services/pluginLoader.d.ts.map +1 -1
  20. package/dist/services/pluginLoader.js +29 -0
  21. package/dist/types/plugins.d.ts +2 -0
  22. package/dist/types/plugins.d.ts.map +1 -1
  23. package/dist/utils/constants.d.ts +2 -2
  24. package/dist/utils/constants.d.ts.map +1 -1
  25. package/dist/utils/constants.js +2 -2
  26. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  27. package/dist/utils/convertMessagesForAPI.js +27 -8
  28. package/dist/utils/stringUtils.d.ts +8 -0
  29. package/dist/utils/stringUtils.d.ts.map +1 -1
  30. package/dist/utils/stringUtils.js +45 -0
  31. package/dist/utils/subagentParser.d.ts +8 -1
  32. package/dist/utils/subagentParser.d.ts.map +1 -1
  33. package/dist/utils/subagentParser.js +18 -3
  34. package/package.json +1 -1
  35. package/src/managers/aiManager.ts +141 -110
  36. package/src/managers/forkedAgentManager.ts +3 -0
  37. package/src/managers/pluginManager.ts +10 -0
  38. package/src/managers/subagentManager.ts +49 -0
  39. package/src/services/autoMemoryService.ts +1 -0
  40. package/src/services/pluginLoader.ts +37 -0
  41. package/src/types/plugins.ts +2 -0
  42. package/src/utils/constants.ts +2 -2
  43. package/src/utils/convertMessagesForAPI.ts +31 -9
  44. package/src/utils/stringUtils.ts +43 -0
  45. package/src/utils/subagentParser.ts +31 -4
@@ -187,10 +187,57 @@ export class SubagentManager {
187
187
  * Find subagent by exact name match
188
188
  */
189
189
  async findSubagent(name: string) {
190
+ // Check cached configurations first (includes plugin agents)
191
+ if (this.cachedConfigurations !== null) {
192
+ const cached = this.cachedConfigurations.find(
193
+ (config) => config.name === name,
194
+ );
195
+ if (cached) return cached;
196
+ }
197
+ // Fall back to filesystem scan for non-plugin agents
190
198
  const { findSubagentByName } = await import("../utils/subagentParser.js");
191
199
  return findSubagentByName(name, this.workdir);
192
200
  }
193
201
 
202
+ /**
203
+ * Register plugin agents into the cached configurations.
204
+ * Names each agent as `pluginName:agentName` to avoid collisions.
205
+ */
206
+ registerPluginAgents(
207
+ pluginName: string,
208
+ agents: SubagentConfiguration[],
209
+ ): void {
210
+ if (this.cachedConfigurations === null) {
211
+ // Should not happen if initialization order is correct
212
+ this.cachedConfigurations = [];
213
+ }
214
+
215
+ // Remove any previously registered agents for this plugin (by name prefix)
216
+ this.cachedConfigurations = this.cachedConfigurations.filter(
217
+ (config) => !config.name.startsWith(`${pluginName}:`),
218
+ );
219
+
220
+ for (const agent of agents) {
221
+ const namespacedName = `${pluginName}:${agent.name}`;
222
+ const namespacedAgent: SubagentConfiguration = {
223
+ ...agent,
224
+ name: namespacedName,
225
+ // Safety net: substitute any remaining ${WAVE_PLUGIN_ROOT} placeholders
226
+ systemPrompt: agent.systemPrompt.replace(
227
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
228
+ agent.pluginRoot ?? "",
229
+ ),
230
+ };
231
+ this.cachedConfigurations!.push(namespacedAgent);
232
+ }
233
+
234
+ // Re-sort by priority then name
235
+ this.cachedConfigurations!.sort((a, b) => {
236
+ if (a.priority !== b.priority) return a.priority - b.priority;
237
+ return a.name.localeCompare(b.name);
238
+ });
239
+ }
240
+
194
241
  /**
195
242
  * Create a new subagent instance with isolated managers
196
243
  */
@@ -204,6 +251,7 @@ export class SubagentManager {
204
251
  model?: string;
205
252
  stream?: boolean;
206
253
  permissionModeOverride?: PermissionMode;
254
+ maxTurns?: number;
207
255
  },
208
256
  runInBackground?: boolean,
209
257
  onUpdate?: () => void,
@@ -309,6 +357,7 @@ export class SubagentManager {
309
357
  subagentType: parameters.subagent_type, // Pass subagent type for hook context
310
358
  modelOverride: parameters.model || configuration.model, // Pass model override
311
359
  stream: parameters.stream ?? this.stream, // Pass streaming mode flag
360
+ maxTurns: parameters.maxTurns, // Pass maxTurns limit
312
361
  callbacks: {
313
362
  onUsageAdded: this.onUsageAdded,
314
363
  },
@@ -164,6 +164,7 @@ export class AutoMemoryService {
164
164
  ],
165
165
  model: "fastModel", // Use fast model for background tasks to reduce latency and cost
166
166
  permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
167
+ maxTurns: 5, // Limit turns to prevent verification rabbit-holes
167
168
  },
168
169
  `${prompt}\n\nThe memory directory for this project is: ${memoryDir}`,
169
170
  );
@@ -10,7 +10,12 @@ import {
10
10
  } from "../types/index.js";
11
11
  import { scanCommandsDirectory } from "../utils/customCommands.js";
12
12
  import { parseSkillFile } from "../utils/skillParser.js";
13
+ import {
14
+ parseAgentFile,
15
+ type SubagentConfiguration,
16
+ } from "../utils/subagentParser.js";
13
17
  import { resolveMcpConfig } from "../managers/mcpManager.js";
18
+ import { logger } from "../utils/globalLogger.js";
14
19
 
15
20
  export class PluginLoader {
16
21
  /**
@@ -165,6 +170,38 @@ export class PluginLoader {
165
170
  }
166
171
  }
167
172
 
173
+ /**
174
+ * Load agent configurations from a plugin's agents directory
175
+ */
176
+ static async loadAgents(
177
+ pluginPath: string,
178
+ ): Promise<SubagentConfiguration[]> {
179
+ const agentsPath = path.join(pluginPath, "agents");
180
+ const agents: SubagentConfiguration[] = [];
181
+
182
+ try {
183
+ const entries = await fs.readdir(agentsPath, { withFileTypes: true });
184
+ for (const entry of entries) {
185
+ if (entry.isFile() && entry.name.endsWith(".md")) {
186
+ const agentFilePath = path.join(agentsPath, entry.name);
187
+ try {
188
+ const config = parseAgentFile(agentFilePath, "plugin", pluginPath);
189
+ agents.push(config);
190
+ } catch (parseError) {
191
+ // Log error but continue with other files
192
+ logger?.warn(
193
+ `Warning: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
194
+ );
195
+ }
196
+ }
197
+ }
198
+ } catch {
199
+ // agents directory might not exist
200
+ }
201
+
202
+ return agents;
203
+ }
204
+
168
205
  /**
169
206
  * Validate the plugin manifest structure
170
207
  */
@@ -3,6 +3,7 @@ import { Skill } from "./skills.js";
3
3
  import { LspConfig } from "./lsp.js";
4
4
  import { McpConfig } from "./mcp.js";
5
5
  import { PartialHookConfiguration } from "./configuration.js";
6
+ import { SubagentConfiguration } from "../utils/subagentParser.js";
6
7
 
7
8
  /**
8
9
  * Plugin manifest structure (.wave-plugin/plugin.json)
@@ -31,6 +32,7 @@ export interface Plugin extends PluginManifest {
31
32
  path: string;
32
33
  commands: CustomSlashCommand[];
33
34
  skills: Skill[];
35
+ agents: SubagentConfiguration[];
34
36
  lspConfig?: LspConfig;
35
37
  mcpConfig?: McpConfig;
36
38
  hooksConfig?: PartialHookConfiguration;
@@ -29,5 +29,5 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
29
29
  /**
30
30
  * AI related constants
31
31
  */
32
- export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 96000; // Default token limit
33
- export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 8192; // Default output token limit
32
+ export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 128000; // Default token limit
33
+ export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384; // Default output token limit
@@ -2,7 +2,7 @@ import type { Message } from "../types/index.js";
2
2
  import { convertImageToBase64 } from "./messageOperations.js";
3
3
  import { taskNotificationToXml } from "./notificationXml.js";
4
4
  import { ChatCompletionMessageToolCall } from "openai/resources";
5
- import { stripAnsiColors } from "./stringUtils.js";
5
+ import { recoverTruncatedJson, stripAnsiColors } from "./stringUtils.js";
6
6
  import {
7
7
  ChatCompletionContentPart,
8
8
  ChatCompletionMessageParam,
@@ -10,7 +10,8 @@ import {
10
10
  import { logger } from "./globalLogger.js";
11
11
 
12
12
  /**
13
- * Safely handle tool call parameters, ensuring a legal JSON string is returned
13
+ * Safely handle tool call parameters, ensuring a legal JSON string is returned.
14
+ * Attempts to recover truncated JSON (e.g., missing closing braces).
14
15
  * @param args Tool call parameters
15
16
  * @returns Legal JSON string
16
17
  */
@@ -23,12 +24,18 @@ function safeToolArguments(args: string): string {
23
24
  // Try to parse as JSON to validate format
24
25
  JSON.parse(args);
25
26
  return args;
26
- } catch (error) {
27
- logger.error(`Invalid tool arguments: ${args}`, error);
28
- // If not valid JSON, return a fallback empty object with the original string as a comment or property
29
- return JSON.stringify({
30
- invalid_arguments: args,
31
- });
27
+ } catch {
28
+ // Attempt to recover truncated JSON
29
+ const recovered = recoverTruncatedJson(args);
30
+ try {
31
+ JSON.parse(recovered);
32
+ return recovered;
33
+ } catch {
34
+ // Truly malformed JSON — return sanitized fallback
35
+ return JSON.stringify({
36
+ invalid_arguments: args,
37
+ });
38
+ }
32
39
  }
33
40
  }
34
41
 
@@ -145,6 +152,20 @@ export function convertMessagesForAPI(
145
152
  .join("\n");
146
153
  }
147
154
 
155
+ // Extract reasoning content from reasoning blocks
156
+ const reasoningBlocks = message.blocks.filter(
157
+ (block) =>
158
+ block.type === "reasoning" &&
159
+ block.content &&
160
+ block.content.trim().length > 0,
161
+ );
162
+ let reasoning_content: string | undefined;
163
+ if (reasoningBlocks.length > 0) {
164
+ reasoning_content = reasoningBlocks
165
+ .map((block) => (block.type === "reasoning" ? block.content : ""))
166
+ .join("\n");
167
+ }
168
+
148
169
  // Construct tool calls from tool blocks
149
170
  if (toolBlocks.length > 0) {
150
171
  tool_calls = toolBlocks
@@ -176,8 +197,9 @@ export function convertMessagesForAPI(
176
197
  role: "assistant",
177
198
  content: hasContent ? content : undefined,
178
199
  tool_calls,
200
+ ...(reasoning_content ? { reasoning_content } : {}),
179
201
  ...(message.additionalFields ? { ...message.additionalFields } : {}),
180
- };
202
+ } as ChatCompletionMessageParam;
181
203
 
182
204
  recentMessages.unshift(assistantMessage);
183
205
  }
@@ -92,6 +92,49 @@ export function formatLineNumberPrefix(lineNumber: number): string {
92
92
  return `${lineNumber.toString().padStart(6)}\t`;
93
93
  }
94
94
 
95
+ /**
96
+ * Attempt to recover truncated JSON (e.g., missing closing braces due to max tokens).
97
+ * Tracks brace depth and only recovers if there are unclosed `{` braces.
98
+ * Will NOT recover if there are unclosed `[` brackets (can't guess the content).
99
+ * @param jsonStr Potentially truncated JSON string
100
+ * @returns Recovered JSON string, or the original if unrecoverable
101
+ */
102
+ export function recoverTruncatedJson(jsonStr: string): string {
103
+ let braceDepth = 0;
104
+ let bracketDepth = 0;
105
+ let inString = false;
106
+ let escaped = false;
107
+
108
+ for (const ch of jsonStr) {
109
+ if (escaped) {
110
+ escaped = false;
111
+ continue;
112
+ }
113
+ if (ch === "\\" && inString) {
114
+ escaped = true;
115
+ continue;
116
+ }
117
+ if (ch === '"') {
118
+ inString = !inString;
119
+ continue;
120
+ }
121
+ if (!inString) {
122
+ if (ch === "{") braceDepth++;
123
+ if (ch === "}") braceDepth--;
124
+ if (ch === "[") bracketDepth++;
125
+ if (ch === "]") bracketDepth--;
126
+ }
127
+ }
128
+
129
+ // Build recovery suffix
130
+ let suffix = "";
131
+ if (inString) suffix += '"'; // Close unclosed string
132
+ if (braceDepth > 0 && bracketDepth === 0) {
133
+ suffix += "}".repeat(braceDepth);
134
+ }
135
+ return suffix ? jsonStr + suffix : jsonStr;
136
+ }
137
+
95
138
  /**
96
139
  * Efficiently get the last N lines of a string without splitting the whole string.
97
140
  */
@@ -10,8 +10,10 @@ export interface SubagentConfiguration {
10
10
  model?: string;
11
11
  systemPrompt: string;
12
12
  filePath: string;
13
- scope: "project" | "user" | "builtin";
13
+ scope: "project" | "user" | "builtin" | "plugin";
14
14
  priority: number;
15
+ /** Plugin root directory path, set when scope is "plugin" */
16
+ pluginRoot?: string;
15
17
  }
16
18
 
17
19
  interface SubagentFrontmatter {
@@ -119,11 +121,12 @@ function validateConfiguration(
119
121
  }
120
122
 
121
123
  /**
122
- * Parse a single subagent markdown file
124
+ * Parse a single subagent markdown file with optional pluginRoot support
123
125
  */
124
126
  function parseSubagentFile(
125
127
  filePath: string,
126
- scope: "project" | "user" | "builtin",
128
+ scope: "project" | "user" | "builtin" | "plugin",
129
+ pluginRoot?: string,
127
130
  ): SubagentConfiguration {
128
131
  try {
129
132
  const content = readFileSync(filePath, "utf-8");
@@ -143,16 +146,28 @@ function parseSubagentFile(
143
146
  let priority = 1;
144
147
  if (scope === "user") priority = 2;
145
148
  if (scope === "builtin") priority = 3;
149
+ if (scope === "plugin") priority = 2; // Same priority as user-level
150
+
151
+ let systemPrompt = body;
152
+
153
+ // Substitute ${WAVE_PLUGIN_ROOT} for plugin scope at parse time
154
+ if (scope === "plugin" && pluginRoot) {
155
+ systemPrompt = systemPrompt.replace(
156
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
157
+ pluginRoot,
158
+ );
159
+ }
146
160
 
147
161
  return {
148
162
  name: frontmatter.name!,
149
163
  description: frontmatter.description!,
150
164
  tools: frontmatter.tools,
151
165
  model: frontmatter.model,
152
- systemPrompt: body,
166
+ systemPrompt,
153
167
  filePath,
154
168
  scope,
155
169
  priority,
170
+ pluginRoot: scope === "plugin" ? pluginRoot : undefined,
156
171
  };
157
172
  } catch (error) {
158
173
  throw new Error(
@@ -161,6 +176,18 @@ function parseSubagentFile(
161
176
  }
162
177
  }
163
178
 
179
+ /**
180
+ * Parse a plugin agent markdown file.
181
+ * Exposed as a public API for PluginLoader to use.
182
+ */
183
+ export function parseAgentFile(
184
+ filePath: string,
185
+ scope: "plugin",
186
+ pluginRoot: string,
187
+ ): SubagentConfiguration {
188
+ return parseSubagentFile(filePath, scope, pluginRoot);
189
+ }
190
+
164
191
  /**
165
192
  * Scan directory for subagent files
166
193
  */