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.
- package/builtin/skills/settings/ENV.md +1 -0
- package/builtin/skills/settings/MODELS.md +3 -0
- package/builtin/subagents/vision.md +18 -0
- package/dist/agent.d.ts +11 -0
- package/dist/agent.js +15 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/managers/aiManager.js +46 -7
- package/dist/managers/messageManager.d.ts +1 -1
- package/dist/managers/messageManager.js +9 -3
- package/dist/managers/permissionManager.d.ts +5 -0
- package/dist/managers/permissionManager.js +18 -2
- package/dist/managers/pluginManager.d.ts +9 -0
- package/dist/managers/pluginManager.js +20 -0
- package/dist/managers/subagentManager.d.ts +12 -0
- package/dist/managers/subagentManager.js +32 -2
- package/dist/managers/toolManager.js +7 -0
- package/dist/services/aiService.d.ts +3 -4
- package/dist/services/aiService.js +7 -20
- package/dist/services/artifactAvailability.d.ts +7 -0
- package/dist/services/artifactAvailability.js +26 -0
- package/dist/services/artifactSession.d.ts +27 -0
- package/dist/services/artifactSession.js +52 -0
- package/dist/services/configurationService.d.ts +9 -1
- package/dist/services/configurationService.js +26 -1
- package/dist/services/initializationService.js +5 -0
- package/dist/tools/agentTool.js +2 -1
- package/dist/tools/artifactTool.d.ts +2 -0
- package/dist/tools/artifactTool.js +357 -0
- package/dist/tools/bashTool.js +11 -11
- package/dist/tools/types.d.ts +3 -1
- package/dist/tools/webFetchTool.js +141 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/configuration.d.ts +2 -0
- package/dist/types/core.d.ts +1 -34
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messaging.d.ts +3 -1
- package/dist/types/permissions.d.ts +3 -1
- package/dist/types/permissions.js +2 -1
- package/dist/utils/bashParser.d.ts +14 -0
- package/dist/utils/bashParser.js +45 -1
- package/dist/utils/cacheControlUtils.d.ts +5 -27
- package/dist/utils/cacheControlUtils.js +12 -73
- package/dist/utils/convertMessagesForAPI.js +51 -4
- package/dist/utils/messageOperations.d.ts +4 -2
- package/dist/utils/messageOperations.js +23 -7
- package/dist/utils/subagentParser.d.ts +5 -2
- package/dist/utils/subagentParser.js +14 -4
- 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) {
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -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 */
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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
|
package/dist/types/core.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/utils/bashParser.js
CHANGED
|
@@ -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:
|
|
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
|
|
89
|
-
*
|
|
90
|
-
* @param
|
|
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(
|
|
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
|
|
211
|
-
*
|
|
212
|
-
* @param
|
|
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(
|
|
215
|
+
export function extendUsageWithCacheMetrics(usage) {
|
|
217
216
|
const baseUsage = {
|
|
218
|
-
prompt_tokens:
|
|
219
|
-
completion_tokens:
|
|
220
|
-
total_tokens:
|
|
217
|
+
prompt_tokens: usage.prompt_tokens,
|
|
218
|
+
completion_tokens: usage.completion_tokens,
|
|
219
|
+
total_tokens: usage.total_tokens,
|
|
221
220
|
};
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
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,
|
|
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
|
};
|