wave-agent-sdk 0.19.9 → 1.0.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/builtin/plugins/sdd/scripts/session-start.js +1 -1
- package/builtin/plugins/sdd/skills/specify/SKILL.md +3 -4
- package/builtin/skills/settings/ENV.md +15 -9
- package/builtin/skills/settings/HOOKS.md +27 -2
- package/dist/agent.d.ts +9 -20
- package/dist/agent.js +28 -99
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +71 -8
- package/dist/managers/aiManager.js +290 -85
- package/dist/managers/backgroundTaskManager.d.ts +6 -0
- package/dist/managers/backgroundTaskManager.js +11 -0
- package/dist/managers/bangManager.d.ts +6 -0
- package/dist/managers/bangManager.js +11 -0
- package/dist/managers/hookManager.d.ts +8 -2
- package/dist/managers/hookManager.js +14 -4
- package/dist/managers/mcpManager.d.ts +18 -4
- package/dist/managers/mcpManager.js +40 -18
- package/dist/managers/messageManager.d.ts +9 -5
- package/dist/managers/messageManager.js +36 -12
- package/dist/managers/subagentManager.d.ts +6 -0
- package/dist/managers/subagentManager.js +33 -22
- package/dist/managers/toolManager.js +5 -0
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/services/aiService.d.ts +1 -34
- package/dist/services/aiService.js +18 -130
- package/dist/services/autoMemoryService.d.ts +27 -2
- package/dist/services/autoMemoryService.js +124 -36
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +86 -24
- package/dist/services/initializationService.js +14 -4
- package/dist/services/interactionService.js +35 -7
- package/dist/services/remoteSettingsService.d.ts +12 -0
- package/dist/services/remoteSettingsService.js +15 -1
- package/dist/services/session.d.ts +13 -0
- package/dist/services/session.js +64 -0
- package/dist/services/taskManager.js +7 -1
- package/dist/tools/bashTool.js +1 -0
- package/dist/tools/enterWorktreeTool.js +14 -3
- package/dist/tools/exitWorktreeTool.js +11 -10
- package/dist/tools/types.d.ts +7 -0
- package/dist/types/agent.d.ts +0 -2
- package/dist/types/config.d.ts +9 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/containerSetup.js +13 -4
- package/dist/utils/openaiClient.js +2 -1
- package/dist/utils/pathEncoder.js +7 -2
- package/dist/utils/worktreeUtils.d.ts +17 -0
- package/dist/utils/worktreeUtils.js +339 -1
- package/package.json +1 -1
- package/src/agent.ts +43 -112
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +389 -110
- package/src/managers/backgroundTaskManager.ts +15 -0
- package/src/managers/bangManager.ts +15 -0
- package/src/managers/hookManager.ts +20 -5
- package/src/managers/mcpManager.ts +60 -18
- package/src/managers/messageManager.ts +51 -23
- package/src/managers/subagentManager.ts +36 -25
- package/src/managers/toolManager.ts +7 -0
- package/src/prompts/index.ts +0 -4
- package/src/services/aiService.ts +25 -203
- package/src/services/autoMemoryService.ts +145 -39
- package/src/services/configurationService.ts +100 -24
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/session.ts +68 -0
- package/src/services/taskManager.ts +10 -1
- package/src/tools/bashTool.ts +1 -0
- package/src/tools/enterWorktreeTool.ts +19 -2
- package/src/tools/exitWorktreeTool.ts +15 -12
- package/src/tools/types.ts +7 -0
- package/src/types/agent.ts +0 -6
- package/src/types/config.ts +9 -0
- package/src/types/core.ts +1 -1
- package/src/types/hooks.ts +2 -2
- package/src/utils/containerSetup.ts +15 -5
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
- package/dist/constants/goalPrompts.d.ts +0 -1
- package/dist/constants/goalPrompts.js +0 -10
- package/dist/managers/goalManager.d.ts +0 -42
- package/dist/managers/goalManager.js +0 -177
- package/src/constants/goalPrompts.ts +0 -10
- package/src/managers/goalManager.ts +0 -232
|
@@ -14,7 +14,7 @@ const guidance = [
|
|
|
14
14
|
"Spec-First Workflow(规格优先工作流):",
|
|
15
15
|
"- 需求增加或变更时,优先更新 spec:先更新对应规格说明(新增用户故事、验收场景),待用户确认 spec 后再实现代码。spec 是功能设计的权威来源,不是 changelog。",
|
|
16
16
|
"- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
|
|
17
|
-
"-
|
|
17
|
+
"- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
|
|
18
18
|
`- 新增或修改 spec 后运行校验:${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。`,
|
|
19
19
|
].join("\n");
|
|
20
20
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specify
|
|
3
3
|
description: 根据自然语言描述创建或更新功能规格说明,生成包含用户故事与验收场景的规格文件。
|
|
4
|
+
user-invocable: false
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
## 用户输入
|
|
@@ -9,13 +10,11 @@ description: 根据自然语言描述创建或更新功能规格说明,生成
|
|
|
9
10
|
$ARGUMENTS
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
你**必须**在继续之前考虑用户输入(如果不为空)。
|
|
13
|
-
|
|
14
13
|
## 流程
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
本技能由 AI 在会话中自动触发(不占用手动斜杠命令)。触发时机:用户提出新的需求、修改需求或涉及功能边界时,若对应规格尚未创建或已过期,则主动创建或更新规格说明。$ARGUMENTS 通常为空——需求描述直接来自对话上下文,不要让用户重复。
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
根据对话中的功能描述,执行以下步骤:
|
|
19
18
|
|
|
20
19
|
1. **确定规格文件路径**:
|
|
21
20
|
- **确定规格根目录**:优先复用项目中已有的规格目录——若 `docs/specs/` 存在则用之,否则若 `specs/` 存在则用之,否则默认 `specs/`(并在完成报告中说明所选目录,便于用户纠正)。
|
|
@@ -17,13 +17,13 @@ Environment variables are configured in the `env` field of `settings.json`. It i
|
|
|
17
17
|
|
|
18
18
|
## Supported `WAVE_*` Environment Variables
|
|
19
19
|
|
|
20
|
-
Wave uses several environment variables to control its core functionality.
|
|
20
|
+
Wave uses several environment variables to control its core functionality. Variables marked **OS env only** are read from the OS environment (or constructor / stdio `initialize` params) and are **NOT** read from settings.json `env` — set them in your shell, not in the `env` field.
|
|
21
21
|
|
|
22
22
|
| Variable | Description | Default |
|
|
23
23
|
| :--- | :--- | :--- |
|
|
24
24
|
| `WAVE_API_KEY` | API key for the AI gateway. | - |
|
|
25
25
|
| `WAVE_BASE_URL` | Base URL for the AI gateway. | - |
|
|
26
|
-
| `WAVE_SERVER_URL` | Server URL for SSO authentication. | `https://codechat.codewave.163.com` |
|
|
26
|
+
| `WAVE_SERVER_URL` | Server URL for SSO authentication. **OS env only** — set via OS env or `options.serverUrl`; not read from settings.json `env` (avoids a startup 401 race). | `https://codechat.codewave.163.com` |
|
|
27
27
|
| `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
|
|
28
28
|
| `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
|
|
29
29
|
| `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
|
|
@@ -32,23 +32,29 @@ Wave uses several environment variables to control its core functionality.
|
|
|
32
32
|
| `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
|
|
33
33
|
| `WAVE_AUTO_MEMORY_FREQUENCY` | Auto memory update frequency. `1` = every turn, `2` = every 2 turns, etc. | `1` |
|
|
34
34
|
| `WAVE_TASK_LIST_ID` | Explicitly set the task list ID for the session. | (Session ID) |
|
|
35
|
-
| `WAVE_PLUGIN_GIT_TIMEOUT_MS` | Timeout in milliseconds for git operations when installing plugins. | `300000` |
|
|
35
|
+
| `WAVE_PLUGIN_GIT_TIMEOUT_MS` | Timeout in milliseconds for git operations when installing plugins. **OS env only** (infrastructure). | `300000` |
|
|
36
36
|
|
|
37
37
|
## Configuration Scopes
|
|
38
38
|
|
|
39
|
-
Environment variables can be set in different scopes
|
|
39
|
+
Environment variables can be set in different scopes. Wave merges scopes from lowest to highest priority and stores the result in the agent's **per-session environment snapshot**. The snapshot takes priority over OS environment variables but is **NOT written to `process.env`** — this keeps multiple sessions in one `wave --stdio` process from polluting each other.
|
|
40
|
+
|
|
41
|
+
Precedence (highest to lowest):
|
|
40
42
|
|
|
41
43
|
1. **Local Scope**: `.wave/settings.local.json` (Local overrides, ignored by git)
|
|
42
44
|
2. **Project Scope**: `.wave/settings.json` (Project-specific settings, shared via git)
|
|
43
45
|
3. **User Scope**: `~/.wave/settings.json` (Global settings for all projects)
|
|
44
|
-
4. **System Environment**: Variables set in your shell (e.g., `export WAVE_API_KEY=...`)
|
|
46
|
+
4. **System Environment**: Variables set in your shell (e.g., `export WAVE_API_KEY=...`). Used as a fallback when a key is absent from the settings snapshot.
|
|
47
|
+
|
|
48
|
+
> Settings `env` shadows (does not mutate) OS env: a key set in both settings.json `env` and the OS environment resolves to the settings value for that session, while the OS value remains untouched and visible to unrelated processes.
|
|
45
49
|
|
|
46
50
|
## Custom Environment Variables
|
|
47
51
|
|
|
48
|
-
You can also define custom environment variables in the `env` field. These variables will be available to:
|
|
52
|
+
You can also define custom environment variables in the `env` field. These variables are stored in the session's environment snapshot and will be available to:
|
|
53
|
+
|
|
54
|
+
- **Hooks**: Any shell command executed as a hook will have these variables in its environment (merged on top of OS env).
|
|
55
|
+
- **Tools**: Tools like `Bash` will have access to these variables (merged on top of OS env).
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
- **Tools**: Tools like `Bash` will have access to these variables.
|
|
57
|
+
In `wave --stdio` mode one process hosts multiple sessions; each session keeps its own snapshot, so sessions with different `env` do not pollute each other (no "last session wins").
|
|
52
58
|
|
|
53
59
|
Example:
|
|
54
60
|
```json
|
|
@@ -62,7 +68,7 @@ Example:
|
|
|
62
68
|
|
|
63
69
|
## Live Reload
|
|
64
70
|
|
|
65
|
-
Environment variables configured in `settings.json` support **live reload**. When you modify the `env` field in any `settings.json` file (user, project, or local scope), the changes take effect immediately without requiring a Wave session restart.
|
|
71
|
+
Environment variables configured in `settings.json` support **live reload**. When you modify the `env` field in any `settings.json` file (user, project, or local scope), the changes take effect immediately without requiring a Wave session restart — the session's environment snapshot is refreshed and subsequent resolve calls / subprocess spawns use the new values.
|
|
66
72
|
|
|
67
73
|
## Best Practices
|
|
68
74
|
|
|
@@ -13,7 +13,7 @@ Wave supports the following hook events:
|
|
|
13
13
|
- `Stop`: Triggered when Wave finishes its response cycle (no more tool calls).
|
|
14
14
|
- `SubagentStop`: Triggered when a subagent finishes its response cycle.
|
|
15
15
|
- `WorktreeCreate`: Triggered when a new worktree is created.
|
|
16
|
-
- `WorktreeRemove`: Triggered
|
|
16
|
+
- `WorktreeRemove`: Triggered before a worktree is removed (e.g., via ExitWorktree with `action: "remove"`). Non-blocking. Fires **before** the worktree directory is deleted so hooks can still read files inside it. The hook receives `worktree_path` in the JSON input. Useful for cleanup tasks (e.g., `docker compose -p $(basename "$worktree_path") down`).
|
|
17
17
|
- `CwdChanged`: Triggered when the working directory changes (e.g., entering/exiting a worktree). Non-blocking.
|
|
18
18
|
- `SessionStart`: Triggered during session initialization. Hooks can inject `additionalContext` and `initialUserMessage` via stdout.
|
|
19
19
|
- `SessionEnd`: Triggered during agent destruction (fire-and-forget, non-blocking). Useful for cleanup, resource teardown, and analytics.
|
|
@@ -79,7 +79,7 @@ Wave provides detailed context to hook processes via `stdin` as a JSON object. T
|
|
|
79
79
|
- `user_prompt`: (UserPromptSubmit) The text submitted by the user.
|
|
80
80
|
- `subagent_type`: (If executed by a subagent) The type of the subagent.
|
|
81
81
|
- `name`: (WorktreeCreate) The name of the new worktree.
|
|
82
|
-
- `worktree_path`: (WorktreeRemove) The absolute path
|
|
82
|
+
- `worktree_path`: (WorktreeRemove) The absolute path of the worktree about to be removed. Derive the worktree name with `basename "$worktree_path"`.
|
|
83
83
|
- `old_cwd`: (CwdChanged) The previous working directory.
|
|
84
84
|
- `new_cwd`: (CwdChanged) The new working directory.
|
|
85
85
|
- `compact_instructions`: (PreCompact) Custom instructions for the compaction, if any.
|
|
@@ -165,6 +165,31 @@ SessionEnd hooks receive `end_source` in the JSON input indicating how the sessi
|
|
|
165
165
|
}
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
## WorktreeRemove Hooks
|
|
169
|
+
|
|
170
|
+
`WorktreeRemove` hooks fire **before** the worktree directory is deleted, so they can still read files inside it. They are non-blocking (Notification type): the hook never replaces `git worktree remove` itself. Useful for cleaning up external resources that were provisioned for the worktree (databases, containers, etc.).
|
|
171
|
+
|
|
172
|
+
### Input
|
|
173
|
+
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"`.
|
|
174
|
+
|
|
175
|
+
### Example Configuration
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"hooks": {
|
|
179
|
+
"WorktreeRemove": [
|
|
180
|
+
{
|
|
181
|
+
"hooks": [
|
|
182
|
+
{
|
|
183
|
+
"command": "worktree_path=$(jq -r '.worktree_path') && docker compose -p \"$(basename \"$worktree_path\")\" down || true",
|
|
184
|
+
"description": "Tear down the worktree's docker compose project before removal"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
168
193
|
## Live Reload
|
|
169
194
|
|
|
170
195
|
Hook configurations support **live reload**. When you modify hooks in `settings.json`, the changes take effect immediately without restarting Wave.
|
package/dist/agent.d.ts
CHANGED
|
@@ -19,7 +19,6 @@ export declare class Agent {
|
|
|
19
19
|
private pluginManager;
|
|
20
20
|
private skillManager;
|
|
21
21
|
private cronManager;
|
|
22
|
-
private goalManager;
|
|
23
22
|
private hookManager;
|
|
24
23
|
private reversionManager;
|
|
25
24
|
private messageQueue;
|
|
@@ -92,10 +91,6 @@ export declare class Agent {
|
|
|
92
91
|
get isCommandRunning(): boolean;
|
|
93
92
|
/** Get queued user-facing messages (excludes background notifications) */
|
|
94
93
|
get queuedMessages(): QueuedMessage[];
|
|
95
|
-
/** Get goal status string */
|
|
96
|
-
get goalStatus(): string;
|
|
97
|
-
/** Check if a goal is active */
|
|
98
|
-
get isGoalActive(): boolean;
|
|
99
94
|
/**
|
|
100
95
|
* Remove a queued message by index
|
|
101
96
|
* @param index - The index of the message to remove
|
|
@@ -231,19 +226,6 @@ export declare class Agent {
|
|
|
231
226
|
* @param customInstructions - Optional custom instructions for compaction
|
|
232
227
|
*/
|
|
233
228
|
compact(customInstructions?: string): Promise<void>;
|
|
234
|
-
/**
|
|
235
|
-
* Set an autonomous goal for the session
|
|
236
|
-
* @param condition - The goal condition to achieve
|
|
237
|
-
*/
|
|
238
|
-
setGoal(condition: string): Promise<void>;
|
|
239
|
-
/**
|
|
240
|
-
* Clear the current autonomous goal
|
|
241
|
-
*/
|
|
242
|
-
clearGoal(): Promise<void>;
|
|
243
|
-
/**
|
|
244
|
-
* Show the current goal status
|
|
245
|
-
*/
|
|
246
|
-
showGoalStatus(): Promise<void>;
|
|
247
229
|
/** Unified interrupt method, interrupts both AI messages and command execution */
|
|
248
230
|
abortMessage(): void;
|
|
249
231
|
/** Interrupt bash command execution */
|
|
@@ -275,11 +257,18 @@ export declare class Agent {
|
|
|
275
257
|
*/
|
|
276
258
|
triggerShowRewind(): void;
|
|
277
259
|
/**
|
|
278
|
-
* Ask a side question without
|
|
260
|
+
* Ask a side question without interrupting the main agent.
|
|
261
|
+
*
|
|
262
|
+
* Runs a single-turn fork of the conversation that reuses the main loop's
|
|
263
|
+
* system prompt, tools, and memory injection (prompt-cache friendly) and
|
|
264
|
+
* never executes tools. The abort signal lets the UI cancel a pending
|
|
265
|
+
* question (loading dismiss); aborts propagate as thrown errors.
|
|
266
|
+
*
|
|
279
267
|
* @param question - The side question to ask
|
|
268
|
+
* @param abortSignal - Optional signal to abort the pending request
|
|
280
269
|
* @returns Promise that resolves to the AI's answer
|
|
281
270
|
*/
|
|
282
|
-
askBtw(question: string): Promise<string>;
|
|
271
|
+
askBtw(question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void, onReasoning?: (content: string) => void): Promise<string>;
|
|
283
272
|
/**
|
|
284
273
|
* Send a message to the AI agent with optional images
|
|
285
274
|
*
|
package/dist/agent.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { LspManager } from "./managers/lspManager.js";
|
|
2
2
|
import { configValidator } from "./utils/configValidator.js";
|
|
3
|
-
import { btw } from "./services/aiService.js";
|
|
4
|
-
import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
|
|
5
|
-
import { supportsVision } from "./utils/modelCapabilities.js";
|
|
6
3
|
import { parseTaskNotificationXml } from "./utils/notificationXml.js";
|
|
7
4
|
import { InitializationService } from "./services/initializationService.js";
|
|
8
5
|
import { InteractionService } from "./services/interactionService.js";
|
|
@@ -14,7 +11,10 @@ import { remoteSettingsService } from "./services/remoteSettingsService.js";
|
|
|
14
11
|
export class Agent {
|
|
15
12
|
// Dynamic configuration getter methods
|
|
16
13
|
getGatewayConfig() {
|
|
17
|
-
return
|
|
14
|
+
return {
|
|
15
|
+
...this.configurationService.resolveGatewayConfig(),
|
|
16
|
+
sessionId: this.messageManager.getSessionId(),
|
|
17
|
+
};
|
|
18
18
|
}
|
|
19
19
|
getModelConfig() {
|
|
20
20
|
return this.configurationService.resolveModelConfig(undefined, undefined, undefined, this.getPermissionMode());
|
|
@@ -111,7 +111,6 @@ export class Agent {
|
|
|
111
111
|
this.pluginManager = this.container.get("PluginManager");
|
|
112
112
|
this.bangManager = this.container.get("BangManager");
|
|
113
113
|
this.cronManager = this.container.get("CronManager");
|
|
114
|
-
this.goalManager = this.container.get("GoalManager");
|
|
115
114
|
this.messageQueue = this.container.get("MessageQueue");
|
|
116
115
|
// Wire up CWD change callback from AIManager to sync Agent's workdir
|
|
117
116
|
this.aiManager.setOnCwdChange((newCwd) => {
|
|
@@ -138,14 +137,6 @@ export class Agent {
|
|
|
138
137
|
if (options.permissionMode) {
|
|
139
138
|
this.setPermissionMode(options.permissionMode);
|
|
140
139
|
}
|
|
141
|
-
// Wire up goal state change callback
|
|
142
|
-
this.goalManager.setOnGoalStateChange((active, condition, elapsed) => {
|
|
143
|
-
this.options.callbacks?.onGoalStateChange?.(active, condition, elapsed);
|
|
144
|
-
});
|
|
145
|
-
// Wire up goal evaluating callback
|
|
146
|
-
this.goalManager.setOnGoalEvaluating((evaluating) => {
|
|
147
|
-
this.options.callbacks?.onGoalEvaluating?.(evaluating);
|
|
148
|
-
});
|
|
149
140
|
}
|
|
150
141
|
// Public getter methods
|
|
151
142
|
get sessionId() {
|
|
@@ -216,14 +207,6 @@ export class Agent {
|
|
|
216
207
|
.getQueue()
|
|
217
208
|
.filter((m) => m.type !== "notification");
|
|
218
209
|
}
|
|
219
|
-
/** Get goal status string */
|
|
220
|
-
get goalStatus() {
|
|
221
|
-
return this.goalManager.getStatusString();
|
|
222
|
-
}
|
|
223
|
-
/** Check if a goal is active */
|
|
224
|
-
get isGoalActive() {
|
|
225
|
-
return this.goalManager.isGoalActive();
|
|
226
|
-
}
|
|
227
210
|
/**
|
|
228
211
|
* Remove a queued message by index
|
|
229
212
|
* @param index - The index of the message to remove
|
|
@@ -521,8 +504,6 @@ export class Agent {
|
|
|
521
504
|
}
|
|
522
505
|
async clearMessages() {
|
|
523
506
|
this.aiManager.abortAIMessage();
|
|
524
|
-
// Clear any active goal
|
|
525
|
-
this.goalManager.clearGoal();
|
|
526
507
|
// Capture old session info before clearing
|
|
527
508
|
const oldSessionId = this.messageManager.getSessionId();
|
|
528
509
|
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
@@ -571,68 +552,6 @@ export class Agent {
|
|
|
571
552
|
});
|
|
572
553
|
await this.messageManager.saveSession();
|
|
573
554
|
}
|
|
574
|
-
/**
|
|
575
|
-
* Set an autonomous goal for the session
|
|
576
|
-
* @param condition - The goal condition to achieve
|
|
577
|
-
*/
|
|
578
|
-
async setGoal(condition) {
|
|
579
|
-
// Check plan mode
|
|
580
|
-
if (this.getPermissionMode() === "plan") {
|
|
581
|
-
this.messageManager.addUserMessage({
|
|
582
|
-
content: "<system-reminder>Cannot set a goal in plan mode. Exit plan mode first.</system-reminder>",
|
|
583
|
-
isMeta: true,
|
|
584
|
-
});
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
this.goalManager.setGoal(condition);
|
|
588
|
-
this.messageManager.addUserMessage({
|
|
589
|
-
content: `<system-reminder>Goal set: ${condition}. The agent will work autonomously until this goal is achieved.</system-reminder>`,
|
|
590
|
-
isMeta: true,
|
|
591
|
-
});
|
|
592
|
-
// Add the goal as a user directive to start working
|
|
593
|
-
this.messageManager.addUserMessage({
|
|
594
|
-
content: condition,
|
|
595
|
-
});
|
|
596
|
-
this.aiManager.sendAIMessage();
|
|
597
|
-
await this.messageManager.saveSession();
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Clear the current autonomous goal
|
|
601
|
-
*/
|
|
602
|
-
async clearGoal() {
|
|
603
|
-
if (this.goalManager.isGoalActive()) {
|
|
604
|
-
this.goalManager.clearGoal();
|
|
605
|
-
this.messageManager.addUserMessage({
|
|
606
|
-
content: "<system-reminder>Goal cleared.</system-reminder>",
|
|
607
|
-
isMeta: true,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
this.messageManager.addUserMessage({
|
|
612
|
-
content: "<system-reminder>No active goal to clear.</system-reminder>",
|
|
613
|
-
isMeta: true,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
await this.messageManager.saveSession();
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Show the current goal status
|
|
620
|
-
*/
|
|
621
|
-
async showGoalStatus() {
|
|
622
|
-
if (this.goalManager.isGoalActive()) {
|
|
623
|
-
this.messageManager.addUserMessage({
|
|
624
|
-
content: `<system-reminder>${this.goalManager.getStatusString()}</system-reminder>`,
|
|
625
|
-
isMeta: true,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
else {
|
|
629
|
-
this.messageManager.addUserMessage({
|
|
630
|
-
content: "<system-reminder>No active goal. Use /goal <condition> to set one.</system-reminder>",
|
|
631
|
-
isMeta: true,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
await this.messageManager.saveSession();
|
|
635
|
-
}
|
|
636
555
|
/** Unified interrupt method, interrupts both AI messages and command execution */
|
|
637
556
|
abortMessage() {
|
|
638
557
|
// Guard: prevent tryDispatch (triggered by abortAIMessage → setIsLoading(false))
|
|
@@ -702,7 +621,7 @@ export class Agent {
|
|
|
702
621
|
transcriptPath,
|
|
703
622
|
cwd: this.workdir,
|
|
704
623
|
worktreePath,
|
|
705
|
-
env: Object.fromEntries(Object.entries(
|
|
624
|
+
env: Object.fromEntries(Object.entries(this.configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
|
|
706
625
|
});
|
|
707
626
|
// Process results via messageManager (may not be visible during shutdown)
|
|
708
627
|
this.hookManager.processHookResults("WorktreeRemove", hookResults, this.messageManager);
|
|
@@ -754,6 +673,17 @@ export class Agent {
|
|
|
754
673
|
this.subagentManager.cleanup();
|
|
755
674
|
// Cleanup forked agent manager
|
|
756
675
|
await this.forkedAgentManager.cleanup();
|
|
676
|
+
// Drain an in-flight auto-memory extraction fork so the process doesn't
|
|
677
|
+
// exit mid-extraction
|
|
678
|
+
try {
|
|
679
|
+
const autoMemoryService = this.container.get("AutoMemoryService");
|
|
680
|
+
if (autoMemoryService) {
|
|
681
|
+
await autoMemoryService.drain();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
this.logger?.warn(`Auto-memory extraction drain failed: ${error.message}`);
|
|
686
|
+
}
|
|
757
687
|
// Cleanup skill manager
|
|
758
688
|
await this.skillManager.destroy();
|
|
759
689
|
// Cleanup live configuration reload
|
|
@@ -774,21 +704,20 @@ export class Agent {
|
|
|
774
704
|
this.messageManager.triggerShowRewind();
|
|
775
705
|
}
|
|
776
706
|
/**
|
|
777
|
-
* Ask a side question without
|
|
707
|
+
* Ask a side question without interrupting the main agent.
|
|
708
|
+
*
|
|
709
|
+
* Runs a single-turn fork of the conversation that reuses the main loop's
|
|
710
|
+
* system prompt, tools, and memory injection (prompt-cache friendly) and
|
|
711
|
+
* never executes tools. The abort signal lets the UI cancel a pending
|
|
712
|
+
* question (loading dismiss); aborts propagate as thrown errors.
|
|
713
|
+
*
|
|
778
714
|
* @param question - The side question to ask
|
|
715
|
+
* @param abortSignal - Optional signal to abort the pending request
|
|
779
716
|
* @returns Promise that resolves to the AI's answer
|
|
780
717
|
*/
|
|
781
|
-
async askBtw(question) {
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
});
|
|
785
|
-
const result = await btw({
|
|
786
|
-
gatewayConfig: this.getGatewayConfig(),
|
|
787
|
-
modelConfig: this.getModelConfig(),
|
|
788
|
-
messages,
|
|
789
|
-
question,
|
|
790
|
-
});
|
|
791
|
-
return result.content;
|
|
718
|
+
async askBtw(question, abortSignal, onContent, onReasoning) {
|
|
719
|
+
const result = await this.aiManager.runBtwFork(question, abortSignal, onContent, onReasoning);
|
|
720
|
+
return result.content ?? result.error ?? "No response received";
|
|
792
721
|
}
|
|
793
722
|
/**
|
|
794
723
|
* Send a message to the AI agent with optional images
|
|
@@ -818,7 +747,7 @@ export class Agent {
|
|
|
818
747
|
*/
|
|
819
748
|
async sendMessage(content, images) {
|
|
820
749
|
// If the agent is busy, enqueue the message — unless it's an immediate
|
|
821
|
-
// slash command (e.g., /
|
|
750
|
+
// slash command (e.g., /clear, /compact) that should execute
|
|
822
751
|
// right away even while AI is processing
|
|
823
752
|
if (this.aiManager.isLoading || this.isCommandRunning) {
|
|
824
753
|
const trimmed = content.trim();
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./utils/tokenCalculation.js";
|
|
|
22
22
|
export * from "./utils/gitUtils.js";
|
|
23
23
|
export * from "./utils/nameGenerator.js";
|
|
24
24
|
export * from "./utils/worktreeSession.js";
|
|
25
|
+
export * from "./utils/worktreeUtils.js";
|
|
25
26
|
export { loadMergedWaveConfig } from "./services/configurationService.js";
|
|
26
27
|
export * from "./types/index.js";
|
|
27
28
|
export * from "./tools/buildTool.js";
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export * from "./utils/tokenCalculation.js";
|
|
|
26
26
|
export * from "./utils/gitUtils.js";
|
|
27
27
|
export * from "./utils/nameGenerator.js";
|
|
28
28
|
export * from "./utils/worktreeSession.js";
|
|
29
|
+
export * from "./utils/worktreeUtils.js";
|
|
29
30
|
export { loadMergedWaveConfig } from "./services/configurationService.js";
|
|
30
31
|
export * from "./types/index.js";
|
|
31
32
|
// Export tool building utilities
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import type { GatewayConfig, ModelConfig, Usage } from "../types/index.js";
|
|
1
|
+
import type { GatewayConfig, ModelConfig, Usage, Message } from "../types/index.js";
|
|
2
2
|
import { Container } from "../utils/container.js";
|
|
3
3
|
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
4
|
+
/** Result of a fork-path agent loop (compaction or auto-memory extraction). */
|
|
5
|
+
interface ForkLoopResult {
|
|
6
|
+
content?: string;
|
|
7
|
+
usage?: {
|
|
8
|
+
prompt_tokens: number;
|
|
9
|
+
completion_tokens: number;
|
|
10
|
+
total_tokens: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
4
13
|
export interface AIManagerCallbacks {
|
|
5
14
|
onCompactionStateChange?: (isCompacting: boolean) => void;
|
|
6
15
|
onUsageAdded?: (usage: Usage) => void;
|
|
@@ -53,6 +62,14 @@ export declare class AIManager {
|
|
|
53
62
|
private get permissionManager();
|
|
54
63
|
private get planManager();
|
|
55
64
|
private get configurationService();
|
|
65
|
+
/**
|
|
66
|
+
* OS env merged with the per-session env snapshot. Falls back to process.env
|
|
67
|
+
* when ConfigurationService is absent or its getMergedEnv is missing (e.g. in
|
|
68
|
+
* unit tests with partial mocks), so hook-context env construction never
|
|
69
|
+
* throws. Use this (not the non-null `configurationService` getter) when
|
|
70
|
+
* building hook context env.
|
|
71
|
+
*/
|
|
72
|
+
private get mergedEnv();
|
|
56
73
|
getGatewayConfig(): GatewayConfig;
|
|
57
74
|
getModelConfig(): ModelConfig;
|
|
58
75
|
getMaxInputTokens(): number;
|
|
@@ -111,15 +128,60 @@ export declare class AIManager {
|
|
|
111
128
|
private buildMainSystemPrompt;
|
|
112
129
|
private resolveFilteredTools;
|
|
113
130
|
/**
|
|
114
|
-
* Fork-path
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* another turn. Returns undefined content when the model never
|
|
120
|
-
* text; the caller treats that as a
|
|
131
|
+
* Fork-path loop: run a bounded agent loop over a copy of the conversation
|
|
132
|
+
* using the same system prompt, tools, model, and generation params as the
|
|
133
|
+
* main loop, so the forked request prefix matches exactly and the prompt
|
|
134
|
+
* cache is reused. A `canUseTool` gate decides whether each tool call
|
|
135
|
+
* executes locally (with a stripped context) or is denied and fed back to
|
|
136
|
+
* the model for another turn. Returns undefined content when the model never
|
|
137
|
+
* produces text; the caller treats that as a failure.
|
|
138
|
+
*/
|
|
139
|
+
private runForkLoop;
|
|
140
|
+
/**
|
|
141
|
+
* Fork-path compaction: deny all tool calls locally (the model is told to
|
|
142
|
+
* summarize, not act) and feed the rejections back for another turn.
|
|
121
143
|
*/
|
|
122
144
|
private runCompactFork;
|
|
145
|
+
/**
|
|
146
|
+
* Auto-memory extraction via the perfect fork: the extraction prompt is run
|
|
147
|
+
* against the same request prefix as the main conversation (same system
|
|
148
|
+
* prompt, tools, model, and message history) so the prompt cache is reused.
|
|
149
|
+
* Gate-approved tools execute locally in a stripped context; everything else
|
|
150
|
+
* is denied. Usage is reported with operation_type "agent" so extraction
|
|
151
|
+
* token costs stay visible in session accounting.
|
|
152
|
+
*/
|
|
153
|
+
runAutoMemoryFork(messages: Message[], prompt: string, options: {
|
|
154
|
+
canUseTool: (name: string, args: Record<string, unknown>) => boolean;
|
|
155
|
+
deniedToolMessage?: string;
|
|
156
|
+
maxTurns?: number;
|
|
157
|
+
}, abortSignal?: AbortSignal): Promise<ForkLoopResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Parse a fork tool call's JSON arguments, recovering truncated JSON the
|
|
160
|
+
* same way the main loop does. Unparseable arguments fall back to `{}` and
|
|
161
|
+
* are rejected by the gate or the tool's own parameter validation.
|
|
162
|
+
*/
|
|
163
|
+
private parseForkToolArgs;
|
|
164
|
+
/**
|
|
165
|
+
* Execute a single tool call inside a fork with a stripped context: no
|
|
166
|
+
* permission manager (never prompts the user), no message manager (no
|
|
167
|
+
* conditional-rule triggering), no messageId (no file-history snapshots),
|
|
168
|
+
* and no background task manager (commands run in the foreground). Only
|
|
169
|
+
* gate-approved tool names reach this path.
|
|
170
|
+
*/
|
|
171
|
+
private executeForkTool;
|
|
172
|
+
/**
|
|
173
|
+
* Fork-path side question ("/btw"): run a single-turn fork of the
|
|
174
|
+
* conversation using the same system prompt, tools, model, and generation
|
|
175
|
+
* params as the main loop, so the forked request prefix matches exactly and
|
|
176
|
+
* the prompt cache is reused. The in-progress assistant message (if any) is
|
|
177
|
+
* stripped so the fork starts from the last completed request prefix. Tools
|
|
178
|
+
* are never executed — the wrapped question instructs the model to answer
|
|
179
|
+
* directly; an attempted tool call is surfaced as an error string.
|
|
180
|
+
*/
|
|
181
|
+
runBtwFork(question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void, onReasoning?: (content: string) => void): Promise<{
|
|
182
|
+
content?: string;
|
|
183
|
+
error?: string;
|
|
184
|
+
}>;
|
|
123
185
|
/**
|
|
124
186
|
* Build post-compact context restoration content.
|
|
125
187
|
* Restores file reads, working directory, plan mode, skills, and background tasks.
|
|
@@ -158,3 +220,4 @@ export declare class AIManager {
|
|
|
158
220
|
*/
|
|
159
221
|
private executePostToolUseHooks;
|
|
160
222
|
}
|
|
223
|
+
export {};
|