bingocode 1.1.183 → 1.1.185
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/.claude/scheduled_tasks.json +34 -0
- package/package.json +1 -1
- package/src/bootstrap/state.ts +3 -53
- package/src/hooks/useGoalAutoUpdater.ts +187 -0
- package/src/hooks/useGoalEvaluator.ts +71 -37
- package/src/query.ts +21 -0
- package/src/skills/bundled/goal.ts +41 -32
- package/src/types/goal.ts +322 -0
- package/src/utils/__tests__/goalDag.test.ts +177 -0
- package/src/utils/__tests__/goalDrift.test.ts +132 -0
- package/src/utils/__tests__/goalIntegration.test.ts +217 -0
- package/src/utils/__tests__/goalStore.test.ts +592 -0
- package/src/utils/goalDag.ts +408 -0
- package/src/utils/goalDrift.ts +243 -0
- package/src/utils/goalEvaluator.ts +708 -122
- package/src/utils/goalHelpers.ts +155 -0
- package/src/utils/goalPersistence.ts +206 -0
- package/src/utils/goalStore.ts +463 -0
|
@@ -1,15 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal Evaluator v2 — environment-aware assessment for goal progress.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the text-only evaluator with a 3-tier system:
|
|
5
|
+
* 1. Rule Engine (deterministic checks — file existence, test output, etc.)
|
|
6
|
+
* 2. Semantic Evaluation (Haiku-level — interprets evidence against goal)
|
|
7
|
+
* 3. Final Verification (main model — for ambiguous cases)
|
|
8
|
+
*
|
|
9
|
+
* Still supports the EVAL block format for agent self-reporting, but now
|
|
10
|
+
* also reads actual tool results, file diffs, and system state to verify
|
|
11
|
+
* progress independently of what the agent claims.
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
import Anthropic from '@anthropic-ai/sdk'
|
|
2
15
|
import type { MessageType } from '../components/messages.js'
|
|
16
|
+
import type {
|
|
17
|
+
EvaluationContext,
|
|
18
|
+
EvalResult,
|
|
19
|
+
EvalEvidence,
|
|
20
|
+
RuleCheck,
|
|
21
|
+
ToolResult,
|
|
22
|
+
FileDiff,
|
|
23
|
+
TestOutput,
|
|
24
|
+
CompileOutput,
|
|
25
|
+
GitStatus,
|
|
26
|
+
ErrorLog,
|
|
27
|
+
} from '../types/goal.js'
|
|
28
|
+
import type { SubGoal, GoalDag } from '../types/goal.js'
|
|
29
|
+
import { getGoalState } from './goalStore.js'
|
|
30
|
+
import type { GoalState } from '../types/goal.js'
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Model constants
|
|
34
|
+
// ============================================================================
|
|
3
35
|
|
|
4
36
|
export const GOAL_EVALUATOR_MODEL = 'claude-haiku-4-5'
|
|
37
|
+
const EVAL_MAX_TOKENS = 512
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Client factory
|
|
41
|
+
// ============================================================================
|
|
5
42
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
43
|
+
function createClient(): Anthropic {
|
|
44
|
+
return new Anthropic({
|
|
45
|
+
baseURL: process.env.ANTHROPIC_BASE_URL ?? undefined,
|
|
46
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? undefined,
|
|
47
|
+
})
|
|
10
48
|
}
|
|
11
49
|
|
|
12
|
-
//
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// EVAL Block Parser (retained from v1)
|
|
52
|
+
// ============================================================================
|
|
13
53
|
|
|
14
54
|
type EvalBlock = {
|
|
15
55
|
metric: string
|
|
@@ -17,32 +57,16 @@ type EvalBlock = {
|
|
|
17
57
|
passed: boolean
|
|
18
58
|
}
|
|
19
59
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* Supports ASCII and Unicode arrow/check/cross variants for maximum compatibility.
|
|
29
|
-
*/
|
|
60
|
+
const arrow = /(?:→|->|=>)/g.source
|
|
61
|
+
const pass = /(?:✓|✔|PASS|pass|Y\b|true|yes|1)/g.source
|
|
62
|
+
const fail = /(?:✗|✘|FAIL|fail|N\b|false|no|0)/g.source
|
|
63
|
+
const full = new RegExp(
|
|
64
|
+
`EVAL:\\s*(.+?):\\s*(.+?)\\s*(?:${arrow}|)\\s*(${pass}|${fail})`,
|
|
65
|
+
'g',
|
|
66
|
+
)
|
|
67
|
+
|
|
30
68
|
function parseEvalBlocks(text: string): EvalBlock[] {
|
|
31
69
|
const blocks: EvalBlock[] = []
|
|
32
|
-
|
|
33
|
-
// Build one combined pattern: capture metric + valuetarget + pass/fail signal.
|
|
34
|
-
// Arrow variants: → (U+2192), -> (ASCII), => (ASCII)
|
|
35
|
-
// Pass variants: ✓ (U+2713), ✔ (U+2714), PASS (case-insensitive), Y, true, yes, 1
|
|
36
|
-
// Fail variants: ✗ (U+2717), ✘ (U+2718), FAIL (case-insensitive), N, false, no, 0
|
|
37
|
-
// NOTE: Removed the requirement for ">" prefix to allow EVAL blocks anywhere in text
|
|
38
|
-
const arrow = /(?:→|->|=>)/g.source
|
|
39
|
-
const pass = /(?:✓|✔|PASS|pass|Y\b|true|yes|1)/g.source
|
|
40
|
-
const fail = /(?:✗|✘|FAIL|fail|N\b|false|no|0)/g.source
|
|
41
|
-
const full = new RegExp(
|
|
42
|
-
`EVAL:\\s*(.+?):\\s*(.+?)\\s*(?:${arrow}|)\\s*(${pass}|${fail})`,
|
|
43
|
-
'g',
|
|
44
|
-
)
|
|
45
|
-
|
|
46
70
|
let match: RegExpExecArray | null
|
|
47
71
|
while ((match = full.exec(text)) !== null) {
|
|
48
72
|
const [, metric, valueTarget, signal] = match
|
|
@@ -52,12 +76,10 @@ function parseEvalBlocks(text: string): EvalBlock[] {
|
|
|
52
76
|
return blocks
|
|
53
77
|
}
|
|
54
78
|
|
|
55
|
-
/** Determine if all metrics pass — enabling early termination. */
|
|
56
79
|
function allMetricsPassing(blocks: EvalBlock[]): boolean {
|
|
57
80
|
return blocks.length > 0 && blocks.every(b => b.passed)
|
|
58
81
|
}
|
|
59
82
|
|
|
60
|
-
/** Extract structured EVAL summary from parsed blocks for consumption by evaluator model. */
|
|
61
83
|
function evalSummary(blocks: EvalBlock[]): string {
|
|
62
84
|
if (blocks.length === 0) return '(no EVAL blocks found)'
|
|
63
85
|
const passed = blocks.filter(b => b.passed).length
|
|
@@ -67,21 +89,94 @@ function evalSummary(blocks: EvalBlock[]): string {
|
|
|
67
89
|
].join('\n')
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
//
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Core evaluation
|
|
94
|
+
// ============================================================================
|
|
71
95
|
|
|
72
96
|
/**
|
|
73
|
-
*
|
|
97
|
+
* Evaluate goal progress with environment-aware assessment.
|
|
74
98
|
*
|
|
75
99
|
* Strategy:
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
100
|
+
* 1. Parse EVAL blocks from recent assistant output (fast, no API call)
|
|
101
|
+
* 2. If all metrics pass → short-circuit satisfied
|
|
102
|
+
* 3. Otherwise, build evaluation context and run rule checks
|
|
103
|
+
* 4. If still ambiguous, call Haiku for semantic evaluation
|
|
104
|
+
* 5. For critical goals, optionally escalate to main model
|
|
79
105
|
*/
|
|
80
106
|
export async function evaluateGoal(
|
|
81
107
|
goalCondition: string,
|
|
82
108
|
messages: MessageType[],
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
subGoal?: SubGoal,
|
|
110
|
+
): Promise<EvalResult> {
|
|
111
|
+
// Extract recent assistant messages for EVAL parsing
|
|
112
|
+
const recentAssistantTexts = extractAssistantTexts(messages)
|
|
113
|
+
|
|
114
|
+
// Phase 1: Parse EVAL blocks from agent self-reports
|
|
115
|
+
const evalBlocks = parseEvalBlocks(recentAssistantTexts)
|
|
116
|
+
|
|
117
|
+
// Short-circuit: all metrics pass → goal satisfied
|
|
118
|
+
if (evalBlocks.length > 0 && allMetricsPassing(evalBlocks)) {
|
|
119
|
+
return {
|
|
120
|
+
satisfied: true,
|
|
121
|
+
level: 'rule',
|
|
122
|
+
reason: `all ${evalBlocks.length} EVAL metrics satisfied`,
|
|
123
|
+
gap: null,
|
|
124
|
+
evidence: evalBlocks.map(b => ({
|
|
125
|
+
source: 'agent-output',
|
|
126
|
+
finding: `${b.metric}: ${b.valueTarget}`,
|
|
127
|
+
relevanceScore: 1.0,
|
|
128
|
+
})),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Phase 2: Build environment context
|
|
133
|
+
const ctx = await buildEvaluationContext(messages)
|
|
134
|
+
const evidenceFromEnv = gatherEvidence(ctx)
|
|
135
|
+
|
|
136
|
+
// Phase 3: Check if there's enough evidence to decide
|
|
137
|
+
if (evidenceFromEnv.length > 0) {
|
|
138
|
+
// Has the agent made concrete progress? Check key signals.
|
|
139
|
+
const hasConcreteProgress = checkConcreteProgress(evidenceFromEnv, goalCondition)
|
|
140
|
+
if (hasConcreteProgress.satisfied) {
|
|
141
|
+
return {
|
|
142
|
+
satisfied: true,
|
|
143
|
+
level: 'rule',
|
|
144
|
+
reason: 'Concrete progress detected from environment',
|
|
145
|
+
gap: null,
|
|
146
|
+
evidence: evidenceFromEnv,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Phase 4: Semantic evaluation with Haiku
|
|
152
|
+
const evalInput = buildEvalInput(goalCondition, evalBlocks, ctx, evidenceFromEnv, messages)
|
|
153
|
+
const semanticResult = await callHaiku(goalCondition, evalInput)
|
|
154
|
+
|
|
155
|
+
// If Haiku is confident either way, trust it
|
|
156
|
+
if (semanticResult.satisfied || (!semanticResult.satisfied && evalBlocks.length === 0)) {
|
|
157
|
+
return {
|
|
158
|
+
...semanticResult,
|
|
159
|
+
level: 'semantic',
|
|
160
|
+
evidence: [...evidenceFromEnv, ...semanticResult.evidence],
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Phase 5: Fallback — not satisfied, escalate to main model for final check
|
|
165
|
+
const finalResult = await callMainModel(goalCondition, ctx, evidenceFromEnv, messages)
|
|
166
|
+
return {
|
|
167
|
+
...finalResult,
|
|
168
|
+
level: 'final',
|
|
169
|
+
evidence: [...evidenceFromEnv, ...finalResult.evidence],
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Context builders
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
/** Extract text from the last 5 assistant messages for EVAL parsing. */
|
|
178
|
+
function extractAssistantTexts(messages: MessageType[]): string {
|
|
179
|
+
return messages
|
|
85
180
|
.filter(m => m.type === 'assistant' || m.role === 'assistant')
|
|
86
181
|
.slice(-5)
|
|
87
182
|
.map(m => {
|
|
@@ -96,123 +191,614 @@ export async function evaluateGoal(
|
|
|
96
191
|
})
|
|
97
192
|
.filter(Boolean)
|
|
98
193
|
.join('\n---\n')
|
|
194
|
+
}
|
|
99
195
|
|
|
100
|
-
|
|
101
|
-
|
|
196
|
+
/** Build a full evaluation context from recent messages. */
|
|
197
|
+
async function buildEvaluationContext(messages: MessageType[]): Promise<EvaluationContext> {
|
|
198
|
+
const toolResults = extractRecentToolResults(messages)
|
|
199
|
+
const fileDiffs = extractFileDiffs(messages)
|
|
200
|
+
const testOutputs = extractTestOutputs(messages)
|
|
201
|
+
const compileOutputs = extractCompileOutputs(messages)
|
|
202
|
+
const gitStatus = await getGitStatus()
|
|
203
|
+
const errorLogs = getRecentErrors()
|
|
102
204
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
205
|
+
const fileList = getTrackedFiles(messages)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
toolResults,
|
|
209
|
+
fileDiffs,
|
|
210
|
+
testOutputs,
|
|
211
|
+
compileOutputs,
|
|
212
|
+
gitStatus,
|
|
213
|
+
fileList,
|
|
214
|
+
errorLogs,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// Rule Engine — deterministic checks that produce evidence
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Built-in rule registry. Each rule is a pure function:
|
|
224
|
+
* (EvaluationContext, SubGoal) → EvalEvidence[]
|
|
225
|
+
*
|
|
226
|
+
* Rules are composable — the engine runs all registered rules and
|
|
227
|
+
* aggregates their output. New rules can be added by extending this
|
|
228
|
+
* registry (Phase 4: plugin system).
|
|
229
|
+
*/
|
|
230
|
+
const BUILT_IN_RULES: Record<string, (ctx: EvaluationContext, goal: import('../types/goal.js').SubGoal) => import('../types/goal.js').EvalEvidence[]> = {
|
|
231
|
+
|
|
232
|
+
// === File existence ===
|
|
233
|
+
|
|
234
|
+
/** Check if files mentioned in the goal exist on disk. Uses fs.accessSync
|
|
235
|
+
* to verify each file path without reading the content. */
|
|
236
|
+
fileExists(ctx: EvaluationContext, _goal: import('../types/goal.js').SubGoal): import('../types/goal.js').EvalEvidence[] {
|
|
237
|
+
const evidence: import('../types/goal.js').EvalEvidence[] = []
|
|
238
|
+
for (const filePath of ctx.fileList) {
|
|
239
|
+
try {
|
|
240
|
+
const { statSync } = require('fs') as typeof import('fs')
|
|
241
|
+
statSync(filePath)
|
|
242
|
+
evidence.push({
|
|
243
|
+
source: 'filesystem',
|
|
244
|
+
finding: `File exists: ${filePath}`,
|
|
245
|
+
relevanceScore: 0.8,
|
|
246
|
+
})
|
|
247
|
+
} catch {
|
|
248
|
+
// File doesn't exist — no evidence (missing file is not proof)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return evidence
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// === Test output ===
|
|
255
|
+
|
|
256
|
+
/** Check if any test output indicates passing tests. Parses stdout/stderr
|
|
257
|
+
* for pass/fail patterns and produces evidence accordingly. */
|
|
258
|
+
testsPass(ctx: EvaluationContext, _goal: import('../types/goal.js').SubGoal): import('../types/goal.js').EvalEvidence[] {
|
|
259
|
+
const evidence: import('../types/goal.js').EvalEvidence[] = []
|
|
260
|
+
for (const test of ctx.testOutputs) {
|
|
261
|
+
if (test.passed) {
|
|
262
|
+
evidence.push({
|
|
263
|
+
source: 'test-runner',
|
|
264
|
+
finding: `Tests passed: ${test.command} (exit ${test.exitCode})`,
|
|
265
|
+
relevanceScore: 0.9,
|
|
266
|
+
})
|
|
267
|
+
} else {
|
|
268
|
+
evidence.push({
|
|
269
|
+
source: 'test-runner',
|
|
270
|
+
finding: `Tests failed: ${test.command} (${test.stderr.slice(0, 200)})`,
|
|
271
|
+
relevanceScore: 0.8,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return evidence
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// === Compilation ===
|
|
279
|
+
|
|
280
|
+
/** Check if build/compilation succeeded. Looks for successful build outputs
|
|
281
|
+
* in the compileOutputs array. */
|
|
282
|
+
buildPassed(ctx: EvaluationContext, _goal: import('../types/goal.js').SubGoal): import('../types/goal.js').EvalEvidence[] {
|
|
283
|
+
const evidence: import('../types/goal.js').EvalEvidence[] = []
|
|
284
|
+
for (const compile of ctx.compileOutputs) {
|
|
285
|
+
if (compile.passed) {
|
|
286
|
+
evidence.push({
|
|
287
|
+
source: 'compiler',
|
|
288
|
+
finding: `Build passed: ${compile.command}`,
|
|
289
|
+
relevanceScore: 0.9,
|
|
290
|
+
})
|
|
291
|
+
} else {
|
|
292
|
+
evidence.push({
|
|
293
|
+
source: 'compiler',
|
|
294
|
+
finding: `Build failed: ${compile.command} — ${compile.stderr.slice(0, 200)}`,
|
|
295
|
+
relevanceScore: 0.7,
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return evidence
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// === Schema validation ===
|
|
303
|
+
|
|
304
|
+
/** Check if data schemas are valid. Currently checks TypeScript compilation
|
|
305
|
+
* errors in the error log and compile outputs as a proxy for schema validity.
|
|
306
|
+
* Future: integrate with actual schema validators (JSON Schema, Zod, etc.) */
|
|
307
|
+
schemaValid(ctx: EvaluationContext, _goal: import('../types/goal.js').SubGoal): import('../types/goal.js').EvalEvidence[] {
|
|
308
|
+
const evidence: import('../types/goal.js').EvalEvidence[] = []
|
|
309
|
+
// Check for TypeScript errors as a proxy for type-safety
|
|
310
|
+
for (const error of ctx.errorLogs) {
|
|
311
|
+
if (error.error.includes('error TS')) {
|
|
312
|
+
evidence.push({
|
|
313
|
+
source: 'type-checker',
|
|
314
|
+
finding: `TypeScript error: ${error.error.slice(0, 200)}`,
|
|
315
|
+
relevanceScore: 0.6,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return evidence
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Run all built-in rules against the current evaluation context and target
|
|
324
|
+
* sub-goal. Returns combined evidence from all rule checks.
|
|
325
|
+
*
|
|
326
|
+
* This is the engine that drives the rule-based evaluation tier.
|
|
327
|
+
* Future: add plugin registration for custom project-specific rules. */
|
|
328
|
+
function runRules(ctx: EvaluationContext, goal: import('../types/goal.js').SubGoal): import('../types/goal.js').EvalEvidence[] {
|
|
329
|
+
const results: import('../types/goal.js').EvalEvidence[] = []
|
|
330
|
+
|
|
331
|
+
// File existence rules
|
|
332
|
+
results.push(...BUILT_IN_RULES.fileExists(ctx, goal))
|
|
333
|
+
|
|
334
|
+
// Test output rules
|
|
335
|
+
results.push(...BUILT_IN_RULES.testsPass(ctx, goal))
|
|
336
|
+
|
|
337
|
+
// Build/compilation rules
|
|
338
|
+
results.push(...BUILT_IN_RULES.buildPassed(ctx, goal))
|
|
339
|
+
|
|
340
|
+
// Schema validation rules
|
|
341
|
+
results.push(...BUILT_IN_RULES.schemaValid(ctx, goal))
|
|
342
|
+
|
|
343
|
+
return results
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Evidence gathering
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
/** Generate a human-readable evaluation summary from context + evidence. */
|
|
351
|
+
function gatherEvidence(ctx: EvaluationContext): EvalEvidence[] {
|
|
352
|
+
const evidence: EvalEvidence[] = []
|
|
353
|
+
|
|
354
|
+
// Tool results → evidence
|
|
355
|
+
for (const tr of ctx.toolResults) {
|
|
356
|
+
if (!tr.success && tr.toolName !== 'Read') {
|
|
357
|
+
evidence.push({
|
|
358
|
+
source: tr.toolName,
|
|
359
|
+
finding: `Tool ${tr.toolName} failed: ${tr.toolOutput.slice(0, 200)}`,
|
|
360
|
+
relevanceScore: 0.7,
|
|
361
|
+
})
|
|
362
|
+
} else if (tr.success) {
|
|
363
|
+
evidence.push({
|
|
364
|
+
source: tr.toolName,
|
|
365
|
+
finding: `Tool ${tr.toolName} succeeded`,
|
|
366
|
+
relevanceScore: 0.5,
|
|
367
|
+
})
|
|
109
368
|
}
|
|
110
369
|
}
|
|
111
370
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
371
|
+
// File changes → evidence of progress
|
|
372
|
+
for (const diff of ctx.fileDiffs) {
|
|
373
|
+
if (diff.added > 0 || diff.changed > 0) {
|
|
374
|
+
evidence.push({
|
|
375
|
+
source: diff.path,
|
|
376
|
+
finding: `Changed ${diff.path}: +${diff.added} -${diff.removed}`,
|
|
377
|
+
relevanceScore: 0.6,
|
|
378
|
+
})
|
|
118
379
|
}
|
|
119
380
|
}
|
|
120
381
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
382
|
+
// Test outputs → strong signal
|
|
383
|
+
for (const test of ctx.testOutputs) {
|
|
384
|
+
evidence.push({
|
|
385
|
+
source: 'test-runner',
|
|
386
|
+
finding: test.passed
|
|
387
|
+
? `Tests passed: ${test.command}`
|
|
388
|
+
: `Tests failed: ${test.command} (exit ${test.exitCode})`,
|
|
389
|
+
relevanceScore: test.passed ? 0.9 : 0.8,
|
|
390
|
+
})
|
|
391
|
+
}
|
|
128
392
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
393
|
+
// Compile outputs → strong signal
|
|
394
|
+
for (const compile of ctx.compileOutputs) {
|
|
395
|
+
evidence.push({
|
|
396
|
+
source: 'compiler',
|
|
397
|
+
finding: compile.passed
|
|
398
|
+
? `Build passed: ${compile.command}`
|
|
399
|
+
: `Build failed: ${compile.command} — ${compile.stderr.slice(0, 200)}`,
|
|
400
|
+
relevanceScore: compile.passed ? 0.9 : 0.8,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
133
403
|
|
|
134
|
-
|
|
404
|
+
return evidence
|
|
405
|
+
}
|
|
135
406
|
|
|
136
|
-
|
|
407
|
+
// ============================================================================
|
|
408
|
+
// Haiku evaluation
|
|
409
|
+
// ============================================================================
|
|
137
410
|
|
|
138
|
-
|
|
139
|
-
|
|
411
|
+
async function callHaiku(
|
|
412
|
+
goalCondition: string,
|
|
413
|
+
evalInput: string,
|
|
414
|
+
): Promise<EvalResult> {
|
|
415
|
+
try {
|
|
416
|
+
const client = createClient()
|
|
417
|
+
const response = await client.messages.create({
|
|
418
|
+
model: GOAL_EVALUATOR_MODEL,
|
|
419
|
+
max_tokens: EVAL_MAX_TOKENS,
|
|
420
|
+
system: `You are a goal evaluator. Determine if the goal "${goalCondition}" is satisfied based on the evidence provided. Respond ONLY with valid JSON.`,
|
|
421
|
+
messages: [{ role: 'user', content: evalInput.slice(0, 4000) }],
|
|
422
|
+
})
|
|
140
423
|
|
|
141
|
-
|
|
424
|
+
const text = response.content.find((b: any) => b.type === 'text')?.text || ''
|
|
425
|
+
const parsed = parseEvalResponse(text)
|
|
426
|
+
return {
|
|
427
|
+
satisfied: parsed.satisfied ?? false,
|
|
428
|
+
level: 'semantic',
|
|
429
|
+
reason: parsed.reason ?? 'No reason provided',
|
|
430
|
+
gap: parsed.gap ?? null,
|
|
431
|
+
evidence: [],
|
|
432
|
+
}
|
|
433
|
+
} catch (err) {
|
|
434
|
+
return {
|
|
435
|
+
satisfied: false,
|
|
436
|
+
level: 'semantic',
|
|
437
|
+
reason: `Evaluator API error: ${err instanceof Error ? err.message : String(err)}`,
|
|
438
|
+
gap: 'API call failed',
|
|
439
|
+
evidence: [],
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ============================================================================
|
|
445
|
+
// Main model evaluation (final tier)
|
|
446
|
+
// ============================================================================
|
|
142
447
|
|
|
143
|
-
|
|
448
|
+
async function callMainModel(
|
|
449
|
+
goalCondition: string,
|
|
450
|
+
ctx: EvaluationContext,
|
|
451
|
+
evidence: EvalEvidence[],
|
|
452
|
+
messages: MessageType[],
|
|
453
|
+
): Promise<EvalResult> {
|
|
144
454
|
try {
|
|
455
|
+
const client = createClient()
|
|
456
|
+
const summary = buildEvaluationSummary(goalCondition, ctx, evidence, messages)
|
|
145
457
|
const response = await client.messages.create({
|
|
146
|
-
model:
|
|
147
|
-
max_tokens:
|
|
148
|
-
|
|
458
|
+
model: getMainModel(),
|
|
459
|
+
max_tokens: EVAL_MAX_TOKENS,
|
|
460
|
+
system: `You are a goal evaluator. Assess whether the goal "${goalCondition}" is achieved based on the following evidence. Respond ONLY with valid JSON ({"satisfied": true/false, "reason": "...", "gap": "..."}). Be thorough — this is the final check before marking the goal complete.`,
|
|
461
|
+
messages: [{ role: 'user', content: summary.slice(0, 4000) }],
|
|
149
462
|
})
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
463
|
+
|
|
464
|
+
const text = response.content.find((b: any) => b.type === 'text')?.text || ''
|
|
465
|
+
const parsed = parseEvalResponse(text)
|
|
466
|
+
return {
|
|
467
|
+
satisfied: parsed.satisfied ?? false,
|
|
468
|
+
level: 'final',
|
|
469
|
+
reason: parsed.reason ?? 'No reason provided',
|
|
470
|
+
gap: parsed.gap ?? null,
|
|
471
|
+
evidence,
|
|
472
|
+
}
|
|
473
|
+
} catch (err) {
|
|
153
474
|
return {
|
|
154
475
|
satisfied: false,
|
|
155
|
-
|
|
156
|
-
|
|
476
|
+
level: 'final',
|
|
477
|
+
reason: `Final evaluator error: ${err instanceof Error ? err.message : String(err)}`,
|
|
478
|
+
gap: 'API call failed',
|
|
479
|
+
evidence,
|
|
157
480
|
}
|
|
158
481
|
}
|
|
482
|
+
}
|
|
159
483
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// Helpers
|
|
486
|
+
// ============================================================================
|
|
487
|
+
|
|
488
|
+
/** Get the main model name from environment or default. */
|
|
489
|
+
function getMainModel(): string {
|
|
490
|
+
return process.env.ANTHROPIC_MODEL ?? 'claude-sonnet-4-6-20250514'
|
|
491
|
+
}
|
|
167
492
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
493
|
+
/** Parse evaluation response JSON. Handles markdown and prefix text. */
|
|
494
|
+
function parseEvalResponse(text: string): { satisfied?: boolean; reason?: string; gap?: string | null } {
|
|
495
|
+
try {
|
|
496
|
+
let cleaned = text
|
|
497
|
+
.replace(/```(?:json)?\s*/gi, '')
|
|
498
|
+
.replace(/```/g, '')
|
|
499
|
+
.trim()
|
|
500
|
+
const start = cleaned.indexOf('{')
|
|
501
|
+
const end = cleaned.lastIndexOf('}')
|
|
502
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
177
503
|
cleaned = cleaned.slice(start, end + 1)
|
|
178
504
|
const parsed = JSON.parse(cleaned)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
505
|
+
return {
|
|
506
|
+
satisfied: !!parsed.satisfied,
|
|
507
|
+
reason: parsed.reason || '',
|
|
508
|
+
gap: parsed.gap || null,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return {}
|
|
512
|
+
} catch {
|
|
513
|
+
return {}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Build comprehensive evaluation input for Haiku from context + evidence. */
|
|
518
|
+
function buildEvalInput(
|
|
519
|
+
goalCondition: string,
|
|
520
|
+
evalBlocks: EvalBlock[],
|
|
521
|
+
ctx: EvaluationContext,
|
|
522
|
+
evidence: EvalEvidence[],
|
|
523
|
+
messages: MessageType[],
|
|
524
|
+
): string {
|
|
525
|
+
const parts: string[] = []
|
|
526
|
+
|
|
527
|
+
// EVAL blocks summary
|
|
528
|
+
if (evalBlocks.length > 0) {
|
|
529
|
+
parts.push(evalSummary(evalBlocks))
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Evidence summary
|
|
533
|
+
if (evidence.length > 0) {
|
|
534
|
+
parts.push(`Environmental evidence (${evidence.length} findings):`)
|
|
535
|
+
parts.push(...evidence.map(e => `- ${e.source}: ${e.finding} [${e.relevanceScore.toFixed(1)}]`))
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Recent agent output
|
|
539
|
+
const recentTexts = extractAssistantTexts(messages).slice(-2000)
|
|
540
|
+
if (recentTexts) {
|
|
541
|
+
parts.push(`Recent agent output:\n${recentTexts}`)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return parts.join('\n')
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ============================================================================
|
|
548
|
+
// Environment extraction helpers (from messages)
|
|
549
|
+
// ============================================================================
|
|
550
|
+
|
|
551
|
+
function extractRecentToolResults(messages: MessageType[]): ToolResult[] {
|
|
552
|
+
const results: ToolResult[] = []
|
|
553
|
+
for (const msg of messages) {
|
|
554
|
+
if (msg.type === 'assistant' || msg.role === 'assistant') {
|
|
555
|
+
const content = msg.message?.content
|
|
556
|
+
if (Array.isArray(content)) {
|
|
557
|
+
for (const block of content) {
|
|
558
|
+
if (block.type === 'tool_result') {
|
|
559
|
+
results.push({
|
|
560
|
+
toolName: block.tool_name ?? 'unknown',
|
|
561
|
+
toolInput: (block as any).tool_input ?? {},
|
|
562
|
+
toolOutput: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
|
|
563
|
+
success: !(block as any).is_error,
|
|
564
|
+
timestamp: Date.now(),
|
|
565
|
+
})
|
|
566
|
+
}
|
|
187
567
|
}
|
|
188
568
|
}
|
|
189
|
-
return { ok: false }
|
|
190
|
-
} catch {
|
|
191
|
-
return { ok: false }
|
|
192
569
|
}
|
|
193
570
|
}
|
|
571
|
+
return results
|
|
572
|
+
}
|
|
194
573
|
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
574
|
+
function extractFileDiffs(messages: MessageType[]): FileDiff[] {
|
|
575
|
+
const diffs: FileDiff[] = []
|
|
576
|
+
for (const msg of messages) {
|
|
577
|
+
if (msg.type === 'assistant' || msg.role === 'assistant') {
|
|
578
|
+
const content = msg.message?.content
|
|
579
|
+
if (Array.isArray(content)) {
|
|
580
|
+
for (const block of content) {
|
|
581
|
+
if (block.type === 'tool_result' && block.tool_name === 'Bash') {
|
|
582
|
+
const text = typeof block.content === 'string' ? block.content : ''
|
|
583
|
+
if (text.includes('diff --git')) {
|
|
584
|
+
diffs.push(parseDiffLine(text))
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (block.type === 'text') {
|
|
588
|
+
const text = block.text
|
|
589
|
+
if (text.includes('added') && text.includes('removed')) {
|
|
590
|
+
diffs.push(parseEditStats(text))
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return diffs
|
|
598
|
+
}
|
|
198
599
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
(lower.includes('satisfied') && lower.includes('true')) ||
|
|
204
|
-
/goal\s+is\s+(?:met|satisfied|achieved)/.test(lower) ||
|
|
205
|
-
/condition\s+is\s+(?:met|satisfied|fulfilled)/.test(lower)
|
|
600
|
+
/** Parse a git diff --stat line into FileDiff counts. */
|
|
601
|
+
function parseDiffLine(text: string): FileDiff {
|
|
602
|
+
const match = text.match(/^diff --git a\/(.*) b\/(.*)$/m)
|
|
603
|
+
let added = 0; let removed = 0; let changed = 0
|
|
206
604
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
605
|
+
for (const line of text.split('\n')) {
|
|
606
|
+
if (line.startsWith('+++') || line.startsWith('---')) continue
|
|
607
|
+
if (line.startsWith('+')) added++
|
|
608
|
+
else if (line.startsWith('-')) removed++
|
|
609
|
+
else changed++
|
|
211
610
|
}
|
|
212
611
|
|
|
213
612
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
613
|
+
path: match?.[1] ?? 'unknown',
|
|
614
|
+
added,
|
|
615
|
+
removed,
|
|
616
|
+
changed,
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function extractTestOutputs(messages: MessageType[]): TestOutput[] {
|
|
621
|
+
const outputs: TestOutput[] = []
|
|
622
|
+
for (const msg of messages) {
|
|
623
|
+
if (msg.type === 'user' || msg.role === 'user') {
|
|
624
|
+
// Check user messages for injected test output (from test runner hooks)
|
|
625
|
+
const text = typeof msg.message?.content === 'string'
|
|
626
|
+
? msg.message.content
|
|
627
|
+
: Array.isArray(msg.message?.content)
|
|
628
|
+
? msg.message.content.map((b: any) => b.text ?? '').join('\n')
|
|
629
|
+
: ''
|
|
630
|
+
if (text.includes('PASS') || text.includes('FAIL')) {
|
|
631
|
+
outputs.push({
|
|
632
|
+
command: extractCommand(text),
|
|
633
|
+
exitCode: text.includes('FAIL') ? 1 : 0,
|
|
634
|
+
stdout: text,
|
|
635
|
+
stderr: '',
|
|
636
|
+
passed: !text.includes('FAIL'),
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
}
|
|
217
640
|
}
|
|
218
|
-
|
|
641
|
+
return outputs
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function extractCompileOutputs(messages: MessageType[]): CompileOutput[] {
|
|
645
|
+
const outputs: CompileOutput[] = []
|
|
646
|
+
for (const msg of messages) {
|
|
647
|
+
if (msg.type === 'user' || msg.role === 'user') {
|
|
648
|
+
const text = typeof msg.message?.content === 'string'
|
|
649
|
+
? msg.message.content
|
|
650
|
+
: ''
|
|
651
|
+
if (text.includes('error TS') || text.includes('Error: ') || text.includes('BUILD FAILED')) {
|
|
652
|
+
outputs.push({
|
|
653
|
+
command: extractCommand(text),
|
|
654
|
+
exitCode: 1,
|
|
655
|
+
stdout: '',
|
|
656
|
+
stderr: text,
|
|
657
|
+
passed: false,
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return outputs
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Extract the command that produced this output. */
|
|
666
|
+
function extractCommand(text: string): string {
|
|
667
|
+
const match = text.match(/^(?:> |\$ )?(.+?)(?:\n|$)/)
|
|
668
|
+
return match?.[1] ?? 'unknown'
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Parse edit stats from agent text (e.g., "Modified 3 files: +45 -12"). */
|
|
672
|
+
function parseEditStats(text: string): FileDiff {
|
|
673
|
+
const m = text.match(/\+(\d+)\s*-\s*(\d+)/)
|
|
674
|
+
return {
|
|
675
|
+
path: 'unknown',
|
|
676
|
+
added: m ? parseInt(m[1]) : 0,
|
|
677
|
+
removed: m ? parseInt(m[2]) : 0,
|
|
678
|
+
changed: 0,
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ============================================================================
|
|
683
|
+
// Git status
|
|
684
|
+
// ============================================================================
|
|
685
|
+
|
|
686
|
+
async function getGitStatus(): Promise<GitStatus> {
|
|
687
|
+
try {
|
|
688
|
+
// Use child_process directly to avoid circular imports
|
|
689
|
+
const { execSync } = await import('child_process')
|
|
690
|
+
const output = execSync('git status --porcelain', { encoding: 'utf8', cwd: process.cwd() })
|
|
691
|
+
const lines = output.trim().split('\n').filter(Boolean)
|
|
692
|
+
return {
|
|
693
|
+
staged: lines.filter(l => l[0] !== ' ' && l[1] !== ' ').map(l => l.slice(3)),
|
|
694
|
+
unstaged: lines.filter(l => l[0] === ' ' || l[1] === ' ').map(l => l.slice(3)),
|
|
695
|
+
untracked: lines.filter(l => l.startsWith('??')).map(l => l.slice(3)),
|
|
696
|
+
branch: execSync('git branch --show-current', { encoding: 'utf8' }).trim(),
|
|
697
|
+
clean: lines.length === 0,
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
return { staged: [], unstaged: [], untracked: [], branch: '', clean: true }
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ============================================================================
|
|
705
|
+
// System state accessors
|
|
706
|
+
// ============================================================================
|
|
707
|
+
|
|
708
|
+
/** Get tracked files from goal store artifacts and git status. */
|
|
709
|
+
function getTrackedFiles(messages: MessageType[]): string[] {
|
|
710
|
+
// Combine artifacts from goal store with any files mentioned in recent tool calls
|
|
711
|
+
const files = new Set<string>()
|
|
712
|
+
for (const msg of messages) {
|
|
713
|
+
if (msg.type === 'user' || msg.role === 'user') continue
|
|
714
|
+
const content = msg.message?.content
|
|
715
|
+
if (Array.isArray(content)) {
|
|
716
|
+
for (const block of content) {
|
|
717
|
+
if (block.type === 'tool_result') {
|
|
718
|
+
// Extract file paths from tool results (e.g., "Wrote to file.txt")
|
|
719
|
+
const text = typeof block.content === 'string' ? block.content : ''
|
|
720
|
+
const paths = extractFilePaths(text)
|
|
721
|
+
for (const p of paths) files.add(p)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return Array.from(files)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Extract file paths from tool output text. */
|
|
730
|
+
function extractFilePaths(text: string): string[] {
|
|
731
|
+
const paths: string[] = []
|
|
732
|
+
// Match patterns like "Wrote contents to /path/to/file.txt"
|
|
733
|
+
const wroteMatch = /(?:Wrote|Saved|Created)\s+(?:contents\s+)?(?:to\s+)?([^\s]+)/gi
|
|
734
|
+
let match: RegExpExecArray | null
|
|
735
|
+
while ((match = wroteMatch.exec(text)) !== null) {
|
|
736
|
+
paths.push(match[1])
|
|
737
|
+
}
|
|
738
|
+
return paths
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** Get recent errors from the in-memory error log ring buffer. */
|
|
742
|
+
function getRecentErrors(): ErrorLog[] {
|
|
743
|
+
try {
|
|
744
|
+
// Access the global STATE via bootstrap
|
|
745
|
+
const { getState } = require('../bootstrap/state.js')
|
|
746
|
+
// The inMemoryErrorLog field exists on the STATE object
|
|
747
|
+
const errors = (getState() as any).inMemoryErrorLog ?? []
|
|
748
|
+
return errors.slice(-20).map((e: any) => ({ error: e.error, timestamp: e.timestamp }))
|
|
749
|
+
} catch {
|
|
750
|
+
return []
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ============================================================================
|
|
755
|
+
// Progress checking
|
|
756
|
+
// ============================================================================
|
|
757
|
+
|
|
758
|
+
/** Check if there's concrete progress in the environment that indicates
|
|
759
|
+
* the goal is likely achieved. */
|
|
760
|
+
function checkConcreteProgress(
|
|
761
|
+
evidence: EvalEvidence[],
|
|
762
|
+
goalCondition: string,
|
|
763
|
+
): { satisfied: boolean; reason: string } {
|
|
764
|
+
// If no evidence at all, can't claim progress
|
|
765
|
+
if (evidence.length === 0) {
|
|
766
|
+
return { satisfied: false, reason: 'No environmental evidence found' }
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Check for strong positive signals
|
|
770
|
+
const hasSuccessfulTool = evidence.some(e => e.relevanceScore >= 0.8)
|
|
771
|
+
const hasCodeChange = evidence.some(e => e.source === 'test-runner' || e.source === 'compiler')
|
|
772
|
+
const hasFileOutput = evidence.some(e => e.source !== 'agent-output')
|
|
773
|
+
|
|
774
|
+
if (hasSuccessfulTool || hasCodeChange || hasFileOutput) {
|
|
775
|
+
return { satisfied: true, reason: `Concrete progress detected (${evidence.length} signals)` }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return { satisfied: false, reason: 'Insufficient concrete progress evidence' }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ============================================================================
|
|
782
|
+
// State accessors (delegate to goal store)
|
|
783
|
+
// ============================================================================
|
|
784
|
+
|
|
785
|
+
/** Get the current goal state for evaluation context building. */
|
|
786
|
+
function getGoalStateSnapshot(): GoalState {
|
|
787
|
+
try {
|
|
788
|
+
return getGoalState()
|
|
789
|
+
} catch {
|
|
790
|
+
return {
|
|
791
|
+
activeGoalId: null,
|
|
792
|
+
userGoal: null,
|
|
793
|
+
operationalGoal: null,
|
|
794
|
+
dag: { nodes: new Map(), edges: [], readyQueue: [] },
|
|
795
|
+
immediateGoal: null,
|
|
796
|
+
progress: 0,
|
|
797
|
+
artifacts: [],
|
|
798
|
+
metrics: { totalSubGoals: 0, completedSubGoals: 0, iterationCount: 0, maxIterations: 20, evalHistory: { lastGap: null, consecutiveSameGapCount: 0 } },
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Keep backwards compatibility with existing exports used by useGoalEvaluator.ts
|
|
804
|
+
export { parseEvalBlocks as parseEvalBlocks_legacy, allMetricsPassing, evalSummary }
|