wave-agent-sdk 1.1.0 → 1.1.2
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/dist/agent.d.ts +7 -1
- package/dist/agent.js +9 -1
- package/dist/builtin/skills/settings.js +17 -2
- package/dist/managers/aiManager.d.ts +13 -1
- package/dist/managers/aiManager.js +49 -26
- package/dist/managers/backgroundTaskManager.js +12 -0
- package/dist/managers/pluginScopeManager.js +13 -5
- package/dist/services/GitService.js +6 -6
- package/dist/services/aiService.js +6 -2
- package/dist/services/configurationService.d.ts +4 -4
- package/dist/services/configurationService.js +14 -7
- package/dist/tools/bashTool.js +10 -2
- package/dist/types/config.d.ts +2 -1
- package/dist/utils/markdownParser.js +23 -5
- package/dist/utils/path.d.ts +9 -0
- package/dist/utils/path.js +32 -0
- package/dist/utils/tokenCalculation.d.ts +19 -1
- package/dist/utils/tokenCalculation.js +64 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type QueuedMessage } from "./managers/messageQueue.js";
|
|
2
2
|
import { SlashCommand, CustomSlashCommand, AgentOptions } from "./types/index.js";
|
|
3
|
-
import type { Message, McpServerStatus, GatewayConfig, ModelConfig, Usage, PermissionMode, ForegroundTask } from "./types/index.js";
|
|
3
|
+
import type { Message, McpServerStatus, GatewayConfig, ModelConfig, Usage, PermissionMode, ForegroundTask, SkillMetadata } from "./types/index.js";
|
|
4
4
|
import type { WorktreeSession } from "./utils/worktreeSession.js";
|
|
5
5
|
export declare class Agent {
|
|
6
6
|
private messageManager;
|
|
@@ -422,6 +422,12 @@ export declare class Agent {
|
|
|
422
422
|
* @returns The list of subagent configurations
|
|
423
423
|
*/
|
|
424
424
|
getSubagentConfigurations(): import("./utils/subagentParser.js").SubagentConfiguration[];
|
|
425
|
+
/**
|
|
426
|
+
* Get all skill metadata visible in this session (builtin, personal,
|
|
427
|
+
* project, and plugin skills).
|
|
428
|
+
* @returns The list of skill metadata
|
|
429
|
+
*/
|
|
430
|
+
getSkillMetadata(): SkillMetadata[];
|
|
425
431
|
/**
|
|
426
432
|
* Get currently active subagent instances (status active/initializing).
|
|
427
433
|
* @returns The list of active subagent instances
|
package/dist/agent.js
CHANGED
|
@@ -18,7 +18,7 @@ export class Agent {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
getModelConfig() {
|
|
21
|
-
return this.configurationService.resolveModelConfig(undefined, undefined,
|
|
21
|
+
return this.configurationService.resolveModelConfig(undefined, undefined, this.getPermissionMode());
|
|
22
22
|
}
|
|
23
23
|
getMaxInputTokens() {
|
|
24
24
|
return this.configurationService.resolveMaxInputTokens();
|
|
@@ -983,6 +983,14 @@ export class Agent {
|
|
|
983
983
|
getSubagentConfigurations() {
|
|
984
984
|
return this.subagentManager.getConfigurations();
|
|
985
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Get all skill metadata visible in this session (builtin, personal,
|
|
988
|
+
* project, and plugin skills).
|
|
989
|
+
* @returns The list of skill metadata
|
|
990
|
+
*/
|
|
991
|
+
getSkillMetadata() {
|
|
992
|
+
return this.skillManager.getAvailableSkills();
|
|
993
|
+
}
|
|
986
994
|
/**
|
|
987
995
|
* Get currently active subagent instances (status active/initializing).
|
|
988
996
|
* @returns The list of active subagent instances
|
|
@@ -29,7 +29,7 @@ Wave uses several environment variables to control its core functionality. Varia
|
|
|
29
29
|
| \`WAVE_MODEL\` | The primary AI model to use for the agent. | \`gemini-3-flash\` |
|
|
30
30
|
| \`WAVE_FAST_MODEL\` | The fast AI model to use for quick tasks. | \`gemini-2.5-flash\` |
|
|
31
31
|
| \`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) |
|
|
32
|
-
| \`WAVE_MAX_INPUT_TOKENS\` | Maximum number of input tokens allowed. | \`200000\` |
|
|
32
|
+
| \`WAVE_MAX_INPUT_TOKENS\` | Maximum number of input tokens allowed. Overridden per-model by \`models[<model>].maxInputTokens\`. | \`200000\` |
|
|
33
33
|
| \`WAVE_MAX_OUTPUT_TOKENS\` | Maximum number of output tokens allowed. | \`32000\` |
|
|
34
34
|
| \`WAVE_DISABLE_AUTO_MEMORY\` | Set to \`1\` or \`true\` to disable the auto-memory feature. | \`false\` |
|
|
35
35
|
| \`WAVE_AUTO_MEMORY_FREQUENCY\` | Auto memory update frequency. \`1\` = every turn, \`2\` = every 2 turns, etc. | \`1\` |
|
|
@@ -585,12 +585,27 @@ You can define overrides for specific models in the \`models\` field. The key sh
|
|
|
585
585
|
Generation parameters are nested under the \`options\` field within each model's configuration. Wave supports passing arbitrary parameters to the underlying AI provider. Common parameters include:
|
|
586
586
|
|
|
587
587
|
- \`temperature\`: Controls randomness (0.0 to 2.0).
|
|
588
|
-
- \`
|
|
588
|
+
- \`max_tokens\`: Maximum number of tokens to generate in the response.
|
|
589
589
|
- \`reasoning_effort\`: (OpenAI specific) Controls the reasoning effort for models like \`o1\` and \`o3-mini\`. Values: \`low\`, \`medium\`, \`high\`.
|
|
590
590
|
- \`thinking\`: (Claude specific) Configures the thinking/reasoning capabilities for Claude 3.7+ models.
|
|
591
591
|
- \`type\`: \`"enabled"\` or \`"disabled"\`.
|
|
592
592
|
- \`budget_tokens\`: Maximum tokens to use for thinking.
|
|
593
593
|
|
|
594
|
+
## Per-Model Input Context Window
|
|
595
|
+
|
|
596
|
+
Set \`maxInputTokens\` at the top level of a model entry (not inside \`options\`) to define that model's input context window. It overrides the global \`WAVE_MAX_INPUT_TOKENS\`, so compaction thresholds and usage display follow each model's own value:
|
|
597
|
+
|
|
598
|
+
\`\`\`json
|
|
599
|
+
{
|
|
600
|
+
"models": {
|
|
601
|
+
"deepseek-v4-flash": { "maxInputTokens": 200000 },
|
|
602
|
+
"kimi-k3": { "maxInputTokens": 131072 }
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
\`\`\`
|
|
606
|
+
|
|
607
|
+
Resolution priority: constructor > \`options\` > \`models[<model>].maxInputTokens\` > \`WAVE_MAX_INPUT_TOKENS\` > default. Subagents (\`fastModel\`/\`visionModel\`) resolve against the model they actually use.
|
|
608
|
+
|
|
594
609
|
## Model Capabilities
|
|
595
610
|
|
|
596
611
|
Wave needs to know whether a model supports certain features. Instead of guessing from the model name, you declare these explicitly via the \`capabilities\` field. Note that \`capabilities\` is set at the top level of the model configuration, **not** inside the \`options\` field — \`options\` is reserved for generation parameters only.
|
|
@@ -133,7 +133,19 @@ export declare class AIManager {
|
|
|
133
133
|
setIsLoading(isLoading: boolean): void;
|
|
134
134
|
abortAIMessage(): void;
|
|
135
135
|
private generateCompactParams;
|
|
136
|
-
private
|
|
136
|
+
private updateLatestTotalTokens;
|
|
137
|
+
/**
|
|
138
|
+
* Pre-request auto-compaction check (aligned with Claude Code's
|
|
139
|
+
* autoCompactIfNeeded). Runs before every API request is issued: estimates
|
|
140
|
+
* the context that is about to be sent (last response's total_tokens plus
|
|
141
|
+
* character estimates for newer messages) and compacts first when it
|
|
142
|
+
* exceeds maxInputTokens, so over-limit requests are never sent. Only
|
|
143
|
+
* total_tokens is used — under OpenAI-compatible usage it already includes
|
|
144
|
+
* cache hits; adding cache fields would double-count and fire early.
|
|
145
|
+
* Fork paths (compaction / auto-memory) never reach here: they run in
|
|
146
|
+
* runForkLoop, not sendAIMessage.
|
|
147
|
+
*/
|
|
148
|
+
private maybeAutoCompactBeforeRequest;
|
|
137
149
|
/**
|
|
138
150
|
* Manually compact the conversation history.
|
|
139
151
|
* Called by /compact slash command or auto-compaction trigger.
|
|
@@ -3,7 +3,7 @@ import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
|
3
3
|
import { supportsVision } from "../utils/modelCapabilities.js";
|
|
4
4
|
import { persistToolImages } from "../utils/toolImagePersistence.js";
|
|
5
5
|
import { parseTaskNotificationXml, taskNotificationToXml, } from "../utils/notificationXml.js";
|
|
6
|
-
import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
|
|
6
|
+
import { calculateComprehensiveTotalTokens, estimateContextTokens, } from "../utils/tokenCalculation.js";
|
|
7
7
|
import { estimateTokens } from "../utils/tokenEstimate.js";
|
|
8
8
|
import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
|
|
9
9
|
import { createWriteStream, existsSync } from "node:fs";
|
|
@@ -144,7 +144,7 @@ export class AIManager {
|
|
|
144
144
|
const permissionMode = this.container.has("PermissionMode")
|
|
145
145
|
? this.container.get("PermissionMode")
|
|
146
146
|
: undefined;
|
|
147
|
-
const parentModelConfig = this.configurationService.resolveModelConfig(undefined, undefined,
|
|
147
|
+
const parentModelConfig = this.configurationService.resolveModelConfig(undefined, undefined, permissionMode);
|
|
148
148
|
let modelToUse;
|
|
149
149
|
if (this.modelOverride) {
|
|
150
150
|
if (this.modelOverride === "fastModel") {
|
|
@@ -157,10 +157,13 @@ export class AIManager {
|
|
|
157
157
|
modelToUse = this.modelOverride;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
return this.configurationService.resolveModelConfig(modelToUse, undefined,
|
|
160
|
+
return this.configurationService.resolveModelConfig(modelToUse, undefined, permissionMode);
|
|
161
161
|
}
|
|
162
162
|
getMaxInputTokens() {
|
|
163
|
-
|
|
163
|
+
// Pass the resolved model (including fastModel/visionModel/override
|
|
164
|
+
// resolution) so subagents using a different model get that model's
|
|
165
|
+
// per-model maxInputTokens instead of the main model's value.
|
|
166
|
+
return this.configurationService.resolveMaxInputTokens(undefined, this.getModelConfig().model);
|
|
164
167
|
}
|
|
165
168
|
getLanguage() {
|
|
166
169
|
return this.configurationService.resolveLanguage();
|
|
@@ -366,30 +369,43 @@ export class AIManager {
|
|
|
366
369
|
}
|
|
367
370
|
return "";
|
|
368
371
|
}
|
|
369
|
-
// Private method to
|
|
370
|
-
|
|
372
|
+
// Private method to update the displayed token statistics from a response.
|
|
373
|
+
// The comprehensive value (including cache tokens) is intentionally kept:
|
|
374
|
+
// it drives cost/usage display. Auto-compaction is NOT decided here — it
|
|
375
|
+
// happens pre-request via maybeAutoCompactBeforeRequest so that over-limit
|
|
376
|
+
// requests never go out (aligned with Claude Code's proactive autocompact).
|
|
377
|
+
updateLatestTotalTokens(usage) {
|
|
371
378
|
if (!usage)
|
|
372
379
|
return;
|
|
373
380
|
// Update token statistics - display comprehensive token usage including cache tokens
|
|
374
381
|
const comprehensiveTotalTokens = calculateComprehensiveTotalTokens(usage);
|
|
375
382
|
this.messageManager.setlatestTotalTokens(comprehensiveTotalTokens);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Pre-request auto-compaction check (aligned with Claude Code's
|
|
386
|
+
* autoCompactIfNeeded). Runs before every API request is issued: estimates
|
|
387
|
+
* the context that is about to be sent (last response's total_tokens plus
|
|
388
|
+
* character estimates for newer messages) and compacts first when it
|
|
389
|
+
* exceeds maxInputTokens, so over-limit requests are never sent. Only
|
|
390
|
+
* total_tokens is used — under OpenAI-compatible usage it already includes
|
|
391
|
+
* cache hits; adding cache fields would double-count and fire early.
|
|
392
|
+
* Fork paths (compaction / auto-memory) never reach here: they run in
|
|
393
|
+
* runForkLoop, not sendAIMessage.
|
|
394
|
+
*/
|
|
395
|
+
async maybeAutoCompactBeforeRequest(abortController) {
|
|
396
|
+
// Circuit breaker: skip compaction after 3 consecutive failures
|
|
397
|
+
if (this.consecutiveCompactionFailures >= 3) {
|
|
398
|
+
logger?.warn(`Skipping compaction: ${this.consecutiveCompactionFailures} consecutive failures`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const messages = this.messageManager.getMessages();
|
|
402
|
+
if (messages.length === 0)
|
|
403
|
+
return;
|
|
404
|
+
const maxTokens = this.getMaxInputTokens();
|
|
405
|
+
const estimatedTokens = estimateContextTokens(messages);
|
|
406
|
+
if (estimatedTokens > maxTokens) {
|
|
407
|
+
logger?.debug(`Estimated context tokens ${estimatedTokens} exceeded ${maxTokens}, compacting before request...`);
|
|
408
|
+
await this.compactConversation({ abortSignal: abortController.signal });
|
|
393
409
|
}
|
|
394
410
|
}
|
|
395
411
|
/**
|
|
@@ -598,6 +614,7 @@ export class AIManager {
|
|
|
598
614
|
workdir,
|
|
599
615
|
tools: toolsConfig,
|
|
600
616
|
systemPrompt,
|
|
617
|
+
maxTokens: this.configurationService.resolveMaxOutputTokens(),
|
|
601
618
|
toolChoice: this.toolChoiceOverride,
|
|
602
619
|
// Stream so a slow reasoning model emits first bytes before the
|
|
603
620
|
// gateway's idle timeout fires (non-streaming waits for the full
|
|
@@ -1029,6 +1046,7 @@ ${question}`;
|
|
|
1029
1046
|
workdir,
|
|
1030
1047
|
tools: toolsConfig,
|
|
1031
1048
|
systemPrompt,
|
|
1049
|
+
maxTokens: this.configurationService.resolveMaxOutputTokens(),
|
|
1032
1050
|
toolChoice: this.toolChoiceOverride,
|
|
1033
1051
|
// Stream so a slow reasoning model emits first bytes before the
|
|
1034
1052
|
// gateway's idle timeout fires (same rationale as runCompactFork).
|
|
@@ -1281,6 +1299,10 @@ ${question}`;
|
|
|
1281
1299
|
isMeta: true,
|
|
1282
1300
|
});
|
|
1283
1301
|
}
|
|
1302
|
+
// Pre-request auto-compaction: estimate the context about to be sent
|
|
1303
|
+
// and compact BEFORE issuing the request, so an over-limit request
|
|
1304
|
+
// never goes out. Skipped on fork paths (they use runForkLoop).
|
|
1305
|
+
await this.maybeAutoCompactBeforeRequest(abortController);
|
|
1284
1306
|
// Get recent message history
|
|
1285
1307
|
const rawMessages = this.messageManager.getMessages();
|
|
1286
1308
|
const currentModelConfig = this.getModelConfig();
|
|
@@ -1305,7 +1327,7 @@ ${question}`;
|
|
|
1305
1327
|
tools: toolsConfig, // Pass filtered tool configuration
|
|
1306
1328
|
model: model, // Use passed model
|
|
1307
1329
|
systemPrompt: mainSystemPrompt, // Pass custom system prompt
|
|
1308
|
-
maxTokens: maxTokens, // Pass max tokens override
|
|
1330
|
+
maxTokens: maxTokens ?? this.configurationService.resolveMaxOutputTokens(), // Pass max tokens override, falling back to the resolved global value
|
|
1309
1331
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
1310
1332
|
// Fast-model subagents send disable-thinking params only when
|
|
1311
1333
|
// explicitly configured (never in the agent loop).
|
|
@@ -1489,8 +1511,9 @@ ${question}`;
|
|
|
1489
1511
|
}
|
|
1490
1512
|
}
|
|
1491
1513
|
}
|
|
1492
|
-
//
|
|
1493
|
-
|
|
1514
|
+
// Update token statistics for display (compaction decision is
|
|
1515
|
+
// pre-request, see maybeAutoCompactBeforeRequest)
|
|
1516
|
+
this.updateLatestTotalTokens(result.usage);
|
|
1494
1517
|
// Finalize text/reasoning blocks for the final response (no tools)
|
|
1495
1518
|
this.messageManager.finalizeStreamingBlocks();
|
|
1496
1519
|
// Check if there are tool operations or response was truncated, if so automatically initiate next AI service call
|
|
@@ -61,6 +61,13 @@ export class BackgroundTaskManager {
|
|
|
61
61
|
// Create log file
|
|
62
62
|
const logPath = path.join(os.tmpdir(), `wave-task-${id}.log`);
|
|
63
63
|
const logStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
64
|
+
// A failed open/write of the log file (e.g. EBUSY on Windows when a stale
|
|
65
|
+
// handle from a previous run still locks the temp file) must not surface as
|
|
66
|
+
// an uncaught stream error — the log is best-effort side output and the
|
|
67
|
+
// task itself keeps running regardless.
|
|
68
|
+
logStream.on("error", (error) => {
|
|
69
|
+
logger.warn(`Failed to write background task log ${logPath}:`, error);
|
|
70
|
+
});
|
|
64
71
|
const shell = {
|
|
65
72
|
id,
|
|
66
73
|
type: "shell",
|
|
@@ -232,6 +239,11 @@ export class BackgroundTaskManager {
|
|
|
232
239
|
// Create log file
|
|
233
240
|
const logPath = path.join(os.tmpdir(), `wave-task-${id}.log`);
|
|
234
241
|
const logStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
242
|
+
// Same best-effort handling as startShell: a locked temp file (EBUSY on
|
|
243
|
+
// Windows) must not become an uncaught stream error.
|
|
244
|
+
logStream.on("error", (error) => {
|
|
245
|
+
logger.warn(`Failed to write background task log ${logPath}:`, error);
|
|
246
|
+
});
|
|
235
247
|
// Write initial output to log file
|
|
236
248
|
if (initialStdout) {
|
|
237
249
|
logStream.write(stripAnsiColors(initialStdout));
|
|
@@ -32,15 +32,23 @@ export class PluginScopeManager {
|
|
|
32
32
|
* Priority: local > project > user
|
|
33
33
|
*/
|
|
34
34
|
findPluginScope(pluginId) {
|
|
35
|
-
const projectPaths = this.configurationService.getConfigurationPaths(this.workdir)
|
|
36
|
-
const
|
|
35
|
+
const { projectPaths, userPaths } = this.configurationService.getConfigurationPaths(this.workdir);
|
|
36
|
+
const userPathSet = new Set(userPaths);
|
|
37
|
+
// When the workdir is the user's home directory, projectPaths overlaps
|
|
38
|
+
// userPaths (both point at ~/.wave/settings.json). Treat such a file as the
|
|
39
|
+
// user config, otherwise user-scope plugins would be mislabeled "project".
|
|
37
40
|
const checkPaths = [
|
|
38
41
|
{ path: projectPaths[0], scope: "local" },
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
...(userPathSet.has(projectPaths[1])
|
|
43
|
+
? []
|
|
44
|
+
: [{ path: projectPaths[1], scope: "project" }]),
|
|
45
|
+
...userPaths.map((path) => ({ path, scope: "user" })),
|
|
42
46
|
];
|
|
47
|
+
const seen = new Set();
|
|
43
48
|
for (const { path, scope } of checkPaths) {
|
|
49
|
+
if (!path || seen.has(path))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(path);
|
|
44
52
|
const config = this.configurationService.loadWaveConfigFromFile(path);
|
|
45
53
|
if (config?.enabledPlugins && pluginId in config.enabledPlugins) {
|
|
46
54
|
return scope;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
-
const
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
4
|
export class GitService {
|
|
5
5
|
getTimeout() {
|
|
6
6
|
const envTimeout = process.env.WAVE_PLUGIN_GIT_TIMEOUT_MS;
|
|
@@ -17,7 +17,7 @@ export class GitService {
|
|
|
17
17
|
*/
|
|
18
18
|
async isGitAvailable() {
|
|
19
19
|
try {
|
|
20
|
-
await
|
|
20
|
+
await execFileAsync("git", ["--version"]);
|
|
21
21
|
return true;
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
@@ -43,8 +43,8 @@ export class GitService {
|
|
|
43
43
|
url = `https://github.com/${urlOrRepo}.git`;
|
|
44
44
|
}
|
|
45
45
|
try {
|
|
46
|
-
const refArgs = ref ?
|
|
47
|
-
await
|
|
46
|
+
const refArgs = ref ? ["-b", ref] : ["--depth", "1"];
|
|
47
|
+
await execFileAsync("git", ["clone", ...refArgs, url, targetPath], {
|
|
48
48
|
env: { ...process.env, LC_ALL: "C" },
|
|
49
49
|
timeout: this.getTimeout(),
|
|
50
50
|
});
|
|
@@ -64,7 +64,7 @@ export class GitService {
|
|
|
64
64
|
throw new Error("Git is not installed or not found in PATH. Please install Git to use Git/GitHub marketplaces.");
|
|
65
65
|
}
|
|
66
66
|
try {
|
|
67
|
-
await
|
|
67
|
+
await execFileAsync("git", ["-C", targetPath, "pull"], {
|
|
68
68
|
env: { ...process.env, LC_ALL: "C" },
|
|
69
69
|
timeout: this.getTimeout(),
|
|
70
70
|
});
|
|
@@ -4,6 +4,7 @@ import { addOnceAbortListener } from "../utils/abortUtils.js";
|
|
|
4
4
|
import { ConfigurationError, CONFIG_ERRORS } from "../types/index.js";
|
|
5
5
|
import { transformMessagesForExplicitCache, extendUsageWithCacheMetrics, } from "../utils/cacheControlUtils.js";
|
|
6
6
|
import { supportsPromptCaching } from "../utils/modelCapabilities.js";
|
|
7
|
+
import { DEFAULT_WAVE_MAX_OUTPUT_TOKENS } from "../utils/constants.js";
|
|
7
8
|
import * as os from "os";
|
|
8
9
|
import * as fs from "fs";
|
|
9
10
|
import * as path from "path";
|
|
@@ -110,8 +111,11 @@ export async function callAgent(options) {
|
|
|
110
111
|
fetchOptions: gatewayConfig.fetchOptions,
|
|
111
112
|
fetch: gatewayConfig.fetch,
|
|
112
113
|
});
|
|
113
|
-
// Determine model early (needed for system prompt construction)
|
|
114
|
-
|
|
114
|
+
// Determine model early (needed for system prompt construction).
|
|
115
|
+
// Per-model overrides come from modelConfig.options.max_tokens (spread
|
|
116
|
+
// after this default below); callers pass the resolved global value
|
|
117
|
+
// (AgentOptions.maxTokens / WAVE_MAX_OUTPUT_TOKENS) via options.maxTokens.
|
|
118
|
+
const resolvedMaxTokens = options.maxTokens ?? DEFAULT_WAVE_MAX_OUTPUT_TOKENS;
|
|
115
119
|
// Build system message content
|
|
116
120
|
let systemMessage;
|
|
117
121
|
if (Array.isArray(systemPrompt)) {
|
|
@@ -79,19 +79,19 @@ export declare class ConfigurationService {
|
|
|
79
79
|
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
80
80
|
* @param model - Agent model override (optional)
|
|
81
81
|
* @param fastModel - Fast model override (optional)
|
|
82
|
-
* @param maxTokens - Max output tokens override (optional)
|
|
83
82
|
* @param permissionMode - Permission mode override (optional)
|
|
84
83
|
* @param visionModel - Vision model override (optional)
|
|
85
84
|
* @returns Resolved model configuration with defaults
|
|
86
85
|
*/
|
|
87
|
-
resolveModelConfig(model?: string, fastModel?: string,
|
|
86
|
+
resolveModelConfig(model?: string, fastModel?: string, permissionMode?: PermissionMode, visionModel?: string): ModelConfig;
|
|
88
87
|
/**
|
|
89
88
|
* Resolves token limit with fallbacks
|
|
90
|
-
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
89
|
+
* Resolution priority: override > options > models[model].maxInputTokens > env (from settings.json) > process.env > default
|
|
91
90
|
* @param constructorLimit - Token limit override (optional)
|
|
91
|
+
* @param model - Model to resolve per-model maxInputTokens for (optional; falls back to the resolved agent model chain)
|
|
92
92
|
* @returns Resolved token limit
|
|
93
93
|
*/
|
|
94
|
-
resolveMaxInputTokens(constructorLimit?: number): number;
|
|
94
|
+
resolveMaxInputTokens(constructorLimit?: number, model?: string): number;
|
|
95
95
|
/**
|
|
96
96
|
* Resolves preferred language with fallbacks
|
|
97
97
|
* Resolution priority: override > options > settings.json > undefined
|
|
@@ -457,12 +457,11 @@ export class ConfigurationService {
|
|
|
457
457
|
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
458
458
|
* @param model - Agent model override (optional)
|
|
459
459
|
* @param fastModel - Fast model override (optional)
|
|
460
|
-
* @param maxTokens - Max output tokens override (optional)
|
|
461
460
|
* @param permissionMode - Permission mode override (optional)
|
|
462
461
|
* @param visionModel - Vision model override (optional)
|
|
463
462
|
* @returns Resolved model configuration with defaults
|
|
464
463
|
*/
|
|
465
|
-
resolveModelConfig(model, fastModel,
|
|
464
|
+
resolveModelConfig(model, fastModel, permissionMode, visionModel) {
|
|
466
465
|
// Resolve agent model: override > options > currentConfiguration (settings.json model, possibly remote-merged) > process.env
|
|
467
466
|
// Priority: user's explicit model field > admin's env.WAVE_MODEL default.
|
|
468
467
|
// If admin wants hard enforcement, they set the `model` scalar field (overwrites local in mergeRemoteSettings).
|
|
@@ -478,13 +477,10 @@ export class ConfigurationService {
|
|
|
478
477
|
const resolvedVisionModel = visionModel ||
|
|
479
478
|
this.options.visionModel ||
|
|
480
479
|
(this.envSnapshot.WAVE_VISION_MODEL ?? process.env.WAVE_VISION_MODEL);
|
|
481
|
-
// Resolve max output tokens
|
|
482
|
-
const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
|
|
483
480
|
const baseConfig = {
|
|
484
481
|
model: resolvedAgentModel,
|
|
485
482
|
fastModel: resolvedFastModel,
|
|
486
483
|
visionModel: resolvedVisionModel,
|
|
487
|
-
maxTokens: resolvedMaxTokens,
|
|
488
484
|
permissionMode: permissionMode ?? this.options.permissionMode,
|
|
489
485
|
};
|
|
490
486
|
// Resolve fast model generation params from models[fastModel].options.
|
|
@@ -520,11 +516,12 @@ export class ConfigurationService {
|
|
|
520
516
|
}
|
|
521
517
|
/**
|
|
522
518
|
* Resolves token limit with fallbacks
|
|
523
|
-
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
519
|
+
* Resolution priority: override > options > models[model].maxInputTokens > env (from settings.json) > process.env > default
|
|
524
520
|
* @param constructorLimit - Token limit override (optional)
|
|
521
|
+
* @param model - Model to resolve per-model maxInputTokens for (optional; falls back to the resolved agent model chain)
|
|
525
522
|
* @returns Resolved token limit
|
|
526
523
|
*/
|
|
527
|
-
resolveMaxInputTokens(constructorLimit) {
|
|
524
|
+
resolveMaxInputTokens(constructorLimit, model) {
|
|
528
525
|
// If override value provided, use it
|
|
529
526
|
if (constructorLimit !== undefined) {
|
|
530
527
|
return constructorLimit;
|
|
@@ -533,6 +530,16 @@ export class ConfigurationService {
|
|
|
533
530
|
if (this.options.maxInputTokens !== undefined) {
|
|
534
531
|
return this.options.maxInputTokens;
|
|
535
532
|
}
|
|
533
|
+
// Per-model config (models[X].maxInputTokens). Resolve the model with the
|
|
534
|
+
// same chain as resolveModelConfig: caller > options > currentConfiguration > env.
|
|
535
|
+
const resolvedModel = model ||
|
|
536
|
+
this.options.model ||
|
|
537
|
+
this.currentConfiguration?.model ||
|
|
538
|
+
(this.envSnapshot.WAVE_MODEL ?? process.env.WAVE_MODEL);
|
|
539
|
+
const modelConfig = resolvedModel && this.currentConfiguration?.models?.[resolvedModel];
|
|
540
|
+
if (modelConfig && modelConfig.maxInputTokens !== undefined) {
|
|
541
|
+
return modelConfig.maxInputTokens;
|
|
542
|
+
}
|
|
536
543
|
// Try env (settings.json snapshot) first, then process.env
|
|
537
544
|
const envMaxInputTokens = this.envSnapshot.WAVE_MAX_INPUT_TOKENS ??
|
|
538
545
|
process.env.WAVE_MAX_INPUT_TOKENS;
|
package/dist/tools/bashTool.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import { logger } from "../utils/globalLogger.js";
|
|
6
6
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
7
|
-
import { toPosixPath } from "../utils/path.js";
|
|
7
|
+
import { toPosixPath, toWindowsPath } from "../utils/path.js";
|
|
8
8
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
9
9
|
import { WindowsStreamDecoder } from "../utils/encoding.js";
|
|
10
10
|
import { processToolResult } from "../utils/toolResultStorage.js";
|
|
@@ -498,12 +498,20 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
498
498
|
try {
|
|
499
499
|
if (fs.existsSync(tempCwdFile)) {
|
|
500
500
|
newCwd = fs.readFileSync(tempCwdFile, "utf8").trim();
|
|
501
|
+
// Git Bash `pwd -P` writes MSYS-style POSIX paths (/c/Users/...);
|
|
502
|
+
// convert back to a native Windows path before validating, otherwise
|
|
503
|
+
// accessSync resolves /c/... to C:\c\... (ENOENT) on every command.
|
|
504
|
+
if (process.platform === "win32") {
|
|
505
|
+
newCwd = toWindowsPath(newCwd);
|
|
506
|
+
}
|
|
501
507
|
// Validate the path exists before calling the callback
|
|
502
508
|
fs.accessSync(newCwd, fs.constants.F_OK);
|
|
503
509
|
}
|
|
504
510
|
}
|
|
505
511
|
catch (fileError) {
|
|
506
|
-
|
|
512
|
+
// Best-effort CWD tracking: a stale/deleted target directory (e.g. a
|
|
513
|
+
// removed worktree) shouldn't fail the command or spam WARN logs.
|
|
514
|
+
logger.debug(`Could not read or validate new CWD from temp file ${tempCwdFile}:`, fileError);
|
|
507
515
|
newCwd = undefined;
|
|
508
516
|
}
|
|
509
517
|
finally {
|
package/dist/types/config.d.ts
CHANGED
|
@@ -24,7 +24,8 @@ export interface ModelConfig {
|
|
|
24
24
|
fastModel?: string;
|
|
25
25
|
/** Vision-capable model for image recognition subagents (resolved from WAVE_VISION_MODEL env var). */
|
|
26
26
|
visionModel?: string;
|
|
27
|
-
|
|
27
|
+
/** Per-model input context window in tokens. Overrides the global `WAVE_MAX_INPUT_TOKENS` for this model. */
|
|
28
|
+
maxInputTokens?: number;
|
|
28
29
|
permissionMode?: PermissionMode;
|
|
29
30
|
capabilities?: ModelCapabilities;
|
|
30
31
|
/** Generation params passed through to the API provider (temperature, thinking, etc.) */
|
|
@@ -5,6 +5,7 @@ import { join } from "path";
|
|
|
5
5
|
import { tmpdir } from "os";
|
|
6
6
|
import { SKILL_BASH_MAX_OUTPUT_CHARS, PREVIEW_SIZE_BYTES, } from "../constants/toolLimits.js";
|
|
7
7
|
import { logger } from "./globalLogger.js";
|
|
8
|
+
import { resolveShellPath } from "./shellResolver.js";
|
|
8
9
|
const execAsync = promisify(exec);
|
|
9
10
|
/**
|
|
10
11
|
* Parse YAML frontmatter from markdown content
|
|
@@ -198,12 +199,29 @@ export function replaceBashCommandsWithOutput(content, results) {
|
|
|
198
199
|
*/
|
|
199
200
|
export async function executeBashCommands(commands, workdir, timeout = 30000) {
|
|
200
201
|
const results = [];
|
|
201
|
-
for (const
|
|
202
|
+
for (const originalCommand of commands) {
|
|
203
|
+
let command = originalCommand;
|
|
204
|
+
const execOptions = {
|
|
205
|
+
cwd: workdir,
|
|
206
|
+
timeout,
|
|
207
|
+
};
|
|
208
|
+
// Skill templates (e.g. !`gh pr view ... 2>/dev/null || echo '{}'`) use
|
|
209
|
+
// bash-only syntax. On Windows, child_process.exec defaults to cmd.exe,
|
|
210
|
+
// where `/dev/null` fails with a localized "path not found" error whose
|
|
211
|
+
// GBK bytes get mis-decoded as UTF-8 (mojibake). Run through Git Bash
|
|
212
|
+
// when available (same contract as the bash tool); otherwise force the
|
|
213
|
+
// UTF-8 codepage so cmd's own messages decode correctly.
|
|
214
|
+
if (process.platform === "win32") {
|
|
215
|
+
const bashPath = resolveShellPath();
|
|
216
|
+
if (bashPath) {
|
|
217
|
+
execOptions.shell = bashPath;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
command = `chcp 65001 >NUL && ${command}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
202
223
|
try {
|
|
203
|
-
const { stdout, stderr } = await execAsync(command,
|
|
204
|
-
cwd: workdir,
|
|
205
|
-
timeout,
|
|
206
|
-
});
|
|
224
|
+
const { stdout, stderr } = await execAsync(command, execOptions);
|
|
207
225
|
results.push({
|
|
208
226
|
command,
|
|
209
227
|
output: (stdout + (stderr || "")).trim(),
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -25,3 +25,12 @@ export declare function getDisplayPath(filePath: string, workdir: string): strin
|
|
|
25
25
|
* Returns the path as-is on non-Windows platforms.
|
|
26
26
|
*/
|
|
27
27
|
export declare function toPosixPath(p: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Convert a POSIX-style path to a native Windows path. Git Bash / MSYS shells
|
|
30
|
+
* print POSIX paths (e.g. `pwd -P` outputs `/c/Users/foo`), which Node.js on
|
|
31
|
+
* Windows would otherwise resolve against the current drive root (e.g. `/c/...`
|
|
32
|
+
* becomes `C:\c\...`) and fail with ENOENT. Handles `/c/...` (MSYS2/Git Bash),
|
|
33
|
+
* `/cygdrive/c/...` and `//server/share` (UNC) forms. Returns the path as-is on
|
|
34
|
+
* non-Windows platforms.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toWindowsPath(p: string): string;
|
package/dist/utils/path.js
CHANGED
|
@@ -61,3 +61,35 @@ export function getDisplayPath(filePath, workdir) {
|
|
|
61
61
|
export function toPosixPath(p) {
|
|
62
62
|
return process.platform === "win32" ? p.replace(/\\/g, "/") : p;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Convert a POSIX-style path to a native Windows path. Git Bash / MSYS shells
|
|
66
|
+
* print POSIX paths (e.g. `pwd -P` outputs `/c/Users/foo`), which Node.js on
|
|
67
|
+
* Windows would otherwise resolve against the current drive root (e.g. `/c/...`
|
|
68
|
+
* becomes `C:\c\...`) and fail with ENOENT. Handles `/c/...` (MSYS2/Git Bash),
|
|
69
|
+
* `/cygdrive/c/...` and `//server/share` (UNC) forms. Returns the path as-is on
|
|
70
|
+
* non-Windows platforms.
|
|
71
|
+
*/
|
|
72
|
+
export function toWindowsPath(p) {
|
|
73
|
+
if (process.platform !== "win32")
|
|
74
|
+
return p;
|
|
75
|
+
// UNC paths: //server/share -> \\server\share
|
|
76
|
+
if (p.startsWith("//")) {
|
|
77
|
+
return p.replace(/\//g, "\\");
|
|
78
|
+
}
|
|
79
|
+
// /cygdrive/c/... -> C:\...
|
|
80
|
+
const cygdriveMatch = p.match(/^\/cygdrive\/([A-Za-z])(\/|$)/);
|
|
81
|
+
if (cygdriveMatch) {
|
|
82
|
+
const driveLetter = cygdriveMatch[1].toUpperCase();
|
|
83
|
+
const rest = p.slice(("/cygdrive/" + cygdriveMatch[1]).length);
|
|
84
|
+
return driveLetter + ":" + (rest || "\\").replace(/\//g, "\\");
|
|
85
|
+
}
|
|
86
|
+
// /c/... (MSYS2/Git Bash) -> C:\...
|
|
87
|
+
const driveMatch = p.match(/^\/([A-Za-z])(\/|$)/);
|
|
88
|
+
if (driveMatch) {
|
|
89
|
+
const driveLetter = driveMatch[1].toUpperCase();
|
|
90
|
+
const rest = p.slice(2);
|
|
91
|
+
return driveLetter + ":" + (rest || "\\").replace(/\//g, "\\");
|
|
92
|
+
}
|
|
93
|
+
// Already Windows or relative — just flip slashes
|
|
94
|
+
return p.replace(/\//g, "\\");
|
|
95
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Usage } from "../types/index.js";
|
|
1
|
+
import type { Message, Usage } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Calculate comprehensive total tokens including cache-related tokens
|
|
4
4
|
*
|
|
@@ -23,3 +23,21 @@ export declare function calculateComprehensiveTotalTokens(usage: Usage): number;
|
|
|
23
23
|
export declare function extractLatestTotalTokens(messages: Array<{
|
|
24
24
|
usage?: Usage;
|
|
25
25
|
}>): number;
|
|
26
|
+
/**
|
|
27
|
+
* Estimate the full context size of a message list for pre-request
|
|
28
|
+
* auto-compaction checks (aligned with Claude Code's tokenCountWithEstimation).
|
|
29
|
+
*
|
|
30
|
+
* Anchors on the most recent message carrying real usage (the last API
|
|
31
|
+
* response): that response's total_tokens (OpenAI-compatible semantics —
|
|
32
|
+
* already includes cache hits) plus a rough character-based estimate for
|
|
33
|
+
* every message added after it. Falls back to a pure character estimate
|
|
34
|
+
* when no usage anchor exists yet (e.g. the first request of a session).
|
|
35
|
+
*/
|
|
36
|
+
export declare function estimateContextTokens(messages: Message[]): number;
|
|
37
|
+
/**
|
|
38
|
+
* Rough per-message token estimate over all message blocks.
|
|
39
|
+
* Used to estimate the messages added since the last usage-bearing response.
|
|
40
|
+
* Image blocks are ignored (images are stripped before compaction) and file
|
|
41
|
+
* history snapshots live on disk, so their content is not in the context.
|
|
42
|
+
*/
|
|
43
|
+
export declare function roughTokenCountForMessages(messages: Message[]): number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { estimateTokens } from "./tokenEstimate.js";
|
|
1
2
|
/**
|
|
2
3
|
* Calculate comprehensive total tokens including cache-related tokens
|
|
3
4
|
*
|
|
@@ -34,3 +35,66 @@ export function extractLatestTotalTokens(messages) {
|
|
|
34
35
|
}
|
|
35
36
|
return 0; // No usage data found
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Estimate the full context size of a message list for pre-request
|
|
40
|
+
* auto-compaction checks (aligned with Claude Code's tokenCountWithEstimation).
|
|
41
|
+
*
|
|
42
|
+
* Anchors on the most recent message carrying real usage (the last API
|
|
43
|
+
* response): that response's total_tokens (OpenAI-compatible semantics —
|
|
44
|
+
* already includes cache hits) plus a rough character-based estimate for
|
|
45
|
+
* every message added after it. Falls back to a pure character estimate
|
|
46
|
+
* when no usage anchor exists yet (e.g. the first request of a session).
|
|
47
|
+
*/
|
|
48
|
+
export function estimateContextTokens(messages) {
|
|
49
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
50
|
+
const usage = messages[i].usage;
|
|
51
|
+
if (usage) {
|
|
52
|
+
return (usage.total_tokens + roughTokenCountForMessages(messages.slice(i + 1)));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return roughTokenCountForMessages(messages);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rough per-message token estimate over all message blocks.
|
|
59
|
+
* Used to estimate the messages added since the last usage-bearing response.
|
|
60
|
+
* Image blocks are ignored (images are stripped before compaction) and file
|
|
61
|
+
* history snapshots live on disk, so their content is not in the context.
|
|
62
|
+
*/
|
|
63
|
+
export function roughTokenCountForMessages(messages) {
|
|
64
|
+
let total = 0;
|
|
65
|
+
for (const message of messages) {
|
|
66
|
+
for (const block of message.blocks) {
|
|
67
|
+
switch (block.type) {
|
|
68
|
+
case "text":
|
|
69
|
+
case "reasoning":
|
|
70
|
+
case "error":
|
|
71
|
+
case "compact":
|
|
72
|
+
total += estimateTokens(block.content);
|
|
73
|
+
break;
|
|
74
|
+
case "tool":
|
|
75
|
+
if (block.parameters) {
|
|
76
|
+
total += estimateTokens(block.parameters, "json");
|
|
77
|
+
}
|
|
78
|
+
if (block.result) {
|
|
79
|
+
total += estimateTokens(block.result);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case "bang":
|
|
83
|
+
if (block.command) {
|
|
84
|
+
total += estimateTokens(block.command);
|
|
85
|
+
}
|
|
86
|
+
if (block.output) {
|
|
87
|
+
total += estimateTokens(block.output);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case "task_notification":
|
|
91
|
+
total += estimateTokens(block.summary);
|
|
92
|
+
break;
|
|
93
|
+
case "image":
|
|
94
|
+
case "file_history":
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return total;
|
|
100
|
+
}
|