wave-agent-sdk 1.0.0 → 1.0.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 +9 -20
- package/dist/agent.js +35 -97
- package/dist/managers/aiManager.d.ts +63 -8
- package/dist/managers/aiManager.js +274 -80
- package/dist/managers/messageManager.d.ts +9 -5
- package/dist/managers/messageManager.js +36 -12
- package/dist/managers/permissionManager.js +13 -11
- package/dist/managers/subagentManager.d.ts +6 -0
- package/dist/managers/subagentManager.js +33 -22
- package/dist/prompts/index.d.ts +0 -2
- package/dist/prompts/index.js +71 -47
- 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.js +14 -1
- package/dist/services/session.d.ts +13 -0
- package/dist/services/session.js +64 -0
- package/dist/types/agent.d.ts +0 -2
- package/dist/types/config.d.ts +7 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/utils/containerSetup.js +12 -3
- package/package.json +1 -1
- package/src/agent.ts +50 -110
- package/src/managers/aiManager.ts +370 -105
- package/src/managers/messageManager.ts +51 -23
- package/src/managers/permissionManager.ts +15 -13
- package/src/managers/subagentManager.ts +36 -25
- package/src/prompts/index.ts +75 -56
- package/src/services/aiService.ts +25 -203
- package/src/services/autoMemoryService.ts +145 -39
- package/src/services/configurationService.ts +16 -1
- package/src/services/session.ts +68 -0
- package/src/types/agent.ts +0 -6
- package/src/types/config.ts +7 -0
- package/src/types/core.ts +1 -1
- package/src/utils/containerSetup.ts +12 -4
- 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
|
@@ -3,6 +3,14 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import { logger } from "../utils/globalLogger.js";
|
|
4
4
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
5
5
|
import { buildAutoMemoryExtractionPrompt } from "../prompts/autoMemoryExtraction.js";
|
|
6
|
+
import { READ_ONLY_COMMANDS, splitBashCommand, hasWriteRedirections, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, isDangerousFind, } from "../utils/bashParser.js";
|
|
7
|
+
/**
|
|
8
|
+
* Message fed back to the extraction fork when the model requests a tool
|
|
9
|
+
* outside the allowed set (Bash rm, MCP tools, Agent, out-of-dir writes...).
|
|
10
|
+
*/
|
|
11
|
+
const DENIED_TOOL_MESSAGE = "This tool call was denied during auto-memory extraction. Available tools: " +
|
|
12
|
+
"Read, Grep, Glob, read-only Bash commands, and Write/Edit inside the " +
|
|
13
|
+
"memory directory only.";
|
|
6
14
|
/**
|
|
7
15
|
* Service responsible for managing the auto-memory background agent lifecycle.
|
|
8
16
|
* Extracts and updates persistent project-level memory from conversation history.
|
|
@@ -12,12 +20,14 @@ export class AutoMemoryService {
|
|
|
12
20
|
this.container = container;
|
|
13
21
|
this.lastMemoryMessageId = null;
|
|
14
22
|
this.turnsSinceLastExtraction = 0;
|
|
23
|
+
this.extractionInProgress = false;
|
|
24
|
+
this.pendingExtraction = null;
|
|
15
25
|
}
|
|
16
26
|
get messageManager() {
|
|
17
27
|
return this.container.get("MessageManager");
|
|
18
28
|
}
|
|
19
|
-
get
|
|
20
|
-
return this.container.get("
|
|
29
|
+
get aiManager() {
|
|
30
|
+
return this.container.get("AIManager");
|
|
21
31
|
}
|
|
22
32
|
get memoryService() {
|
|
23
33
|
return this.container.get("MemoryService");
|
|
@@ -54,7 +64,12 @@ export class AutoMemoryService {
|
|
|
54
64
|
const recentMessages = messages.slice(startIndex);
|
|
55
65
|
const hasManualMemoryWrite = recentMessages.some((m) => m.role === "assistant" &&
|
|
56
66
|
m.blocks.some((b) => {
|
|
57
|
-
if (b.type === "tool" &&
|
|
67
|
+
if (b.type === "tool" &&
|
|
68
|
+
(b.name === "Write" || b.name === "Edit") &&
|
|
69
|
+
// Only a successful manual write counts as a manual update. A
|
|
70
|
+
// denied/failed write didn't touch memory, so the extraction fork
|
|
71
|
+
// must still run or the information is lost.
|
|
72
|
+
b.success !== false) {
|
|
58
73
|
try {
|
|
59
74
|
const params = b.parameters ? JSON.parse(b.parameters) : null;
|
|
60
75
|
const filePath = params?.file_path || params?.path;
|
|
@@ -77,20 +92,51 @@ export class AutoMemoryService {
|
|
|
77
92
|
this.turnsSinceLastExtraction = 0;
|
|
78
93
|
return;
|
|
79
94
|
}
|
|
80
|
-
// 3.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
catch (error) {
|
|
87
|
-
logger.error("Auto-memory extraction failed to trigger:", error);
|
|
95
|
+
// 3. Concurrency guard: if an extraction is already in flight, skip this
|
|
96
|
+
// turn. turnsSinceLastExtraction is intentionally NOT reset so the next
|
|
97
|
+
// eligible turn retriggers the extraction.
|
|
98
|
+
if (this.extractionInProgress) {
|
|
99
|
+
logger.debug("Skipping auto-memory extraction: another extraction is still in progress.");
|
|
100
|
+
return;
|
|
88
101
|
}
|
|
102
|
+
// 4. Trigger the perfect-fork extraction fire-and-forget. The message
|
|
103
|
+
// snapshot and new-message count are computed now; lastMemoryMessageId
|
|
104
|
+
// advances at trigger time so the next extraction starts from a later
|
|
105
|
+
// window even while this one runs.
|
|
106
|
+
const lastExtractedIndex = this.lastMemoryMessageId
|
|
107
|
+
? messages.findIndex((m) => m.id === this.lastMemoryMessageId)
|
|
108
|
+
: -1;
|
|
109
|
+
const newMessageCount = lastExtractedIndex === -1
|
|
110
|
+
? messages.length
|
|
111
|
+
: messages.length - 1 - lastExtractedIndex;
|
|
112
|
+
this.turnsSinceLastExtraction = 0;
|
|
113
|
+
this.lastMemoryMessageId = messages[messages.length - 1].id || null;
|
|
114
|
+
this.extractionInProgress = true;
|
|
115
|
+
const extraction = this.runExtraction(workdir, messages, newMessageCount)
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
logger.error("Auto-memory extraction failed:", error);
|
|
118
|
+
})
|
|
119
|
+
.finally(() => {
|
|
120
|
+
this.extractionInProgress = false;
|
|
121
|
+
this.pendingExtraction = null;
|
|
122
|
+
});
|
|
123
|
+
this.pendingExtraction = extraction;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Wait for an in-flight extraction to settle. Called from Agent.dispose so
|
|
127
|
+
* the process doesn't exit while the extraction fork is mid-flight.
|
|
128
|
+
*/
|
|
129
|
+
async drain() {
|
|
130
|
+
await this.pendingExtraction;
|
|
89
131
|
}
|
|
90
132
|
/**
|
|
91
|
-
* Initialize and execute the
|
|
133
|
+
* Initialize and execute the extraction in a perfect fork: same system
|
|
134
|
+
* prompt, tools, model, and message prefix as the main conversation, so the
|
|
135
|
+
* prompt cache is reused. A tool gate confines the fork to read-only
|
|
136
|
+
* inspection and memory-directory writes. Runs in-process; callers treat it
|
|
137
|
+
* as fire-and-forget.
|
|
92
138
|
*/
|
|
93
|
-
async runExtraction(workdir, messages) {
|
|
139
|
+
async runExtraction(workdir, messages, newMessageCount) {
|
|
94
140
|
const memoryDir = this.memoryService.getAutoMemoryDirectory(workdir);
|
|
95
141
|
// Ensure memory directory exists before starting
|
|
96
142
|
await this.memoryService.ensureAutoMemoryDirectory(workdir);
|
|
@@ -106,30 +152,72 @@ export class AutoMemoryService {
|
|
|
106
152
|
catch {
|
|
107
153
|
// Ignore if directory doesn't exist yet
|
|
108
154
|
}
|
|
109
|
-
// Calculate how many new messages to analyze
|
|
110
|
-
let newMessageCount = messages.length;
|
|
111
|
-
if (this.lastMemoryMessageId) {
|
|
112
|
-
const lastIndex = messages.findIndex((m) => m.id === this.lastMemoryMessageId);
|
|
113
|
-
if (lastIndex !== -1) {
|
|
114
|
-
newMessageCount = messages.length - 1 - lastIndex;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
155
|
const prompt = buildAutoMemoryExtractionPrompt(newMessageCount, existingMemoriesManifest);
|
|
118
|
-
|
|
119
|
-
await this.forkedAgentManager.forkAndExecute("general-purpose", messages, {
|
|
120
|
-
description: "Auto-memory extraction background agent",
|
|
121
|
-
allowedTools: [
|
|
122
|
-
"Read",
|
|
123
|
-
"Glob",
|
|
124
|
-
"Grep",
|
|
125
|
-
`Write(${memoryDir}/**/*)`,
|
|
126
|
-
`Edit(${memoryDir}/**/*)`,
|
|
127
|
-
`Bash(rm ${memoryDir}/**/*)`,
|
|
128
|
-
],
|
|
129
|
-
model: "fastModel", // Use fast model for background tasks to reduce latency and cost
|
|
130
|
-
permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
|
|
156
|
+
await this.aiManager.runAutoMemoryFork(messages, `${prompt}\n\nThe memory directory for this project is: ${memoryDir}`, {
|
|
131
157
|
maxTurns: 5, // Limit turns to prevent verification rabbit-holes
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
canUseTool: (name, args) => this.isAllowedForkTool(name, args, memoryDir, workdir),
|
|
159
|
+
deniedToolMessage: DENIED_TOOL_MESSAGE,
|
|
160
|
+
});
|
|
161
|
+
logger.debug("Auto-memory extraction completed.");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Tool gate for the extraction fork: Read/Grep/Glob are always allowed;
|
|
165
|
+
* Write/Edit only when the target path is inside the memory directory; Bash
|
|
166
|
+
* only for read-only commands (aligned with the permission manager's
|
|
167
|
+
* read-only bash classification). Everything else — Bash rm, MCP tools,
|
|
168
|
+
* Agent, out-of-dir writes — is denied.
|
|
169
|
+
*/
|
|
170
|
+
isAllowedForkTool(name, args, memoryDir, workdir) {
|
|
171
|
+
if (name === "Read" || name === "Grep" || name === "Glob") {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (name === "Write" || name === "Edit") {
|
|
175
|
+
const filePath = args.file_path ?? args.path;
|
|
176
|
+
if (typeof filePath !== "string" || !filePath)
|
|
177
|
+
return false;
|
|
178
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
179
|
+
? filePath
|
|
180
|
+
: path.resolve(workdir, filePath);
|
|
181
|
+
return isPathInside(absolutePath, memoryDir);
|
|
182
|
+
}
|
|
183
|
+
if (name === "Bash") {
|
|
184
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
185
|
+
return this.isReadOnlyBashCommand(command);
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A bash command is read-only when every part is a READ_ONLY_COMMANDS entry
|
|
191
|
+
* without write redirections, command/process substitution, sed -i, or
|
|
192
|
+
* dangerous find flags. Mirrors PermissionManager.isAutoAllowedPart.
|
|
193
|
+
*/
|
|
194
|
+
isReadOnlyBashCommand(command) {
|
|
195
|
+
if (!command.trim())
|
|
196
|
+
return false;
|
|
197
|
+
if (hasWriteRedirections(command))
|
|
198
|
+
return false;
|
|
199
|
+
if (hasCommandSubstitution(command))
|
|
200
|
+
return false;
|
|
201
|
+
if (hasProcessSubstitution(command))
|
|
202
|
+
return false;
|
|
203
|
+
if (hasSedInPlace(command))
|
|
204
|
+
return false;
|
|
205
|
+
const parts = splitBashCommand(command);
|
|
206
|
+
if (parts.length === 0)
|
|
207
|
+
return false;
|
|
208
|
+
return parts.every((part) => {
|
|
209
|
+
const trimmed = part.trim();
|
|
210
|
+
if (!trimmed)
|
|
211
|
+
return true;
|
|
212
|
+
const commandMatch = trimmed.match(/^(\w+)(\s+.*)?$/);
|
|
213
|
+
if (!commandMatch)
|
|
214
|
+
return false;
|
|
215
|
+
const cmd = commandMatch[1];
|
|
216
|
+
if (!READ_ONLY_COMMANDS.includes(cmd))
|
|
217
|
+
return false;
|
|
218
|
+
if (cmd === "find" && isDangerousFind(part))
|
|
219
|
+
return false;
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
134
222
|
}
|
|
135
223
|
}
|
|
@@ -488,14 +488,27 @@ export class ConfigurationService {
|
|
|
488
488
|
if (fastModelSource && fastModelSource.options) {
|
|
489
489
|
baseConfig.fastModelOptions = fastModelSource.options;
|
|
490
490
|
}
|
|
491
|
+
// Resolve fast-model disable-thinking params from models[fastModel].disableThinkingOptions
|
|
492
|
+
const fastModelDisableThinking = fastModelSource && fastModelSource.disableThinkingOptions
|
|
493
|
+
? fastModelSource.disableThinkingOptions
|
|
494
|
+
: undefined;
|
|
495
|
+
if (fastModelDisableThinking) {
|
|
496
|
+
baseConfig.disableThinkingOptions = fastModelDisableThinking;
|
|
497
|
+
}
|
|
491
498
|
// Merge model-specific settings from configuration
|
|
492
499
|
const modelSpecificConfig = resolvedAgentModel &&
|
|
493
500
|
this.currentConfiguration?.models?.[resolvedAgentModel];
|
|
494
501
|
if (modelSpecificConfig) {
|
|
495
|
-
|
|
502
|
+
const resolved = {
|
|
496
503
|
...baseConfig,
|
|
497
504
|
...modelSpecificConfig,
|
|
498
505
|
};
|
|
506
|
+
// Re-apply after the spread so the agent model's own
|
|
507
|
+
// disableThinkingOptions cannot clobber the fast-model value.
|
|
508
|
+
if (fastModelDisableThinking) {
|
|
509
|
+
resolved.disableThinkingOptions = fastModelDisableThinking;
|
|
510
|
+
}
|
|
511
|
+
return resolved;
|
|
499
512
|
}
|
|
500
513
|
return baseConfig;
|
|
501
514
|
}
|
|
@@ -139,6 +139,19 @@ export declare function cleanupExpiredSessionsFromJsonl(workdir: string): Promis
|
|
|
139
139
|
* Clean up empty project directories in the session directory
|
|
140
140
|
*/
|
|
141
141
|
export declare function cleanupEmptyProjectDirectories(): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Clean up "ghost" session files that contain only meta messages
|
|
144
|
+
* (isMeta: true) — e.g. sessions where a SessionStart hook injected a
|
|
145
|
+
* system-reminder but no real user/assistant message was ever sent.
|
|
146
|
+
*
|
|
147
|
+
* Such sessions are no longer created thanks to lazy materialization in
|
|
148
|
+
* saveSession(); this one-time sweep removes files that predate that change
|
|
149
|
+
* so they stop showing up as "0 tokens / No content" entries in the resume
|
|
150
|
+
* list.
|
|
151
|
+
*
|
|
152
|
+
* @returns Promise that resolves to the number of files deleted
|
|
153
|
+
*/
|
|
154
|
+
export declare function cleanupMetaOnlySessions(): Promise<number>;
|
|
142
155
|
/**
|
|
143
156
|
* Check if a session exists in JSONL storage (new approach)
|
|
144
157
|
*
|
package/dist/services/session.js
CHANGED
|
@@ -481,6 +481,70 @@ export async function cleanupEmptyProjectDirectories() {
|
|
|
481
481
|
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Clean up "ghost" session files that contain only meta messages
|
|
486
|
+
* (isMeta: true) — e.g. sessions where a SessionStart hook injected a
|
|
487
|
+
* system-reminder but no real user/assistant message was ever sent.
|
|
488
|
+
*
|
|
489
|
+
* Such sessions are no longer created thanks to lazy materialization in
|
|
490
|
+
* saveSession(); this one-time sweep removes files that predate that change
|
|
491
|
+
* so they stop showing up as "0 tokens / No content" entries in the resume
|
|
492
|
+
* list.
|
|
493
|
+
*
|
|
494
|
+
* @returns Promise that resolves to the number of files deleted
|
|
495
|
+
*/
|
|
496
|
+
export async function cleanupMetaOnlySessions() {
|
|
497
|
+
// Do not perform cleanup operations in test environment
|
|
498
|
+
if (process.env.NODE_ENV === "test") {
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
let deletedCount = 0;
|
|
502
|
+
try {
|
|
503
|
+
const projectDirs = await fs.readdir(SESSION_DIR);
|
|
504
|
+
for (const projectDirName of projectDirs) {
|
|
505
|
+
const projectPath = join(SESSION_DIR, projectDirName);
|
|
506
|
+
try {
|
|
507
|
+
const stat = await fs.stat(projectPath);
|
|
508
|
+
if (!stat.isDirectory()) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const files = await fs.readdir(projectPath);
|
|
512
|
+
for (const file of files) {
|
|
513
|
+
if (!file.endsWith(".jsonl")) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const filePath = join(projectPath, file);
|
|
517
|
+
try {
|
|
518
|
+
// Fast path: a file whose last message is not meta contains a
|
|
519
|
+
// real message, so it can never be meta-only.
|
|
520
|
+
const jsonlHandler = new JsonlHandler();
|
|
521
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
522
|
+
if (!lastMessage?.isMeta) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const messages = await jsonlHandler.read(filePath);
|
|
526
|
+
if (messages.length > 0 && messages.every((m) => m.isMeta)) {
|
|
527
|
+
await fs.unlink(filePath);
|
|
528
|
+
deletedCount++;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Skip corrupted or unreadable files
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// Skip directories we can't access
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
545
|
+
}
|
|
546
|
+
return deletedCount;
|
|
547
|
+
}
|
|
484
548
|
/**
|
|
485
549
|
* Check if a session exists in JSONL storage (new approach)
|
|
486
550
|
*
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -98,6 +98,4 @@ export interface AgentCallbacks extends MessageManagerCallbacks, BackgroundTaskM
|
|
|
98
98
|
onCommandRunningChange?: (running: boolean) => void;
|
|
99
99
|
onWorkdirChange?: (newCwd: string) => void;
|
|
100
100
|
onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
|
|
101
|
-
onGoalStateChange?: (active: boolean, condition?: string, elapsed?: string) => void;
|
|
102
|
-
onGoalEvaluating?: (evaluating: boolean) => void;
|
|
103
101
|
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -29,4 +29,11 @@ export interface ModelConfig {
|
|
|
29
29
|
options?: Record<string, unknown>;
|
|
30
30
|
/** Fast model generation params (resolved from models[fastModel].options) */
|
|
31
31
|
fastModelOptions?: Record<string, unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Fast-model-only disable-thinking params passed through verbatim
|
|
34
|
+
* (e.g. `{ thinking: { type: "disabled" } }`). Applied only in fast-model
|
|
35
|
+
* scenarios (webFetch content processing, `model: fastModel` subagents),
|
|
36
|
+
* never in the agent loop. `{}` clears the default.
|
|
37
|
+
*/
|
|
38
|
+
disableThinkingOptions?: Record<string, unknown>;
|
|
32
39
|
}
|
package/dist/types/core.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface Usage {
|
|
|
22
22
|
completion_tokens: number;
|
|
23
23
|
total_tokens: number;
|
|
24
24
|
model?: string;
|
|
25
|
-
operation_type?: "agent" | "compact"
|
|
25
|
+
operation_type?: "agent" | "compact";
|
|
26
26
|
cache_read_input_tokens?: number;
|
|
27
27
|
cache_creation_input_tokens?: number;
|
|
28
28
|
cache_creation?: {
|
|
@@ -16,7 +16,6 @@ import { SlashCommandManager } from "../managers/slashCommandManager.js";
|
|
|
16
16
|
import { PluginManager } from "../managers/pluginManager.js";
|
|
17
17
|
import { BangManager } from "../managers/bangManager.js";
|
|
18
18
|
import { CronManager } from "../managers/cronManager.js";
|
|
19
|
-
import { GoalManager } from "../managers/goalManager.js";
|
|
20
19
|
import { WorkflowManager } from "../managers/workflowManager.js";
|
|
21
20
|
import { MemoryRuleManager } from "../managers/MemoryRuleManager.js";
|
|
22
21
|
import { ReversionManager } from "../managers/reversionManager.js";
|
|
@@ -24,6 +23,7 @@ import { SubagentManager } from "../managers/subagentManager.js";
|
|
|
24
23
|
import { ForkedAgentManager } from "../managers/forkedAgentManager.js";
|
|
25
24
|
import { LiveConfigManager } from "../managers/liveConfigManager.js";
|
|
26
25
|
import { ReversionService } from "../services/reversionService.js";
|
|
26
|
+
import { cleanupMetaOnlySessions } from "../services/session.js";
|
|
27
27
|
import { MemoryService } from "../services/memory.js";
|
|
28
28
|
import { AutoMemoryService } from "../services/autoMemoryService.js";
|
|
29
29
|
import { USER_MEMORY_FILE } from "./constants.js";
|
|
@@ -144,6 +144,17 @@ export function setupAgentContainer(setupOptions) {
|
|
|
144
144
|
reversionService.cleanupOldSessions(30).catch((error) => {
|
|
145
145
|
logger.error("Failed to cleanup old file history:", error);
|
|
146
146
|
});
|
|
147
|
+
// Remove pre-existing meta-only session files (SessionStart hook context
|
|
148
|
+
// with no real messages) that were persisted before lazy materialization.
|
|
149
|
+
cleanupMetaOnlySessions()
|
|
150
|
+
.then((count) => {
|
|
151
|
+
if (count > 0) {
|
|
152
|
+
logger.debug(`Removed ${count} meta-only session file(s)`);
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.catch((error) => {
|
|
156
|
+
logger.error("Failed to cleanup meta-only session files:", error);
|
|
157
|
+
});
|
|
147
158
|
const reversionManager = new ReversionManager(container);
|
|
148
159
|
container.register("ReversionManager", reversionManager);
|
|
149
160
|
const canUseToolWithPermissionRequest = options.canUseTool
|
|
@@ -255,8 +266,6 @@ export function setupAgentContainer(setupOptions) {
|
|
|
255
266
|
const cronManager = new CronManager(container, messageManager.getSessionId());
|
|
256
267
|
container.register("CronManager", cronManager);
|
|
257
268
|
cronManager.start();
|
|
258
|
-
const goalManager = new GoalManager(container);
|
|
259
|
-
container.register("GoalManager", goalManager);
|
|
260
269
|
const workflowManager = new WorkflowManager(container);
|
|
261
270
|
container.register("WorkflowManager", workflowManager);
|
|
262
271
|
return container;
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { McpManager } from "./managers/mcpManager.js";
|
|
|
8
8
|
import { LspManager } from "./managers/lspManager.js";
|
|
9
9
|
import { BangManager } from "./managers/bangManager.js";
|
|
10
10
|
import { CronManager } from "./managers/cronManager.js";
|
|
11
|
-
import { GoalManager } from "./managers/goalManager.js";
|
|
12
11
|
import { BackgroundTaskManager } from "./managers/backgroundTaskManager.js";
|
|
13
12
|
import { MessageQueue, type QueuedMessage } from "./managers/messageQueue.js";
|
|
14
13
|
import { SlashCommandManager } from "./managers/slashCommandManager.js";
|
|
@@ -38,9 +37,6 @@ import { LiveConfigManager } from "./managers/liveConfigManager.js";
|
|
|
38
37
|
import { configValidator } from "./utils/configValidator.js";
|
|
39
38
|
import { SkillManager } from "./managers/skillManager.js";
|
|
40
39
|
import { TaskManager } from "./services/taskManager.js";
|
|
41
|
-
import { btw } from "./services/aiService.js";
|
|
42
|
-
import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
|
|
43
|
-
import { supportsVision } from "./utils/modelCapabilities.js";
|
|
44
40
|
import type { WorktreeSession } from "./utils/worktreeSession.js";
|
|
45
41
|
import { parseTaskNotificationXml } from "./utils/notificationXml.js";
|
|
46
42
|
import { InitializationService } from "./services/initializationService.js";
|
|
@@ -73,7 +69,6 @@ export class Agent {
|
|
|
73
69
|
private pluginManager: PluginManager; // Add plugin manager instance
|
|
74
70
|
private skillManager: SkillManager; // Add skill manager instance
|
|
75
71
|
private cronManager: CronManager; // Add cron manager instance
|
|
76
|
-
private goalManager: GoalManager; // Add goal manager instance
|
|
77
72
|
private hookManager: HookManager; // Add hooks manager instance
|
|
78
73
|
private reversionManager: ReversionManager;
|
|
79
74
|
private messageQueue: MessageQueue; // Unified queue for messages, bang commands, and notifications
|
|
@@ -208,7 +203,6 @@ export class Agent {
|
|
|
208
203
|
this.pluginManager = this.container.get("PluginManager")!;
|
|
209
204
|
this.bangManager = this.container.get("BangManager")!;
|
|
210
205
|
this.cronManager = this.container.get("CronManager")!;
|
|
211
|
-
this.goalManager = this.container.get("GoalManager")!;
|
|
212
206
|
this.messageQueue = this.container.get("MessageQueue")!;
|
|
213
207
|
|
|
214
208
|
// Wire up CWD change callback from AIManager to sync Agent's workdir
|
|
@@ -238,16 +232,6 @@ export class Agent {
|
|
|
238
232
|
if (options.permissionMode) {
|
|
239
233
|
this.setPermissionMode(options.permissionMode);
|
|
240
234
|
}
|
|
241
|
-
|
|
242
|
-
// Wire up goal state change callback
|
|
243
|
-
this.goalManager.setOnGoalStateChange((active, condition, elapsed) => {
|
|
244
|
-
this.options.callbacks?.onGoalStateChange?.(active, condition, elapsed);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Wire up goal evaluating callback
|
|
248
|
-
this.goalManager.setOnGoalEvaluating((evaluating) => {
|
|
249
|
-
this.options.callbacks?.onGoalEvaluating?.(evaluating);
|
|
250
|
-
});
|
|
251
235
|
}
|
|
252
236
|
|
|
253
237
|
// Public getter methods
|
|
@@ -340,16 +324,6 @@ export class Agent {
|
|
|
340
324
|
.filter((m) => m.type !== "notification");
|
|
341
325
|
}
|
|
342
326
|
|
|
343
|
-
/** Get goal status string */
|
|
344
|
-
public get goalStatus(): string {
|
|
345
|
-
return this.goalManager.getStatusString();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** Check if a goal is active */
|
|
349
|
-
public get isGoalActive(): boolean {
|
|
350
|
-
return this.goalManager.isGoalActive();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
327
|
/**
|
|
354
328
|
* Remove a queued message by index
|
|
355
329
|
* @param index - The index of the message to remove
|
|
@@ -707,10 +681,15 @@ export class Agent {
|
|
|
707
681
|
}
|
|
708
682
|
|
|
709
683
|
public async clearMessages(): Promise<void> {
|
|
710
|
-
|
|
684
|
+
// Aligned with webview (ChatApp handleClearChat): /clear is ignored
|
|
685
|
+
// while the agent is running, instead of aborting the AI mid-turn
|
|
686
|
+
// (which used to inject a late "Request was aborted" error into the
|
|
687
|
+
// newly cleared session).
|
|
688
|
+
if (this.aiManager.isLoading) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
711
691
|
|
|
712
|
-
|
|
713
|
-
this.goalManager.clearGoal();
|
|
692
|
+
this.aiManager.abortAIMessage();
|
|
714
693
|
|
|
715
694
|
// Capture old session info before clearing
|
|
716
695
|
const oldSessionId = this.messageManager.getSessionId();
|
|
@@ -772,6 +751,12 @@ export class Agent {
|
|
|
772
751
|
* @param customInstructions - Optional custom instructions for compaction
|
|
773
752
|
*/
|
|
774
753
|
public async compact(customInstructions?: string): Promise<void> {
|
|
754
|
+
// Aligned with webview: /compact is ignored while the agent is running,
|
|
755
|
+
// instead of aborting the AI mid-turn before compacting.
|
|
756
|
+
if (this.aiManager.isLoading) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
775
760
|
this.aiManager.abortAIMessage();
|
|
776
761
|
|
|
777
762
|
await this.aiManager.compactConversation({
|
|
@@ -781,75 +766,6 @@ export class Agent {
|
|
|
781
766
|
await this.messageManager.saveSession();
|
|
782
767
|
}
|
|
783
768
|
|
|
784
|
-
/**
|
|
785
|
-
* Set an autonomous goal for the session
|
|
786
|
-
* @param condition - The goal condition to achieve
|
|
787
|
-
*/
|
|
788
|
-
public async setGoal(condition: string): Promise<void> {
|
|
789
|
-
// Check plan mode
|
|
790
|
-
if (this.getPermissionMode() === "plan") {
|
|
791
|
-
this.messageManager.addUserMessage({
|
|
792
|
-
content:
|
|
793
|
-
"<system-reminder>Cannot set a goal in plan mode. Exit plan mode first.</system-reminder>",
|
|
794
|
-
isMeta: true,
|
|
795
|
-
});
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
this.goalManager.setGoal(condition);
|
|
800
|
-
this.messageManager.addUserMessage({
|
|
801
|
-
content: `<system-reminder>Goal set: ${condition}. The agent will work autonomously until this goal is achieved.</system-reminder>`,
|
|
802
|
-
isMeta: true,
|
|
803
|
-
});
|
|
804
|
-
// Add the goal as a user directive to start working
|
|
805
|
-
this.messageManager.addUserMessage({
|
|
806
|
-
content: condition,
|
|
807
|
-
});
|
|
808
|
-
this.aiManager.sendAIMessage();
|
|
809
|
-
|
|
810
|
-
await this.messageManager.saveSession();
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Clear the current autonomous goal
|
|
815
|
-
*/
|
|
816
|
-
public async clearGoal(): Promise<void> {
|
|
817
|
-
if (this.goalManager.isGoalActive()) {
|
|
818
|
-
this.goalManager.clearGoal();
|
|
819
|
-
this.messageManager.addUserMessage({
|
|
820
|
-
content: "<system-reminder>Goal cleared.</system-reminder>",
|
|
821
|
-
isMeta: true,
|
|
822
|
-
});
|
|
823
|
-
} else {
|
|
824
|
-
this.messageManager.addUserMessage({
|
|
825
|
-
content: "<system-reminder>No active goal to clear.</system-reminder>",
|
|
826
|
-
isMeta: true,
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
await this.messageManager.saveSession();
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Show the current goal status
|
|
835
|
-
*/
|
|
836
|
-
public async showGoalStatus(): Promise<void> {
|
|
837
|
-
if (this.goalManager.isGoalActive()) {
|
|
838
|
-
this.messageManager.addUserMessage({
|
|
839
|
-
content: `<system-reminder>${this.goalManager.getStatusString()}</system-reminder>`,
|
|
840
|
-
isMeta: true,
|
|
841
|
-
});
|
|
842
|
-
} else {
|
|
843
|
-
this.messageManager.addUserMessage({
|
|
844
|
-
content:
|
|
845
|
-
"<system-reminder>No active goal. Use /goal <condition> to set one.</system-reminder>",
|
|
846
|
-
isMeta: true,
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
await this.messageManager.saveSession();
|
|
851
|
-
}
|
|
852
|
-
|
|
853
769
|
/** Unified interrupt method, interrupts both AI messages and command execution */
|
|
854
770
|
public abortMessage(): void {
|
|
855
771
|
// Guard: prevent tryDispatch (triggered by abortAIMessage → setIsLoading(false))
|
|
@@ -994,6 +910,21 @@ export class Agent {
|
|
|
994
910
|
this.subagentManager.cleanup();
|
|
995
911
|
// Cleanup forked agent manager
|
|
996
912
|
await this.forkedAgentManager.cleanup();
|
|
913
|
+
// Drain an in-flight auto-memory extraction fork so the process doesn't
|
|
914
|
+
// exit mid-extraction
|
|
915
|
+
try {
|
|
916
|
+
const autoMemoryService =
|
|
917
|
+
this.container.get<
|
|
918
|
+
import("./services/autoMemoryService.js").AutoMemoryService
|
|
919
|
+
>("AutoMemoryService");
|
|
920
|
+
if (autoMemoryService) {
|
|
921
|
+
await autoMemoryService.drain();
|
|
922
|
+
}
|
|
923
|
+
} catch (error) {
|
|
924
|
+
this.logger?.warn(
|
|
925
|
+
`Auto-memory extraction drain failed: ${(error as Error).message}`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
997
928
|
// Cleanup skill manager
|
|
998
929
|
await this.skillManager.destroy();
|
|
999
930
|
// Cleanup live configuration reload
|
|
@@ -1018,21 +949,30 @@ export class Agent {
|
|
|
1018
949
|
}
|
|
1019
950
|
|
|
1020
951
|
/**
|
|
1021
|
-
* Ask a side question without
|
|
952
|
+
* Ask a side question without interrupting the main agent.
|
|
953
|
+
*
|
|
954
|
+
* Runs a single-turn fork of the conversation that reuses the main loop's
|
|
955
|
+
* system prompt, tools, and memory injection (prompt-cache friendly) and
|
|
956
|
+
* never executes tools. The abort signal lets the UI cancel a pending
|
|
957
|
+
* question (loading dismiss); aborts propagate as thrown errors.
|
|
958
|
+
*
|
|
1022
959
|
* @param question - The side question to ask
|
|
960
|
+
* @param abortSignal - Optional signal to abort the pending request
|
|
1023
961
|
* @returns Promise that resolves to the AI's answer
|
|
1024
962
|
*/
|
|
1025
|
-
public async askBtw(
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
messages,
|
|
963
|
+
public async askBtw(
|
|
964
|
+
question: string,
|
|
965
|
+
abortSignal?: AbortSignal,
|
|
966
|
+
onContent?: (content: string) => void,
|
|
967
|
+
onReasoning?: (content: string) => void,
|
|
968
|
+
): Promise<string> {
|
|
969
|
+
const result = await this.aiManager.runBtwFork(
|
|
1033
970
|
question,
|
|
1034
|
-
|
|
1035
|
-
|
|
971
|
+
abortSignal,
|
|
972
|
+
onContent,
|
|
973
|
+
onReasoning,
|
|
974
|
+
);
|
|
975
|
+
return result.content ?? result.error ?? "No response received";
|
|
1036
976
|
}
|
|
1037
977
|
|
|
1038
978
|
/**
|
|
@@ -1066,7 +1006,7 @@ export class Agent {
|
|
|
1066
1006
|
images?: Array<{ path: string; mimeType: string }>,
|
|
1067
1007
|
): Promise<void> {
|
|
1068
1008
|
// If the agent is busy, enqueue the message — unless it's an immediate
|
|
1069
|
-
// slash command (e.g., /
|
|
1009
|
+
// slash command (e.g., /clear, /compact) that should execute
|
|
1070
1010
|
// right away even while AI is processing
|
|
1071
1011
|
if (this.aiManager.isLoading || this.isCommandRunning) {
|
|
1072
1012
|
const trimmed = content.trim();
|