claude-flow 3.10.4 → 3.10.5
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/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent-planning.smoke.d.ts +18 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent-planning.smoke.js +253 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent.d.ts +198 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent.js +651 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.d.ts +133 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.js +281 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.smoke.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.smoke.js +300 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.d.ts +138 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.js +260 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.smoke.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.smoke.js +246 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.d.ts +123 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.js +312 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.smoke.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.smoke.js +327 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.d.ts +125 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.js +350 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.smoke.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.smoke.js +228 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-e2e-smoke.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-e2e-smoke.js +136 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-extract.smoke.d.ts +45 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-extract.smoke.js +242 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/features.d.ts +46 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/features.js +170 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.d.ts +105 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.js +260 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.smoke.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.smoke.js +235 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/train-data-loader.d.ts +51 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/train-data-loader.js +179 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-judge.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-judge.js +437 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-loader.d.ts +87 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-loader.js +326 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/file_read.d.ts +35 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/file_read.js +403 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/grounded_query.d.ts +126 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/grounded_query.js +225 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/index.d.ts +32 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/index.js +36 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/types.d.ts +62 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/types.js +12 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/web_search.d.ts +30 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/web_search.js +210 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.js +297 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.smoke.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.smoke.js +332 -0
- package/v3/@claude-flow/cli/dist/src/commands/gaia-bench.d.ts +40 -0
- package/v3/@claude-flow/cli/dist/src/commands/gaia-bench.js +597 -0
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +41 -4
- package/v3/@claude-flow/cli/dist/src/init/executor.js +25 -6
- package/v3/@claude-flow/cli/dist/src/init/mcp-generator.js +9 -4
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests — ADR-135 Track I: Causal failure-avoidance edges
|
|
3
|
+
*
|
|
4
|
+
* All I/O is mocked (no real filesystem or network calls).
|
|
5
|
+
*
|
|
6
|
+
* Tests:
|
|
7
|
+
* 1. Record one failure → retrieve same-signature question → returns hint
|
|
8
|
+
* 2. Record 3 failures → retrieve unrelated question → returns empty hint
|
|
9
|
+
* 3. Same edge recorded twice → occurrenceCount=2, not duplicated
|
|
10
|
+
* 4. File doesn't exist → graceful empty result on retrieve
|
|
11
|
+
* 5. Corrupted JSONL line → skipped, doesn't crash
|
|
12
|
+
* 6. maxEdgesPerSignature cap respected (no new edge after cap)
|
|
13
|
+
* 7. Signature computation is deterministic (same input → same output)
|
|
14
|
+
* 8. Correct answer trajectory → no edges recorded
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* npx tsx src/benchmarks/gaia-causal-memory.smoke.ts
|
|
18
|
+
*
|
|
19
|
+
* Refs: ADR-135, #2156
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as os from 'node:os';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { computeQuestionSignature, inferFailureType, recordCausalFailures, retrieveCausalHints, } from './gaia-causal-memory.js';
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Mini test runner (zero external deps)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
const results = [];
|
|
31
|
+
async function test(name, fn) {
|
|
32
|
+
try {
|
|
33
|
+
await fn();
|
|
34
|
+
passed++;
|
|
35
|
+
results.push({ name, ok: true });
|
|
36
|
+
console.log(` PASS ${name}`);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
failed++;
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
results.push({ name, ok: false, error: msg });
|
|
42
|
+
console.error(` FAIL ${name}`);
|
|
43
|
+
console.error(` ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assert(condition, message) {
|
|
47
|
+
if (!condition) {
|
|
48
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function assertEqual(actual, expected, label) {
|
|
52
|
+
if (actual !== expected) {
|
|
53
|
+
throw new Error(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Fixtures
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
function makeQuestion(text) {
|
|
60
|
+
return {
|
|
61
|
+
task_id: 'smoke-' + text.slice(0, 8),
|
|
62
|
+
level: 1,
|
|
63
|
+
question: text,
|
|
64
|
+
final_answer: 'ignored-in-smoke',
|
|
65
|
+
file_name: null,
|
|
66
|
+
file_path: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function makeResult(overrides = {}) {
|
|
70
|
+
return {
|
|
71
|
+
questionId: 'smoke-q1',
|
|
72
|
+
finalAnswer: 'wrong answer',
|
|
73
|
+
turns: 3,
|
|
74
|
+
toolCallsByName: { web_search: 2 },
|
|
75
|
+
totalInputTokens: 100,
|
|
76
|
+
totalOutputTokens: 50,
|
|
77
|
+
wallMs: 1500,
|
|
78
|
+
...overrides,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Test suite
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
async function runAll() {
|
|
85
|
+
console.log('\nADR-135 Track I — causal failure-avoidance edges (smoke)\n');
|
|
86
|
+
// We use a fresh temp dir per test group to isolate state.
|
|
87
|
+
const tmpBase = path.join(os.tmpdir(), 'ruflo-causal-smoke-' + process.pid);
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
// Test 1: Record one failure → retrieve same-signature question → hint returned
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
await test('1. record one failure → retrieve same question → hint returned', async () => {
|
|
92
|
+
const dir = path.join(tmpBase, 't1');
|
|
93
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
94
|
+
const storePath = path.join(dir, 'edges.jsonl');
|
|
95
|
+
const question = makeQuestion('What is the capital of France?');
|
|
96
|
+
const result = makeResult({ finalAnswer: 'London' });
|
|
97
|
+
const recRes = await recordCausalFailures(question, result, false, { storePath });
|
|
98
|
+
assert(recRes.edgesRecorded > 0, 'should have recorded at least one edge');
|
|
99
|
+
const retRes = await retrieveCausalHints(question, { storePath });
|
|
100
|
+
assert(retRes.edgesMatched > 0, 'should match the stored edge');
|
|
101
|
+
assert(retRes.hint.length > 0, 'hint should be non-empty');
|
|
102
|
+
assert(retRes.hint.includes('[PRIOR FAILURES]'), 'hint should contain [PRIOR FAILURES] header');
|
|
103
|
+
assert(retRes.hint.includes('web_search'), 'hint should mention the failed tool');
|
|
104
|
+
});
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// Test 2: Record 3 failures → retrieve unrelated question → empty hint
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
await test('2. record 3 failures → retrieve unrelated question → empty hint', async () => {
|
|
109
|
+
const dir = path.join(tmpBase, 't2');
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
const storePath = path.join(dir, 'edges.jsonl');
|
|
112
|
+
const questions = [
|
|
113
|
+
makeQuestion('What is the capital of France?'),
|
|
114
|
+
makeQuestion('Who wrote Hamlet?'),
|
|
115
|
+
makeQuestion('What is 2 + 2?'),
|
|
116
|
+
];
|
|
117
|
+
for (const q of questions) {
|
|
118
|
+
await recordCausalFailures(q, makeResult(), false, { storePath });
|
|
119
|
+
}
|
|
120
|
+
const unrelated = makeQuestion('Completely different and unrelated question about astrophysics and quantum phenomena XYZ123');
|
|
121
|
+
const retRes = await retrieveCausalHints(unrelated, { storePath });
|
|
122
|
+
assertEqual(retRes.edgesMatched, 0, 'edgesMatched for unrelated question');
|
|
123
|
+
assertEqual(retRes.hint, '', 'hint for unrelated question');
|
|
124
|
+
});
|
|
125
|
+
// -------------------------------------------------------------------------
|
|
126
|
+
// Test 3: Same edge recorded twice → occurrenceCount=2, not duplicated
|
|
127
|
+
// -------------------------------------------------------------------------
|
|
128
|
+
await test('3. same edge recorded twice → occurrenceCount=2, not duplicated', async () => {
|
|
129
|
+
const dir = path.join(tmpBase, 't3');
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
const storePath = path.join(dir, 'edges.jsonl');
|
|
132
|
+
const question = makeQuestion('What year did World War II end?');
|
|
133
|
+
const result = makeResult({ toolCallsByName: { web_search: 1 } });
|
|
134
|
+
// Record same failure twice
|
|
135
|
+
await recordCausalFailures(question, result, false, { storePath });
|
|
136
|
+
await recordCausalFailures(question, result, false, { storePath });
|
|
137
|
+
// Read the JSONL directly and count lines / occurrence counts
|
|
138
|
+
const raw = fs.readFileSync(storePath, 'utf8');
|
|
139
|
+
const lines = raw.trim().split('\n').filter((l) => l.trim() !== '');
|
|
140
|
+
const edges = lines.map((l) => JSON.parse(l));
|
|
141
|
+
// Should have exactly one edge for this signature+tool+step
|
|
142
|
+
const sig = computeQuestionSignature(question.question);
|
|
143
|
+
const sigEdges = edges.filter((e) => e.questionSignature === sig && e.failedTool === 'web_search');
|
|
144
|
+
assertEqual(sigEdges.length, 1, 'should have exactly one edge per signature+tool+step');
|
|
145
|
+
assertEqual(sigEdges[0].occurrenceCount, 2, 'occurrenceCount after two recordings');
|
|
146
|
+
});
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
// Test 4: File doesn't exist → graceful empty result on retrieve
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
await test('4. file does not exist → graceful empty result on retrieve', async () => {
|
|
151
|
+
const storePath = path.join(tmpBase, 'nonexistent', 'edges.jsonl');
|
|
152
|
+
const question = makeQuestion('How many legs does a spider have?');
|
|
153
|
+
const retRes = await retrieveCausalHints(question, { storePath });
|
|
154
|
+
assertEqual(retRes.edgesMatched, 0, 'edgesMatched when file absent');
|
|
155
|
+
assertEqual(retRes.hint, '', 'hint when file absent');
|
|
156
|
+
});
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
// Test 5: Corrupted JSONL line → skipped, doesn't crash
|
|
159
|
+
// -------------------------------------------------------------------------
|
|
160
|
+
await test('5. corrupted JSONL line → skipped, does not crash', async () => {
|
|
161
|
+
const dir = path.join(tmpBase, 't5');
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
163
|
+
const storePath = path.join(dir, 'edges.jsonl');
|
|
164
|
+
const goodQuestion = makeQuestion('Name the largest planet in the solar system.');
|
|
165
|
+
const goodEdge = {
|
|
166
|
+
questionSignature: computeQuestionSignature(goodQuestion.question),
|
|
167
|
+
failedTool: 'python_exec',
|
|
168
|
+
failedTrajectoryStep: 'python_exec called once; failure type=wrong_answer',
|
|
169
|
+
observedFailureType: 'wrong_answer',
|
|
170
|
+
createdAt: new Date().toISOString(),
|
|
171
|
+
occurrenceCount: 1,
|
|
172
|
+
};
|
|
173
|
+
// Write one valid edge + one corrupted line
|
|
174
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
175
|
+
fs.writeFileSync(storePath, JSON.stringify(goodEdge) + '\n' +
|
|
176
|
+
'{"this": is not valid JSON!!!\n' +
|
|
177
|
+
'{"partial":true\n', 'utf8');
|
|
178
|
+
// retrieve should work and not throw
|
|
179
|
+
const retRes = await retrieveCausalHints(goodQuestion, { storePath });
|
|
180
|
+
assertEqual(retRes.edgesMatched, 1, 'should still find the one valid edge');
|
|
181
|
+
assert(retRes.hint.includes('[PRIOR FAILURES]'), 'hint should be returned for valid edge');
|
|
182
|
+
assert(retRes.hint.includes('python_exec'), 'hint should reference python_exec');
|
|
183
|
+
});
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
// Test 6: maxEdgesPerSignature cap respected
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
await test('6. maxEdgesPerSignature cap respected', async () => {
|
|
188
|
+
const dir = path.join(tmpBase, 't6');
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
const storePath = path.join(dir, 'edges.jsonl');
|
|
191
|
+
const maxEdgesPerSignature = 2;
|
|
192
|
+
const question = makeQuestion('Explain the theory of relativity in simple terms.');
|
|
193
|
+
// Record 3 failures with different tools — only first 2 should be stored
|
|
194
|
+
for (const tool of ['web_search', 'file_read', 'python_exec']) {
|
|
195
|
+
const result = makeResult({ toolCallsByName: { [tool]: 1 } });
|
|
196
|
+
await recordCausalFailures(question, result, false, {
|
|
197
|
+
storePath,
|
|
198
|
+
maxEdgesPerSignature,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const raw = fs.readFileSync(storePath, 'utf8');
|
|
202
|
+
const lines = raw.trim().split('\n').filter((l) => l.trim() !== '');
|
|
203
|
+
const sig = computeQuestionSignature(question.question);
|
|
204
|
+
const sigEdges = lines
|
|
205
|
+
.map((l) => JSON.parse(l))
|
|
206
|
+
.filter((e) => e.questionSignature === sig);
|
|
207
|
+
assert(sigEdges.length <= maxEdgesPerSignature, `should have at most ${maxEdgesPerSignature} edges, got ${sigEdges.length}`);
|
|
208
|
+
});
|
|
209
|
+
// -------------------------------------------------------------------------
|
|
210
|
+
// Test 7: Signature computation is deterministic
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
await test('7. signature computation is deterministic', async () => {
|
|
213
|
+
const text = 'What is the boiling point of water at sea level?';
|
|
214
|
+
const sig1 = computeQuestionSignature(text);
|
|
215
|
+
const sig2 = computeQuestionSignature(text);
|
|
216
|
+
const sig3 = computeQuestionSignature(text.trim()); // trim should not change result
|
|
217
|
+
const sig4 = computeQuestionSignature(text.toUpperCase().toLowerCase()); // normalisation
|
|
218
|
+
assertEqual(sig1, sig2, 'same input produces same signature (call 1 vs 2)');
|
|
219
|
+
assertEqual(sig1, sig3, 'trimmed input produces same signature');
|
|
220
|
+
assertEqual(sig1, sig4, 'case-normalised input produces same signature');
|
|
221
|
+
assertEqual(sig1.length, 16, 'signature is 16 hex characters');
|
|
222
|
+
// Different text should produce different signature (with overwhelming probability)
|
|
223
|
+
const sigOther = computeQuestionSignature('Completely different question about astronomy.');
|
|
224
|
+
assert(sig1 !== sigOther, 'different inputs produce different signatures');
|
|
225
|
+
});
|
|
226
|
+
// -------------------------------------------------------------------------
|
|
227
|
+
// Test 8: Correct answer trajectory → no edges recorded
|
|
228
|
+
// -------------------------------------------------------------------------
|
|
229
|
+
await test('8. correct answer trajectory → no edges recorded', async () => {
|
|
230
|
+
const dir = path.join(tmpBase, 't8');
|
|
231
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
232
|
+
const storePath = path.join(dir, 'edges.jsonl');
|
|
233
|
+
const question = makeQuestion('What color is the sky on a clear day?');
|
|
234
|
+
const result = makeResult({ finalAnswer: 'blue' });
|
|
235
|
+
// wasCorrect = true
|
|
236
|
+
const recRes = await recordCausalFailures(question, result, true, { storePath });
|
|
237
|
+
assertEqual(recRes.edgesRecorded, 0, 'no edges for correct trajectory');
|
|
238
|
+
assert(!fs.existsSync(storePath), 'store file should not be created for correct answer');
|
|
239
|
+
});
|
|
240
|
+
// -------------------------------------------------------------------------
|
|
241
|
+
// Bonus: inferFailureType unit checks
|
|
242
|
+
// -------------------------------------------------------------------------
|
|
243
|
+
await test('inferFailureType: null for correct answers', async () => {
|
|
244
|
+
const result = makeResult({ finalAnswer: 'blue' });
|
|
245
|
+
const ft = inferFailureType(result, true);
|
|
246
|
+
assertEqual(ft, null, 'inferFailureType returns null for wasCorrect=true');
|
|
247
|
+
});
|
|
248
|
+
await test('inferFailureType: timeout when timedOut=true', async () => {
|
|
249
|
+
const result = makeResult({ timedOut: true });
|
|
250
|
+
const ft = inferFailureType(result, false);
|
|
251
|
+
assertEqual(ft, 'timeout', 'inferFailureType=timeout when timedOut flag set');
|
|
252
|
+
});
|
|
253
|
+
await test('inferFailureType: tool_error when error present', async () => {
|
|
254
|
+
const result = makeResult({ error: 'network timeout' });
|
|
255
|
+
const ft = inferFailureType(result, false);
|
|
256
|
+
assertEqual(ft, 'tool_error', 'inferFailureType=tool_error when error field set');
|
|
257
|
+
});
|
|
258
|
+
await test('inferFailureType: empty_result when finalAnswer is null', async () => {
|
|
259
|
+
const result = makeResult({ finalAnswer: null });
|
|
260
|
+
const ft = inferFailureType(result, false);
|
|
261
|
+
assertEqual(ft, 'empty_result', 'inferFailureType=empty_result when finalAnswer=null');
|
|
262
|
+
});
|
|
263
|
+
await test('inferFailureType: wrong_answer for normal failure', async () => {
|
|
264
|
+
const result = makeResult({ finalAnswer: 'incorrect value' });
|
|
265
|
+
const ft = inferFailureType(result, false);
|
|
266
|
+
assertEqual(ft, 'wrong_answer', 'inferFailureType=wrong_answer for normal wrong answer');
|
|
267
|
+
});
|
|
268
|
+
// -------------------------------------------------------------------------
|
|
269
|
+
// Cleanup
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
try {
|
|
272
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Non-fatal cleanup failure; temp files will be cleared by OS.
|
|
276
|
+
}
|
|
277
|
+
// -------------------------------------------------------------------------
|
|
278
|
+
// Summary
|
|
279
|
+
// -------------------------------------------------------------------------
|
|
280
|
+
console.log(`\n${'─'.repeat(60)}`);
|
|
281
|
+
console.log(`Results: ${passed} passed, ${failed} failed (${passed + failed} total)`);
|
|
282
|
+
if (failed > 0) {
|
|
283
|
+
console.error('\nFailed tests:');
|
|
284
|
+
for (const r of results.filter((r) => !r.ok)) {
|
|
285
|
+
console.error(` ${r.name}: ${r.error}`);
|
|
286
|
+
}
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log('\nAll smoke tests passed.');
|
|
291
|
+
console.log('\nNote: Track I lift is compound (+0pp first run, +2-5pp after 5+ runs).');
|
|
292
|
+
console.log('Wiring into gaia-bench.ts is a follow-up PR (in-flight conflict avoidance).');
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
runAll().catch((err) => {
|
|
297
|
+
console.error('Unhandled error in smoke runner:', err);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
});
|
|
300
|
+
//# sourceMappingURL=gaia-causal-memory.smoke.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GAIA Convergence Layer — deterministic finalization for saturated agent loops.
|
|
3
|
+
*
|
|
4
|
+
* Detects three failure modes that cause empty FINAL_ANSWER extraction:
|
|
5
|
+
* 1. max_turns hit without a final_answer call
|
|
6
|
+
* 2. Loop detected (same tool + same args called 3× in a 5-turn window)
|
|
7
|
+
* 3. Token budget exceeded (>120k tokens of conversation context)
|
|
8
|
+
*
|
|
9
|
+
* On detection, runs a FORCED COMMIT phase:
|
|
10
|
+
* - Injects a stripped-down summary prompt: "Based on all observations, answer with
|
|
11
|
+
* FINAL_ANSWER: X. Do NOT explore further."
|
|
12
|
+
* - Makes 1 final API call with strict instruction and no tools.
|
|
13
|
+
* - If still no FINAL_ANSWER in the response, runs the Stage 1 extraction cascade
|
|
14
|
+
* against ALL prior assistant messages (last to first), returns first non-empty hit.
|
|
15
|
+
* - If still empty: returns null but logs the failure mode.
|
|
16
|
+
*
|
|
17
|
+
* This is NOT a new cognition layer — it is a convergence controller.
|
|
18
|
+
* It STRIPS information rather than adding it: one final chance to commit,
|
|
19
|
+
* no tools available, prior context summarized rather than appended.
|
|
20
|
+
*
|
|
21
|
+
* Architecture principle (iter 60 post-mortem):
|
|
22
|
+
* "More information can reduce agent reliability. Past a certain point —
|
|
23
|
+
* retrieval depth, context size, browsing breadth, tool diversity — information
|
|
24
|
+
* increases trajectory entropy. The system becomes less likely to finalize coherently."
|
|
25
|
+
* This layer is the entropy-reducer.
|
|
26
|
+
*
|
|
27
|
+
* Refs: #2156, iter 60 (19/25 empty FINAL_ANSWER failures), iter 62
|
|
28
|
+
*/
|
|
29
|
+
/** Token threshold (sum of input tokens across all turns) that triggers overflow detection. */
|
|
30
|
+
export declare const TOKEN_OVERFLOW_THRESHOLD = 120000;
|
|
31
|
+
/** Number of repeated identical tool+args calls in a window that signals a loop. */
|
|
32
|
+
export declare const LOOP_REPEAT_THRESHOLD = 3;
|
|
33
|
+
/** Sliding window size (turns) for loop detection. */
|
|
34
|
+
export declare const LOOP_WINDOW_SIZE = 5;
|
|
35
|
+
/** Tracks state that the convergence layer needs to evaluate triggers. */
|
|
36
|
+
export interface ConvergenceState {
|
|
37
|
+
/** How many agent turns have elapsed so far. */
|
|
38
|
+
turnCount: number;
|
|
39
|
+
/** Sum of input tokens across all turns (used for overflow detection). */
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
/** Ordered log of tool calls for loop detection. */
|
|
42
|
+
toolCalls: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
argsHash: string;
|
|
45
|
+
turn: number;
|
|
46
|
+
}>;
|
|
47
|
+
/** Set by checkConvergenceTriggers when a failure mode is detected. */
|
|
48
|
+
detectedFailureMode: 'max_turns' | 'loop' | 'token_overflow' | null;
|
|
49
|
+
}
|
|
50
|
+
/** Result of a forced-commit attempt. */
|
|
51
|
+
export interface ForceCommitResult {
|
|
52
|
+
/** The extracted answer, or null if even forced commit could not extract one. */
|
|
53
|
+
answer: string | null;
|
|
54
|
+
/** True when the answer was recovered from prior message history rather than
|
|
55
|
+
* from the forced-commit API call. */
|
|
56
|
+
usedFallback: boolean;
|
|
57
|
+
/** The failure mode that triggered this forced commit. */
|
|
58
|
+
triggerMode: ConvergenceState['detectedFailureMode'];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Produce a stable hash of a tool call's name + args for loop detection.
|
|
62
|
+
*
|
|
63
|
+
* Uses SHA-256 truncated to 16 hex chars — collision probability is
|
|
64
|
+
* negligible for the small call volumes in a single agent run.
|
|
65
|
+
*
|
|
66
|
+
* The hash is deterministic: same toolName + same args always → same hash.
|
|
67
|
+
* Different args always → different hash (within SHA-256 collision bounds).
|
|
68
|
+
*/
|
|
69
|
+
export declare function argsHash(toolName: string, args: object): string;
|
|
70
|
+
/**
|
|
71
|
+
* Evaluate the current ConvergenceState and return the first failure mode
|
|
72
|
+
* detected, or null if no trigger has fired.
|
|
73
|
+
*
|
|
74
|
+
* Evaluation order:
|
|
75
|
+
* 1. max_turns — turnCount >= maxTurns
|
|
76
|
+
* 2. token_overflow — totalTokens >= TOKEN_OVERFLOW_THRESHOLD
|
|
77
|
+
* 3. loop — same tool+argsHash appears >= LOOP_REPEAT_THRESHOLD times
|
|
78
|
+
* in the last LOOP_WINDOW_SIZE entries of toolCalls
|
|
79
|
+
*
|
|
80
|
+
* Only the FIRST matching trigger is returned (stops at first detection).
|
|
81
|
+
* The caller is responsible for setting state.detectedFailureMode.
|
|
82
|
+
*/
|
|
83
|
+
export declare function checkConvergenceTriggers(state: ConvergenceState, maxTurns: number): ConvergenceState['detectedFailureMode'];
|
|
84
|
+
/**
|
|
85
|
+
* Run Stage 1 extraction (FINAL_ANSWER: pattern) against a raw text string.
|
|
86
|
+
* Returns the matched answer or null.
|
|
87
|
+
*/
|
|
88
|
+
export declare function extractFinalAnswerFromText(text: string): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* Scan prior assistant messages from last to first, looking for FINAL_ANSWER.
|
|
91
|
+
*
|
|
92
|
+
* This is the fallback path when the forced-commit API call still does not
|
|
93
|
+
* produce a FINAL_ANSWER. We search backwards because the most recent
|
|
94
|
+
* assistant message is most likely to contain the best answer.
|
|
95
|
+
*/
|
|
96
|
+
export declare function extractFromPriorMessages(messages: Array<{
|
|
97
|
+
role: string;
|
|
98
|
+
content: string | unknown;
|
|
99
|
+
}>): string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Run the forced-commit phase for a saturated agent loop.
|
|
102
|
+
*
|
|
103
|
+
* Steps:
|
|
104
|
+
* 1. Append a stripped directive prompt to the message history.
|
|
105
|
+
* 2. Call the model once with NO tools — forces text-only response.
|
|
106
|
+
* 3. If FINAL_ANSWER found in the response → return it.
|
|
107
|
+
* 4. If not → scan prior assistant messages last-to-first for FINAL_ANSWER.
|
|
108
|
+
* 5. If still empty → return { answer: null, usedFallback: false }.
|
|
109
|
+
*
|
|
110
|
+
* The callModel callback must return the raw text response (assistant turn
|
|
111
|
+
* text blocks concatenated). It should be called WITHOUT tool definitions
|
|
112
|
+
* so the model cannot call tools in this turn.
|
|
113
|
+
*
|
|
114
|
+
* @param messages Current conversation history (mutated: 1 user turn appended)
|
|
115
|
+
* @param callModel Callback that makes a single API call and returns the response text
|
|
116
|
+
* @param triggerMode The failure mode that triggered this forced commit
|
|
117
|
+
*/
|
|
118
|
+
export declare function forceCommit(messages: Array<{
|
|
119
|
+
role: string;
|
|
120
|
+
content: string | unknown;
|
|
121
|
+
}>, callModel: (messages: Array<{
|
|
122
|
+
role: string;
|
|
123
|
+
content: string | unknown;
|
|
124
|
+
}>) => Promise<string>, triggerMode?: ConvergenceState['detectedFailureMode']): Promise<ForceCommitResult>;
|
|
125
|
+
/** Create a new ConvergenceState with zero counters. */
|
|
126
|
+
export declare function createConvergenceState(): ConvergenceState;
|
|
127
|
+
/**
|
|
128
|
+
* Update ConvergenceState after each agent turn completes.
|
|
129
|
+
*
|
|
130
|
+
* @param state Mutable state object (modified in place)
|
|
131
|
+
* @param inputTokens Tokens consumed in this turn
|
|
132
|
+
* @param toolCallsThisTurn Tool calls made in this turn (name + args)
|
|
133
|
+
*/
|
|
134
|
+
export declare function recordTurn(state: ConvergenceState, inputTokens: number, toolCallsThisTurn: Array<{
|
|
135
|
+
name: string;
|
|
136
|
+
args: object;
|
|
137
|
+
}>): void;
|
|
138
|
+
//# sourceMappingURL=gaia-convergence.d.ts.map
|