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.
- package/builtin/skills/settings/SKILL.md +1 -1
- package/builtin/skills/settings/SKILLS.md +3 -0
- package/builtin/skills/settings/SUBAGENTS.md +21 -2
- package/dist/managers/aiManager.d.ts +3 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +107 -82
- package/dist/managers/forkedAgentManager.d.ts +1 -0
- package/dist/managers/forkedAgentManager.d.ts.map +1 -1
- package/dist/managers/forkedAgentManager.js +1 -0
- package/dist/managers/pluginManager.d.ts +1 -0
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +7 -0
- package/dist/managers/subagentManager.d.ts +6 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +36 -0
- package/dist/services/autoMemoryService.d.ts.map +1 -1
- package/dist/services/autoMemoryService.js +1 -0
- package/dist/services/pluginLoader.d.ts +5 -0
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +29 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +2 -2
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +27 -8
- package/dist/utils/stringUtils.d.ts +8 -0
- package/dist/utils/stringUtils.d.ts.map +1 -1
- package/dist/utils/stringUtils.js +45 -0
- package/dist/utils/subagentParser.d.ts +8 -1
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +18 -3
- package/package.json +1 -1
- package/src/managers/aiManager.ts +141 -110
- package/src/managers/forkedAgentManager.ts +3 -0
- package/src/managers/pluginManager.ts +10 -0
- package/src/managers/subagentManager.ts +49 -0
- package/src/services/autoMemoryService.ts +1 -0
- package/src/services/pluginLoader.ts +37 -0
- package/src/types/plugins.ts +2 -0
- package/src/utils/constants.ts +2 -2
- package/src/utils/convertMessagesForAPI.ts +31 -9
- package/src/utils/stringUtils.ts +43 -0
- 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
|
*/
|
package/src/types/plugins.ts
CHANGED
|
@@ -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;
|
package/src/utils/constants.ts
CHANGED
|
@@ -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 =
|
|
33
|
-
export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS =
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
}
|
package/src/utils/stringUtils.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|