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
|
@@ -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"}`;
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { Container } from "../utils/container.js";
|
|
2
|
-
import type { MessageManager } from "./messageManager.js";
|
|
3
|
-
import type { AIManager } from "./aiManager.js";
|
|
4
|
-
import type { Usage } from "../types/index.js";
|
|
5
|
-
import { evaluateGoal as aiEvaluateGoal } from "../services/aiService.js";
|
|
6
|
-
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
7
|
-
import { logger } from "../utils/globalLogger.js";
|
|
8
|
-
|
|
9
|
-
export interface GoalState {
|
|
10
|
-
condition: string;
|
|
11
|
-
startedAt: number;
|
|
12
|
-
turnCount: number;
|
|
13
|
-
tokenBaseline: number;
|
|
14
|
-
lastReason?: string;
|
|
15
|
-
consecutiveEvalFailures: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const MAX_GOAL_TURNS = 50;
|
|
19
|
-
const MAX_GOAL_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
|
20
|
-
const MAX_CONSECUTIVE_EVAL_FAILURES = 3;
|
|
21
|
-
const MAX_CONDITION_LENGTH = 4000;
|
|
22
|
-
|
|
23
|
-
export class GoalManager {
|
|
24
|
-
private state: GoalState | null = null;
|
|
25
|
-
private onGoalStateChange?: (
|
|
26
|
-
active: boolean,
|
|
27
|
-
condition?: string,
|
|
28
|
-
elapsed?: string,
|
|
29
|
-
) => void;
|
|
30
|
-
private onGoalEvaluating?: (evaluating: boolean) => void;
|
|
31
|
-
|
|
32
|
-
constructor(private container: Container) {}
|
|
33
|
-
|
|
34
|
-
private get messageManager(): MessageManager {
|
|
35
|
-
return this.container.get<MessageManager>("MessageManager")!;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private get aiManager(): AIManager {
|
|
39
|
-
return this.container.get<AIManager>("AIManager")!;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public setOnGoalStateChange(
|
|
43
|
-
callback: (active: boolean, condition?: string, elapsed?: string) => void,
|
|
44
|
-
): void {
|
|
45
|
-
this.onGoalStateChange = callback;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public setOnGoalEvaluating(callback: (evaluating: boolean) => void): void {
|
|
49
|
-
this.onGoalEvaluating = callback;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public setGoal(condition: string): void {
|
|
53
|
-
if (condition.length > MAX_CONDITION_LENGTH) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`Goal condition exceeds maximum length of ${MAX_CONDITION_LENGTH} characters`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const totalTokens = this.messageManager.getLatestTotalTokens?.() ?? 0;
|
|
60
|
-
|
|
61
|
-
this.state = {
|
|
62
|
-
condition,
|
|
63
|
-
startedAt: Date.now(),
|
|
64
|
-
turnCount: 0,
|
|
65
|
-
tokenBaseline: totalTokens,
|
|
66
|
-
consecutiveEvalFailures: 0,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
this.onGoalStateChange?.(true, condition, "0m");
|
|
70
|
-
logger?.info(`[Goal] Set goal: ${condition}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public clearGoal(): void {
|
|
74
|
-
if (this.state) {
|
|
75
|
-
logger?.info(`[Goal] Cleared goal: ${this.state.condition}`);
|
|
76
|
-
this.state = null;
|
|
77
|
-
this.onGoalStateChange?.(false);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
public getGoal(): GoalState | null {
|
|
82
|
-
return this.state;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public isGoalActive(): boolean {
|
|
86
|
-
return this.state !== null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
public incrementTurnCount(): void {
|
|
90
|
-
if (this.state) {
|
|
91
|
-
this.state.turnCount++;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
public getStatusString(): string {
|
|
96
|
-
if (!this.state) return "No active goal";
|
|
97
|
-
const elapsed = this.formatElapsed(Date.now() - this.state.startedAt);
|
|
98
|
-
let status = `Goal: ${this.state.condition}\nElapsed: ${elapsed}\nTurns: ${this.state.turnCount}`;
|
|
99
|
-
if (this.state.lastReason) {
|
|
100
|
-
status += `\nLast evaluation: ${this.state.lastReason}`;
|
|
101
|
-
}
|
|
102
|
-
return status;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check circuit breakers. Returns a clear reason if goal should be force-cleared, null otherwise.
|
|
107
|
-
*/
|
|
108
|
-
public checkCircuitBreakers(): string | null {
|
|
109
|
-
if (!this.state) return null;
|
|
110
|
-
|
|
111
|
-
if (this.state.turnCount >= MAX_GOAL_TURNS) {
|
|
112
|
-
return `Goal cancelled: maximum turns (${MAX_GOAL_TURNS}) exceeded`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (Date.now() - this.state.startedAt >= MAX_GOAL_DURATION_MS) {
|
|
116
|
-
return `Goal cancelled: time limit (${MAX_GOAL_DURATION_MS / 60000} minutes) exceeded`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (this.state.consecutiveEvalFailures >= MAX_CONSECUTIVE_EVAL_FAILURES) {
|
|
120
|
-
return `Goal cancelled: ${MAX_CONSECUTIVE_EVAL_FAILURES} consecutive evaluation failures`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Evaluate whether the goal has been met using the fast model.
|
|
128
|
-
*/
|
|
129
|
-
public async evaluateGoal(abortSignal?: AbortSignal): Promise<{
|
|
130
|
-
isMet: boolean;
|
|
131
|
-
reason: string;
|
|
132
|
-
}> {
|
|
133
|
-
if (!this.state) {
|
|
134
|
-
return { isMet: false, reason: "No active goal" };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const messages = this.messageManager.getMessages();
|
|
139
|
-
const apiMessages = convertMessagesForAPI(messages);
|
|
140
|
-
const gatewayConfig = this.aiManager.getGatewayConfig();
|
|
141
|
-
const modelConfig = this.aiManager.getModelConfig();
|
|
142
|
-
const fastModel = modelConfig.fastModel || modelConfig.model;
|
|
143
|
-
if (!fastModel) {
|
|
144
|
-
return {
|
|
145
|
-
isMet: false,
|
|
146
|
-
reason: "No model configured for goal evaluation",
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.onGoalEvaluating?.(true);
|
|
151
|
-
const result = await aiEvaluateGoal({
|
|
152
|
-
gatewayConfig,
|
|
153
|
-
modelConfig,
|
|
154
|
-
model: fastModel,
|
|
155
|
-
goalCondition: this.state.condition,
|
|
156
|
-
messages: apiMessages,
|
|
157
|
-
abortSignal,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Track evaluation tokens separately
|
|
161
|
-
if (result.usage) {
|
|
162
|
-
const usage: Usage = {
|
|
163
|
-
...result.usage,
|
|
164
|
-
operation_type: "goal_evaluation",
|
|
165
|
-
model: fastModel,
|
|
166
|
-
};
|
|
167
|
-
this.messageManager.addUsage(usage);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Reset failure counter on success
|
|
171
|
-
this.state.consecutiveEvalFailures = 0;
|
|
172
|
-
|
|
173
|
-
this.onGoalEvaluating?.(false);
|
|
174
|
-
|
|
175
|
-
// Parse the response
|
|
176
|
-
return this.parseEvaluationResponse(result.content);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
this.onGoalEvaluating?.(false);
|
|
179
|
-
this.state.consecutiveEvalFailures++;
|
|
180
|
-
logger?.warn(
|
|
181
|
-
`[Goal] Evaluation failed (${this.state.consecutiveEvalFailures}/${MAX_CONSECUTIVE_EVAL_FAILURES}): ${(error as Error).message}`,
|
|
182
|
-
);
|
|
183
|
-
return {
|
|
184
|
-
isMet: false,
|
|
185
|
-
reason: `Evaluation failed: ${(error as Error).message}`,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Parse the evaluation response from the fast model.
|
|
192
|
-
*/
|
|
193
|
-
private parseEvaluationResponse(content: string): {
|
|
194
|
-
isMet: boolean;
|
|
195
|
-
reason: string;
|
|
196
|
-
} {
|
|
197
|
-
// Try direct JSON parse
|
|
198
|
-
try {
|
|
199
|
-
const parsed = JSON.parse(content);
|
|
200
|
-
if (typeof parsed.met === "boolean") {
|
|
201
|
-
return {
|
|
202
|
-
isMet: parsed.met,
|
|
203
|
-
reason: parsed.reason || "No reason provided",
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
// Fall through to regex
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Try regex extraction
|
|
211
|
-
const metMatch = content.match(/"met"\s*:\s*(true|false)/);
|
|
212
|
-
const reasonMatch = content.match(/"reason"\s*:\s*"([^"]*)"/);
|
|
213
|
-
if (metMatch) {
|
|
214
|
-
return {
|
|
215
|
-
isMet: metMatch[1] === "true",
|
|
216
|
-
reason: reasonMatch?.[1] || "No reason provided",
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Default: not met
|
|
221
|
-
return { isMet: false, reason: "Could not parse evaluation response" };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private formatElapsed(ms: number): string {
|
|
225
|
-
const minutes = Math.floor(ms / 60000);
|
|
226
|
-
if (minutes < 1) return "<1m";
|
|
227
|
-
if (minutes < 60) return `${minutes}m`;
|
|
228
|
-
const hours = Math.floor(minutes / 60);
|
|
229
|
-
const remainingMin = minutes % 60;
|
|
230
|
-
return `${hours}h${remainingMin}m`;
|
|
231
|
-
}
|
|
232
|
-
}
|