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
@@ -2,12 +2,18 @@ import TurndownService from "turndown";
2
2
  import { LRUCache } from "lru-cache";
3
3
  import { WEB_FETCH_TOOL_NAME } from "../constants/tools.js";
4
4
  import { logger } from "../utils/globalLogger.js";
5
+ import { isArtifactEnabled } from "../services/artifactAvailability.js";
6
+ import { authService, createAuthAwareFetch } from "../services/authService.js";
7
+ import { recordVersion } from "../services/artifactSession.js";
8
+ import { buildPersistedOutputMessage, generatePreview, persistToolResult, } from "../utils/toolResultStorage.js";
5
9
  // --- Security Limits ---
6
10
  const MAX_HTTP_CONTENT_LENGTH = 10 * 1024 * 1024; // 10MB
7
11
  const FETCH_TIMEOUT_MS = 60000; // 60s
8
12
  const MAX_REDIRECTS = 10;
9
13
  const MAX_MARKDOWN_LENGTH = 100000;
10
14
  const USER_AGENT = "Wave-User (+https://github.com/netease-lcap/wave-agent)";
15
+ /** Artifact HTML beyond this size is persisted to a temp file (path + head preview). */
16
+ const ARTIFACT_PREVIEW_BYTES = 2 * 1024; // ~2KB
11
17
  // --- Cache (LRU with 15min TTL, 50MB max) ---
12
18
  const CACHE_TTL = 15 * 60 * 1000; // 15 minutes
13
19
  const CACHE_MAX_BYTES = 50 * 1024 * 1024; // 50MB
@@ -73,6 +79,124 @@ function isPermittedRedirect(originalUrl, redirectUrl) {
73
79
  return false;
74
80
  }
75
81
  }
82
+ // --- Artifact read channel ---
83
+ /** Extract the artifact slug from a `{host}/code/artifact/{slug}` URL. */
84
+ function extractArtifactSlug(url) {
85
+ try {
86
+ const parsed = new URL(url);
87
+ const match = parsed.pathname.match(/^\/code\/artifact\/([^/]+)\/?$/);
88
+ return match ? decodeURIComponent(match[1]) : null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Fetch artifact metadata (`via=model_read`) then the HTML content (Bearer).
96
+ * Returns markdown (HTML converted via turndown) on success.
97
+ */
98
+ async function readArtifact(url, slug, abortSignal) {
99
+ const serverUrl = authService.getServerUrl();
100
+ const authFetch = createAuthAwareFetch(globalThis.fetch);
101
+ const signal = abortSignal
102
+ ? AbortSignal.any([abortSignal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
103
+ : AbortSignal.timeout(FETCH_TIMEOUT_MS);
104
+ let meta;
105
+ try {
106
+ const metaRes = await authFetch(`${serverUrl}/api/frame/${encodeURIComponent(slug)}?via=model_read`, { method: "GET", signal });
107
+ if (metaRes.status === 404) {
108
+ return {
109
+ kind: "error",
110
+ error: `Artifact not found: ${url} (it may have been deleted)`,
111
+ };
112
+ }
113
+ if (!metaRes.ok) {
114
+ return {
115
+ kind: "error",
116
+ error: `Failed to fetch artifact metadata: ${metaRes.status} ${metaRes.statusText}`,
117
+ };
118
+ }
119
+ meta = (await metaRes.json());
120
+ }
121
+ catch (error) {
122
+ logger?.warn("Artifact read metadata failed", {
123
+ slug,
124
+ error: String(error),
125
+ });
126
+ return {
127
+ kind: "error",
128
+ error: `Failed to fetch artifact metadata: ${error instanceof Error ? error.message : String(error)}`,
129
+ };
130
+ }
131
+ const contentUrl = typeof meta.contentUrl === "string" ? meta.contentUrl : "";
132
+ const version = typeof meta.version === "string" ? meta.version : "";
133
+ if (!contentUrl) {
134
+ return {
135
+ kind: "error",
136
+ error: "Artifact metadata did not include a contentUrl",
137
+ };
138
+ }
139
+ let html;
140
+ try {
141
+ const contentRes = await authFetch(new URL(contentUrl, serverUrl).toString(), { method: "GET", signal });
142
+ if (!contentRes.ok) {
143
+ return {
144
+ kind: "error",
145
+ error: `Failed to fetch artifact content: ${contentRes.status} ${contentRes.statusText}`,
146
+ };
147
+ }
148
+ html = await contentRes.text();
149
+ }
150
+ catch (error) {
151
+ logger?.warn("Artifact read content failed", {
152
+ slug,
153
+ error: String(error),
154
+ });
155
+ return {
156
+ kind: "error",
157
+ error: `Failed to fetch artifact content: ${error instanceof Error ? error.message : String(error)}`,
158
+ };
159
+ }
160
+ const turndownService = new TurndownService();
161
+ const markdown = turndownService.turndown(html);
162
+ return {
163
+ kind: "ok",
164
+ slug,
165
+ version,
166
+ markdown,
167
+ bytes: new TextEncoder().encode(markdown).length,
168
+ };
169
+ }
170
+ /**
171
+ * Process an artifact read result: persist content >~2KB to a temp file
172
+ * (return file path + head preview), run the prompt, attach artifactRead metadata.
173
+ */
174
+ async function processArtifactRead(url, prompt, markdown, slug, version, context) {
175
+ const bytes = new TextEncoder().encode(markdown).length;
176
+ let aiInput = markdown;
177
+ let persistedMessage = "";
178
+ if (bytes > ARTIFACT_PREVIEW_BYTES) {
179
+ const filePath = persistToolResult(markdown, "artifact");
180
+ if (filePath) {
181
+ persistedMessage = buildPersistedOutputMessage(markdown.length, filePath, generatePreview(markdown));
182
+ aiInput = persistedMessage;
183
+ }
184
+ else {
185
+ aiInput =
186
+ markdown.substring(0, MAX_MARKDOWN_LENGTH) +
187
+ "\n\n... (content truncated, failed to persist full output)";
188
+ }
189
+ }
190
+ const result = await processWithAI(url, prompt, aiInput, 200, "OK", context, bytes);
191
+ result.metadata = { artifactRead: { slug, ver: version } };
192
+ if (persistedMessage) {
193
+ // Append the persisted-output message so the model can Read the full content.
194
+ result.content = result.content
195
+ ? result.content + "\n\n" + persistedMessage
196
+ : persistedMessage;
197
+ }
198
+ return result;
199
+ }
76
200
  // --- Tool ---
77
201
  export const webFetchTool = {
78
202
  name: WEB_FETCH_TOOL_NAME,
@@ -141,6 +265,23 @@ Usage notes:
141
265
  error: validation.error,
142
266
  };
143
267
  }
268
+ // Artifact URL interception: {host}/code/artifact/{slug} goes through the
269
+ // dedicated read channel (via=model_read + Bearer) instead of a public fetch.
270
+ // Only when the Artifact tool is enabled (spec 6.4: disabled = no interception).
271
+ if (isArtifactEnabled(context.workdir)) {
272
+ const artifactSlug = extractArtifactSlug(url);
273
+ if (artifactSlug) {
274
+ const readResult = await readArtifact(url, artifactSlug, context.abortSignal);
275
+ if (readResult.kind === "error") {
276
+ return { success: false, content: "", error: readResult.error };
277
+ }
278
+ if (context.sessionId && readResult.version) {
279
+ // Keep the stale-version guard in sync with what the model has seen.
280
+ recordVersion(context.sessionId, artifactSlug, readResult.version);
281
+ }
282
+ return processArtifactRead(url, prompt, readResult.markdown, artifactSlug, readResult.version, context);
283
+ }
284
+ }
144
285
  try {
145
286
  const cached = cache.get(url);
146
287
  if (cached) {
@@ -25,6 +25,8 @@ export interface AgentOptions {
25
25
  fetch?: ClientOptions["fetch"];
26
26
  model?: string;
27
27
  fastModel?: string;
28
+ /** Vision-capable model used by the builtin vision subagent (resolved from WAVE_VISION_MODEL env var). */
29
+ visionModel?: string;
28
30
  maxInputTokens?: number;
29
31
  maxTokens?: number;
30
32
  /** Preferred language for agent communication */
@@ -22,6 +22,8 @@ export interface ModelCapabilities {
22
22
  export interface ModelConfig {
23
23
  model?: string;
24
24
  fastModel?: string;
25
+ /** Vision-capable model for image recognition subagents (resolved from WAVE_VISION_MODEL env var). */
26
+ visionModel?: string;
25
27
  maxTokens?: number;
26
28
  permissionMode?: PermissionMode;
27
29
  capabilities?: ModelCapabilities;
@@ -55,6 +55,8 @@ export interface WaveConfiguration {
55
55
  /** Base ref for new worktrees: "fresh" (origin/<default-branch>, default) | "head" (local HEAD) */
56
56
  baseRef?: "fresh" | "head";
57
57
  };
58
+ /** Whether the Artifact tool is enabled. Unset follows the code default constant (ARTIFACT_DEFAULT_ENABLED). */
59
+ enableArtifact?: boolean;
58
60
  }
59
61
  /**
60
62
  * Legacy alias for backward compatibility - will be deprecated
@@ -2,7 +2,6 @@
2
2
  * Core foundational types used across multiple domains
3
3
  * Dependencies: None (foundation layer)
4
4
  */
5
- import type { CompletionUsage } from "openai/resources";
6
5
  /**
7
6
  * Logger interface definition
8
7
  * Compatible with OpenAI package Logger interface
@@ -15,7 +14,7 @@ export interface Logger {
15
14
  }
16
15
  /**
17
16
  * Usage statistics for AI operations
18
- * Extends OpenAI's Usage format with additional tracking fields
17
+ * Extends OpenAI's Usage format with normalized cache fields
19
18
  */
20
19
  export interface Usage {
21
20
  prompt_tokens: number;
@@ -25,38 +24,6 @@ export interface Usage {
25
24
  operation_type?: "agent" | "compact";
26
25
  cache_read_input_tokens?: number;
27
26
  cache_creation_input_tokens?: number;
28
- cache_creation?: {
29
- ephemeral_5m_input_tokens: number;
30
- ephemeral_1h_input_tokens: number;
31
- };
32
- }
33
- /**
34
- * Enhanced usage metrics including Claude cache information
35
- * Backward compatible with standard OpenAI CompletionUsage
36
- */
37
- export interface ClaudeUsage extends CompletionUsage {
38
- prompt_tokens: number;
39
- completion_tokens: number;
40
- total_tokens: number;
41
- /**
42
- * Number of tokens read from existing cache
43
- * Indicates cost savings from cache hits
44
- */
45
- cache_read_input_tokens?: number;
46
- /**
47
- * Number of tokens used to create new cache entries
48
- * Investment in future cache hits
49
- */
50
- cache_creation_input_tokens?: number;
51
- /**
52
- * Detailed breakdown of cache creation by duration
53
- */
54
- cache_creation?: {
55
- /** Tokens cached for 5 minute duration */
56
- ephemeral_5m_input_tokens: number;
57
- /** Tokens cached for 1 hour duration */
58
- ephemeral_1h_input_tokens: number;
59
- };
60
27
  }
61
28
  /**
62
29
  * Represents a diff change for tool parameter-based diff display
@@ -38,3 +38,4 @@ export * from "./workflow.js";
38
38
  export type { SessionMetadata, SessionData } from "../services/session.js";
39
39
  export type { ToolBlockUpdateCallbackParams } from "../utils/messageOperations.js";
40
40
  export type { QueuedMessage } from "../managers/messageQueue.js";
41
+ export type { SubagentConfiguration } from "../utils/subagentParser.js";
@@ -52,7 +52,9 @@ export interface ToolBlock {
52
52
  error?: string | Error;
53
53
  compactParams?: string;
54
54
  parametersChunk?: string;
55
- isManuallyBackgrounded?: boolean;
55
+ backgroundTaskId?: string;
56
+ backgroundedByUser?: boolean;
57
+ assistantAutoBackgrounded?: boolean;
56
58
  timestamp?: number;
57
59
  }
58
60
  export interface ImageBlock {
@@ -36,9 +36,11 @@ export interface ToolPermissionContext {
36
36
  toolCallId?: string;
37
37
  /** The content of the plan being exited from */
38
38
  planContent?: string;
39
+ /** Optional warning line to surface in the confirmation UI (e.g. shared-live redeploy impact) */
40
+ warning?: string;
39
41
  }
40
42
  /** List of tools that require permission checks in default mode */
41
- export declare const RESTRICTED_TOOLS: readonly ["Edit", "Bash", "Write", "EnterPlanMode", "ExitPlanMode", "AskUserQuestion"];
43
+ export declare const RESTRICTED_TOOLS: readonly ["Edit", "Bash", "Write", "EnterPlanMode", "ExitPlanMode", "AskUserQuestion", "Artifact"];
42
44
  /** Type for restricted tool names */
43
45
  export type RestrictedTool = (typeof RESTRICTED_TOOLS)[number];
44
46
  export declare const OPERATION_CANCELLED_BY_USER = "Operation cancelled by user";
@@ -2,7 +2,7 @@
2
2
  * Permission system types for Wave Agent SDK
3
3
  * Dependencies: None
4
4
  */
5
- import { EDIT_TOOL_NAME, BASH_TOOL_NAME, WRITE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "../constants/tools.js";
5
+ import { EDIT_TOOL_NAME, BASH_TOOL_NAME, WRITE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, ARTIFACT_TOOL_NAME, } from "../constants/tools.js";
6
6
  /** List of tools that require permission checks in default mode */
7
7
  export const RESTRICTED_TOOLS = [
8
8
  EDIT_TOOL_NAME,
@@ -11,5 +11,6 @@ export const RESTRICTED_TOOLS = [
11
11
  ENTER_PLAN_MODE_TOOL_NAME,
12
12
  EXIT_PLAN_MODE_TOOL_NAME,
13
13
  ASK_USER_QUESTION_TOOL_NAME,
14
+ ARTIFACT_TOOL_NAME,
14
15
  ];
15
16
  export const OPERATION_CANCELLED_BY_USER = "Operation cancelled by user";
@@ -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.
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { ChatCompletionMessageParam, ChatCompletionContentPart, ChatCompletionContentPartText, CompletionUsage } from "openai/resources";
9
9
  import type { ModelCapabilities } from "../types/config.js";
10
+ import type { Usage } from "../types/core.js";
10
11
  /**
11
12
  * Cache control directive for Claude models
12
13
  */
@@ -28,22 +29,6 @@ export interface ClaudeChatCompletionContentPartText extends ChatCompletionConte
28
29
  export interface ExtendedPromptTokensDetails extends CompletionUsage.PromptTokensDetails {
29
30
  cache_creation_input_tokens?: number;
30
31
  }
31
- /**
32
- * Enhanced usage metrics including cache information
33
- * Supports both Claude-specific top-level fields and OpenAI-standard prompt_tokens_details
34
- */
35
- export interface ClaudeUsage extends CompletionUsage {
36
- prompt_tokens: number;
37
- completion_tokens: number;
38
- total_tokens: number;
39
- cache_read_input_tokens?: number;
40
- cache_creation_input_tokens?: number;
41
- cache_creation?: {
42
- ephemeral_5m_input_tokens: number;
43
- ephemeral_1h_input_tokens: number;
44
- };
45
- prompt_tokens_details?: ExtendedPromptTokensDetails;
46
- }
47
32
  /**
48
33
  * Validates cache control structure
49
34
  * @param control - Object to validate
@@ -85,16 +70,9 @@ export declare function countContentBlocks(messages: ChatCompletionMessageParam[
85
70
  export declare function transformMessagesForExplicitCache(messages: ChatCompletionMessageParam[], capabilities?: ModelCapabilities): ChatCompletionMessageParam[];
86
71
  /**
87
72
  * Extends standard usage with cache metrics
88
- * Extracts cache tokens from both Claude-specific top-level fields and
89
- * OpenAI-standard prompt_tokens_details (used by Gemini, DeepSeek, etc.)
90
- * @param standardUsage - OpenAI usage response
91
- * @param cacheMetrics - Additional cache metrics from the API response
73
+ * Extracts cache tokens from OpenAI-standard prompt_tokens_details
74
+ * (cached_tokens and the gateway-provided cache_creation_input_tokens)
75
+ * @param usage - OpenAI usage response
92
76
  * @returns Extended usage with cache information
93
77
  */
94
- export declare function extendUsageWithCacheMetrics(standardUsage: CompletionUsage, cacheMetrics?: Partial<ClaudeUsage>): ClaudeUsage;
95
- /**
96
- * Validates Claude usage structure
97
- * @param usage - Usage object to validate
98
- * @returns True if usage structure is valid
99
- */
100
- export declare function isValidClaudeUsage(usage: unknown): usage is ClaudeUsage;
78
+ export declare function extendUsageWithCacheMetrics(usage: CompletionUsage): Usage;
@@ -207,84 +207,23 @@ export function transformMessagesForExplicitCache(messages, capabilities) {
207
207
  }
208
208
  /**
209
209
  * Extends standard usage with cache metrics
210
- * Extracts cache tokens from both Claude-specific top-level fields and
211
- * OpenAI-standard prompt_tokens_details (used by Gemini, DeepSeek, etc.)
212
- * @param standardUsage - OpenAI usage response
213
- * @param cacheMetrics - Additional cache metrics from the API response
210
+ * Extracts cache tokens from OpenAI-standard prompt_tokens_details
211
+ * (cached_tokens and the gateway-provided cache_creation_input_tokens)
212
+ * @param usage - OpenAI usage response
214
213
  * @returns Extended usage with cache information
215
214
  */
216
- export function extendUsageWithCacheMetrics(standardUsage, cacheMetrics) {
215
+ export function extendUsageWithCacheMetrics(usage) {
217
216
  const baseUsage = {
218
- prompt_tokens: standardUsage.prompt_tokens,
219
- completion_tokens: standardUsage.completion_tokens,
220
- total_tokens: standardUsage.total_tokens,
217
+ prompt_tokens: usage.prompt_tokens,
218
+ completion_tokens: usage.completion_tokens,
219
+ total_tokens: usage.total_tokens,
221
220
  };
222
- if (!cacheMetrics) {
223
- return baseUsage;
221
+ const details = usage.prompt_tokens_details;
222
+ if (details?.cached_tokens != null) {
223
+ baseUsage.cache_read_input_tokens = details.cached_tokens;
224
224
  }
225
- // Extract cache_read_input_tokens from Claude top-level field
226
- if (typeof cacheMetrics.cache_read_input_tokens === "number") {
227
- baseUsage.cache_read_input_tokens = cacheMetrics.cache_read_input_tokens;
228
- }
229
- // Fallback to prompt_tokens_details.cached_tokens (OpenAI standard)
230
- else if (cacheMetrics.prompt_tokens_details?.cached_tokens != null) {
231
- baseUsage.cache_read_input_tokens =
232
- cacheMetrics.prompt_tokens_details.cached_tokens;
233
- }
234
- // Extract cache_creation_input_tokens from Claude top-level field
235
- if (typeof cacheMetrics.cache_creation_input_tokens === "number") {
236
- baseUsage.cache_creation_input_tokens =
237
- cacheMetrics.cache_creation_input_tokens;
238
- }
239
- // Fallback to prompt_tokens_details.cache_creation_input_tokens
240
- else if (cacheMetrics.prompt_tokens_details?.cache_creation_input_tokens != null) {
241
- baseUsage.cache_creation_input_tokens =
242
- cacheMetrics.prompt_tokens_details.cache_creation_input_tokens;
243
- }
244
- // Extract cache_creation breakdown (Claude-specific)
245
- if (cacheMetrics.cache_creation &&
246
- typeof cacheMetrics.cache_creation.ephemeral_5m_input_tokens === "number" &&
247
- typeof cacheMetrics.cache_creation.ephemeral_1h_input_tokens === "number") {
248
- baseUsage.cache_creation = {
249
- ephemeral_5m_input_tokens: cacheMetrics.cache_creation.ephemeral_5m_input_tokens,
250
- ephemeral_1h_input_tokens: cacheMetrics.cache_creation.ephemeral_1h_input_tokens,
251
- };
225
+ if (details?.cache_creation_input_tokens != null) {
226
+ baseUsage.cache_creation_input_tokens = details.cache_creation_input_tokens;
252
227
  }
253
228
  return baseUsage;
254
229
  }
255
- /**
256
- * Validates Claude usage structure
257
- * @param usage - Usage object to validate
258
- * @returns True if usage structure is valid
259
- */
260
- export function isValidClaudeUsage(usage) {
261
- if (!usage || typeof usage !== "object") {
262
- return false;
263
- }
264
- const usageObj = usage;
265
- // Check required standard fields
266
- const hasStandardFields = typeof usageObj.prompt_tokens === "number" &&
267
- typeof usageObj.completion_tokens === "number" &&
268
- typeof usageObj.total_tokens === "number";
269
- if (!hasStandardFields) {
270
- return false;
271
- }
272
- // Check optional cache fields
273
- const hasCacheFields = (usageObj.cache_read_input_tokens === undefined ||
274
- typeof usageObj.cache_read_input_tokens === "number") &&
275
- (usageObj.cache_creation_input_tokens === undefined ||
276
- typeof usageObj.cache_creation_input_tokens === "number");
277
- if (!hasCacheFields) {
278
- return false;
279
- }
280
- // Check cache_creation object if present
281
- if (usageObj.cache_creation !== undefined) {
282
- const cacheCreation = usageObj.cache_creation;
283
- if (typeof cacheCreation !== "object" ||
284
- typeof cacheCreation.ephemeral_5m_input_tokens !== "number" ||
285
- typeof cacheCreation.ephemeral_1h_input_tokens !== "number") {
286
- return false;
287
- }
288
- }
289
- return true;
290
- }
@@ -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
  };