wave-agent-sdk 1.0.10 → 1.1.1
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 +16 -7
- package/dist/agent.js +44 -34
- package/dist/builtin/skills/settings.js +48 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +23 -1
- package/dist/managers/aiManager.js +80 -30
- package/dist/managers/backgroundTaskManager.js +12 -0
- package/dist/managers/pluginScopeManager.js +13 -5
- package/dist/managers/subagentManager.d.ts +1 -0
- package/dist/managers/subagentManager.js +5 -1
- 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/services/hook.js +41 -7
- package/dist/services/initializationService.js +0 -21
- package/dist/services/jsonlHandler.d.ts +37 -2
- package/dist/services/jsonlHandler.js +55 -6
- package/dist/services/session.d.ts +35 -4
- package/dist/services/session.js +233 -36
- package/dist/services/worktreeHooks.d.ts +45 -0
- package/dist/services/worktreeHooks.js +133 -0
- package/dist/tools/agentTool.js +36 -1
- package/dist/tools/bashTool.js +129 -58
- package/dist/tools/enterWorktreeTool.d.ts +1 -1
- package/dist/tools/enterWorktreeTool.js +44 -31
- package/dist/tools/exitWorktreeTool.js +21 -26
- package/dist/types/agent.d.ts +5 -0
- package/dist/types/config.d.ts +2 -1
- package/dist/types/hooks.d.ts +3 -2
- package/dist/types/skills.d.ts +0 -1
- package/dist/types/skills.js +0 -1
- package/dist/utils/asyncWorkRegistry.d.ts +32 -0
- package/dist/utils/asyncWorkRegistry.js +81 -0
- package/dist/utils/containerSetup.js +5 -0
- 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/skillParser.js +3 -6
- package/dist/utils/tokenCalculation.d.ts +19 -1
- package/dist/utils/tokenCalculation.js +64 -0
- package/dist/utils/windowsPaths.d.ts +28 -0
- package/dist/utils/windowsPaths.js +47 -0
- package/dist/utils/worktreeSession.d.ts +6 -0
- package/dist/utils/worktreeUtils.d.ts +7 -0
- package/dist/utils/worktreeUtils.js +88 -40
- package/package.json +5 -3
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;
|
|
@@ -23,7 +23,9 @@ export declare class Agent {
|
|
|
23
23
|
private messageQueue;
|
|
24
24
|
private dispatchPromise;
|
|
25
25
|
private isAborting;
|
|
26
|
+
private isDestroyed;
|
|
26
27
|
private dispatchAborted;
|
|
28
|
+
private asyncWorkRegistry;
|
|
27
29
|
private memoryRuleManager;
|
|
28
30
|
private liveConfigManager;
|
|
29
31
|
private taskManager;
|
|
@@ -122,6 +124,13 @@ export declare class Agent {
|
|
|
122
124
|
}>;
|
|
123
125
|
type?: "message" | "bang";
|
|
124
126
|
}): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Terminal-lifecycle guard for public APIs. destroy() is terminal: after it
|
|
129
|
+
* returns, no new work may start on this agent. Silent drops hid misuse until
|
|
130
|
+
* a ghost async side effect surfaced later (issue #1808); throwing surfaces
|
|
131
|
+
* the contract violation immediately at the call site.
|
|
132
|
+
*/
|
|
133
|
+
private assertNotDestroyed;
|
|
125
134
|
/**
|
|
126
135
|
* Unified dispatch trigger — checks state machine before processing.
|
|
127
136
|
* Handles user messages, bang commands, and background task notifications
|
|
@@ -244,12 +253,6 @@ export declare class Agent {
|
|
|
244
253
|
* Background the current foreground task
|
|
245
254
|
*/
|
|
246
255
|
backgroundCurrentTask(): Promise<void>;
|
|
247
|
-
/**
|
|
248
|
-
* Trigger WorktreeRemove hook before agent destruction.
|
|
249
|
-
* Called from CLI exit dialog when user chooses to remove the worktree.
|
|
250
|
-
* Non-blocking: errors logged but don't prevent removal.
|
|
251
|
-
*/
|
|
252
|
-
triggerWorktreeRemoveHook(worktreePath: string): Promise<void>;
|
|
253
256
|
/** Destroy managers, clean up resources */
|
|
254
257
|
destroy(): Promise<void>;
|
|
255
258
|
/**
|
|
@@ -419,6 +422,12 @@ export declare class Agent {
|
|
|
419
422
|
* @returns The list of subagent configurations
|
|
420
423
|
*/
|
|
421
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[];
|
|
422
431
|
/**
|
|
423
432
|
* Get currently active subagent instances (status active/initializing).
|
|
424
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();
|
|
@@ -53,7 +53,8 @@ export class Agent {
|
|
|
53
53
|
constructor(options) {
|
|
54
54
|
this.bangManager = null;
|
|
55
55
|
this.dispatchPromise = null; // Track current dispatch for teardown
|
|
56
|
-
this.isAborting = false; //
|
|
56
|
+
this.isAborting = false; // Transient guard: prevents tryDispatch from firing during abortMessage (reset when the abort completes)
|
|
57
|
+
this.isDestroyed = false; // Terminal guard: set in destroy(), never reset — no dispatch may ever start after destroy
|
|
57
58
|
this.dispatchAborted = false; // Set on abort while a dispatch is running: suppress the .finally re-check so preserved notifications don't get dispatched after abort
|
|
58
59
|
this.sessionStartTime = Date.now();
|
|
59
60
|
const { logger, workdir, systemPrompt, stream = true } = options;
|
|
@@ -113,6 +114,7 @@ export class Agent {
|
|
|
113
114
|
this.bangManager = this.container.get("BangManager");
|
|
114
115
|
this.cronManager = this.container.get("CronManager");
|
|
115
116
|
this.messageQueue = this.container.get("MessageQueue");
|
|
117
|
+
this.asyncWorkRegistry = this.container.get("AsyncWorkRegistry");
|
|
116
118
|
// Wire up CWD change callback from AIManager to sync Agent's workdir
|
|
117
119
|
this.aiManager.setOnCwdChange((newCwd) => {
|
|
118
120
|
this.workdir = newCwd;
|
|
@@ -256,6 +258,17 @@ export class Agent {
|
|
|
256
258
|
}
|
|
257
259
|
return updated;
|
|
258
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Terminal-lifecycle guard for public APIs. destroy() is terminal: after it
|
|
263
|
+
* returns, no new work may start on this agent. Silent drops hid misuse until
|
|
264
|
+
* a ghost async side effect surfaced later (issue #1808); throwing surfaces
|
|
265
|
+
* the contract violation immediately at the call site.
|
|
266
|
+
*/
|
|
267
|
+
assertNotDestroyed() {
|
|
268
|
+
if (this.isDestroyed) {
|
|
269
|
+
throw new Error("Agent destroyed");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
259
272
|
/**
|
|
260
273
|
* Unified dispatch trigger — checks state machine before processing.
|
|
261
274
|
* Handles user messages, bang commands, and background task notifications
|
|
@@ -263,6 +276,8 @@ export class Agent {
|
|
|
263
276
|
* onLoadingChange(false), and onCommandRunningChange(false).
|
|
264
277
|
*/
|
|
265
278
|
tryDispatch() {
|
|
279
|
+
if (this.isDestroyed)
|
|
280
|
+
return; // Terminal: agent destroyed, never dispatch again
|
|
266
281
|
if (this.isAborting)
|
|
267
282
|
return; // Suppress dispatch during abort to prevent queued notifications from being dispatched as a side-effect
|
|
268
283
|
if (this.dispatchAborted)
|
|
@@ -274,7 +289,7 @@ export class Agent {
|
|
|
274
289
|
if (this.aiManager.isLoading || this.isCommandRunning)
|
|
275
290
|
return;
|
|
276
291
|
this.messageQueue.transitionTo("dispatching");
|
|
277
|
-
this.dispatchPromise = this.processQueuedMessage()
|
|
292
|
+
this.dispatchPromise = this.asyncWorkRegistry.track(this.processQueuedMessage()
|
|
278
293
|
.catch((error) => {
|
|
279
294
|
this.logger?.error("Failed to process queued message:", error);
|
|
280
295
|
})
|
|
@@ -285,7 +300,7 @@ export class Agent {
|
|
|
285
300
|
this.tryDispatch(); // Re-check after processing
|
|
286
301
|
}
|
|
287
302
|
this.dispatchAborted = false;
|
|
288
|
-
});
|
|
303
|
+
}));
|
|
289
304
|
}
|
|
290
305
|
/**
|
|
291
306
|
* Process the next queued item when the agent becomes idle.
|
|
@@ -499,6 +514,7 @@ export class Agent {
|
|
|
499
514
|
}
|
|
500
515
|
/** Execute bash command (bang command) */
|
|
501
516
|
async bang(command) {
|
|
517
|
+
this.assertNotDestroyed();
|
|
502
518
|
// If the agent is busy, enqueue the bang command
|
|
503
519
|
if (this.aiManager.isLoading || this.isCommandRunning) {
|
|
504
520
|
this.messageQueue.enqueue({ type: "bang", content: command });
|
|
@@ -625,37 +641,14 @@ export class Agent {
|
|
|
625
641
|
await this.foregroundTaskManager.backgroundCurrentTask();
|
|
626
642
|
this.options.callbacks?.onBackgroundCurrentTask?.();
|
|
627
643
|
}
|
|
628
|
-
/**
|
|
629
|
-
* Trigger WorktreeRemove hook before agent destruction.
|
|
630
|
-
* Called from CLI exit dialog when user chooses to remove the worktree.
|
|
631
|
-
* Non-blocking: errors logged but don't prevent removal.
|
|
632
|
-
*/
|
|
633
|
-
async triggerWorktreeRemoveHook(worktreePath) {
|
|
634
|
-
if (!this.hookManager.hasHooks("WorktreeRemove")) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
try {
|
|
638
|
-
const sessionId = this.messageManager.getSessionId();
|
|
639
|
-
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
640
|
-
const hookResults = await this.hookManager.executeHooks("WorktreeRemove", {
|
|
641
|
-
event: "WorktreeRemove",
|
|
642
|
-
projectDir: this.workdir,
|
|
643
|
-
timestamp: new Date(),
|
|
644
|
-
sessionId,
|
|
645
|
-
transcriptPath,
|
|
646
|
-
cwd: this.workdir,
|
|
647
|
-
worktreePath,
|
|
648
|
-
env: Object.fromEntries(Object.entries(this.configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
|
|
649
|
-
});
|
|
650
|
-
// Process results via messageManager (may not be visible during shutdown)
|
|
651
|
-
this.hookManager.processHookResults("WorktreeRemove", hookResults, this.messageManager);
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
this.logger?.warn("WorktreeRemove hooks execution failed:", error);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
644
|
/** Destroy managers, clean up resources */
|
|
658
645
|
async destroy() {
|
|
646
|
+
// Terminal guard: suppress any dispatch triggered during teardown (e.g. the
|
|
647
|
+
// onLoadingChange(false) fired by abortAIMessage() below, or the dispatch
|
|
648
|
+
// .finally re-check). Without this, leftover queued messages would be
|
|
649
|
+
// dispatched fire-and-forget and outlive the agent. Never reset: destroy()
|
|
650
|
+
// is terminal, no dispatch should ever start afterwards.
|
|
651
|
+
this.isDestroyed = true;
|
|
659
652
|
// Log session_end event and shutdown telemetry
|
|
660
653
|
await logOTelEvent("session_end", {
|
|
661
654
|
duration: String(Math.round((Date.now() - this.sessionStartTime) / 1000)),
|
|
@@ -717,7 +710,13 @@ export class Agent {
|
|
|
717
710
|
}
|
|
718
711
|
// Cleanup remote settings polling
|
|
719
712
|
remoteSettingsService.shutdown();
|
|
720
|
-
//
|
|
713
|
+
// Drain live async work (dispatch, background subagents, fork subagents):
|
|
714
|
+
// abort steps above make them settle; wait for them so no async side
|
|
715
|
+
// effect outlives the agent. Timeout fallback keeps destroy bounded.
|
|
716
|
+
const drained = await this.asyncWorkRegistry.drain();
|
|
717
|
+
if (!drained) {
|
|
718
|
+
this.logger?.error(`Async work did not drain: ${this.asyncWorkRegistry.size} live work item(s) remain after destroy`);
|
|
719
|
+
}
|
|
721
720
|
}
|
|
722
721
|
/**
|
|
723
722
|
* Trigger the rewind UI callback
|
|
@@ -738,6 +737,7 @@ export class Agent {
|
|
|
738
737
|
* @returns Promise that resolves to the AI's answer
|
|
739
738
|
*/
|
|
740
739
|
async askBtw(question, abortSignal, onContent, onReasoning) {
|
|
740
|
+
this.assertNotDestroyed();
|
|
741
741
|
const result = await this.aiManager.runBtwFork(question, abortSignal, onContent, onReasoning);
|
|
742
742
|
return result.content ?? result.error ?? "No response received";
|
|
743
743
|
}
|
|
@@ -760,6 +760,7 @@ export class Agent {
|
|
|
760
760
|
* @returns Promise that resolves to the background task ID
|
|
761
761
|
*/
|
|
762
762
|
async forkSubagent(prompt, options, abortSignal) {
|
|
763
|
+
this.assertNotDestroyed();
|
|
763
764
|
return this.aiManager.runForkSubagent(prompt, options, abortSignal);
|
|
764
765
|
}
|
|
765
766
|
/**
|
|
@@ -789,6 +790,7 @@ export class Agent {
|
|
|
789
790
|
* ```
|
|
790
791
|
*/
|
|
791
792
|
async sendMessage(content, images) {
|
|
793
|
+
this.assertNotDestroyed();
|
|
792
794
|
// If the agent is busy, enqueue the message — unless it's an immediate
|
|
793
795
|
// slash command (e.g., /clear, /compact) that should execute
|
|
794
796
|
// right away even while AI is processing
|
|
@@ -981,6 +983,14 @@ export class Agent {
|
|
|
981
983
|
getSubagentConfigurations() {
|
|
982
984
|
return this.subagentManager.getConfigurations();
|
|
983
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
|
+
}
|
|
984
994
|
/**
|
|
985
995
|
* Get currently active subagent instances (status active/initializing).
|
|
986
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\` |
|
|
@@ -92,8 +92,8 @@ Wave supports the following hook events:
|
|
|
92
92
|
- \`PermissionRequest\`: Triggered when Wave requests permission to use a tool.
|
|
93
93
|
- \`Stop\`: Triggered when Wave finishes its response cycle (no more tool calls).
|
|
94
94
|
- \`SubagentStop\`: Triggered when a subagent finishes its response cycle.
|
|
95
|
-
- \`WorktreeCreate\`: Triggered
|
|
96
|
-
- \`WorktreeRemove\`: Triggered
|
|
95
|
+
- \`WorktreeCreate\`: Triggered to create a new worktree, replacing \`git worktree add\`. The hook performs the creation itself (e.g., \`git worktree add\`, or any other VCS/external provisioning) and must output the worktree's absolute path on stdout (path return). Creation is blocked if all hooks fail or produce no output. Receives \`name\` in the JSON input. The resulting session is marked "hook-based".
|
|
96
|
+
- \`WorktreeRemove\`: Triggered when a hook-based worktree (created by a \`WorktreeCreate\` hook) is removed (e.g., via ExitWorktree with \`action: "remove"\`), replacing \`git worktree remove\` for that worktree: the hook performs the removal itself (e.g., \`git worktree remove --force\` plus external resource cleanup). Fires **before** the worktree directory is deleted so hooks can still read files inside it. Receives \`worktree_path\` in the JSON input. Failures are logged but non-blocking. Git-created worktrees are removed by git directly and do not trigger this hook.
|
|
97
97
|
- \`CwdChanged\`: Triggered when the working directory changes (e.g., entering/exiting a worktree). Non-blocking.
|
|
98
98
|
- \`SessionStart\`: Triggered during session initialization. Hooks can inject \`additionalContext\` and \`initialUserMessage\` via stdout.
|
|
99
99
|
- \`SessionEnd\`: Triggered during agent destruction (fire-and-forget, non-blocking). Useful for cleanup, resource teardown, and analytics.
|
|
@@ -245,9 +245,34 @@ SessionEnd hooks receive \`end_source\` in the JSON input indicating how the ses
|
|
|
245
245
|
}
|
|
246
246
|
\`\`\`
|
|
247
247
|
|
|
248
|
+
## WorktreeCreate Hooks
|
|
249
|
+
|
|
250
|
+
\`WorktreeCreate\` hooks replace \`git worktree add\`: when configured, Wave does not create the worktree itself — the hook does. The hook must output the worktree's absolute path on **stdout** (the first successful hook's trimmed stdout is used as the path). All hooks failing or producing no output blocks the creation with \`WorktreeCreate hook failed: ...\`. The resulting session is marked "hook-based" and skips Wave's post-creation setup (\`settings.local.json\` / \`.worktreeinclude\` propagation) — the hook is responsible for any initialization.
|
|
251
|
+
|
|
252
|
+
### Input
|
|
253
|
+
WorktreeCreate hooks receive \`name\` (the worktree name) in the JSON input, alongside the common fields \`session_id\`, \`transcript_path\`, \`cwd\`, \`hook_event_name\`.
|
|
254
|
+
|
|
255
|
+
### Example Configuration
|
|
256
|
+
\`\`\`json
|
|
257
|
+
{
|
|
258
|
+
"hooks": {
|
|
259
|
+
"WorktreeCreate": [
|
|
260
|
+
{
|
|
261
|
+
"hooks": [
|
|
262
|
+
{
|
|
263
|
+
"command": "worktree_path=\\"$WAVE_PROJECT_DIR/.wave/worktrees/$(jq -r '.name')\\" && mkdir -p \\"$worktree_path\\" && git worktree add \\"$worktree_path\\" 2>/dev/null; echo \\"$worktree_path\\"",
|
|
264
|
+
"description": "Create the worktree and print its path"
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
248
273
|
## WorktreeRemove Hooks
|
|
249
274
|
|
|
250
|
-
\`WorktreeRemove\` hooks fire **before** the worktree directory is deleted, so they can still read files inside it.
|
|
275
|
+
\`WorktreeRemove\` hooks replace \`git worktree remove\` for **hook-based** worktrees (those created by a \`WorktreeCreate\` hook). When a hook-based worktree is removed from any entry point (CLI exit, ExitWorktree tool, \`wave -p\`, stdio RPC), Wave calls the hook instead of running \`git worktree remove\` — the hook performs the actual removal, so it can clean up external resources it provisioned (databases, containers, etc.) at the same time. Hooks fire **before** the worktree directory is deleted, so they can still read files inside it. Failures are logged but non-blocking. If no \`WorktreeRemove\` hook is configured for a hook-based worktree, Wave logs a warning and leaves the worktree in place. Git-created worktrees (no \`WorktreeCreate\` hook) are removed by git directly and do **not** trigger this hook.
|
|
251
276
|
|
|
252
277
|
### Input
|
|
253
278
|
WorktreeRemove hooks receive \`worktree_path\` in the JSON input (alongside the common fields \`session_id\`, \`transcript_path\`, \`cwd\`, \`hook_event_name\`). The worktree name can be derived via \`basename "$worktree_path"\`.
|
|
@@ -260,8 +285,8 @@ WorktreeRemove hooks receive \`worktree_path\` in the JSON input (alongside the
|
|
|
260
285
|
{
|
|
261
286
|
"hooks": [
|
|
262
287
|
{
|
|
263
|
-
"command": "worktree_path=$(jq -r '.worktree_path') && docker compose -p \\"$(basename \\"$worktree_path\\")\\" down
|
|
264
|
-
"description": "
|
|
288
|
+
"command": "worktree_path=$(jq -r '.worktree_path') && git worktree remove --force \\"$worktree_path\\" && docker compose -p \\"$(basename \\"$worktree_path\\")\\" down",
|
|
289
|
+
"description": "Remove the worktree and tear down its docker compose project"
|
|
265
290
|
}
|
|
266
291
|
]
|
|
267
292
|
}
|
|
@@ -297,7 +322,7 @@ When hooks are registered via a **plugin**, Wave automatically:
|
|
|
297
322
|
}
|
|
298
323
|
\`\`\`
|
|
299
324
|
|
|
300
|
-
The shell also receives \`WAVE_PLUGIN_ROOT\` as an env var, so \`$WAVE_PLUGIN_ROOT\` works in the hook script itself.
|
|
325
|
+
The shell also receives \`WAVE_PLUGIN_ROOT\` as an env var, so \`$WAVE_PLUGIN_ROOT\` works in the hook script itself. For \`WorktreeCreate\`, the script must print the created worktree's absolute path to stdout.
|
|
301
326
|
|
|
302
327
|
## Best Practices
|
|
303
328
|
|
|
@@ -560,12 +585,27 @@ You can define overrides for specific models in the \`models\` field. The key sh
|
|
|
560
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:
|
|
561
586
|
|
|
562
587
|
- \`temperature\`: Controls randomness (0.0 to 2.0).
|
|
563
|
-
- \`
|
|
588
|
+
- \`max_tokens\`: Maximum number of tokens to generate in the response.
|
|
564
589
|
- \`reasoning_effort\`: (OpenAI specific) Controls the reasoning effort for models like \`o1\` and \`o3-mini\`. Values: \`low\`, \`medium\`, \`high\`.
|
|
565
590
|
- \`thinking\`: (Claude specific) Configures the thinking/reasoning capabilities for Claude 3.7+ models.
|
|
566
591
|
- \`type\`: \`"enabled"\` or \`"disabled"\`.
|
|
567
592
|
- \`budget_tokens\`: Maximum tokens to use for thinking.
|
|
568
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
|
+
|
|
569
609
|
## Model Capabilities
|
|
570
610
|
|
|
571
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./core/session.js";
|
|
2
2
|
export * from "./services/authService.js";
|
|
3
|
+
export * from "./services/worktreeHooks.js";
|
|
3
4
|
export * from "./constants/tools.js";
|
|
4
5
|
export * from "./agent.js";
|
|
5
6
|
export * from "./core/plugin.js";
|
|
@@ -21,6 +22,7 @@ export * from "./utils/hookMatcher.js";
|
|
|
21
22
|
export * from "./utils/tokenCalculation.js";
|
|
22
23
|
export * from "./utils/gitUtils.js";
|
|
23
24
|
export * from "./utils/nameGenerator.js";
|
|
25
|
+
export * from "./utils/pathEncoder.js";
|
|
24
26
|
export * from "./utils/worktreeSession.js";
|
|
25
27
|
export * from "./utils/worktreeUtils.js";
|
|
26
28
|
export { loadMergedWaveConfig, loadUserConfigEnv, } from "./services/configurationService.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Export all services
|
|
2
2
|
export * from "./core/session.js";
|
|
3
3
|
export * from "./services/authService.js";
|
|
4
|
+
export * from "./services/worktreeHooks.js";
|
|
4
5
|
// Export constants
|
|
5
6
|
export * from "./constants/tools.js";
|
|
6
7
|
// Export main agent
|
|
@@ -25,6 +26,7 @@ export * from "./utils/hookMatcher.js";
|
|
|
25
26
|
export * from "./utils/tokenCalculation.js";
|
|
26
27
|
export * from "./utils/gitUtils.js";
|
|
27
28
|
export * from "./utils/nameGenerator.js";
|
|
29
|
+
export * from "./utils/pathEncoder.js";
|
|
28
30
|
export * from "./utils/worktreeSession.js";
|
|
29
31
|
export * from "./utils/worktreeUtils.js";
|
|
30
32
|
export { loadMergedWaveConfig, loadUserConfigEnv, } from "./services/configurationService.js";
|
|
@@ -12,6 +12,15 @@ interface ForkLoopResult {
|
|
|
12
12
|
}
|
|
13
13
|
export interface AIManagerCallbacks {
|
|
14
14
|
onCompactionStateChange?: (isCompacting: boolean) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Streaming content from the compaction fork (accumulated, same semantics
|
|
17
|
+
* as `onContentUpdate` in CallAgentOptions). Reasoning chunks fall back to
|
|
18
|
+
* this channel when no reasoning callback is provided. Consumed by the CLI
|
|
19
|
+
* to show the compaction loading tail.
|
|
20
|
+
*/
|
|
21
|
+
onCompactionContentUpdate?: (content: string) => void;
|
|
22
|
+
/** Streaming reasoning from the compaction fork (accumulated). */
|
|
23
|
+
onCompactionReasoningUpdate?: (content: string) => void;
|
|
15
24
|
onUsageAdded?: (usage: Usage) => void;
|
|
16
25
|
onCwdChange?: (newCwd: string) => void;
|
|
17
26
|
}
|
|
@@ -59,6 +68,7 @@ export declare class AIManager {
|
|
|
59
68
|
private get memoryService();
|
|
60
69
|
private get taskManager();
|
|
61
70
|
private get backgroundTaskManager();
|
|
71
|
+
private get asyncWorkRegistry();
|
|
62
72
|
private get hookManager();
|
|
63
73
|
private get reversionManager();
|
|
64
74
|
private get permissionManager();
|
|
@@ -123,7 +133,19 @@ export declare class AIManager {
|
|
|
123
133
|
setIsLoading(isLoading: boolean): void;
|
|
124
134
|
abortAIMessage(): void;
|
|
125
135
|
private generateCompactParams;
|
|
126
|
-
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;
|
|
127
149
|
/**
|
|
128
150
|
* Manually compact the conversation history.
|
|
129
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";
|
|
@@ -103,6 +103,9 @@ export class AIManager {
|
|
|
103
103
|
get backgroundTaskManager() {
|
|
104
104
|
return this.container.get("BackgroundTaskManager");
|
|
105
105
|
}
|
|
106
|
+
get asyncWorkRegistry() {
|
|
107
|
+
return this.container.get("AsyncWorkRegistry");
|
|
108
|
+
}
|
|
106
109
|
get hookManager() {
|
|
107
110
|
return this.container.get("HookManager");
|
|
108
111
|
}
|
|
@@ -141,7 +144,7 @@ export class AIManager {
|
|
|
141
144
|
const permissionMode = this.container.has("PermissionMode")
|
|
142
145
|
? this.container.get("PermissionMode")
|
|
143
146
|
: undefined;
|
|
144
|
-
const parentModelConfig = this.configurationService.resolveModelConfig(undefined, undefined,
|
|
147
|
+
const parentModelConfig = this.configurationService.resolveModelConfig(undefined, undefined, permissionMode);
|
|
145
148
|
let modelToUse;
|
|
146
149
|
if (this.modelOverride) {
|
|
147
150
|
if (this.modelOverride === "fastModel") {
|
|
@@ -154,10 +157,13 @@ export class AIManager {
|
|
|
154
157
|
modelToUse = this.modelOverride;
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
|
-
return this.configurationService.resolveModelConfig(modelToUse, undefined,
|
|
160
|
+
return this.configurationService.resolveModelConfig(modelToUse, undefined, permissionMode);
|
|
158
161
|
}
|
|
159
162
|
getMaxInputTokens() {
|
|
160
|
-
|
|
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);
|
|
161
167
|
}
|
|
162
168
|
getLanguage() {
|
|
163
169
|
return this.configurationService.resolveLanguage();
|
|
@@ -363,30 +369,43 @@ export class AIManager {
|
|
|
363
369
|
}
|
|
364
370
|
return "";
|
|
365
371
|
}
|
|
366
|
-
// Private method to
|
|
367
|
-
|
|
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) {
|
|
368
378
|
if (!usage)
|
|
369
379
|
return;
|
|
370
380
|
// Update token statistics - display comprehensive token usage including cache tokens
|
|
371
381
|
const comprehensiveTotalTokens = calculateComprehensiveTotalTokens(usage);
|
|
372
382
|
this.messageManager.setlatestTotalTokens(comprehensiveTotalTokens);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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 });
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
411
|
/**
|
|
@@ -431,7 +450,7 @@ export class AIManager {
|
|
|
431
450
|
// 4. Fork path: fork the conversation with the same system prompt,
|
|
432
451
|
// tools, model, and generation params as the main loop so the forked
|
|
433
452
|
// request prefix matches exactly and the prompt cache is reused.
|
|
434
|
-
const forkResult = await this.runCompactFork(recentChatMessages, compactPrompt, options.abortSignal);
|
|
453
|
+
const forkResult = await this.runCompactFork(recentChatMessages, compactPrompt, options.abortSignal, this.callbacks?.onCompactionContentUpdate, this.callbacks?.onCompactionReasoningUpdate);
|
|
435
454
|
const summaryContent = forkResult.content;
|
|
436
455
|
const compactTokens = forkResult.usage;
|
|
437
456
|
if (!summaryContent) {
|
|
@@ -595,11 +614,31 @@ export class AIManager {
|
|
|
595
614
|
workdir,
|
|
596
615
|
tools: toolsConfig,
|
|
597
616
|
systemPrompt,
|
|
617
|
+
maxTokens: this.configurationService.resolveMaxOutputTokens(),
|
|
598
618
|
toolChoice: this.toolChoiceOverride,
|
|
599
619
|
// Stream so a slow reasoning model emits first bytes before the
|
|
600
620
|
// gateway's idle timeout fires (non-streaming waits for the full
|
|
601
621
|
// response, which exceeds the timeout on large contexts).
|
|
602
622
|
stream: true,
|
|
623
|
+
// Surface partial output to the caller (e.g. the compaction loading
|
|
624
|
+
// tail) as it arrives. Reasoning chunks from thinking models go to
|
|
625
|
+
// the reasoning channel when the caller supplies one; otherwise they
|
|
626
|
+
// fall back to the content channel (CLI mixes both).
|
|
627
|
+
onContentUpdate: (content) => {
|
|
628
|
+
if (content.trim()) {
|
|
629
|
+
options.onContentUpdate?.(content);
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
onReasoningUpdate: (reasoning) => {
|
|
633
|
+
if (reasoning.trim()) {
|
|
634
|
+
if (options.onReasoningUpdate) {
|
|
635
|
+
options.onReasoningUpdate(reasoning);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
options.onContentUpdate?.(reasoning);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
},
|
|
603
642
|
});
|
|
604
643
|
if (result.usage) {
|
|
605
644
|
totalUsage = {
|
|
@@ -652,10 +691,12 @@ export class AIManager {
|
|
|
652
691
|
* Fork-path compaction: deny all tool calls locally (the model is told to
|
|
653
692
|
* summarize, not act) and feed the rejections back for another turn.
|
|
654
693
|
*/
|
|
655
|
-
async runCompactFork(historyMessages, compactPrompt, abortSignal) {
|
|
694
|
+
async runCompactFork(historyMessages, compactPrompt, abortSignal, onContentUpdate, onReasoningUpdate) {
|
|
656
695
|
return this.runForkLoop(historyMessages, compactPrompt, {
|
|
657
696
|
maxTurns: MAX_FORK_TURNS,
|
|
658
697
|
deniedToolMessage: "Tool use is not allowed during compaction",
|
|
698
|
+
onContentUpdate,
|
|
699
|
+
onReasoningUpdate,
|
|
659
700
|
}, abortSignal);
|
|
660
701
|
}
|
|
661
702
|
/**
|
|
@@ -811,8 +852,10 @@ export class AIManager {
|
|
|
811
852
|
}
|
|
812
853
|
};
|
|
813
854
|
// Fire-and-forget: the fork runs to completion in the background; the
|
|
814
|
-
// caller gets the task ID immediately.
|
|
815
|
-
|
|
855
|
+
// caller gets the task ID immediately. Tracked in the async work registry
|
|
856
|
+
// so destroy() drains it before returning (abort via the task's onStop
|
|
857
|
+
// makes the loop settle).
|
|
858
|
+
const forkPromise = (async () => {
|
|
816
859
|
try {
|
|
817
860
|
const result = await this.runForkLoop(historyMessages, prompt, {
|
|
818
861
|
// /subtask aligns with Claude Code's fork subagent
|
|
@@ -859,6 +902,7 @@ export class AIManager {
|
|
|
859
902
|
abortCleanup?.();
|
|
860
903
|
}
|
|
861
904
|
})();
|
|
905
|
+
this.asyncWorkRegistry?.track(forkPromise);
|
|
862
906
|
return taskId;
|
|
863
907
|
}
|
|
864
908
|
/**
|
|
@@ -1002,6 +1046,7 @@ ${question}`;
|
|
|
1002
1046
|
workdir,
|
|
1003
1047
|
tools: toolsConfig,
|
|
1004
1048
|
systemPrompt,
|
|
1049
|
+
maxTokens: this.configurationService.resolveMaxOutputTokens(),
|
|
1005
1050
|
toolChoice: this.toolChoiceOverride,
|
|
1006
1051
|
// Stream so a slow reasoning model emits first bytes before the
|
|
1007
1052
|
// gateway's idle timeout fires (same rationale as runCompactFork).
|
|
@@ -1254,6 +1299,10 @@ ${question}`;
|
|
|
1254
1299
|
isMeta: true,
|
|
1255
1300
|
});
|
|
1256
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);
|
|
1257
1306
|
// Get recent message history
|
|
1258
1307
|
const rawMessages = this.messageManager.getMessages();
|
|
1259
1308
|
const currentModelConfig = this.getModelConfig();
|
|
@@ -1278,7 +1327,7 @@ ${question}`;
|
|
|
1278
1327
|
tools: toolsConfig, // Pass filtered tool configuration
|
|
1279
1328
|
model: model, // Use passed model
|
|
1280
1329
|
systemPrompt: mainSystemPrompt, // Pass custom system prompt
|
|
1281
|
-
maxTokens: maxTokens, // Pass max tokens override
|
|
1330
|
+
maxTokens: maxTokens ?? this.configurationService.resolveMaxOutputTokens(), // Pass max tokens override, falling back to the resolved global value
|
|
1282
1331
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
1283
1332
|
// Fast-model subagents send disable-thinking params only when
|
|
1284
1333
|
// explicitly configured (never in the agent loop).
|
|
@@ -1462,8 +1511,9 @@ ${question}`;
|
|
|
1462
1511
|
}
|
|
1463
1512
|
}
|
|
1464
1513
|
}
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1514
|
+
// Update token statistics for display (compaction decision is
|
|
1515
|
+
// pre-request, see maybeAutoCompactBeforeRequest)
|
|
1516
|
+
this.updateLatestTotalTokens(result.usage);
|
|
1467
1517
|
// Finalize text/reasoning blocks for the final response (no tools)
|
|
1468
1518
|
this.messageManager.finalizeStreamingBlocks();
|
|
1469
1519
|
// Check if there are tool operations or response was truncated, if so automatically initiate next AI service call
|