wave-agent-sdk 1.0.6 → 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 (52) 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/agent.d.ts +11 -0
  5. package/dist/agent.js +15 -0
  6. package/dist/constants/tools.d.ts +1 -0
  7. package/dist/constants/tools.js +1 -0
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.js +1 -1
  10. package/dist/managers/aiManager.js +46 -7
  11. package/dist/managers/messageManager.d.ts +1 -1
  12. package/dist/managers/messageManager.js +9 -3
  13. package/dist/managers/permissionManager.d.ts +5 -0
  14. package/dist/managers/permissionManager.js +18 -2
  15. package/dist/managers/pluginManager.d.ts +9 -0
  16. package/dist/managers/pluginManager.js +20 -0
  17. package/dist/managers/subagentManager.d.ts +12 -0
  18. package/dist/managers/subagentManager.js +32 -2
  19. package/dist/managers/toolManager.js +7 -0
  20. package/dist/services/aiService.d.ts +3 -4
  21. package/dist/services/aiService.js +7 -20
  22. package/dist/services/artifactAvailability.d.ts +7 -0
  23. package/dist/services/artifactAvailability.js +26 -0
  24. package/dist/services/artifactSession.d.ts +27 -0
  25. package/dist/services/artifactSession.js +52 -0
  26. package/dist/services/configurationService.d.ts +9 -1
  27. package/dist/services/configurationService.js +26 -1
  28. package/dist/services/initializationService.js +5 -0
  29. package/dist/tools/agentTool.js +2 -1
  30. package/dist/tools/artifactTool.d.ts +2 -0
  31. package/dist/tools/artifactTool.js +357 -0
  32. package/dist/tools/bashTool.js +11 -11
  33. package/dist/tools/types.d.ts +3 -1
  34. package/dist/tools/webFetchTool.js +141 -0
  35. package/dist/types/agent.d.ts +2 -0
  36. package/dist/types/config.d.ts +2 -0
  37. package/dist/types/configuration.d.ts +2 -0
  38. package/dist/types/core.d.ts +1 -34
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/messaging.d.ts +3 -1
  41. package/dist/types/permissions.d.ts +3 -1
  42. package/dist/types/permissions.js +2 -1
  43. package/dist/utils/bashParser.d.ts +14 -0
  44. package/dist/utils/bashParser.js +45 -1
  45. package/dist/utils/cacheControlUtils.d.ts +5 -27
  46. package/dist/utils/cacheControlUtils.js +12 -73
  47. package/dist/utils/convertMessagesForAPI.js +51 -4
  48. package/dist/utils/messageOperations.d.ts +4 -2
  49. package/dist/utils/messageOperations.js +23 -7
  50. package/dist/utils/subagentParser.d.ts +5 -2
  51. package/dist/utils/subagentParser.js +14 -4
  52. package/package.json +2 -1
@@ -27,6 +27,7 @@ Wave uses several environment variables to control its core functionality. Varia
27
27
  | `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
28
28
  | `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
29
29
  | `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
30
+ | `WAVE_VISION_MODEL` | Vision-capable model used by the built-in `vision` subagent for image recognition. When set, the built-in `vision` subagent is registered (its frontmatter `model: visionModel` resolves to this value); when unset, the subagent is not loaded. Useful when the main model is fast but non-vision (e.g. DeepSeek). | - (not registered) |
30
31
  | `WAVE_MAX_INPUT_TOKENS` | Maximum number of input tokens allowed. | `200000` |
31
32
  | `WAVE_MAX_OUTPUT_TOKENS` | Maximum number of output tokens allowed. | `32000` |
32
33
  | `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
@@ -105,12 +105,15 @@ You can also set the default models Wave uses via environment variables in `sett
105
105
  "env": {
106
106
  "WAVE_MODEL": "gemini-3-flash",
107
107
  "WAVE_FAST_MODEL": "gemini-2.5-flash",
108
+ "WAVE_VISION_MODEL": "qwen-vl-max",
108
109
  "WAVE_MAX_INPUT_TOKENS": "100000",
109
110
  "WAVE_MAX_OUTPUT_TOKENS": "4096"
110
111
  }
111
112
  }
112
113
  ```
113
114
 
115
+ `WAVE_VISION_MODEL` names a vision-capable model for the built-in `vision` subagent. Setting it registers the subagent, whose frontmatter `model: visionModel` resolves to this value — so a fast non-vision main model can delegate image recognition. Leave it unset to disable the built-in `vision` subagent.
116
+
114
117
  ## Live Reload
115
118
 
116
119
  Model configurations support **live reload**. When you modify the `models` field or model-related environment variables in `settings.json`, the changes take effect immediately without restarting Wave.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: vision
3
+ description: 'Image recognition specialist that runs on the model specified by the WAVE_VISION_MODEL environment variable. Use this when the current model does not support image recognition but the user has shared an image (identify the image by its "[Image source: <path>]" metadata). Pass the image file path(s) in the prompt; this agent reads the image with the Read tool and returns a detailed text description of its contents.'
4
+ tools: [Read]
5
+ model: visionModel
6
+ ---
7
+
8
+ You are an image recognition specialist. You run on a vision-capable model and your job is to look at image files and return detailed text descriptions of their contents.
9
+
10
+ When given image file path(s):
11
+ - Use the Read tool on each image path to load it. The Read tool returns the image as base64 image data that you can see directly.
12
+ - Describe the image contents in detail: what is shown, any visible text (transcribe verbatim where relevant), layout, colors, objects, and anything else the caller asked about.
13
+ - If an image cannot be read (file missing, not an image, or too large), report the error clearly and state which path failed.
14
+ - Do not invent or guess content you cannot see — only describe what the image actually shows.
15
+ - Return your description directly as a text message. Do NOT create files.
16
+ - Avoid using emojis in your response.
17
+
18
+ Complete the image recognition task and report your findings clearly.
package/dist/agent.d.ts CHANGED
@@ -413,6 +413,17 @@ export declare class Agent {
413
413
  * @returns The subagent instance or null if not found
414
414
  */
415
415
  getSubagentInstance(subagentId: string): import("./managers/subagentManager.js").SubagentInstance | null;
416
+ /**
417
+ * Get all subagent configurations visible in this session (built-in,
418
+ * user, project, and plugin agents).
419
+ * @returns The list of subagent configurations
420
+ */
421
+ getSubagentConfigurations(): import("./utils/subagentParser.js").SubagentConfiguration[];
422
+ /**
423
+ * Get currently active subagent instances (status active/initializing).
424
+ * @returns The list of active subagent instances
425
+ */
426
+ getActiveSubagentInstances(): import("./managers/subagentManager.js").SubagentInstance[];
416
427
  /**
417
428
  * Get the current task list ID
418
429
  */
package/dist/agent.js CHANGED
@@ -973,6 +973,21 @@ export class Agent {
973
973
  getSubagentInstance(subagentId) {
974
974
  return this.subagentManager.getInstance(subagentId);
975
975
  }
976
+ /**
977
+ * Get all subagent configurations visible in this session (built-in,
978
+ * user, project, and plugin agents).
979
+ * @returns The list of subagent configurations
980
+ */
981
+ getSubagentConfigurations() {
982
+ return this.subagentManager.getConfigurations();
983
+ }
984
+ /**
985
+ * Get currently active subagent instances (status active/initializing).
986
+ * @returns The list of active subagent instances
987
+ */
988
+ getActiveSubagentInstances() {
989
+ return this.subagentManager.getActiveInstances();
990
+ }
976
991
  /**
977
992
  * Get the current task list ID
978
993
  */
@@ -22,3 +22,4 @@ export declare const WEB_FETCH_TOOL_NAME = "WebFetch";
22
22
  export declare const ENTER_WORKTREE_TOOL_NAME = "EnterWorktree";
23
23
  export declare const EXIT_WORKTREE_TOOL_NAME = "ExitWorktree";
24
24
  export declare const WORKFLOW_TOOL_NAME = "Workflow";
25
+ export declare const ARTIFACT_TOOL_NAME = "Artifact";
@@ -22,3 +22,4 @@ 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
24
  export const WORKFLOW_TOOL_NAME = "Workflow";
25
+ export const ARTIFACT_TOOL_NAME = "Artifact";
package/dist/index.d.ts CHANGED
@@ -23,7 +23,9 @@ export * from "./utils/gitUtils.js";
23
23
  export * from "./utils/nameGenerator.js";
24
24
  export * from "./utils/worktreeSession.js";
25
25
  export * from "./utils/worktreeUtils.js";
26
- export { loadMergedWaveConfig } from "./services/configurationService.js";
26
+ export { loadMergedWaveConfig, loadUserConfigEnv, } from "./services/configurationService.js";
27
27
  export * from "./types/index.js";
28
+ export type { SubagentConfiguration } from "./utils/subagentParser.js";
29
+ export type { SubagentInstance } from "./managers/subagentManager.js";
28
30
  export * from "./tools/buildTool.js";
29
31
  export type { ToolPlugin, ToolResult, ToolContext } from "./tools/types.js";
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ export * from "./utils/gitUtils.js";
27
27
  export * from "./utils/nameGenerator.js";
28
28
  export * from "./utils/worktreeSession.js";
29
29
  export * from "./utils/worktreeUtils.js";
30
- export { loadMergedWaveConfig } from "./services/configurationService.js";
30
+ export { loadMergedWaveConfig, loadUserConfigEnv, } from "./services/configurationService.js";
31
31
  export * from "./types/index.js";
32
32
  // Export tool building utilities
33
33
  export * from "./tools/buildTool.js";
@@ -20,6 +20,14 @@ import { logOTelEvent } from "../telemetry/events.js";
20
20
  const MAX_FORK_TURNS = 3;
21
21
  /** Max turns for the auto-memory extraction fork. */
22
22
  const MAX_AUTO_MEMORY_FORK_TURNS = 5;
23
+ /**
24
+ * Max consecutive auto-resumes after `finish_reason === "length"` when the
25
+ * truncated turn produced NO tool calls (aligned with Claude Code's
26
+ * MAX_OUTPUT_TOKENS_RECOVERY_LIMIT). A tool call resets the counter — it is
27
+ * real progress. Exhausting the limit terminates the turn with an error
28
+ * instead of re-planning forever.
29
+ */
30
+ const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3;
23
31
  // Truncate text to `max` chars and append a "… [+N chars]" marker when exceeded.
24
32
  // Used for background_tasks description/command fields (≤1000 chars per spec FR-063).
25
33
  function truncateWithMarker(text, max) {
@@ -138,6 +146,9 @@ export class AIManager {
138
146
  if (this.modelOverride === "fastModel") {
139
147
  modelToUse = parentModelConfig.fastModel;
140
148
  }
149
+ else if (this.modelOverride === "visionModel") {
150
+ modelToUse = parentModelConfig.visionModel;
151
+ }
141
152
  else if (this.modelOverride !== "inherit") {
142
153
  modelToUse = this.modelOverride;
143
154
  }
@@ -1217,6 +1228,10 @@ ${question}`;
1217
1228
  toolAbortController = this.toolAbortController;
1218
1229
  }
1219
1230
  let turnDepth = turnOffset;
1231
+ // Consecutive truncation recoveries within this turn (no tool calls
1232
+ // in between). Reset to 0 on tool calls / at each sendAIMessage entry,
1233
+ // matching Claude Code's maxOutputTokensRecoveryCount semantics.
1234
+ let maxOutputTokensRecoveryCount = 0;
1220
1235
  inner: while (true) {
1221
1236
  let llmSpan;
1222
1237
  try {
@@ -1300,10 +1315,15 @@ ${question}`;
1300
1315
  // Use parametersChunk as compact param for better performance
1301
1316
  // No need to extract params or generate compact params during streaming
1302
1317
  // Update tool block with streaming parameters using parametersChunk as compact param
1318
+ // `parameters` is only present on start/running/end (authoritative); streaming
1319
+ // carries only `parametersChunk`, so don't forward `parameters: undefined`
1320
+ // (it would overwrite the accumulated block params in consumers)
1303
1321
  this.messageManager.updateToolBlock({
1304
1322
  id: toolCall.id,
1305
1323
  name: toolCall.name,
1306
- parameters: toolCall.parameters,
1324
+ ...(toolCall.parameters !== undefined
1325
+ ? { parameters: toolCall.parameters }
1326
+ : {}),
1307
1327
  parametersChunk: toolCall.parametersChunk,
1308
1328
  stage: toolCall.stage || "streaming", // Default to streaming if stage not provided
1309
1329
  });
@@ -1383,9 +1403,6 @@ ${question}`;
1383
1403
  ...(result.usage.cache_creation_input_tokens !== undefined && {
1384
1404
  cache_creation_input_tokens: result.usage.cache_creation_input_tokens,
1385
1405
  }),
1386
- ...(result.usage.cache_creation && {
1387
- cache_creation: result.usage.cache_creation,
1388
- }),
1389
1406
  };
1390
1407
  }
1391
1408
  // Set usage on the assistant message if available
@@ -1465,13 +1482,34 @@ ${question}`;
1465
1482
  const lastMessage = this.messageManager.getMessages()[this.messageManager.getMessages().length - 1];
1466
1483
  const toolBlocks = lastMessage?.blocks.filter((block) => block.type === "tool") || [];
1467
1484
  const hasBackgrounded = toolBlocks.length > 0 &&
1468
- toolBlocks.some((block) => block.isManuallyBackgrounded);
1485
+ toolBlocks.some((block) => block.backgroundedByUser);
1469
1486
  if (hasBackgrounded) {
1470
1487
  logger?.info("Some tools were manually backgrounded, stopping.");
1471
1488
  }
1472
1489
  else if (!isCurrentlyAborted) {
1490
+ // If the response was truncated WITHOUT any tool calls,
1491
+ // enforce the consecutive recovery limit (aligned with Claude
1492
+ // Code's MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3). Re-planning
1493
+ // loops that never produce output would otherwise continue
1494
+ // until the platform kills the task.
1495
+ if (result.finish_reason === "length" &&
1496
+ toolCalls.length === 0 &&
1497
+ maxOutputTokensRecoveryCount >=
1498
+ MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
1499
+ this.messageManager.addErrorBlock(`Response exceeded the output token limit ${MAX_OUTPUT_TOKENS_RECOVERY_LIMIT + 1} consecutive times without producing output or tool calls. Stopped to avoid an infinite loop. Break the remaining work into smaller pieces and try again.`);
1500
+ break inner;
1501
+ }
1473
1502
  // If response was truncated, add a hidden continuation message
1474
1503
  if (result.finish_reason === "length") {
1504
+ if (toolCalls.length === 0) {
1505
+ // Pure truncation — count toward the recovery limit.
1506
+ maxOutputTokensRecoveryCount++;
1507
+ }
1508
+ else {
1509
+ // Truncated with tool calls — real progress, reset the
1510
+ // consecutive counter.
1511
+ maxOutputTokensRecoveryCount = 0;
1512
+ }
1475
1513
  this.messageManager.addUserMessage({
1476
1514
  content: "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
1477
1515
  isMeta: true,
@@ -1884,7 +1922,9 @@ ${question}`;
1884
1922
  name: toolName,
1885
1923
  compactParams,
1886
1924
  shortResult: toolResult.shortResult,
1887
- isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
1925
+ backgroundTaskId: toolResult.backgroundTaskId,
1926
+ backgroundedByUser: toolResult.backgroundedByUser,
1927
+ assistantAutoBackgrounded: toolResult.assistantAutoBackgrounded,
1888
1928
  startLineNumber: toolResult.startLineNumber,
1889
1929
  images: toolResult.images,
1890
1930
  timestamp: Date.now(),
@@ -1903,7 +1943,6 @@ ${question}`;
1903
1943
  stage: "end",
1904
1944
  name: toolName,
1905
1945
  compactParams,
1906
- isManuallyBackgrounded: false,
1907
1946
  timestamp: Date.now(),
1908
1947
  });
1909
1948
  }
@@ -26,7 +26,7 @@ export interface MessageManagerCallbacks {
26
26
  onCompactionStateChange?: (isCompacting: boolean) => void;
27
27
  onAddBangMessage?: (command: string, messageId: string) => void;
28
28
  onUpdateBangMessage?: (command: string, output: string, messageId: string) => void;
29
- onCompleteBangMessage?: (command: string, exitCode: number, messageId: string) => void;
29
+ onCompleteBangMessage?: (command: string, exitCode: number, messageId: string, output?: string) => void;
30
30
  onInfoBlockAdded?: (content: string) => void;
31
31
  onShowRewind?: () => void;
32
32
  onFileHistoryBlockAdded?: (snapshots: import("../types/reversion.js").FileSnapshot[]) => void;
@@ -446,7 +446,7 @@ export class MessageManager {
446
446
  });
447
447
  this.setMessages(updatedMessages);
448
448
  const messageId = this.findBangMessageId(command) ?? "";
449
- this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId);
449
+ this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId, output?.trim());
450
450
  }
451
451
  /**
452
452
  * Find the message ID of the most recent message containing a bang block
@@ -686,7 +686,9 @@ export class MessageManager {
686
686
  stage: "end",
687
687
  success: false,
688
688
  error: errorMessage,
689
- isManuallyBackgrounded: block.isManuallyBackgrounded,
689
+ backgroundTaskId: block.backgroundTaskId,
690
+ backgroundedByUser: block.backgroundedByUser,
691
+ assistantAutoBackgrounded: block.assistantAutoBackgrounded,
690
692
  timestamp,
691
693
  });
692
694
  }
@@ -730,7 +732,11 @@ export class MessageManager {
730
732
  // only sees messages from the latest compact summary forward — matching
731
733
  // the compact and resume behaviors (which also fold memory).
732
734
  this.setMessages(sliceFromLastCompact(newMessages));
733
- this.savedMessageCount = newMessages.length;
735
+ // savedMessageCount tracks in-memory progress, so it must be the folded
736
+ // length. Using the full disk count here would make saveSession's
737
+ // slice(savedMessageCount) empty after a rewind past a compact boundary
738
+ // and silently drop every subsequent message from the session file.
739
+ this.savedMessageCount = this.messages.length;
734
740
  }
735
741
  /**
736
742
  * Rewrite the session file with the current messages.
@@ -82,6 +82,11 @@ export declare class PermissionManager {
82
82
  * Get all instance-specific denied rules
83
83
  */
84
84
  getInstanceDeniedRules(): string[];
85
+ /**
86
+ * Add an instance-level allowed rule (session-level, in-memory only).
87
+ * Unlike addPermissionRule, this does NOT persist to settings.local.json.
88
+ */
89
+ addInstanceAllowedRule(rule: string): void;
85
90
  /**
86
91
  * Get all additional directories
87
92
  */
@@ -8,7 +8,7 @@
8
8
  import path from "node:path";
9
9
  import { minimatch } from "minimatch";
10
10
  import { RESTRICTED_TOOLS } from "../types/permissions.js";
11
- import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, } from "../utils/bashParser.js";
11
+ import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, stripGitScopePrefix, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, } from "../utils/bashParser.js";
12
12
  import { isPathInside } from "../utils/pathSafety.js";
13
13
  import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "../constants/tools.js";
14
14
  const DEFAULT_ALLOWED_RULES = [
@@ -140,6 +140,15 @@ export class PermissionManager {
140
140
  getInstanceDeniedRules() {
141
141
  return [...this.instanceDeniedRules];
142
142
  }
143
+ /**
144
+ * Add an instance-level allowed rule (session-level, in-memory only).
145
+ * Unlike addPermissionRule, this does NOT persist to settings.local.json.
146
+ */
147
+ addInstanceAllowedRule(rule) {
148
+ if (!this.instanceAllowedRules.includes(rule)) {
149
+ this.instanceAllowedRules.push(rule);
150
+ }
151
+ }
143
152
  /**
144
153
  * Get all additional directories
145
154
  */
@@ -676,7 +685,14 @@ export class PermissionManager {
676
685
  .replace(/\*/g, ".*"); // Replace * with .*
677
686
  const regex = new RegExp(`^${regexPattern}$`, "s");
678
687
  const matched = regex.test(processedPart);
679
- return matched;
688
+ if (matched)
689
+ return true;
690
+ // Leading git global scope flags (e.g. `git -C <path>`) only change the
691
+ // target repository, not the subcommand being run, so rules like
692
+ // Bash(git status*) also cover `git -C <path> status`. The raw command
693
+ // is checked first so path-specific rules (e.g. deny rules on
694
+ // `git -C /secret status`) still match.
695
+ return regex.test(stripGitScopePrefix(processedPart));
680
696
  }
681
697
  // Handle path-based rules (e.g., "Read(**/*.env)")
682
698
  const pathTools = [READ_TOOL_NAME, WRITE_TOOL_NAME, EDIT_TOOL_NAME];
@@ -6,10 +6,19 @@ export interface PluginManagerOptions {
6
6
  }
7
7
  export declare class PluginManager {
8
8
  private container;
9
+ /**
10
+ * Read-only helper scripts shipped by builtin plugins that the agent runs via
11
+ * Bash. When such a plugin is enabled, its rules are registered as instance
12
+ * level allow rules (in-memory only, never persisted) so the scripts run
13
+ * without a permission prompt. Wildcards keep the rules valid across install
14
+ * locations; each rule anchors on the script filename.
15
+ */
16
+ private static readonly BUILTIN_PLUGIN_ALLOW_RULES;
9
17
  private plugins;
10
18
  private workdir;
11
19
  private enabledPlugins;
12
20
  constructor(container: Container, options: PluginManagerOptions);
21
+ private get permissionManager();
13
22
  private get skillManager();
14
23
  private get hookManager();
15
24
  private get lspManager();
@@ -11,6 +11,9 @@ export class PluginManager {
11
11
  this.workdir = options.workdir;
12
12
  this.enabledPlugins = options.enabledPlugins || {};
13
13
  }
14
+ get permissionManager() {
15
+ return this.container.get("PermissionManager");
16
+ }
14
17
  get skillManager() {
15
18
  return this.container.get("SkillManager");
16
19
  }
@@ -221,6 +224,13 @@ export class PluginManager {
221
224
  if (this.enabledPlugins[`${entry.name}@builtin`] !== true)
222
225
  continue;
223
226
  await this.loadSinglePlugin(path.join(builtinDir, entry.name));
227
+ // Register allow rules only after the plugin actually loaded, so a
228
+ // failed load never leaves permission grants behind.
229
+ if (this.plugins.has(entry.name)) {
230
+ for (const rule of PluginManager.BUILTIN_PLUGIN_ALLOW_RULES[entry.name] || []) {
231
+ this.permissionManager?.addInstanceAllowedRule(rule);
232
+ }
233
+ }
224
234
  }
225
235
  }
226
236
  catch (error) {
@@ -240,3 +250,13 @@ export class PluginManager {
240
250
  return this.plugins.get(name);
241
251
  }
242
252
  }
253
+ /**
254
+ * Read-only helper scripts shipped by builtin plugins that the agent runs via
255
+ * Bash. When such a plugin is enabled, its rules are registered as instance
256
+ * level allow rules (in-memory only, never persisted) so the scripts run
257
+ * without a permission prompt. Wildcards keep the rules valid across install
258
+ * locations; each rule anchors on the script filename.
259
+ */
260
+ PluginManager.BUILTIN_PLUGIN_ALLOW_RULES = {
261
+ sdd: ["Bash(node *spec-count.js*)"],
262
+ };
@@ -87,6 +87,18 @@ export declare class SubagentManager {
87
87
  * Load all available subagent configurations and cache them
88
88
  */
89
89
  loadConfigurations(): Promise<SubagentConfiguration[]>;
90
+ /**
91
+ * Rebuild the cached subagent configurations. Called after settings.json
92
+ * env becomes available (post loadMergedConfiguration) so conditional
93
+ * builtin subagents (e.g. `model: visionModel` requiring WAVE_VISION_MODEL)
94
+ * register correctly. Plugin agents are preserved across the rebuild.
95
+ */
96
+ refreshConfigurations(): Promise<SubagentConfiguration[]>;
97
+ /**
98
+ * Get the merged environment (OS env overlaid with settings.json env) used
99
+ * for conditional subagent registration (e.g. WAVE_VISION_MODEL).
100
+ */
101
+ private getMergedEnv;
90
102
  /**
91
103
  * Get cached configurations synchronously (must call loadConfigurations first)
92
104
  */
@@ -70,10 +70,40 @@ export class SubagentManager {
70
70
  async loadConfigurations() {
71
71
  if (this.cachedConfigurations === null) {
72
72
  const { loadSubagentConfigurations } = await import("../utils/subagentParser.js");
73
- this.cachedConfigurations = await loadSubagentConfigurations(this.workdir);
73
+ this.cachedConfigurations = await loadSubagentConfigurations(this.workdir, this.getMergedEnv());
74
74
  }
75
75
  return this.cachedConfigurations;
76
76
  }
77
+ /**
78
+ * Rebuild the cached subagent configurations. Called after settings.json
79
+ * env becomes available (post loadMergedConfiguration) so conditional
80
+ * builtin subagents (e.g. `model: visionModel` requiring WAVE_VISION_MODEL)
81
+ * register correctly. Plugin agents are preserved across the rebuild.
82
+ */
83
+ async refreshConfigurations() {
84
+ // Preserve plugin agents (namespaced `pluginName:agentName`) across the rebuild
85
+ const pluginAgents = (this.cachedConfigurations ?? []).filter((config) => config.name.includes(":"));
86
+ this.cachedConfigurations = null;
87
+ await this.loadConfigurations();
88
+ for (const agent of pluginAgents) {
89
+ this.cachedConfigurations.push(agent);
90
+ }
91
+ // Re-sort by priority then name (matches registerPluginAgents)
92
+ this.cachedConfigurations.sort((a, b) => {
93
+ if (a.priority !== b.priority)
94
+ return a.priority - b.priority;
95
+ return a.name.localeCompare(b.name);
96
+ });
97
+ return this.cachedConfigurations;
98
+ }
99
+ /**
100
+ * Get the merged environment (OS env overlaid with settings.json env) used
101
+ * for conditional subagent registration (e.g. WAVE_VISION_MODEL).
102
+ */
103
+ getMergedEnv() {
104
+ return (this.configurationService?.getMergedEnv?.() ??
105
+ process.env);
106
+ }
77
107
  /**
78
108
  * Get cached configurations synchronously (must call loadConfigurations first)
79
109
  */
@@ -95,7 +125,7 @@ export class SubagentManager {
95
125
  }
96
126
  // Fall back to filesystem scan for non-plugin agents
97
127
  const { findSubagentByName } = await import("../utils/subagentParser.js");
98
- return findSubagentByName(name, this.workdir);
128
+ return findSubagentByName(name, this.workdir, this.getMergedEnv());
99
129
  }
100
130
  /**
101
131
  * Register plugin agents into the cached configurations.
@@ -9,6 +9,8 @@ import { cronCreateTool } from "../tools/cronCreateTool.js";
9
9
  import { cronDeleteTool } from "../tools/cronDeleteTool.js";
10
10
  import { cronListTool } from "../tools/cronListTool.js";
11
11
  import { webFetchTool } from "../tools/webFetchTool.js";
12
+ import { artifactTool } from "../tools/artifactTool.js";
13
+ import { isArtifactEnabled } from "../services/artifactAvailability.js";
12
14
  // New tools
13
15
  import { globTool } from "../tools/globTool.js";
14
16
  import { grepTool } from "../tools/grepTool.js";
@@ -95,6 +97,11 @@ class ToolManager {
95
97
  exitWorktreeTool,
96
98
  workflowTool,
97
99
  ];
100
+ // Artifact is a feature-gated tool: not registered at all while the frame
101
+ // backend is not live, unless settings.json opts in via enableArtifact: true.
102
+ if (isArtifactEnabled(this.container.get("Workdir"))) {
103
+ builtInTools.push(artifactTool);
104
+ }
98
105
  for (const tool of builtInTools) {
99
106
  if (this.shouldEnableTool(tool.name)) {
100
107
  this.toolsRegistry.set(tool.name, tool);
@@ -1,7 +1,6 @@
1
1
  import { ChatCompletionMessageToolCall } from "openai/resources";
2
2
  import { ChatCompletionMessageParam, ChatCompletionFunctionTool } from "openai/resources.js";
3
- import type { GatewayConfig, ModelConfig } from "../types/index.js";
4
- import { type ClaudeUsage } from "../utils/cacheControlUtils.js";
3
+ import type { GatewayConfig, ModelConfig, Usage } from "../types/index.js";
5
4
  import { type SystemPromptBlock } from "../prompts/index.js";
6
5
  /**
7
6
  * Resets the rate limiter state. Primarily used for testing.
@@ -29,7 +28,7 @@ export interface CallAgentOptions {
29
28
  onToolUpdate?: (toolCall: {
30
29
  id: string;
31
30
  name: string;
32
- parameters: string;
31
+ parameters?: string;
33
32
  parametersChunk?: string;
34
33
  stage?: "start" | "streaming" | "running" | "end";
35
34
  }) => void;
@@ -40,7 +39,7 @@ export interface CallAgentResult {
40
39
  content?: string;
41
40
  tool_calls?: ChatCompletionMessageToolCall[];
42
41
  reasoning_content?: string;
43
- usage?: ClaudeUsage;
42
+ usage?: Usage;
44
43
  finish_reason?: "stop" | "length" | "tool_calls" | "content_filter" | "function_call" | null;
45
44
  response_headers?: Record<string, string>;
46
45
  additionalFields?: Record<string, unknown>;
@@ -202,17 +202,9 @@ export async function callAgent(options) {
202
202
  });
203
203
  const finalMessage = response.choices[0]?.message;
204
204
  const finishReason = response.choices[0]?.finish_reason || null;
205
- let totalUsage = response.usage
206
- ? {
207
- prompt_tokens: response.usage.prompt_tokens,
208
- completion_tokens: response.usage.completion_tokens,
209
- total_tokens: response.usage.total_tokens,
210
- }
205
+ const totalUsage = response.usage
206
+ ? extendUsageWithCacheMetrics(response.usage)
211
207
  : undefined;
212
- // Extend usage with cache metrics (Claude top-level + OpenAI prompt_tokens_details)
213
- if (totalUsage && response.usage) {
214
- totalUsage = extendUsageWithCacheMetrics(totalUsage, response.usage);
215
- }
216
208
  const result = {};
217
209
  if (finalMessage) {
218
210
  const { content: finalContent, tool_calls: finalToolCalls, reasoning_content: finalReasoningContent, ...otherFields } = finalMessage;
@@ -353,14 +345,8 @@ async function processStreamingResponse(stream, onContentUpdate, onToolUpdate, o
353
345
  }
354
346
  // Check for usage information in any chunk
355
347
  if (chunk.usage) {
356
- let chunkUsage = {
357
- prompt_tokens: chunk.usage.prompt_tokens,
358
- completion_tokens: chunk.usage.completion_tokens,
359
- total_tokens: chunk.usage.total_tokens,
360
- };
361
- // Extend usage with cache metrics (Claude top-level + OpenAI prompt_tokens_details)
362
- chunkUsage = extendUsageWithCacheMetrics(chunkUsage, chunk.usage);
363
- usage = chunkUsage;
348
+ // Extend usage with cache metrics from OpenAI prompt_tokens_details
349
+ usage = extendUsageWithCacheMetrics(chunk.usage);
364
350
  }
365
351
  // Check for finish_reason in the choice
366
352
  const choice = chunk.choices?.[0];
@@ -439,7 +425,9 @@ async function processStreamingResponse(stream, onContentUpdate, onToolUpdate, o
439
425
  if (functionDelta.arguments) {
440
426
  existingCall.function.arguments += functionDelta.arguments;
441
427
  }
442
- // Emit streaming updates for all chunks with actual content (including first chunk)
428
+ // Emit streaming updates for all chunks with actual content (including first chunk).
429
+ // Streaming carries only the delta `parametersChunk` — consumers accumulate it;
430
+ // authoritative `parameters` arrives at `start` (empty) / `running` / `end`.
443
431
  if (onToolUpdate &&
444
432
  existingCall.function.name &&
445
433
  functionDelta.arguments &&
@@ -448,7 +436,6 @@ async function processStreamingResponse(stream, onContentUpdate, onToolUpdate, o
448
436
  onToolUpdate({
449
437
  id: existingCall.id,
450
438
  name: existingCall.function.name,
451
- parameters: existingCall.function.arguments,
452
439
  parametersChunk: functionDelta.arguments,
453
440
  stage: "streaming",
454
441
  });
@@ -0,0 +1,7 @@
1
+ /** Code default for Artifact availability. Flip to true after the frame backend goes live. */
2
+ export declare const ARTIFACT_DEFAULT_ENABLED = false;
3
+ /**
4
+ * Whether the Artifact tool should be registered / usable for the given workdir.
5
+ * Explicit `enableArtifact` in merged settings wins over the code default.
6
+ */
7
+ export declare function isArtifactEnabled(workdir?: string): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Artifact feature availability.
3
+ *
4
+ * The frame backend (`POST /api/frame/deploy/direct`) is not live yet, so the
5
+ * code default is DISABLED. Internal beta / gradual rollout enables the feature
6
+ * via settings.json `enableArtifact: true` without code changes. Once the
7
+ * backend goes live, flip `ARTIFACT_DEFAULT_ENABLED` to `true` so an unset
8
+ * `enableArtifact` defaults to enabled (matching Claude Code's `enableArtifact`
9
+ * "unset follows feature availability" semantics).
10
+ */
11
+ import { loadMergedWaveConfig } from "./configurationService.js";
12
+ /** Code default for Artifact availability. Flip to true after the frame backend goes live. */
13
+ export const ARTIFACT_DEFAULT_ENABLED = false;
14
+ /**
15
+ * Whether the Artifact tool should be registered / usable for the given workdir.
16
+ * Explicit `enableArtifact` in merged settings wins over the code default.
17
+ */
18
+ export function isArtifactEnabled(workdir) {
19
+ if (workdir) {
20
+ const config = loadMergedWaveConfig(workdir);
21
+ if (config?.enableArtifact !== undefined) {
22
+ return config.enableArtifact;
23
+ }
24
+ }
25
+ return ARTIFACT_DEFAULT_ENABLED;
26
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Session-scoped state shared between the Artifact tool and the WebFetch tool.
3
+ *
4
+ * - file_path → artifact record: lets a model republish a file it already
5
+ * published this session without passing `url` again, and enables the
6
+ * same-session auto-allow (the publish was already confirmed once).
7
+ * - slug → latest known version: powers the stale-version guard
8
+ * (local knowledge older than the live version blocks a redeploy
9
+ * unless `force` is set) and lets WebFetch record versions it observed.
10
+ *
11
+ * State is keyed by sessionId so parallel sessions never observe each other.
12
+ */
13
+ export interface ArtifactRecord {
14
+ url: string;
15
+ slug: string;
16
+ version: string;
17
+ }
18
+ /** Record a successful publish so same-session republishes auto-allow. */
19
+ export declare function recordArtifact(sessionId: string, filePath: string, record: ArtifactRecord): void;
20
+ /** Look up an artifact previously published in this session by file path. */
21
+ export declare function getArtifactByFilePath(sessionId: string, filePath: string): ArtifactRecord | undefined;
22
+ /** Latest version of a slug this session has observed (publish, conflict, or read). */
23
+ export declare function getRecordedVersion(sessionId: string, slug: string): string | undefined;
24
+ /** Record an observed version (from a publish, a 409 conflict, or a WebFetch read). */
25
+ export declare function recordVersion(sessionId: string, slug: string, version: string): void;
26
+ /** Clear all session-scoped artifact state (used when a session ends). */
27
+ export declare function clearArtifactSession(sessionId: string): void;