wave-agent-sdk 1.0.7 → 1.0.8

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 (43) hide show
  1. package/builtin/skills/settings/ENV.md +1 -0
  2. package/builtin/skills/settings/MODELS.md +3 -0
  3. package/builtin/subagents/vision.md +18 -0
  4. package/dist/constants/tools.d.ts +1 -0
  5. package/dist/constants/tools.js +1 -0
  6. package/dist/managers/aiManager.js +46 -4
  7. package/dist/managers/messageManager.js +8 -2
  8. package/dist/managers/permissionManager.d.ts +5 -0
  9. package/dist/managers/permissionManager.js +18 -2
  10. package/dist/managers/pluginManager.d.ts +9 -0
  11. package/dist/managers/pluginManager.js +20 -0
  12. package/dist/managers/subagentManager.d.ts +12 -0
  13. package/dist/managers/subagentManager.js +32 -2
  14. package/dist/managers/toolManager.js +7 -0
  15. package/dist/services/aiService.d.ts +1 -1
  16. package/dist/services/aiService.js +3 -2
  17. package/dist/services/artifactAvailability.d.ts +7 -0
  18. package/dist/services/artifactAvailability.js +26 -0
  19. package/dist/services/artifactSession.d.ts +27 -0
  20. package/dist/services/artifactSession.js +52 -0
  21. package/dist/services/configurationService.d.ts +2 -1
  22. package/dist/services/configurationService.js +16 -1
  23. package/dist/services/initializationService.js +5 -0
  24. package/dist/tools/agentTool.js +2 -1
  25. package/dist/tools/artifactTool.d.ts +2 -0
  26. package/dist/tools/artifactTool.js +357 -0
  27. package/dist/tools/bashTool.js +11 -11
  28. package/dist/tools/types.d.ts +3 -1
  29. package/dist/tools/webFetchTool.js +141 -0
  30. package/dist/types/agent.d.ts +2 -0
  31. package/dist/types/config.d.ts +2 -0
  32. package/dist/types/configuration.d.ts +2 -0
  33. package/dist/types/messaging.d.ts +3 -1
  34. package/dist/types/permissions.d.ts +3 -1
  35. package/dist/types/permissions.js +2 -1
  36. package/dist/utils/bashParser.d.ts +14 -0
  37. package/dist/utils/bashParser.js +45 -1
  38. package/dist/utils/convertMessagesForAPI.js +51 -4
  39. package/dist/utils/messageOperations.d.ts +4 -2
  40. package/dist/utils/messageOperations.js +23 -7
  41. package/dist/utils/subagentParser.d.ts +5 -2
  42. package/dist/utils/subagentParser.js +14 -4
  43. package/package.json +2 -1
@@ -46,6 +46,12 @@ export interface ToolRule {
46
46
  depth: number;
47
47
  scopeFlags?: string[];
48
48
  }
49
+ /**
50
+ * Global scope flags for git that only change the target repository or
51
+ * configuration, not the subcommand being run. Shared by TOOL_RULES (smart
52
+ * prefix extraction) and stripGitScopePrefix (rule matching).
53
+ */
54
+ export declare const GIT_SCOPE_FLAGS: string[];
49
55
  export declare const TOOL_RULES: Record<string, ToolRule>;
50
56
  /**
51
57
  * Registry of dangerous subcommands for specific tools.
@@ -71,6 +77,14 @@ export declare function hasProcessSubstitution(command: string): boolean;
71
77
  * sed -i modifies files in place and must NOT be auto-allowed. FR-019.5.
72
78
  */
73
79
  export declare function hasSedInPlace(command: string): boolean;
80
+ /**
81
+ * Removes leading git global scope flags (e.g. `git -C <path>`, `git -c <key>=<value>`,
82
+ * `git --git-dir <path>`) from a command string, so that `git -C /tmp/foo status` is
83
+ * classified the same as `git status`. Only the leading sequence before the git
84
+ * subcommand is stripped; the remainder is re-joined with single spaces.
85
+ * Returns the input unchanged when nothing is stripped.
86
+ */
87
+ export declare function stripGitScopePrefix(command: string): string;
74
88
  /**
75
89
  * Extracts a "smart prefix" from a bash command based on common developer tools.
76
90
  * Returns null if the command is blacklisted or cannot be safely prefix-matched.
@@ -500,6 +500,18 @@ export const READ_ONLY_COMMANDS = [
500
500
  "bc",
501
501
  "sleep",
502
502
  ];
503
+ /**
504
+ * Global scope flags for git that only change the target repository or
505
+ * configuration, not the subcommand being run. Shared by TOOL_RULES (smart
506
+ * prefix extraction) and stripGitScopePrefix (rule matching).
507
+ */
508
+ export const GIT_SCOPE_FLAGS = [
509
+ "-C",
510
+ "-c",
511
+ "--directory",
512
+ "--work-tree",
513
+ "--git-dir",
514
+ ];
503
515
  export const TOOL_RULES = {
504
516
  // Node/JS
505
517
  npm: { depth: 2, scopeFlags: ["--prefix", "-C", "--registry"] },
@@ -517,7 +529,7 @@ export const TOOL_RULES = {
517
529
  // Git
518
530
  git: {
519
531
  depth: 2,
520
- scopeFlags: ["-C", "-c", "--directory", "--work-tree", "--git-dir"],
532
+ scopeFlags: GIT_SCOPE_FLAGS,
521
533
  },
522
534
  // Python
523
535
  python: { depth: 2 },
@@ -670,6 +682,38 @@ export function hasSedInPlace(command) {
670
682
  return false;
671
683
  return tokens.some((token) => /^-i(\..*)?$/.test(token));
672
684
  }
685
+ /**
686
+ * Removes leading git global scope flags (e.g. `git -C <path>`, `git -c <key>=<value>`,
687
+ * `git --git-dir <path>`) from a command string, so that `git -C /tmp/foo status` is
688
+ * classified the same as `git status`. Only the leading sequence before the git
689
+ * subcommand is stripped; the remainder is re-joined with single spaces.
690
+ * Returns the input unchanged when nothing is stripped.
691
+ */
692
+ export function stripGitScopePrefix(command) {
693
+ const trimmed = command.trim();
694
+ if (!/^git(?:\s|$)/.test(trimmed))
695
+ return command;
696
+ const tokens = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
697
+ if (tokens.length === 0 || tokens[0] !== "git")
698
+ return command;
699
+ let i = 1;
700
+ while (i < tokens.length) {
701
+ const token = tokens[i];
702
+ const eqIndex = token.indexOf("=");
703
+ const flag = eqIndex > 0 ? token.slice(0, eqIndex) : token;
704
+ if (!GIT_SCOPE_FLAGS.includes(flag))
705
+ break;
706
+ if (eqIndex > 0) {
707
+ i++; // --flag=value form carries its own value
708
+ }
709
+ else {
710
+ i += 2; // skip the flag and its argument
711
+ }
712
+ }
713
+ if (i === 1)
714
+ return command;
715
+ return ["git", ...tokens.slice(i)].join(" ");
716
+ }
673
717
  /**
674
718
  * Extracts a "smart prefix" from a bash command based on common developer tools.
675
719
  * Returns null if the command is blacklisted or cannot be safely prefix-matched.
@@ -158,14 +158,38 @@ export function convertMessagesForAPI(messages, options) {
158
158
  tool_calls = undefined;
159
159
  }
160
160
  }
161
- // Construct assistant message - only add if there is meaningful content or tool calls
161
+ // Construct assistant message - only add if there is meaningful content,
162
+ // tool calls, or reasoning content. Reasoning-only messages (a truncated
163
+ // turn that produced nothing but thinking) must be preserved so the next
164
+ // round can continue from the previous reasoning instead of starting
165
+ // over (aligned with Claude Code's thinking trajectory preservation).
162
166
  const hasContent = content && content.trim().length > 0;
163
167
  const hasToolCalls = tool_calls && tool_calls.length > 0;
164
- if (hasContent || hasToolCalls) {
168
+ const hasReasoning = reasoning_content && reasoning_content.trim().length > 0;
169
+ if (hasContent || hasToolCalls || hasReasoning) {
170
+ // OpenAI-compatible upstreams reject assistant messages where neither
171
+ // content nor tool_calls is set — a reasoning_content-only message
172
+ // gets stripped and returns 400 "content or tool_calls must be set"
173
+ // (this is fine for Claude Code, whose thinking blocks ARE content).
174
+ // When a turn produced only thinking (interrupted/truncated
175
+ // mid-reasoning), the thinking stays on its native reasoning_content
176
+ // field (the channel reasoning models natively continue from), and
177
+ // content carries an explanatory note so the request stays valid and
178
+ // the model knows the thinking was cut off and auto-preserved.
179
+ // Tool calls are meaningful content of their own — a reasoning +
180
+ // tool-call turn is not truncated, so no note is injected.
181
+ const fallbackContent = hasContent
182
+ ? content
183
+ : hasReasoning && !hasToolCalls
184
+ ? "[Note: The reasoning above was cut off before completion. It was auto-preserved from an interrupted or truncated turn — continue from where it left off, or disregard it if no longer relevant.]"
185
+ : undefined;
165
186
  const assistantMessage = {
166
187
  role: "assistant",
167
- content: hasContent ? content : undefined,
188
+ content: fallbackContent,
168
189
  tool_calls,
190
+ // Sent whenever reasoning exists: alongside real text it is the
191
+ // turn's own thinking; in the fallback case it carries the
192
+ // preserved thinking itself (content only holds the note).
169
193
  ...(reasoning_content ? { reasoning_content } : {}),
170
194
  ...(message.additionalFields ? { ...message.additionalFields } : {}),
171
195
  };
@@ -196,12 +220,25 @@ export function convertMessagesForAPI(messages, options) {
196
220
  type: "text",
197
221
  text: "[User shared an image, but the current model does not support image recognition]",
198
222
  });
223
+ // Append source path metadata for local-file images so the main
224
+ // model can delegate recognition to a vision subagent (which
225
+ // reads the path with the Read tool). Inline dataURLs are
226
+ // skipped — only persisted paths are delegatable.
227
+ block.imageUrls.forEach((imageUrl) => {
228
+ if (!imageUrl.startsWith("data:image/")) {
229
+ contentParts.push({
230
+ type: "text",
231
+ text: `[Image source: ${imageUrl}]`,
232
+ });
233
+ }
234
+ });
199
235
  }
200
236
  else {
201
237
  block.imageUrls.forEach((imageUrl) => {
202
238
  // Check if it's already base64, convert if not
239
+ const isDataUrl = imageUrl.startsWith("data:image/");
203
240
  let finalImageUrl = imageUrl;
204
- if (!imageUrl.startsWith("data:image/")) {
241
+ if (!isDataUrl) {
205
242
  // If it's a file path, it needs to be converted to base64
206
243
  try {
207
244
  finalImageUrl = convertImageToBase64(imageUrl);
@@ -219,6 +256,16 @@ export function convertMessagesForAPI(messages, options) {
219
256
  detail: "auto",
220
257
  },
221
258
  });
259
+ // Aligned with Claude Code: when the image comes from a local
260
+ // file (not an inline dataURL), append its source path as text
261
+ // metadata so the model can reference the file with tools
262
+ // (e.g. a vision-model subagent reading the image).
263
+ if (!isDataUrl) {
264
+ contentParts.push({
265
+ type: "text",
266
+ text: `[Image source: ${imageUrl}]`,
267
+ });
268
+ }
222
269
  });
223
270
  }
224
271
  }
@@ -40,7 +40,9 @@ export interface UpdateToolBlockParams {
40
40
  }>;
41
41
  compactParams?: string;
42
42
  parametersChunk?: string;
43
- isManuallyBackgrounded?: boolean;
43
+ backgroundTaskId?: string;
44
+ backgroundedByUser?: boolean;
45
+ assistantAutoBackgrounded?: boolean;
44
46
  timestamp?: number;
45
47
  }
46
48
  export type AgentToolBlockUpdateParams = Omit<UpdateToolBlockParams, "messages">;
@@ -86,7 +88,7 @@ export declare const addToolBlockToMessageInMessages: (messages: Message[], mess
86
88
  messages: Message[];
87
89
  toolBlockId: string;
88
90
  };
89
- export declare const updateToolBlockInMessage: ({ messages, id, messageId, parameters, result, success, error, stage, name, shortResult, startLineNumber, images, compactParams, parametersChunk, isManuallyBackgrounded, }: UpdateToolBlockParams) => {
91
+ export declare const updateToolBlockInMessage: ({ messages, id, messageId, parameters, result, success, error, stage, name, shortResult, startLineNumber, images, compactParams, parametersChunk, backgroundTaskId, backgroundedByUser, assistantAutoBackgrounded, }: UpdateToolBlockParams) => {
90
92
  messages: Message[];
91
93
  messageId?: string;
92
94
  };
@@ -158,7 +158,7 @@ export const addToolBlockToMessageInMessages = (messages, messageId, params) =>
158
158
  return { messages: newMessages, toolBlockId };
159
159
  };
160
160
  // Update Tool Block of the last assistant or user message
161
- export const updateToolBlockInMessage = ({ messages, id, messageId, parameters, result, success, error, stage, name, shortResult, startLineNumber, images, compactParams, parametersChunk, isManuallyBackgrounded, }) => {
161
+ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters, result, success, error, stage, name, shortResult, startLineNumber, images, compactParams, parametersChunk, backgroundTaskId, backgroundedByUser, assistantAutoBackgrounded, }) => {
162
162
  const newMessages = [...messages];
163
163
  // If messageId is provided, target that specific message
164
164
  if (messageId) {
@@ -170,6 +170,9 @@ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters,
170
170
  if (toolBlock.type === "tool") {
171
171
  if (parameters !== undefined)
172
172
  toolBlock.parameters = parameters;
173
+ else if (parametersChunk !== undefined)
174
+ toolBlock.parameters =
175
+ (toolBlock.parameters || "") + parametersChunk;
173
176
  if (result !== undefined)
174
177
  toolBlock.result = result;
175
178
  if (shortResult !== undefined)
@@ -188,8 +191,12 @@ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters,
188
191
  toolBlock.compactParams = compactParams;
189
192
  if (parametersChunk !== undefined)
190
193
  toolBlock.parametersChunk = parametersChunk;
191
- if (isManuallyBackgrounded !== undefined)
192
- toolBlock.isManuallyBackgrounded = isManuallyBackgrounded;
194
+ if (backgroundTaskId !== undefined)
195
+ toolBlock.backgroundTaskId = backgroundTaskId;
196
+ if (backgroundedByUser !== undefined)
197
+ toolBlock.backgroundedByUser = backgroundedByUser;
198
+ if (assistantAutoBackgrounded !== undefined)
199
+ toolBlock.assistantAutoBackgrounded = assistantAutoBackgrounded;
193
200
  }
194
201
  }
195
202
  }
@@ -204,6 +211,9 @@ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters,
204
211
  if (toolBlock.type === "tool") {
205
212
  if (parameters !== undefined)
206
213
  toolBlock.parameters = parameters;
214
+ else if (parametersChunk !== undefined)
215
+ toolBlock.parameters =
216
+ (toolBlock.parameters || "") + parametersChunk;
207
217
  if (result !== undefined)
208
218
  toolBlock.result = result;
209
219
  if (shortResult !== undefined)
@@ -222,8 +232,12 @@ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters,
222
232
  toolBlock.compactParams = compactParams;
223
233
  if (parametersChunk !== undefined)
224
234
  toolBlock.parametersChunk = parametersChunk;
225
- if (isManuallyBackgrounded !== undefined)
226
- toolBlock.isManuallyBackgrounded = isManuallyBackgrounded;
235
+ if (backgroundTaskId !== undefined)
236
+ toolBlock.backgroundTaskId = backgroundTaskId;
237
+ if (backgroundedByUser !== undefined)
238
+ toolBlock.backgroundedByUser = backgroundedByUser;
239
+ if (assistantAutoBackgrounded !== undefined)
240
+ toolBlock.assistantAutoBackgrounded = assistantAutoBackgrounded;
227
241
  }
228
242
  const foundMessageId = newMessages[i].id;
229
243
  return { messages: newMessages, messageId: foundMessageId };
@@ -233,7 +247,7 @@ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters,
233
247
  // This handles cases where we're streaming tool parameters before execution
234
248
  newMessages[i].blocks.push({
235
249
  type: "tool",
236
- parameters: parameters,
250
+ parameters: parameters !== undefined ? parameters : (parametersChunk ?? ""),
237
251
  result: result || "",
238
252
  shortResult: shortResult,
239
253
  startLineNumber: startLineNumber,
@@ -245,7 +259,9 @@ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters,
245
259
  stage: stage ?? "start",
246
260
  compactParams: compactParams,
247
261
  parametersChunk: parametersChunk,
248
- isManuallyBackgrounded: isManuallyBackgrounded,
262
+ backgroundTaskId: backgroundTaskId,
263
+ backgroundedByUser: backgroundedByUser,
264
+ assistantAutoBackgrounded: assistantAutoBackgrounded,
249
265
  });
250
266
  const foundMessageId = newMessages[i].id;
251
267
  return { messages: newMessages, messageId: foundMessageId };
@@ -17,9 +17,12 @@ export interface SubagentConfiguration {
17
17
  export declare function parseAgentFile(filePath: string, scope: "plugin", pluginRoot: string): SubagentConfiguration;
18
18
  /**
19
19
  * Load all subagent configurations from project and user directories, plus built-in subagents
20
+ * @param workdir - Working directory to scan for project-level subagents
21
+ * @param env - Merged environment (settings.json env over OS env). Defaults to process.env.
22
+ * Used for conditional registration of builtin subagents (e.g. WAVE_VISION_MODEL).
20
23
  */
21
- export declare function loadSubagentConfigurations(workdir: string): Promise<SubagentConfiguration[]>;
24
+ export declare function loadSubagentConfigurations(workdir: string, env?: Record<string, string>): Promise<SubagentConfiguration[]>;
22
25
  /**
23
26
  * Find subagent by exact name match
24
27
  */
25
- export declare function findSubagentByName(name: string, workdir: string): Promise<SubagentConfiguration | null>;
28
+ export declare function findSubagentByName(name: string, workdir: string, env?: Record<string, string>): Promise<SubagentConfiguration | null>;
@@ -158,15 +158,25 @@ function scanSubagentDirectory(dirPath, scope) {
158
158
  }
159
159
  /**
160
160
  * Load all subagent configurations from project and user directories, plus built-in subagents
161
+ * @param workdir - Working directory to scan for project-level subagents
162
+ * @param env - Merged environment (settings.json env over OS env). Defaults to process.env.
163
+ * Used for conditional registration of builtin subagents (e.g. WAVE_VISION_MODEL).
161
164
  */
162
- export async function loadSubagentConfigurations(workdir) {
165
+ export async function loadSubagentConfigurations(workdir, env = process.env) {
163
166
  const projectWaveDir = join(workdir, ".wave", "agents");
164
167
  const projectClaudeDir = join(workdir, ".claude", "agents");
165
168
  const userWaveDir = join(process.env.HOME || "~", ".wave", "agents");
166
169
  const userClaudeDir = join(process.env.HOME || "~", ".claude", "agents");
167
170
  const builtinDir = getBuiltinSubagentsDir();
168
171
  // Load configurations from all sources
169
- const builtinConfigs = scanSubagentDirectory(builtinDir, "builtin");
172
+ let builtinConfigs = scanSubagentDirectory(builtinDir, "builtin");
173
+ // Conditional registration: builtin subagents whose frontmatter requires the
174
+ // WAVE_VISION_MODEL env var (`model: visionModel`) are only loaded when it is
175
+ // set — otherwise the main model would delegate image recognition to a
176
+ // subagent that resolves to a non-vision model.
177
+ if (!env.WAVE_VISION_MODEL) {
178
+ builtinConfigs = builtinConfigs.filter((config) => config.model !== "visionModel");
179
+ }
170
180
  const userClaudeConfigs = scanSubagentDirectory(userClaudeDir, "user");
171
181
  const userWaveConfigs = scanSubagentDirectory(userWaveDir, "user");
172
182
  const projectClaudeConfigs = scanSubagentDirectory(projectClaudeDir, "project");
@@ -193,7 +203,7 @@ export async function loadSubagentConfigurations(workdir) {
193
203
  /**
194
204
  * Find subagent by exact name match
195
205
  */
196
- export async function findSubagentByName(name, workdir) {
197
- const configurations = await loadSubagentConfigurations(workdir);
206
+ export async function findSubagentByName(name, workdir, env) {
207
+ const configurations = await loadSubagentConfigurations(workdir, env);
198
208
  return configurations.find((config) => config.name === name) || null;
199
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,6 +55,7 @@
55
55
  "fuzzysort": "^3.1.0",
56
56
  "glob": "^13.0.0",
57
57
  "lru-cache": "^11.3.5",
58
+ "marked": "^17.0.2",
58
59
  "minimatch": "^10.0.3",
59
60
  "openai": "^5.12.2",
60
61
  "turndown": "^7.2.2"