wave-agent-sdk 1.0.0 → 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/dist/agent.d.ts +9 -20
- package/dist/agent.js +23 -97
- package/dist/managers/aiManager.d.ts +63 -8
- package/dist/managers/aiManager.js +270 -80
- 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/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.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 +36 -110
- package/src/managers/aiManager.ts +366 -105
- package/src/managers/messageManager.ts +51 -23
- package/src/managers/subagentManager.ts +36 -25
- 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 +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
|
@@ -2,14 +2,32 @@ import * as path from "node:path";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { Container } from "../utils/container.js";
|
|
4
4
|
import { MessageManager } from "../managers/messageManager.js";
|
|
5
|
-
import {
|
|
5
|
+
import { AIManager } from "../managers/aiManager.js";
|
|
6
6
|
import { MemoryService } from "./memory.js";
|
|
7
7
|
import { ConfigurationService } from "./configurationService.js";
|
|
8
8
|
import { logger } from "../utils/globalLogger.js";
|
|
9
9
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
10
10
|
import { buildAutoMemoryExtractionPrompt } from "../prompts/autoMemoryExtraction.js";
|
|
11
|
+
import {
|
|
12
|
+
READ_ONLY_COMMANDS,
|
|
13
|
+
splitBashCommand,
|
|
14
|
+
hasWriteRedirections,
|
|
15
|
+
hasCommandSubstitution,
|
|
16
|
+
hasProcessSubstitution,
|
|
17
|
+
hasSedInPlace,
|
|
18
|
+
isDangerousFind,
|
|
19
|
+
} from "../utils/bashParser.js";
|
|
11
20
|
import type { Message } from "../types/index.js";
|
|
12
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Message fed back to the extraction fork when the model requests a tool
|
|
24
|
+
* outside the allowed set (Bash rm, MCP tools, Agent, out-of-dir writes...).
|
|
25
|
+
*/
|
|
26
|
+
const DENIED_TOOL_MESSAGE =
|
|
27
|
+
"This tool call was denied during auto-memory extraction. Available tools: " +
|
|
28
|
+
"Read, Grep, Glob, read-only Bash commands, and Write/Edit inside the " +
|
|
29
|
+
"memory directory only.";
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Service responsible for managing the auto-memory background agent lifecycle.
|
|
15
33
|
* Extracts and updates persistent project-level memory from conversation history.
|
|
@@ -17,6 +35,8 @@ import type { Message } from "../types/index.js";
|
|
|
17
35
|
export class AutoMemoryService {
|
|
18
36
|
private lastMemoryMessageId: string | null = null;
|
|
19
37
|
private turnsSinceLastExtraction: number = 0;
|
|
38
|
+
private extractionInProgress: boolean = false;
|
|
39
|
+
private pendingExtraction: Promise<void> | null = null;
|
|
20
40
|
|
|
21
41
|
constructor(private container: Container) {}
|
|
22
42
|
|
|
@@ -24,8 +44,8 @@ export class AutoMemoryService {
|
|
|
24
44
|
return this.container.get<MessageManager>("MessageManager")!;
|
|
25
45
|
}
|
|
26
46
|
|
|
27
|
-
private get
|
|
28
|
-
return this.container.get<
|
|
47
|
+
private get aiManager(): AIManager {
|
|
48
|
+
return this.container.get<AIManager>("AIManager")!;
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
private get memoryService(): MemoryService {
|
|
@@ -71,7 +91,14 @@ export class AutoMemoryService {
|
|
|
71
91
|
(m) =>
|
|
72
92
|
m.role === "assistant" &&
|
|
73
93
|
m.blocks.some((b) => {
|
|
74
|
-
if (
|
|
94
|
+
if (
|
|
95
|
+
b.type === "tool" &&
|
|
96
|
+
(b.name === "Write" || b.name === "Edit") &&
|
|
97
|
+
// Only a successful manual write counts as a manual update. A
|
|
98
|
+
// denied/failed write didn't touch memory, so the extraction fork
|
|
99
|
+
// must still run or the information is lost.
|
|
100
|
+
b.success !== false
|
|
101
|
+
) {
|
|
75
102
|
try {
|
|
76
103
|
const params = b.parameters ? JSON.parse(b.parameters) : null;
|
|
77
104
|
const filePath = params?.file_path || params?.path;
|
|
@@ -98,22 +125,61 @@ export class AutoMemoryService {
|
|
|
98
125
|
return;
|
|
99
126
|
}
|
|
100
127
|
|
|
101
|
-
// 3.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
// 3. Concurrency guard: if an extraction is already in flight, skip this
|
|
129
|
+
// turn. turnsSinceLastExtraction is intentionally NOT reset so the next
|
|
130
|
+
// eligible turn retriggers the extraction.
|
|
131
|
+
if (this.extractionInProgress) {
|
|
132
|
+
logger.debug(
|
|
133
|
+
"Skipping auto-memory extraction: another extraction is still in progress.",
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
108
136
|
}
|
|
137
|
+
|
|
138
|
+
// 4. Trigger the perfect-fork extraction fire-and-forget. The message
|
|
139
|
+
// snapshot and new-message count are computed now; lastMemoryMessageId
|
|
140
|
+
// advances at trigger time so the next extraction starts from a later
|
|
141
|
+
// window even while this one runs.
|
|
142
|
+
const lastExtractedIndex = this.lastMemoryMessageId
|
|
143
|
+
? messages.findIndex((m) => m.id === this.lastMemoryMessageId)
|
|
144
|
+
: -1;
|
|
145
|
+
const newMessageCount =
|
|
146
|
+
lastExtractedIndex === -1
|
|
147
|
+
? messages.length
|
|
148
|
+
: messages.length - 1 - lastExtractedIndex;
|
|
149
|
+
|
|
150
|
+
this.turnsSinceLastExtraction = 0;
|
|
151
|
+
this.lastMemoryMessageId = messages[messages.length - 1].id || null;
|
|
152
|
+
this.extractionInProgress = true;
|
|
153
|
+
const extraction = this.runExtraction(workdir, messages, newMessageCount)
|
|
154
|
+
.catch((error) => {
|
|
155
|
+
logger.error("Auto-memory extraction failed:", error);
|
|
156
|
+
})
|
|
157
|
+
.finally(() => {
|
|
158
|
+
this.extractionInProgress = false;
|
|
159
|
+
this.pendingExtraction = null;
|
|
160
|
+
});
|
|
161
|
+
this.pendingExtraction = extraction;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Wait for an in-flight extraction to settle. Called from Agent.dispose so
|
|
166
|
+
* the process doesn't exit while the extraction fork is mid-flight.
|
|
167
|
+
*/
|
|
168
|
+
async drain(): Promise<void> {
|
|
169
|
+
await this.pendingExtraction;
|
|
109
170
|
}
|
|
110
171
|
|
|
111
172
|
/**
|
|
112
|
-
* Initialize and execute the
|
|
173
|
+
* Initialize and execute the extraction in a perfect fork: same system
|
|
174
|
+
* prompt, tools, model, and message prefix as the main conversation, so the
|
|
175
|
+
* prompt cache is reused. A tool gate confines the fork to read-only
|
|
176
|
+
* inspection and memory-directory writes. Runs in-process; callers treat it
|
|
177
|
+
* as fire-and-forget.
|
|
113
178
|
*/
|
|
114
179
|
private async runExtraction(
|
|
115
180
|
workdir: string,
|
|
116
181
|
messages: Message[],
|
|
182
|
+
newMessageCount: number,
|
|
117
183
|
): Promise<void> {
|
|
118
184
|
const memoryDir = this.memoryService.getAutoMemoryDirectory(workdir);
|
|
119
185
|
|
|
@@ -132,43 +198,83 @@ export class AutoMemoryService {
|
|
|
132
198
|
// Ignore if directory doesn't exist yet
|
|
133
199
|
}
|
|
134
200
|
|
|
135
|
-
// Calculate how many new messages to analyze
|
|
136
|
-
let newMessageCount = messages.length;
|
|
137
|
-
if (this.lastMemoryMessageId) {
|
|
138
|
-
const lastIndex = messages.findIndex(
|
|
139
|
-
(m) => m.id === this.lastMemoryMessageId,
|
|
140
|
-
);
|
|
141
|
-
if (lastIndex !== -1) {
|
|
142
|
-
newMessageCount = messages.length - 1 - lastIndex;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
201
|
const prompt = buildAutoMemoryExtractionPrompt(
|
|
147
202
|
newMessageCount,
|
|
148
203
|
existingMemoriesManifest,
|
|
149
204
|
);
|
|
150
205
|
|
|
151
|
-
|
|
152
|
-
await this.forkedAgentManager.forkAndExecute(
|
|
153
|
-
"general-purpose",
|
|
206
|
+
await this.aiManager.runAutoMemoryFork(
|
|
154
207
|
messages,
|
|
208
|
+
`${prompt}\n\nThe memory directory for this project is: ${memoryDir}`,
|
|
155
209
|
{
|
|
156
|
-
description: "Auto-memory extraction background agent",
|
|
157
|
-
allowedTools: [
|
|
158
|
-
"Read",
|
|
159
|
-
"Glob",
|
|
160
|
-
"Grep",
|
|
161
|
-
`Write(${memoryDir}/**/*)`,
|
|
162
|
-
`Edit(${memoryDir}/**/*)`,
|
|
163
|
-
`Bash(rm ${memoryDir}/**/*)`,
|
|
164
|
-
],
|
|
165
|
-
model: "fastModel", // Use fast model for background tasks to reduce latency and cost
|
|
166
|
-
permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
|
|
167
210
|
maxTurns: 5, // Limit turns to prevent verification rabbit-holes
|
|
211
|
+
canUseTool: (name, args) =>
|
|
212
|
+
this.isAllowedForkTool(name, args, memoryDir, workdir),
|
|
213
|
+
deniedToolMessage: DENIED_TOOL_MESSAGE,
|
|
168
214
|
},
|
|
169
|
-
`${prompt}\n\nThe memory directory for this project is: ${memoryDir}`,
|
|
170
215
|
);
|
|
171
216
|
|
|
172
|
-
logger.debug("Auto-memory extraction
|
|
217
|
+
logger.debug("Auto-memory extraction completed.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Tool gate for the extraction fork: Read/Grep/Glob are always allowed;
|
|
222
|
+
* Write/Edit only when the target path is inside the memory directory; Bash
|
|
223
|
+
* only for read-only commands (aligned with the permission manager's
|
|
224
|
+
* read-only bash classification). Everything else — Bash rm, MCP tools,
|
|
225
|
+
* Agent, out-of-dir writes — is denied.
|
|
226
|
+
*/
|
|
227
|
+
private isAllowedForkTool(
|
|
228
|
+
name: string,
|
|
229
|
+
args: Record<string, unknown>,
|
|
230
|
+
memoryDir: string,
|
|
231
|
+
workdir: string,
|
|
232
|
+
): boolean {
|
|
233
|
+
if (name === "Read" || name === "Grep" || name === "Glob") {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (name === "Write" || name === "Edit") {
|
|
238
|
+
const filePath = args.file_path ?? args.path;
|
|
239
|
+
if (typeof filePath !== "string" || !filePath) return false;
|
|
240
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
241
|
+
? filePath
|
|
242
|
+
: path.resolve(workdir, filePath);
|
|
243
|
+
return isPathInside(absolutePath, memoryDir);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (name === "Bash") {
|
|
247
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
248
|
+
return this.isReadOnlyBashCommand(command);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A bash command is read-only when every part is a READ_ONLY_COMMANDS entry
|
|
256
|
+
* without write redirections, command/process substitution, sed -i, or
|
|
257
|
+
* dangerous find flags. Mirrors PermissionManager.isAutoAllowedPart.
|
|
258
|
+
*/
|
|
259
|
+
private isReadOnlyBashCommand(command: string): boolean {
|
|
260
|
+
if (!command.trim()) return false;
|
|
261
|
+
if (hasWriteRedirections(command)) return false;
|
|
262
|
+
if (hasCommandSubstitution(command)) return false;
|
|
263
|
+
if (hasProcessSubstitution(command)) return false;
|
|
264
|
+
if (hasSedInPlace(command)) return false;
|
|
265
|
+
|
|
266
|
+
const parts = splitBashCommand(command);
|
|
267
|
+
if (parts.length === 0) return false;
|
|
268
|
+
|
|
269
|
+
return parts.every((part) => {
|
|
270
|
+
const trimmed = part.trim();
|
|
271
|
+
if (!trimmed) return true;
|
|
272
|
+
const commandMatch = trimmed.match(/^(\w+)(\s+.*)?$/);
|
|
273
|
+
if (!commandMatch) return false;
|
|
274
|
+
const cmd = commandMatch[1];
|
|
275
|
+
if (!READ_ONLY_COMMANDS.includes(cmd)) return false;
|
|
276
|
+
if (cmd === "find" && isDangerousFind(part)) return false;
|
|
277
|
+
return true;
|
|
278
|
+
});
|
|
173
279
|
}
|
|
174
280
|
}
|
|
@@ -619,16 +619,31 @@ export class ConfigurationService {
|
|
|
619
619
|
baseConfig.fastModelOptions = fastModelSource.options;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
// Resolve fast-model disable-thinking params from models[fastModel].disableThinkingOptions
|
|
623
|
+
const fastModelDisableThinking =
|
|
624
|
+
fastModelSource && fastModelSource.disableThinkingOptions
|
|
625
|
+
? fastModelSource.disableThinkingOptions
|
|
626
|
+
: undefined;
|
|
627
|
+
if (fastModelDisableThinking) {
|
|
628
|
+
baseConfig.disableThinkingOptions = fastModelDisableThinking;
|
|
629
|
+
}
|
|
630
|
+
|
|
622
631
|
// Merge model-specific settings from configuration
|
|
623
632
|
const modelSpecificConfig =
|
|
624
633
|
resolvedAgentModel &&
|
|
625
634
|
this.currentConfiguration?.models?.[resolvedAgentModel];
|
|
626
635
|
|
|
627
636
|
if (modelSpecificConfig) {
|
|
628
|
-
|
|
637
|
+
const resolved: ModelConfig = {
|
|
629
638
|
...baseConfig,
|
|
630
639
|
...modelSpecificConfig,
|
|
631
640
|
};
|
|
641
|
+
// Re-apply after the spread so the agent model's own
|
|
642
|
+
// disableThinkingOptions cannot clobber the fast-model value.
|
|
643
|
+
if (fastModelDisableThinking) {
|
|
644
|
+
resolved.disableThinkingOptions = fastModelDisableThinking;
|
|
645
|
+
}
|
|
646
|
+
return resolved;
|
|
632
647
|
}
|
|
633
648
|
|
|
634
649
|
return baseConfig;
|
package/src/services/session.ts
CHANGED
|
@@ -612,6 +612,74 @@ export async function cleanupEmptyProjectDirectories(): Promise<void> {
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Clean up "ghost" session files that contain only meta messages
|
|
617
|
+
* (isMeta: true) — e.g. sessions where a SessionStart hook injected a
|
|
618
|
+
* system-reminder but no real user/assistant message was ever sent.
|
|
619
|
+
*
|
|
620
|
+
* Such sessions are no longer created thanks to lazy materialization in
|
|
621
|
+
* saveSession(); this one-time sweep removes files that predate that change
|
|
622
|
+
* so they stop showing up as "0 tokens / No content" entries in the resume
|
|
623
|
+
* list.
|
|
624
|
+
*
|
|
625
|
+
* @returns Promise that resolves to the number of files deleted
|
|
626
|
+
*/
|
|
627
|
+
export async function cleanupMetaOnlySessions(): Promise<number> {
|
|
628
|
+
// Do not perform cleanup operations in test environment
|
|
629
|
+
if (process.env.NODE_ENV === "test") {
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let deletedCount = 0;
|
|
634
|
+
try {
|
|
635
|
+
const projectDirs = await fs.readdir(SESSION_DIR);
|
|
636
|
+
|
|
637
|
+
for (const projectDirName of projectDirs) {
|
|
638
|
+
const projectPath = join(SESSION_DIR, projectDirName);
|
|
639
|
+
try {
|
|
640
|
+
const stat = await fs.stat(projectPath);
|
|
641
|
+
if (!stat.isDirectory()) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const files = await fs.readdir(projectPath);
|
|
646
|
+
for (const file of files) {
|
|
647
|
+
if (!file.endsWith(".jsonl")) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const filePath = join(projectPath, file);
|
|
652
|
+
try {
|
|
653
|
+
// Fast path: a file whose last message is not meta contains a
|
|
654
|
+
// real message, so it can never be meta-only.
|
|
655
|
+
const jsonlHandler = new JsonlHandler();
|
|
656
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
657
|
+
if (!lastMessage?.isMeta) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const messages = await jsonlHandler.read(filePath);
|
|
662
|
+
if (messages.length > 0 && messages.every((m) => m.isMeta)) {
|
|
663
|
+
await fs.unlink(filePath);
|
|
664
|
+
deletedCount++;
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
667
|
+
// Skip corrupted or unreadable files
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch {
|
|
672
|
+
// Skip directories we can't access
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
// Ignore errors if base directory doesn't exist or can't be accessed
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return deletedCount;
|
|
681
|
+
}
|
|
682
|
+
|
|
615
683
|
/**
|
|
616
684
|
* Check if a session exists in JSONL storage (new approach)
|
|
617
685
|
*
|
package/src/types/agent.ts
CHANGED
|
@@ -119,10 +119,4 @@ export interface AgentCallbacks
|
|
|
119
119
|
onCommandRunningChange?: (running: boolean) => void;
|
|
120
120
|
onWorkdirChange?: (newCwd: string) => void;
|
|
121
121
|
onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
|
|
122
|
-
onGoalStateChange?: (
|
|
123
|
-
active: boolean,
|
|
124
|
-
condition?: string,
|
|
125
|
-
elapsed?: string,
|
|
126
|
-
) => void;
|
|
127
|
-
onGoalEvaluating?: (evaluating: boolean) => void;
|
|
128
122
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -33,4 +33,11 @@ export interface ModelConfig {
|
|
|
33
33
|
options?: Record<string, unknown>;
|
|
34
34
|
/** Fast model generation params (resolved from models[fastModel].options) */
|
|
35
35
|
fastModelOptions?: Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Fast-model-only disable-thinking params passed through verbatim
|
|
38
|
+
* (e.g. `{ thinking: { type: "disabled" } }`). Applied only in fast-model
|
|
39
|
+
* scenarios (webFetch content processing, `model: fastModel` subagents),
|
|
40
|
+
* never in the agent loop. `{}` clears the default.
|
|
41
|
+
*/
|
|
42
|
+
disableThinkingOptions?: Record<string, unknown>;
|
|
36
43
|
}
|
package/src/types/core.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface Usage {
|
|
|
25
25
|
completion_tokens: number; // Tokens generated in completions
|
|
26
26
|
total_tokens: number; // Sum of prompt + completion tokens
|
|
27
27
|
model?: string; // Model used for the operation (e.g., "gpt-4", "gpt-3.5-turbo")
|
|
28
|
-
operation_type?: "agent" | "compact"
|
|
28
|
+
operation_type?: "agent" | "compact"; // Type of operation that generated usage
|
|
29
29
|
|
|
30
30
|
// Cache-related tokens (Claude top-level + OpenAI prompt_tokens_details)
|
|
31
31
|
cache_read_input_tokens?: number; // Tokens read from cache (Claude) or cached_tokens (OpenAI prompt_tokens_details)
|
|
@@ -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";
|
|
@@ -25,6 +24,7 @@ import { ForkedAgentManager } from "../managers/forkedAgentManager.js";
|
|
|
25
24
|
import { LiveConfigManager } from "../managers/liveConfigManager.js";
|
|
26
25
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
27
26
|
import { ReversionService } from "../services/reversionService.js";
|
|
27
|
+
import { cleanupMetaOnlySessions } from "../services/session.js";
|
|
28
28
|
import { MemoryService } from "../services/memory.js";
|
|
29
29
|
import { AutoMemoryService } from "../services/autoMemoryService.js";
|
|
30
30
|
import { USER_MEMORY_FILE } from "./constants.js";
|
|
@@ -210,6 +210,17 @@ export function setupAgentContainer(
|
|
|
210
210
|
reversionService.cleanupOldSessions(30).catch((error) => {
|
|
211
211
|
logger.error("Failed to cleanup old file history:", error);
|
|
212
212
|
});
|
|
213
|
+
// Remove pre-existing meta-only session files (SessionStart hook context
|
|
214
|
+
// with no real messages) that were persisted before lazy materialization.
|
|
215
|
+
cleanupMetaOnlySessions()
|
|
216
|
+
.then((count) => {
|
|
217
|
+
if (count > 0) {
|
|
218
|
+
logger.debug(`Removed ${count} meta-only session file(s)`);
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
.catch((error) => {
|
|
222
|
+
logger.error("Failed to cleanup meta-only session files:", error);
|
|
223
|
+
});
|
|
213
224
|
const reversionManager = new ReversionManager(container);
|
|
214
225
|
container.register("ReversionManager", reversionManager);
|
|
215
226
|
|
|
@@ -352,9 +363,6 @@ export function setupAgentContainer(
|
|
|
352
363
|
container.register("CronManager", cronManager);
|
|
353
364
|
cronManager.start();
|
|
354
365
|
|
|
355
|
-
const goalManager = new GoalManager(container);
|
|
356
|
-
container.register("GoalManager", goalManager);
|
|
357
|
-
|
|
358
366
|
const workflowManager = new WorkflowManager(container);
|
|
359
367
|
container.register("WorkflowManager", workflowManager);
|
|
360
368
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const GOAL_EVALUATION_SYSTEM_PROMPT = "You are a goal evaluator. Given a goal condition and a conversation, determine whether the goal has been achieved.\n\nRules:\n- Judge ONLY based on what the conversation shows \u2014 tool outputs, test results, file contents, etc.\n- Do NOT assume work is done without evidence in the conversation.\n- Be strict and conservative: when uncertain, return met: false.\n- Be concise: your reason should be 1-2 sentences.\n\nRespond with JSON only, no other text:\n{\"met\": boolean, \"reason\": \"short explanation\"}";
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export const GOAL_EVALUATION_SYSTEM_PROMPT = `You are a goal evaluator. Given a goal condition and a conversation, determine whether the goal has been achieved.
|
|
2
|
-
|
|
3
|
-
Rules:
|
|
4
|
-
- Judge ONLY based on what the conversation shows — tool outputs, test results, file contents, etc.
|
|
5
|
-
- Do NOT assume work is done without evidence in the conversation.
|
|
6
|
-
- Be strict and conservative: when uncertain, return met: false.
|
|
7
|
-
- Be concise: your reason should be 1-2 sentences.
|
|
8
|
-
|
|
9
|
-
Respond with JSON only, no other text:
|
|
10
|
-
{"met": boolean, "reason": "short explanation"}`;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Container } from "../utils/container.js";
|
|
2
|
-
export interface GoalState {
|
|
3
|
-
condition: string;
|
|
4
|
-
startedAt: number;
|
|
5
|
-
turnCount: number;
|
|
6
|
-
tokenBaseline: number;
|
|
7
|
-
lastReason?: string;
|
|
8
|
-
consecutiveEvalFailures: number;
|
|
9
|
-
}
|
|
10
|
-
export declare class GoalManager {
|
|
11
|
-
private container;
|
|
12
|
-
private state;
|
|
13
|
-
private onGoalStateChange?;
|
|
14
|
-
private onGoalEvaluating?;
|
|
15
|
-
constructor(container: Container);
|
|
16
|
-
private get messageManager();
|
|
17
|
-
private get aiManager();
|
|
18
|
-
setOnGoalStateChange(callback: (active: boolean, condition?: string, elapsed?: string) => void): void;
|
|
19
|
-
setOnGoalEvaluating(callback: (evaluating: boolean) => void): void;
|
|
20
|
-
setGoal(condition: string): void;
|
|
21
|
-
clearGoal(): void;
|
|
22
|
-
getGoal(): GoalState | null;
|
|
23
|
-
isGoalActive(): boolean;
|
|
24
|
-
incrementTurnCount(): void;
|
|
25
|
-
getStatusString(): string;
|
|
26
|
-
/**
|
|
27
|
-
* Check circuit breakers. Returns a clear reason if goal should be force-cleared, null otherwise.
|
|
28
|
-
*/
|
|
29
|
-
checkCircuitBreakers(): string | null;
|
|
30
|
-
/**
|
|
31
|
-
* Evaluate whether the goal has been met using the fast model.
|
|
32
|
-
*/
|
|
33
|
-
evaluateGoal(abortSignal?: AbortSignal): Promise<{
|
|
34
|
-
isMet: boolean;
|
|
35
|
-
reason: string;
|
|
36
|
-
}>;
|
|
37
|
-
/**
|
|
38
|
-
* Parse the evaluation response from the fast model.
|
|
39
|
-
*/
|
|
40
|
-
private parseEvaluationResponse;
|
|
41
|
-
private formatElapsed;
|
|
42
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { evaluateGoal as aiEvaluateGoal } from "../services/aiService.js";
|
|
2
|
-
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
3
|
-
import { logger } from "../utils/globalLogger.js";
|
|
4
|
-
const MAX_GOAL_TURNS = 50;
|
|
5
|
-
const MAX_GOAL_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
|
6
|
-
const MAX_CONSECUTIVE_EVAL_FAILURES = 3;
|
|
7
|
-
const MAX_CONDITION_LENGTH = 4000;
|
|
8
|
-
export class GoalManager {
|
|
9
|
-
constructor(container) {
|
|
10
|
-
this.container = container;
|
|
11
|
-
this.state = null;
|
|
12
|
-
}
|
|
13
|
-
get messageManager() {
|
|
14
|
-
return this.container.get("MessageManager");
|
|
15
|
-
}
|
|
16
|
-
get aiManager() {
|
|
17
|
-
return this.container.get("AIManager");
|
|
18
|
-
}
|
|
19
|
-
setOnGoalStateChange(callback) {
|
|
20
|
-
this.onGoalStateChange = callback;
|
|
21
|
-
}
|
|
22
|
-
setOnGoalEvaluating(callback) {
|
|
23
|
-
this.onGoalEvaluating = callback;
|
|
24
|
-
}
|
|
25
|
-
setGoal(condition) {
|
|
26
|
-
if (condition.length > MAX_CONDITION_LENGTH) {
|
|
27
|
-
throw new Error(`Goal condition exceeds maximum length of ${MAX_CONDITION_LENGTH} characters`);
|
|
28
|
-
}
|
|
29
|
-
const totalTokens = this.messageManager.getLatestTotalTokens?.() ?? 0;
|
|
30
|
-
this.state = {
|
|
31
|
-
condition,
|
|
32
|
-
startedAt: Date.now(),
|
|
33
|
-
turnCount: 0,
|
|
34
|
-
tokenBaseline: totalTokens,
|
|
35
|
-
consecutiveEvalFailures: 0,
|
|
36
|
-
};
|
|
37
|
-
this.onGoalStateChange?.(true, condition, "0m");
|
|
38
|
-
logger?.info(`[Goal] Set goal: ${condition}`);
|
|
39
|
-
}
|
|
40
|
-
clearGoal() {
|
|
41
|
-
if (this.state) {
|
|
42
|
-
logger?.info(`[Goal] Cleared goal: ${this.state.condition}`);
|
|
43
|
-
this.state = null;
|
|
44
|
-
this.onGoalStateChange?.(false);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
getGoal() {
|
|
48
|
-
return this.state;
|
|
49
|
-
}
|
|
50
|
-
isGoalActive() {
|
|
51
|
-
return this.state !== null;
|
|
52
|
-
}
|
|
53
|
-
incrementTurnCount() {
|
|
54
|
-
if (this.state) {
|
|
55
|
-
this.state.turnCount++;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
getStatusString() {
|
|
59
|
-
if (!this.state)
|
|
60
|
-
return "No active goal";
|
|
61
|
-
const elapsed = this.formatElapsed(Date.now() - this.state.startedAt);
|
|
62
|
-
let status = `Goal: ${this.state.condition}\nElapsed: ${elapsed}\nTurns: ${this.state.turnCount}`;
|
|
63
|
-
if (this.state.lastReason) {
|
|
64
|
-
status += `\nLast evaluation: ${this.state.lastReason}`;
|
|
65
|
-
}
|
|
66
|
-
return status;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Check circuit breakers. Returns a clear reason if goal should be force-cleared, null otherwise.
|
|
70
|
-
*/
|
|
71
|
-
checkCircuitBreakers() {
|
|
72
|
-
if (!this.state)
|
|
73
|
-
return null;
|
|
74
|
-
if (this.state.turnCount >= MAX_GOAL_TURNS) {
|
|
75
|
-
return `Goal cancelled: maximum turns (${MAX_GOAL_TURNS}) exceeded`;
|
|
76
|
-
}
|
|
77
|
-
if (Date.now() - this.state.startedAt >= MAX_GOAL_DURATION_MS) {
|
|
78
|
-
return `Goal cancelled: time limit (${MAX_GOAL_DURATION_MS / 60000} minutes) exceeded`;
|
|
79
|
-
}
|
|
80
|
-
if (this.state.consecutiveEvalFailures >= MAX_CONSECUTIVE_EVAL_FAILURES) {
|
|
81
|
-
return `Goal cancelled: ${MAX_CONSECUTIVE_EVAL_FAILURES} consecutive evaluation failures`;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Evaluate whether the goal has been met using the fast model.
|
|
87
|
-
*/
|
|
88
|
-
async evaluateGoal(abortSignal) {
|
|
89
|
-
if (!this.state) {
|
|
90
|
-
return { isMet: false, reason: "No active goal" };
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
const messages = this.messageManager.getMessages();
|
|
94
|
-
const apiMessages = convertMessagesForAPI(messages);
|
|
95
|
-
const gatewayConfig = this.aiManager.getGatewayConfig();
|
|
96
|
-
const modelConfig = this.aiManager.getModelConfig();
|
|
97
|
-
const fastModel = modelConfig.fastModel || modelConfig.model;
|
|
98
|
-
if (!fastModel) {
|
|
99
|
-
return {
|
|
100
|
-
isMet: false,
|
|
101
|
-
reason: "No model configured for goal evaluation",
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
this.onGoalEvaluating?.(true);
|
|
105
|
-
const result = await aiEvaluateGoal({
|
|
106
|
-
gatewayConfig,
|
|
107
|
-
modelConfig,
|
|
108
|
-
model: fastModel,
|
|
109
|
-
goalCondition: this.state.condition,
|
|
110
|
-
messages: apiMessages,
|
|
111
|
-
abortSignal,
|
|
112
|
-
});
|
|
113
|
-
// Track evaluation tokens separately
|
|
114
|
-
if (result.usage) {
|
|
115
|
-
const usage = {
|
|
116
|
-
...result.usage,
|
|
117
|
-
operation_type: "goal_evaluation",
|
|
118
|
-
model: fastModel,
|
|
119
|
-
};
|
|
120
|
-
this.messageManager.addUsage(usage);
|
|
121
|
-
}
|
|
122
|
-
// Reset failure counter on success
|
|
123
|
-
this.state.consecutiveEvalFailures = 0;
|
|
124
|
-
this.onGoalEvaluating?.(false);
|
|
125
|
-
// Parse the response
|
|
126
|
-
return this.parseEvaluationResponse(result.content);
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
this.onGoalEvaluating?.(false);
|
|
130
|
-
this.state.consecutiveEvalFailures++;
|
|
131
|
-
logger?.warn(`[Goal] Evaluation failed (${this.state.consecutiveEvalFailures}/${MAX_CONSECUTIVE_EVAL_FAILURES}): ${error.message}`);
|
|
132
|
-
return {
|
|
133
|
-
isMet: false,
|
|
134
|
-
reason: `Evaluation failed: ${error.message}`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Parse the evaluation response from the fast model.
|
|
140
|
-
*/
|
|
141
|
-
parseEvaluationResponse(content) {
|
|
142
|
-
// Try direct JSON parse
|
|
143
|
-
try {
|
|
144
|
-
const parsed = JSON.parse(content);
|
|
145
|
-
if (typeof parsed.met === "boolean") {
|
|
146
|
-
return {
|
|
147
|
-
isMet: parsed.met,
|
|
148
|
-
reason: parsed.reason || "No reason provided",
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// Fall through to regex
|
|
154
|
-
}
|
|
155
|
-
// Try regex extraction
|
|
156
|
-
const metMatch = content.match(/"met"\s*:\s*(true|false)/);
|
|
157
|
-
const reasonMatch = content.match(/"reason"\s*:\s*"([^"]*)"/);
|
|
158
|
-
if (metMatch) {
|
|
159
|
-
return {
|
|
160
|
-
isMet: metMatch[1] === "true",
|
|
161
|
-
reason: reasonMatch?.[1] || "No reason provided",
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
// Default: not met
|
|
165
|
-
return { isMet: false, reason: "Could not parse evaluation response" };
|
|
166
|
-
}
|
|
167
|
-
formatElapsed(ms) {
|
|
168
|
-
const minutes = Math.floor(ms / 60000);
|
|
169
|
-
if (minutes < 1)
|
|
170
|
-
return "<1m";
|
|
171
|
-
if (minutes < 60)
|
|
172
|
-
return `${minutes}m`;
|
|
173
|
-
const hours = Math.floor(minutes / 60);
|
|
174
|
-
const remainingMin = minutes % 60;
|
|
175
|
-
return `${hours}h${remainingMin}m`;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export const GOAL_EVALUATION_SYSTEM_PROMPT = `You are a goal evaluator. Given a goal condition and a conversation, determine whether the goal has been achieved.
|
|
2
|
-
|
|
3
|
-
Rules:
|
|
4
|
-
- Judge ONLY based on what the conversation shows — tool outputs, test results, file contents, etc.
|
|
5
|
-
- Do NOT assume work is done without evidence in the conversation.
|
|
6
|
-
- Be strict and conservative: when uncertain, return met: false.
|
|
7
|
-
- Be concise: your reason should be 1-2 sentences.
|
|
8
|
-
|
|
9
|
-
Respond with JSON only, no other text:
|
|
10
|
-
{"met": boolean, "reason": "short explanation"}`;
|