moving
This commit is contained in:
33
benchmarks/harness/E6-LOCKED-CONFIG.md
Normal file
33
benchmarks/harness/E6-LOCKED-CONFIG.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# E6 Evidence-Ledger — LOCKED CONFIG (matched-50 = 0.7748, gate met)
|
||||
|
||||
Frozen 2026-07-16. Composed matched-50 (convs 1/10/11, 5 Q × 10 abilities) = **0.7748**,
|
||||
above Eywa-on-same-50 (0.7704). This is the config to run on full-700 — do not change the
|
||||
`abilityInstruction()` prompts without re-validating matched-50.
|
||||
|
||||
## The config
|
||||
- **Runner:** `scripts/beam-run-ledger.ts` — `abilityInstruction()` holds the locked,
|
||||
per-ability prompts (cumulative iter2–iter6 additive edits). Each ability's current
|
||||
prompt is the exact version that produced its composing answers.
|
||||
- **Answerer:** `anthropic/claude-sonnet-4.6` via OpenRouter, prompt caching ON.
|
||||
- **Judge:** `openai/gpt-5` via OpenRouter (NEVER bare `gpt-5` — direct account quota is
|
||||
dead). Pass `--judge-model openai/gpt-5`.
|
||||
- **Context:** whole-ledger prefix = P2 STATE (`convN.state.txt`) + P1 ledger
|
||||
(`convN.ledger.txt`); detail abilities also get top-N raw dated turns.
|
||||
|
||||
## Per-ability provenance (which iteration's prompt is locked)
|
||||
information_extraction, abstention = pilot · contradiction_resolution,
|
||||
instruction_following = iter2 · temporal_reasoning, preference_following = iter4 ·
|
||||
event_ordering, knowledge_update = iter5 · summarization, multi_session_reasoning = iter6.
|
||||
|
||||
## Full-700 run (P4)
|
||||
```
|
||||
npx tsx scripts/beam-run-ledger.ts --convs 1-35 --judge-model openai/gpt-5 \
|
||||
--tag e6-ledger-FULL700 --resume --budget 60
|
||||
```
|
||||
Processes grouped by conversation for cache warmth; `--resume` skips done rows. Requires
|
||||
ledgers + states for all 35 convs (P1/P2). Dedup by `instance_id` for final metrics.
|
||||
|
||||
## Reference points (matched-50)
|
||||
baseline 0.5533 · best read-time 0.6198 · pilot 0.6825 · Eywa same-50 0.7704 · E6 0.7748.
|
||||
Full-700 targets: ≥0.8285 = SOTA vs Eywa; ≥0.80 = strong co-SOTA. Old full-700 baseline
|
||||
(pre-E6) = 0.6482.
|
||||
95
benchmarks/harness/results/beam/E2-sonnet-pilot-report.md
Normal file
95
benchmarks/harness/results/beam/E2-sonnet-pilot-report.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# BEAM-1M E2 "protocol-match" pilot — Sonnet 4.6 answerer, dual judge
|
||||
|
||||
**Goal:** isolate how much of Eywa's BEAM lead is pure *answer-model + self-judge*
|
||||
vs architecture, by re-answering our matched 50-Q pilot with Claude Sonnet 4.6 as
|
||||
the ANSWERER and judging the SAME answers twice: Sonnet 4.6 (Eywa-style self-judge)
|
||||
and gpt-5 (our canonical judge).
|
||||
|
||||
## Route + models
|
||||
- **Answerer route:** OpenRouter (`https://openrouter.ai/api/v1`), key `OPENROUTER_API_KEY`.
|
||||
LiteLLM proxy (`localhost:4000`) was **down**, so per plan we used the OpenRouter fallback.
|
||||
- **Answerer model id:** `anthropic/claude-sonnet-4.6` (verified via OpenRouter `/models`; 1-call smoke returned `OK`, provider=Anthropic).
|
||||
- **Canonical judge model:** `gpt-5` (direct OpenAI, unchanged transport).
|
||||
- **Self-judge model:** `anthropic/claude-sonnet-4.6` (same OpenRouter route as answerer).
|
||||
- **Config (identical to gpt-5 baseline):** cell=retrieval, prompt=v2, top_k=30, raw dated turns, minds-1M. Only the answerer model changed; `buildAnswerGenerationPromptV2` and retrieval untouched.
|
||||
|
||||
## Cost
|
||||
| item | $ |
|
||||
|---|---|
|
||||
| Sonnet answer + Sonnet self-judge run (50 Q) | 5.5584 |
|
||||
| gpt-5 re-judge of the 50 Sonnet answers | 1.1050 |
|
||||
| smokes (1-call + 1-question pipeline) | ~0.16 |
|
||||
| **total** | **~6.82** |
|
||||
|
||||
(Slightly over the ~$6 soft cap. Sonnet's answers are long/verbose, inflating both
|
||||
answer-output and judge-input tokens; the gpt-5 re-judge was cheap and is the core deliverable.)
|
||||
|
||||
## The 50 instance_ids (5 per ability × 10) — the EXACT prior gpt-5 matched sample
|
||||
Reused verbatim (all prior 50-row pilots — rawv2/retv2-outline/retv3/ipbv2/hybrid — share the identical id set; verified by diff). Convs {1,10,11}.
|
||||
|
||||
```
|
||||
conv 1 (q0,q1 each): abstention, contradiction_resolution, event_ordering,
|
||||
information_extraction, instruction_following, knowledge_update,
|
||||
multi_session_reasoning, preference_following, summarization, temporal_reasoning
|
||||
conv 10 (q0,q1 each): same 10 abilities
|
||||
conv 11 (q0 only): same 10 abilities
|
||||
```
|
||||
Full list: `beam_1M_{1,10}_{ability}_{q0,q1}` + `beam_1M_11_{ability}_q0` for the 10 abilities above (50 ids). Saved to scratchpad `matched50.txt`.
|
||||
|
||||
## Headline results (same 50 Qs, all three arms)
|
||||
|
||||
| arm | answerer | judge | avg_score | pass% (n=50) |
|
||||
|---|---|---|---|---|
|
||||
| Baseline | gpt-5 | gpt-5 | **0.5533** | 64.0% (32) |
|
||||
| Arm A (Eywa-style) | Sonnet 4.6 | Sonnet 4.6 (self) | **0.5808** | 62.0% (31) |
|
||||
| Arm B | Sonnet 4.6 | gpt-5 | **0.6297** | 68.0% (34) |
|
||||
|
||||
> Note: the FULL-700 gpt-5 headline is 0.6482/74.0%; this specific 50-Q subset is
|
||||
> harder for gpt-5 (0.5533/64%). All comparisons here are apples-to-apples on the SAME 50 ids.
|
||||
|
||||
## Per-ability avg_score (n=5 each)
|
||||
|
||||
| ability | Base (gpt5A/gpt5J) | Arm A (sonA/sonJ) | Arm B (sonA/gpt5J) |
|
||||
|---|---|---|---|
|
||||
| abstention | 0.200 | 0.350 | 0.300 |
|
||||
| contradiction_resolution | 0.450 | 0.350 | 0.375 |
|
||||
| event_ordering | 0.339 | 0.156 | 0.267 |
|
||||
| information_extraction | 0.733 | 0.975 | 1.000 |
|
||||
| instruction_following | 0.750 | 0.650 | 0.700 |
|
||||
| knowledge_update | 0.500 | 0.700 | 0.800 |
|
||||
| multi_session_reasoning | 0.717 | 0.717 | 0.692 |
|
||||
| preference_following | 0.733 | 0.600 | 0.783 |
|
||||
| summarization | 0.511 | 0.511 | 0.581 |
|
||||
| temporal_reasoning | 0.600 | 0.800 | 0.800 |
|
||||
|
||||
## Decomposition (per-question paired deltas, avg_score)
|
||||
- **Answer-model effect** (Arm B − Baseline; gpt-5 judges BOTH): **+0.0764** (0.6297 vs 0.5533). Pass% +4pp (68 vs 64).
|
||||
- **Self-judge effect** (Arm A − Arm B; SAME Sonnet answers, judge swapped): **−0.0489** (Sonnet self-judge 0.5808 vs gpt-5 judge 0.6297).
|
||||
|
||||
## Read (one paragraph)
|
||||
Under our own canonical gpt-5 judge, **Sonnet 4.6 is the better BEAM answerer**:
|
||||
Sonnet answers score **+0.076 avg (+4pp pass)** above gpt-5 answers on the identical
|
||||
50 Qs and identical retrieval/prompt — so the answer-model swap *helps*, and a real
|
||||
chunk of any Sonnet-based system's BEAM number is genuine answer quality (biggest
|
||||
gains: knowledge_update, information_extraction, preference_following, temporal;
|
||||
regressions concentrated in contradiction_resolution and event_ordering). The
|
||||
"self-judge" half tells the opposite of the inflation story: on the very same Sonnet
|
||||
answers, **Sonnet's self-judge is *harsher* than gpt-5 by −0.049** (0.5808 vs 0.6297),
|
||||
i.e. **no self-judge inflation is observed here — if anything, self-judge deflation**.
|
||||
So Eywa's protocol (Sonnet answer + Sonnet self-judge) does *not* win by grading its
|
||||
own homework leniently; on this matched sample it actually under-credits itself
|
||||
relative to gpt-5. The answer-model contribution is positive and real (~+7.6 avg),
|
||||
while the judge-swap contribution is small and negative — meaning the bulk of any
|
||||
Eywa-vs-us BEAM gap that survives is attributable to the answer model and
|
||||
architecture, not to judge leniency. Caveat: n=5/ability (n=50 total), single run,
|
||||
no temperature averaging — treat per-ability cells as directional.
|
||||
|
||||
## Artifacts
|
||||
- Sonnet answers + self-judge: `benchmarks/results/beam/E2-sonnet-answers.jsonl` (+ `.summary.json`)
|
||||
- Same answers re-judged by gpt-5: `benchmarks/results/beam/E2-sonnet-answers.judged-gpt5.jsonl` (+ `.summary.json`)
|
||||
- Baseline (gpt-5/gpt-5, full 700): `benchmarks/results/beam/beam-1m-FULL700-gpt5-retv2.jsonl` (filtered to the 50 ids)
|
||||
|
||||
## Code changes (local, uncommitted — pilot only)
|
||||
- `src/beam-openai-client.ts`: `createBeamOpenAiClient` now routes Claude ids (`/claude|^anthropic\//`) through OpenRouter (`OPENROUTER_API_KEY`); added `anthropic/claude-sonnet-4.6` pricing (3/15 per 1M). gpt-*/o-series path unchanged.
|
||||
- `scripts/beam-run-1m.ts`: added `--instance-ids <file>` exact-allowlist flag (reuses a prior matched sample; overrides `--per-ability`). Answer/retrieval/prompt logic untouched.
|
||||
- `scripts/beam-rejudge.ts` (new): standalone re-judge — re-scores an existing answers jsonl with any judge model via the same transport-agnostic `judgeQuestion`; rubric recovered from each row's `nugget_scores[].nugget`.
|
||||
60
benchmarks/harness/scripts/_belief-smoke.ts
Normal file
60
benchmarks/harness/scripts/_belief-smoke.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env tsx
|
||||
/** Cost-light smoke: build + print the belief block for a few conv-1 questions
|
||||
* (detection LLM = gpt-5-mini only; no answer/judge). Validates imports, the
|
||||
* rolled-back applyConsolidation transaction, and block well-formedness. */
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { detectSupersessionChains, detectEntityGroups, applyConsolidation,
|
||||
type ConsolidationLlm, type Observation, type MemoryFrame } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient } from '../src/beam-openai-client.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const obsDir = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M-obs');
|
||||
const beamChats = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
|
||||
const ROLLBACK = Symbol('rb');
|
||||
const detect = createBeamOpenAiClient({ model: 'gpt-5-mini' });
|
||||
let cost = 0;
|
||||
const llm: ConsolidationLlm = async (system, user) => {
|
||||
const r = await detect.chat({ system, user, jsonMode: true, maxTokens: 1200 });
|
||||
cost += r.costUsd;
|
||||
return r.text;
|
||||
};
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const conv = 1;
|
||||
const embedder = createOllamaEmbedder();
|
||||
const obsSub = createSubstrate({ dbPath: path.join(obsDir, `beam_1M_${conv}.mind`), embedder });
|
||||
// pull a few probing questions likely to have chains/groups
|
||||
const pq = JSON.parse((await import('node:fs')).readFileSync(
|
||||
path.join(beamChats, '1M', String(conv), 'probing_questions', 'probing_questions.json'), 'utf-8')) as Record<string, { question: string }[]>;
|
||||
const picks: { ability: string; question: string }[] = [];
|
||||
for (const ab of ['knowledge_update', 'contradiction_resolution', 'multi_session_reasoning']) {
|
||||
if (pq[ab]?.[0]) picks.push({ ability: ab, question: pq[ab][0].question });
|
||||
}
|
||||
try {
|
||||
for (const p of picks) {
|
||||
const results = await obsSub.search.search(p.question, { limit: 60, gopId: `beam_${conv}` });
|
||||
const obs: Observation[] = results.map(r => ({ id: r.frame.id, content: r.frame.content, created_at: String(r.frame.created_at ?? '') }));
|
||||
const [chains, groups] = await Promise.all([detectSupersessionChains(obs, llm), detectEntityGroups(obs, llm)]);
|
||||
const raw = obsSub.db.getDatabase();
|
||||
let pframes: MemoryFrame[] = []; let bframes: MemoryFrame[] = [];
|
||||
try { raw.transaction(() => { const res = applyConsolidation(obsSub.frames, chains, groups, `beam_${conv}`); pframes = res.pframes; bframes = res.bframes; throw ROLLBACK; })(); }
|
||||
catch (e) { if (e !== ROLLBACK) throw e; }
|
||||
console.log('\n════════', p.ability, '════════');
|
||||
console.log('Q:', p.question);
|
||||
console.log(`retrieved=${results.length} chains=${chains.length} groups=${groups.length} pframes=${pframes.length} bframes=${bframes.length}`);
|
||||
console.log('--- P-frames (current values) ---');
|
||||
for (const f of pframes) console.log(' ' + f.content);
|
||||
console.log('--- B-frames (enumerable sets) ---');
|
||||
for (const f of bframes) console.log(' ' + f.content);
|
||||
}
|
||||
} finally {
|
||||
obsSub.close();
|
||||
}
|
||||
console.log(`\ndetect cost=$${cost.toFixed(4)}`);
|
||||
}
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1); });
|
||||
69
benchmarks/harness/scripts/_e4-aggregate.mjs
Normal file
69
benchmarks/harness/scripts/_e4-aggregate.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
// E4 aggregation — per-ability + overall for each config, on the matched-50 ids.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const R = path.resolve('benchmarks/results/beam');
|
||||
const ids = new Set(fs.readFileSync('benchmarks/harness/scripts/matched50.txt', 'utf8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
const ABIL = ['abstention','contradiction_resolution','event_ordering','information_extraction','instruction_following','knowledge_update','multi_session_reasoning','preference_following','summarization','temporal_reasoning'];
|
||||
|
||||
function load(p, filterIds = true) {
|
||||
if (!fs.existsSync(p)) return null;
|
||||
const rows = fs.readFileSync(p, 'utf8').split('\n').filter(l => l.trim()).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
||||
// dedupe by instance_id, keep LAST
|
||||
const m = new Map();
|
||||
for (const r of rows) if (!filterIds || ids.has(r.instance_id)) m.set(r.instance_id, r);
|
||||
return [...m.values()];
|
||||
}
|
||||
function agg(rows) {
|
||||
if (!rows) return null;
|
||||
const by = {}; let all = [];
|
||||
for (const r of rows) { (by[r.memory_ability] = by[r.memory_ability] || []).push(r.score); all.push(r.score); }
|
||||
const out = { _overall: +(all.reduce((a,b)=>a+b,0)/(all.length||1)).toFixed(4), _n: all.length };
|
||||
for (const a of ABIL) { const s = by[a]; out[a] = s ? +(s.reduce((x,y)=>x+y,0)/s.length).toFixed(3) : null; }
|
||||
return out;
|
||||
}
|
||||
|
||||
const configs = {
|
||||
baseline: load(path.join(R, 'beam-1m-FULL700-gpt5-retv2.jsonl')),
|
||||
'retonly(gpt5,noBel)': load(path.join(R, 'beam-1m-e4-retonly-gpt-5.jsonl')),
|
||||
'combo(gpt5,+Bel)': load(path.join(R, 'beam-1m-e4-combo-gpt5-gpt-5.jsonl')),
|
||||
'combo(Sonnet,+Bel)': load(path.join(R, 'beam-1m-e4-combo-sonnet-anthropic-claude-sonnet-4.6.jsonl')),
|
||||
'abl(gpt5,V+T only)': load(path.join(R, 'beam-1m-e4-abl-noent-gpt-5.jsonl')),
|
||||
};
|
||||
const A = {};
|
||||
for (const [k, v] of Object.entries(configs)) A[k] = agg(v);
|
||||
|
||||
// Print table
|
||||
const cols = Object.keys(A).filter(k => A[k]);
|
||||
const base = A.baseline;
|
||||
console.log('\n=== E4 matched-50 per-ability avg_score ===');
|
||||
const pad = (s, n) => String(s).padEnd(n);
|
||||
console.log(pad('ability', 26) + cols.map(c => pad(c, 20)).join(''));
|
||||
for (const a of ABIL) {
|
||||
let line = pad(a, 26);
|
||||
for (const c of cols) {
|
||||
const v = A[c] ? A[c][a] : null;
|
||||
const d = (c !== 'baseline' && v != null && base[a] != null) ? ` (${v - base[a] >= 0 ? '+' : ''}${(v - base[a]).toFixed(2)})` : '';
|
||||
line += pad(v == null ? '-' : v.toFixed(3) + d, 20);
|
||||
}
|
||||
console.log(line);
|
||||
}
|
||||
let line = pad('OVERALL', 26);
|
||||
for (const c of cols) {
|
||||
const v = A[c] ? A[c]._overall : null;
|
||||
const d = (c !== 'baseline' && v != null) ? ` (${v - base._overall >= 0 ? '+' : ''}${((v - base._overall)*100).toFixed(1)}pp)` : '';
|
||||
line += pad(v == null ? '-' : v.toFixed(4) + d, 20);
|
||||
}
|
||||
console.log(line);
|
||||
line = pad('n', 26);
|
||||
for (const c of cols) line += pad(A[c] ? A[c]._n : '-', 20);
|
||||
console.log(line);
|
||||
|
||||
// Target-ability gate check for combo(Sonnet)
|
||||
const targets = ['temporal_reasoning','event_ordering','multi_session_reasoning','summarization'];
|
||||
for (const cfg of ['combo(Sonnet,+Bel)','combo(gpt5,+Bel)','retonly(gpt5,noBel)']) {
|
||||
if (!A[cfg]) continue;
|
||||
console.log(`\n-- gate check: ${cfg} vs baseline --`);
|
||||
console.log(` overall delta: ${((A[cfg]._overall - base._overall)*100).toFixed(1)}pp (need >=+4pp)`);
|
||||
const regress = targets.filter(t => A[cfg][t] != null && base[t] != null && A[cfg][t] < base[t] - 1e-9);
|
||||
console.log(` target-ability regressions: ${regress.length ? regress.map(t=>`${t}(${(A[cfg][t]-base[t]).toFixed(2)})`).join(', ') : 'NONE'}`);
|
||||
}
|
||||
30
benchmarks/harness/scripts/_judge-diag.ts
Normal file
30
benchmarks/harness/scripts/_judge-diag.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fs from 'node:fs';
|
||||
import { createBeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion, buildNuggetJudgePrompt, BEAM_JUDGE_SYSTEM_PROMPT } from '../src/beam-nugget-judge.js';
|
||||
|
||||
async function main() {
|
||||
loadDotEnv();
|
||||
const judge = createBeamOpenAiClient({ model: 'gpt-5', pricing: OPENAI_PRICING['gpt-5'], timeoutMs: 300_000, maxRetries: 2 });
|
||||
|
||||
// Test A: trivial gpt-5 JSON call
|
||||
console.log('--- TEST A: trivial gpt-5 call ---');
|
||||
const a = await judge.chat({ system: BEAM_JUDGE_SYSTEM_PROMPT, user: 'Return JSON: {"score": 1.0, "reason": "test"}', jsonMode: true, maxTokens: 300 });
|
||||
console.log('A: failureMode=', a.failureMode, 'textLen=', a.text.length, 'in=', a.inputTokens, 'out=', a.outputTokens, 'text=', JSON.stringify(a.text.slice(0, 120)));
|
||||
|
||||
// Test B: re-judge one ITER1 (known-good) answer
|
||||
console.log('\n--- TEST B: re-judge an iter1 answer (was 0.625) ---');
|
||||
const p1 = 'D:/Projects/waggle-os/benchmarks/results/beam/beam-1m-e6-ledger-pilot-anthropic-claude-sonnet-4.6.jsonl';
|
||||
const r1 = fs.readFileSync(p1, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l)).find(r => r.instance_id === 'beam_1M_1_contradiction_resolution_q0');
|
||||
const rubric1 = (r1.nugget_scores || []).map((n: any) => n.nugget);
|
||||
const jb = await judgeQuestion(judge, { question: r1.question, rubric: rubric1, memoryAbility: r1.memory_ability, answer: r1.answer }, { computeTau: false });
|
||||
console.log('B: NEW score=', jb.judgement.score, 'nug=', jb.judgement.nuggetScores.map(n => `${n.score}(${n.reason.slice(0,30)})`).join(' | '));
|
||||
|
||||
// Test C: single raw nugget judge call on an iter2 answer, dump raw text
|
||||
console.log('\n--- TEST C: raw judge call on iter2 answer ---');
|
||||
const p2 = 'D:/Projects/waggle-os/benchmarks/results/beam/beam-1m-e6-ledger-iter2-anthropic-claude-sonnet-4.6.jsonl';
|
||||
const r2 = fs.readFileSync(p2, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l))[0];
|
||||
const nug = r2.nugget_scores[0].nugget;
|
||||
const c = await judge.chat({ system: BEAM_JUDGE_SYSTEM_PROMPT, user: buildNuggetJudgePrompt(r2.question, nug, r2.answer), jsonMode: true, maxTokens: 300 });
|
||||
console.log('C: failureMode=', c.failureMode, 'textLen=', c.text.length, 'in=', c.inputTokens, 'out=', c.outputTokens, 'text=', JSON.stringify(c.text.slice(0, 200)));
|
||||
}
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1); });
|
||||
24
benchmarks/harness/scripts/_or-judge-probe.ts
Normal file
24
benchmarks/harness/scripts/_or-judge-probe.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from 'node:fs';
|
||||
import { BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
|
||||
async function main() {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('no OPENROUTER_API_KEY');
|
||||
// Route gpt-5 judge via OpenRouter (funded). Same model, same pricing.
|
||||
const judge = new BeamOpenAiClient({
|
||||
model: 'openai/gpt-5', apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING['gpt-5'], timeoutMs: 120_000, maxRetries: 2,
|
||||
});
|
||||
const p2 = 'D:/Projects/waggle-os/benchmarks/results/beam/beam-1m-e6-ledger-iter2-anthropic-claude-sonnet-4.6.jsonl';
|
||||
const rows = fs.readFileSync(p2, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
||||
for (const r of rows.slice(0, 3)) {
|
||||
const rubric = (r.nugget_scores || []).map((n: any) => n.nugget);
|
||||
const { judgement } = await judgeQuestion(judge, { question: r.question, rubric, memoryAbility: r.memory_ability, answer: r.answer }, { computeTau: false });
|
||||
const pe = judgement.nuggetScores.some(n => n.reason.startsWith('Parse error'));
|
||||
console.log(`${r.instance_id} old=${r.score} OR-gpt5=${judgement.score.toFixed(3)} parseErr=${pe} nug=[${judgement.nuggetScores.map(n=>n.score).join(',')}]`);
|
||||
console.log(' reason[0]:', judgement.nuggetScores[0].reason.slice(0, 120));
|
||||
}
|
||||
}
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1); });
|
||||
123
benchmarks/harness/scripts/_rejudge-or.ts
Normal file
123
benchmarks/harness/scripts/_rejudge-or.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import fs from 'node:fs';
|
||||
import { BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import type { QuestionJudgement } from '../src/beam-nugget-judge.js';
|
||||
|
||||
/**
|
||||
* SAME-JUDGE rejudge for E6 iter1(pilot)-vs-iter2 on the 6 CHANGED abilities.
|
||||
* Re-scores every changed-ability answer from BOTH runs through OpenRouter
|
||||
* gpt-5 (identical model/price to the OpenAI-direct judge used for the pilot),
|
||||
* so the iter1-vs-iter2 comparison controls for judge drift. The pilot's
|
||||
* original scores were OpenAI-direct; iter2's original scores are all FAIL
|
||||
* (judge quota died mid-run) — this replaces both with OR-gpt5.
|
||||
*
|
||||
* Rubric is recovered from each row's nugget_scores[].nugget (intact even in
|
||||
* the failed iter2 rows). Soft "Parse error" nuggets are retried once.
|
||||
*
|
||||
* WRITES:
|
||||
* (a) beam-1m-e6-ledger-iter2-REJUDGED.jsonl — the 30 iter2 rows with real
|
||||
* OR-gpt5 score/judgment/nugget_scores replacing the FAIL placeholders.
|
||||
* (b) beam-1m-e6-ledger-rejudge-OR.json — per-ability iter1-OR / iter2-OR
|
||||
* means, per-question scores, and a parse-error flag.
|
||||
*/
|
||||
|
||||
const CHANGED = ['contradiction_resolution', 'event_ordering', 'instruction_following', 'knowledge_update', 'preference_following', 'temporal_reasoning'];
|
||||
const RESULTS = 'D:/Projects/waggle-os/benchmarks/results/beam/';
|
||||
const PILOT = 'beam-1m-e6-ledger-pilot-anthropic-claude-sonnet-4.6.jsonl';
|
||||
const ITER2 = 'beam-1m-e6-ledger-iter2-anthropic-claude-sonnet-4.6.jsonl';
|
||||
const OUT_REJUDGED = 'beam-1m-e6-ledger-iter2-REJUDGED.jsonl';
|
||||
const OUT_JSON = 'beam-1m-e6-ledger-rejudge-OR.json';
|
||||
|
||||
type Row = { instance_id: string; memory_ability: string; question: string; answer: string; nugget_scores?: { nugget: string }[]; [k: string]: unknown };
|
||||
|
||||
const load = (f: string): Row[] => fs.readFileSync(RESULTS + f, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l) as Row);
|
||||
|
||||
function hasParseErr(j: QuestionJudgement): boolean {
|
||||
return j.judgment === 'ERROR' || (j.nuggetScores || []).some(n => String(n.reason).startsWith('Parse error'));
|
||||
}
|
||||
|
||||
async function judgeOne(judge: BeamOpenAiClient, r: Row): Promise<QuestionJudgement> {
|
||||
const rubric = (r.nugget_scores || []).map(n => n.nugget).filter(Boolean);
|
||||
let { judgement } = await judgeQuestion(judge, { question: r.question, rubric, memoryAbility: r.memory_ability, answer: r.answer }, { computeTau: false });
|
||||
if (hasParseErr(judgement)) {
|
||||
const retry = await judgeQuestion(judge, { question: r.question, rubric, memoryAbility: r.memory_ability, answer: r.answer }, { computeTau: false });
|
||||
judgement = retry.judgement; // keep retry result; flagged below if still bad
|
||||
}
|
||||
return judgement;
|
||||
}
|
||||
|
||||
async function judgeSet(judge: BeamOpenAiClient, rows: Row[], label: string): Promise<Map<string, QuestionJudgement>> {
|
||||
const out = new Map<string, QuestionJudgement>();
|
||||
let i = 0;
|
||||
for (const r of rows) {
|
||||
const j = await judgeOne(judge, r);
|
||||
out.set(r.instance_id, j);
|
||||
i++;
|
||||
process.stdout.write(` [${label}] ${String(i).padStart(2)}/${rows.length} ${r.instance_id.padEnd(42)} score=${j.score.toFixed(3)} ${hasParseErr(j) ? '(PARSE-ERR)' : ''}\n`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function abilityMeans(rows: Row[], scoreMap: Map<string, QuestionJudgement>): Record<string, number> {
|
||||
const byAb: Record<string, number[]> = {};
|
||||
for (const r of rows) (byAb[r.memory_ability] ??= []).push(scoreMap.get(r.instance_id)!.score);
|
||||
const means: Record<string, number> = {};
|
||||
for (const a of Object.keys(byAb)) means[a] = byAb[a].reduce((x, y) => x + y, 0) / byAb[a].length;
|
||||
return means;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required');
|
||||
const judge = new BeamOpenAiClient({ model: 'openai/gpt-5', apiKey: key, baseUrl: 'https://openrouter.ai/api/v1', pricing: OPENAI_PRICING['gpt-5'], timeoutMs: 120_000, maxRetries: 3 });
|
||||
|
||||
const iter1 = load(PILOT).filter(r => CHANGED.includes(r.memory_ability));
|
||||
const iter2 = load(ITER2).filter(r => CHANGED.includes(r.memory_ability));
|
||||
console.log(`iter1(pilot) changed-ability rows: ${iter1.length}; iter2 changed-ability rows: ${iter2.length}`);
|
||||
for (const r of [...iter1, ...iter2]) if (!(r.nugget_scores || []).length) throw new Error('no recoverable rubric for ' + r.instance_id);
|
||||
|
||||
console.log('\n== Re-judging iter2 on OR-gpt5 ==');
|
||||
const j2 = await judgeSet(judge, iter2, 'iter2');
|
||||
console.log('\n== Re-judging iter1/pilot on OR-gpt5 ==');
|
||||
const j1 = await judgeSet(judge, iter1, 'iter1');
|
||||
|
||||
// (a) REJUDGED iter2 jsonl — preserve every original field, swap judged fields.
|
||||
const rejudgedLines = iter2.map(r => {
|
||||
const j = j2.get(r.instance_id)!;
|
||||
return JSON.stringify({ ...r, score: j.score, judgment: j.judgment, nugget_scores: j.nuggetScores, judge_model: 'openai/gpt-5', rejudged_via: 'openrouter' });
|
||||
});
|
||||
fs.writeFileSync(RESULTS + OUT_REJUDGED, rejudgedLines.join('\n') + '\n');
|
||||
|
||||
// (b) per-ability means + per-question scores.
|
||||
const m1 = abilityMeans(iter1, j1);
|
||||
const m2 = abilityMeans(iter2, j2);
|
||||
const perQuestion: Record<string, unknown> = {};
|
||||
for (const a of CHANGED) {
|
||||
perQuestion[a] = {
|
||||
iter1: iter1.filter(r => r.memory_ability === a).map(r => ({ id: r.instance_id, score: j1.get(r.instance_id)!.score })),
|
||||
iter2: iter2.filter(r => r.memory_ability === a).map(r => ({ id: r.instance_id, score: j2.get(r.instance_id)!.score })),
|
||||
};
|
||||
}
|
||||
const anyPE = [...j1.values(), ...j2.values()].some(hasParseErr);
|
||||
const changed6_i1 = CHANGED.reduce((s, a) => s + m1[a], 0) / CHANGED.length;
|
||||
const changed6_i2 = CHANGED.reduce((s, a) => s + m2[a], 0) / CHANGED.length;
|
||||
fs.writeFileSync(RESULTS + OUT_JSON, JSON.stringify({
|
||||
judge: 'openai/gpt-5 (OpenRouter)', changed_abilities: CHANGED,
|
||||
iter1_OR_ability_means: m1, iter2_OR_ability_means: m2,
|
||||
changed6_iter1_OR_mean: changed6_i1, changed6_iter2_OR_mean: changed6_i2,
|
||||
per_question: perQuestion, parseErrorsRemain: anyPE, generated_at: new Date().toISOString(),
|
||||
}, null, 2));
|
||||
|
||||
console.log('\nability iter1-OR iter2-OR Δ');
|
||||
for (const a of CHANGED.slice().sort()) {
|
||||
const d = m2[a] - m1[a];
|
||||
console.log(`${a.padEnd(26)} ${m1[a].toFixed(3)} ${m2[a].toFixed(3)} ${(d >= 0 ? '+' : '')}${d.toFixed(3)}`);
|
||||
}
|
||||
console.log(`\nchanged-6 iter1-OR mean: ${changed6_i1.toFixed(4)}`);
|
||||
console.log(`changed-6 iter2-OR mean: ${changed6_i2.toFixed(4)} Δ=${(changed6_i2 - changed6_i1 >= 0 ? '+' : '')}${(changed6_i2 - changed6_i1).toFixed(4)}`);
|
||||
console.log('parseErrorsRemain:', anyPE);
|
||||
console.log('wrote', RESULTS + OUT_REJUDGED);
|
||||
console.log('wrote', RESULTS + OUT_JSON);
|
||||
}
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1); });
|
||||
17
benchmarks/harness/scripts/_rejudge-probe.ts
Normal file
17
benchmarks/harness/scripts/_rejudge-probe.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import fs from 'node:fs';
|
||||
import { createBeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
|
||||
async function main() {
|
||||
loadDotEnv();
|
||||
const judge = createBeamOpenAiClient({ model: 'gpt-5', pricing: OPENAI_PRICING['gpt-5'], timeoutMs: 300_000, maxRetries: 2 });
|
||||
const p = 'D:/Projects/waggle-os/benchmarks/results/beam/beam-1m-e6-ledger-iter2-anthropic-claude-sonnet-4.6.jsonl';
|
||||
const rows = fs.readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
||||
for (const r of rows) {
|
||||
const rubric = (r.nugget_scores || []).map((n: any) => n.nugget);
|
||||
const { judgement } = await judgeQuestion(judge, { question: r.question, rubric, memoryAbility: r.memory_ability, answer: r.answer }, { computeTau: false });
|
||||
const anyParseErr = judgement.nuggetScores.some(n => n.reason.startsWith('Parse error'));
|
||||
console.log(`${r.instance_id} old=${r.score} NEW=${judgement.score.toFixed(3)} parseErr=${anyParseErr} nug=[${judgement.nuggetScores.map(n=>n.score).join(',')}]`);
|
||||
}
|
||||
}
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1); });
|
||||
17
benchmarks/harness/scripts/_wait-gpt5.ts
Normal file
17
benchmarks/harness/scripts/_wait-gpt5.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createBeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
|
||||
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
async function main() {
|
||||
loadDotEnv();
|
||||
const judge = createBeamOpenAiClient({ model: 'gpt-5', pricing: OPENAI_PRICING['gpt-5'], timeoutMs: 120_000, maxRetries: 0 });
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
const r = await judge.chat({ system: 'Return only JSON.', user: 'Return JSON: {"score": 1.0, "reason": "ok"}', jsonMode: true, maxTokens: 200 });
|
||||
const ok = !r.failureMode && r.text.trim().length > 0;
|
||||
console.log(`probe ${i}: failureMode=${r.failureMode} textLen=${r.text.length} -> ${ok ? 'READY' : 'still limited'}`);
|
||||
if (ok) { console.log('GPT5_READY'); return; }
|
||||
await sleep(20_000);
|
||||
}
|
||||
console.log('GPT5_STILL_LIMITED after ~4min');
|
||||
}
|
||||
main().catch(e => { console.error('FATAL', e); process.exit(1); });
|
||||
146
benchmarks/harness/scripts/beam-build-ledger-state.ts
Normal file
146
benchmarks/harness/scripts/beam-build-ledger-state.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* P2 — Ingest-time STATE sections over a finished Evidence Ledger (E6).
|
||||
*
|
||||
* Two derived, query-INDEPENDENT sections computed once per conversation from
|
||||
* the whole dated ledger (conv<N>.ledger.txt) and served at the TOP of the
|
||||
* context ahead of the raw ledger:
|
||||
* (a) CURRENT VALUES — supersession chains: an attribute whose value changed
|
||||
* over time → its LATEST value + as-of date + the dated chain of prior
|
||||
* values (fixes knowledge_update / preference / "what is my current X").
|
||||
* (b) CONTRADICTION RECORDS — an assertion later denied, reversed, or
|
||||
* contradicted → BOTH sides with their dates (fixes contradiction_res).
|
||||
*
|
||||
* The real supersede.ts detector operates on embedded Observation frames inside
|
||||
* a substrate; the ledger is plain dated text, so re-ingesting it just to run
|
||||
* that detector is not directly applicable. Instead we run a single gpt-5-mini
|
||||
* pass over the whole ledger with the SAME chain semantics (latest-wins
|
||||
* supersession + explicit contradiction), which is the pragmatic equivalent.
|
||||
*
|
||||
* Output: data/beam/ledgers-1M/conv<N>.state.txt (+ .state.done.json). The
|
||||
* serving runner (beam-run-ledger.ts) prepends this file to the ledger.
|
||||
*
|
||||
* RESUMABLE: per-conv .state.done.json; --resume skips finished convs.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-build-ledger-state.ts --convs 1 --budget 2
|
||||
* tsx scripts/beam-build-ledger-state.ts --convs 1-35 --budget 6 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const OUT_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
|
||||
interface Args { convs: number[]; budget: number; resume: boolean; model: string; }
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = { convs: parseConvSpec('1-35'), budget: 6, resume: false, model: 'gpt-5-mini' };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--model' && next) { a.model = next; i++; }
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
'You are a state-consolidation system. Given a complete, date-ordered evidence ledger of a ' +
|
||||
'conversation, you identify (1) attributes whose value CHANGED over time and report the latest ' +
|
||||
'value with its history, and (2) explicit CONTRADICTIONS where a claim was later denied or ' +
|
||||
'reversed. You reason carefully over dates and never invent facts not present in the ledger.';
|
||||
|
||||
function buildStatePrompt(ledger: string): string {
|
||||
return `Below is the COMPLETE date-ordered evidence ledger for one conversation (each line: [YYYY-MM-DD] fact ("quote") {entities}).
|
||||
|
||||
Produce EXACTLY two sections in this format, and nothing else:
|
||||
|
||||
=== CURRENT VALUES ===
|
||||
For every attribute, setting, decision, preference, plan, or numeric/version value that was STATED MORE THAN ONCE with a DIFFERENT value over time (i.e. it changed / was updated / was superseded), output one line:
|
||||
- <attribute>: CURRENT = <latest value> (as of <YYYY-MM-DD>); history: <older value> (<date>) -> <newer value> (<date>) -> ...
|
||||
Only include attributes that actually CHANGED. If an attribute was stated once and never revised, do NOT list it. If nothing changed, write "(none)".
|
||||
|
||||
=== CONTRADICTION RECORDS ===
|
||||
For every case where a later statement DENIES, REVERSES, or CONFLICTS WITH an earlier statement (e.g. "I decided X" then later "I switched away from X", or "I use A" vs "I use B" for the same thing, or "we shipped Y" vs "Y never happened"), output one line:
|
||||
- <topic>: on <date> — "<earlier claim>"; on <date> — "<conflicting later claim>" [CONFLICT]
|
||||
Include both sides with their dates. If there are no genuine contradictions, write "(none)".
|
||||
|
||||
RULES:
|
||||
- Ground every value, date, and quote in the ledger. Do NOT invent.
|
||||
- Copy numbers, versions, dates, and names EXACTLY as they appear.
|
||||
- Be thorough but precise: only report real changes/conflicts, not mere repetition or elaboration.
|
||||
- Output ONLY the two sections with their headers.
|
||||
|
||||
LEDGER:
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
/** gpt-5-mini caps input at ~272K tokens, but the 1M-token BEAM ledgers exceed
|
||||
* that (conv1 ~390K, conv11 ~430K), so gpt-5-mini returns http_400 on every
|
||||
* conv. Route anthropic/* through OpenRouter (1M context) for whole-ledger state
|
||||
* consolidation; keep gpt-5-mini routing for any conv small enough. */
|
||||
function makeStateClient(model: string): BeamOpenAiClient {
|
||||
if (/claude|anthropic/i.test(model)) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for anthropic/* models');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 2,
|
||||
});
|
||||
}
|
||||
// State detection is genuinely reasoning-heavy (tracking value changes across
|
||||
// dates), so 'medium' effort; long timeout for large ledgers.
|
||||
return createBeamOpenAiClient({
|
||||
model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2, reasoningEffort: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
const client = makeStateClient(args.model);
|
||||
console.log(`[state] model=${args.model} budget=$${args.budget} convs=${args.convs.length}`);
|
||||
let totalCost = 0;
|
||||
|
||||
for (const conv of args.convs) {
|
||||
const ledgerPath = path.join(OUT_DIR, `conv${conv}.ledger.txt`);
|
||||
const statePath = path.join(OUT_DIR, `conv${conv}.state.txt`);
|
||||
const donePath = path.join(OUT_DIR, `conv${conv}.state.done.json`);
|
||||
if (args.resume && fs.existsSync(donePath) && fs.existsSync(statePath)) { console.log(`[state] conv ${conv}: done (skip)`); continue; }
|
||||
if (!fs.existsSync(ledgerPath)) { console.warn(`[state] conv ${conv}: no ledger (skip — build P1 first)`); continue; }
|
||||
if (totalCost >= args.budget) { console.warn(`[state] budget reached — stopping.`); break; }
|
||||
|
||||
const ledger = fs.readFileSync(ledgerPath, 'utf-8');
|
||||
const approxIn = Math.ceil(ledger.length / 4);
|
||||
console.log(`[state] conv ${conv}: ledger ~${approxIn} tok → consolidating...`);
|
||||
const r = await client.chat({ system: SYSTEM_PROMPT, user: buildStatePrompt(ledger), maxTokens: 16384 });
|
||||
totalCost += r.costUsd;
|
||||
if (r.failureMode || !r.text.trim()) { console.error(`[state] conv ${conv}: FAILED (${r.failureMode ?? 'empty'}) — not marking done.`); continue; }
|
||||
|
||||
fs.writeFileSync(statePath, r.text.trim() + '\n');
|
||||
const nCurrent = (r.text.match(/^- /gm) ?? []).length;
|
||||
const stats = { conv, ledger_tokens: approxIn, state_lines: nCurrent, out_tokens: r.outputTokens, cost_usd: +r.costUsd.toFixed(4), model: args.model };
|
||||
fs.writeFileSync(donePath, JSON.stringify(stats, null, 2));
|
||||
console.log(`[state] conv ${conv}: ${nCurrent} state lines, ${r.outputTokens} out tok, $${r.costUsd.toFixed(4)} | running $${totalCost.toFixed(2)}`);
|
||||
}
|
||||
console.log(`[state] DONE. total spend $${totalCost.toFixed(3)}`);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-build-ledger-state] FATAL:', e); process.exit(1); });
|
||||
298
benchmarks/harness/scripts/beam-build-ledger.ts
Normal file
298
benchmarks/harness/scripts/beam-build-ledger.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* P1 — Evidence Ledger builder (E6). Per BEAM 1M conversation, re-distill the
|
||||
* WHOLE raw chat.json into an exhaustive, DATED, QUOTE-GROUNDED evidence ledger.
|
||||
*
|
||||
* WHY (see BEAM-PARADOX-DEEP-DIVE): the entire 18pp gap to Eywa sits in 4
|
||||
* whole-history STATE abilities (abstention / contradiction / event_ordering /
|
||||
* temporal). Top-k retrieval covers <3% of a 1M-token conversation, so no
|
||||
* excerpt can answer them. The fix is a complete DERIVED store, in Eywa's record
|
||||
* format: each line carries a DATE + a VERBATIM SOURCE QUOTE (anti-lossiness —
|
||||
* the prior minds-1M-obs distillation stripped dates/numbers and collapsed
|
||||
* temporal 0.60→0.15). We rebuild from scratch; we do NOT reuse minds-1M-obs.
|
||||
*
|
||||
* UNIT: a "session" = a contiguous same-date block (BEAM anchors each session's
|
||||
* opening main_question with a time_anchor that propagates forward — identical
|
||||
* semantics to src/beam-date-map.ts::buildConvDateMap). Each session (~100K tok)
|
||||
* is chunked into small message windows for EXHAUSTIVE extraction; every chunk
|
||||
* inherits the session date. Both USER and ASSISTANT messages are mined (known
|
||||
* gate facts such as the DeepL/Google pricing live in assistant turns).
|
||||
*
|
||||
* LINE FORMAT (one atomic fact per line, date-ordered):
|
||||
* [YYYY-MM-DD] <fact> ("<verbatim source quote, <=25 words>") {entity1; entity2}
|
||||
*
|
||||
* RESUMABLE: per-chunk cache (data/beam/ledgers-1M/.cache/) + per-conv
|
||||
* .done.json. Rerun with --resume skips finished convs; cached chunks are reused
|
||||
* even for an unfinished conv. --budget hard-caps spend.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-build-ledger.ts --convs 1 --budget 5 # P1 gate: conv 1 first
|
||||
* tsx scripts/beam-build-ledger.ts --convs 1,10,11 --budget 12 # pilot set
|
||||
* tsx scripts/beam-build-ledger.ts --convs 1-35 --budget 30 --resume # full
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { normalizeTimeAnchor } from '../src/beam-date-map.js';
|
||||
|
||||
// ── paths ────────────────────────────────────────────────────────────────────
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..'); // waggle-os
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const OUT_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const CACHE_DIR = path.join(OUT_DIR, '.cache');
|
||||
|
||||
// ── args ─────────────────────────────────────────────────────────────────────
|
||||
interface Args { convs: number[]; budget: number; resume: boolean; model: string; chunkMsgs: number; concurrency: number; }
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = { convs: parseConvSpec('1-35'), budget: 45, resume: false, model: 'gpt-5-mini', chunkMsgs: 20, concurrency: 10 };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--chunk-msgs' && next) { a.chunkMsgs = parseInt(next, 10); i++; }
|
||||
else if (f === '--concurrency' && next) { a.concurrency = parseInt(next, 10); i++; }
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// ── session extraction from chat.json (mirrors buildConvDateMap propagation) ──
|
||||
interface Msg { role: string; content: string; }
|
||||
interface Session { date: string; messages: Msg[]; }
|
||||
interface RawMsg { role?: string; content?: string; time_anchor?: string }
|
||||
interface RawBatch { turns?: RawMsg[][]; time_anchor?: string }
|
||||
|
||||
function loadSessions(conv: number): Session[] {
|
||||
const p = path.join(BEAM_CHATS, '1M', String(conv), 'chat.json');
|
||||
const batches = JSON.parse(fs.readFileSync(p, 'utf-8')) as RawBatch[];
|
||||
let current: string | null = null;
|
||||
const sessions: Session[] = [];
|
||||
let cur: Session | null = null;
|
||||
for (const batch of batches) {
|
||||
const bd = normalizeTimeAnchor(batch.time_anchor);
|
||||
if (bd) current = bd;
|
||||
if (!Array.isArray(batch.turns)) continue;
|
||||
for (const group of batch.turns) {
|
||||
if (!Array.isArray(group)) continue;
|
||||
for (const m of group) {
|
||||
const role = String(m.role ?? 'unknown').toLowerCase();
|
||||
const content = String(m.content ?? '').trim();
|
||||
const md = normalizeTimeAnchor(m.time_anchor);
|
||||
if (md) current = md;
|
||||
if (!content || current === null) continue;
|
||||
if (!cur || cur.date !== current) { cur = { date: current, messages: [] }; sessions.push(cur); }
|
||||
cur.messages.push({ role, content });
|
||||
}
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
function chunk<T>(arr: T[], size: number): T[][] {
|
||||
const out: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── extraction prompt ─────────────────────────────────────────────────────────
|
||||
const SYSTEM_PROMPT =
|
||||
'You are a meticulous memory-extraction system. You convert a dated slice of a ' +
|
||||
'conversation between a user and an AI assistant into an exhaustive list of atomic ' +
|
||||
'evidence facts. You never summarize away detail and you never invent anything.';
|
||||
|
||||
function buildExtractionPrompt(date: string, msgs: Msg[]): string {
|
||||
const slice = msgs.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n\n');
|
||||
return `The following is a slice of a conversation that took place on ${date}. Distill it into an evidence ledger: one line per SUBSTANTIVE fact.
|
||||
|
||||
STRICT OUTPUT FORMAT — one fact per line, and NOTHING else:
|
||||
[${date}] <the fact, self-contained and specific> ("<verbatim quote copied EXACTLY from the slice, <=25 words>") {entity1; entity2}
|
||||
|
||||
RULES:
|
||||
1. COMPLETE ON INFORMATION, not on sentences. Capture every distinct piece of information that could later be asked about: user decisions, preferences, goals, plans, and instructions; every stated tool, library, framework, version number, configuration value, port, price/cost, date, deadline, name, event, problem/error, and its outcome or resolution. Do NOT drop any fact that carries a number, version, date, price, name, or a decision.
|
||||
2. CONSOLIDATE, do not fragment. Put ONE coherent claim on ONE line — fold a recommendation and its rationale, or a setting and its value, into a single line. Do NOT split a bulleted list or a single explanation into many fragment lines. Skip pure pleasantries, filler, restatements, and generic advice that carries no specific detail. Most 20-message slices distill to roughly 25-45 lines, not hundreds.
|
||||
3. The parenthetical quote is MANDATORY on every line and must be copied VERBATIM (character-for-character) from a message in the slice, at most 25 words. It is the evidence anchor — never paraphrase it, never fabricate it. Quote the single most load-bearing <=25-word span (containing the key value/name/number when there is one).
|
||||
4. Copy numbers, versions, dates, prices, filenames, ports, and identifiers EXACTLY as written in the slice — never round or normalize them.
|
||||
5. Attribute the source. Start the fact with "User" or "Assistant" when the source matters (e.g., "User decided to use franc v6.1.0", "Assistant recommended running the backend on port 4000").
|
||||
6. {entities}: the key named entities in the fact (technologies, projects, people, files, services, providers), semicolon-separated. Use {} if there are none.
|
||||
7. If a value CHANGES within the slice (e.g., a version or decision is revised), emit one line for each stated value — do not collapse them.
|
||||
8. Preserve the wording of every stated date ("on March 18", "next Tuesday", "in two weeks") inside the fact so downstream temporal reasoning has the raw reference.
|
||||
9. Output ONLY the evidence lines. No headers, no numbering, no commentary, no blank lines, no markdown.
|
||||
|
||||
CONVERSATION SLICE (date ${date}):
|
||||
${slice}`;
|
||||
}
|
||||
|
||||
// ── line parsing / validation ─────────────────────────────────────────────────
|
||||
const LINE_RE = /^\[(\d{4}-\d{2}-\d{2})\]\s+(.*?)\s*\("([^"]*)"\)\s*(\{[^}]*\})?\s*$/;
|
||||
|
||||
function normWS(s: string): string { return s.replace(/\s+/g, ' ').trim().toLowerCase(); }
|
||||
|
||||
/** Verify the verbatim quote actually appears in the slice (anti-hallucination /
|
||||
* anti-lossiness). Ellipsis-split spans each checked as substrings. */
|
||||
function quoteInSlice(quote: string, sliceNorm: string): boolean {
|
||||
const parts = quote.split(/\.\.\.|…/).map(p => normWS(p)).filter(p => p.length >= 6);
|
||||
if (parts.length === 0) return normWS(quote).length > 0 && sliceNorm.includes(normWS(quote));
|
||||
return parts.every(p => sliceNorm.includes(p));
|
||||
}
|
||||
|
||||
interface ChunkResult { lines: string[]; verified: number; total: number; costUsd: number; failure: string | null; }
|
||||
|
||||
async function extractChunk(client: BeamOpenAiClient, date: string, msgs: Msg[]): Promise<ChunkResult> {
|
||||
const prompt = buildExtractionPrompt(date, msgs);
|
||||
const r = await client.chat({ system: SYSTEM_PROMPT, user: prompt, maxTokens: 16384 });
|
||||
if (r.failureMode) return { lines: [], verified: 0, total: 0, costUsd: r.costUsd, failure: r.failureMode };
|
||||
const sliceNorm = normWS(msgs.map(m => m.content).join(' '));
|
||||
const lines: string[] = [];
|
||||
let verified = 0, total = 0;
|
||||
for (const rawLine of r.text.split('\n')) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
const m = line.match(LINE_RE);
|
||||
if (!m) continue; // drop malformed lines
|
||||
total++;
|
||||
const quote = m[3];
|
||||
const ok = quoteInSlice(quote, sliceNorm);
|
||||
if (ok) verified++;
|
||||
// normalize entity braces to {} when empty/missing
|
||||
const ent = m[4] && m[4] !== '{}' ? ` ${m[4]}` : '';
|
||||
lines.push(`[${m[1]}] ${m[2].trim()} ("${quote}")${ent}`);
|
||||
}
|
||||
return { lines, verified, total, costUsd: r.costUsd, failure: null };
|
||||
}
|
||||
|
||||
// ── per-chunk cache ────────────────────────────────────────────────────────────
|
||||
function cachePath(conv: number, si: number, ci: number): string {
|
||||
return path.join(CACHE_DIR, `conv${conv}_s${si}_c${ci}.json`);
|
||||
}
|
||||
|
||||
async function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, idx: number) => Promise<R>): Promise<R[]> {
|
||||
const results: R[] = new Array(items.length);
|
||||
let next = 0;
|
||||
async function worker() {
|
||||
while (true) {
|
||||
const i = next++;
|
||||
if (i >= items.length) return;
|
||||
results[i] = await fn(items[i], i);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── main ────────────────────────────────────────────────────────────────────
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
// gpt-5-mini emits many output tokens per extraction chunk → the 60s default
|
||||
// timeout aborts mid-generation. Use a long timeout and reasoning_effort:low
|
||||
// (extraction is mechanical; hidden reasoning is wasted latency + $ here).
|
||||
// Any provider-prefixed model ("openai/*", "anthropic/*") is routed through
|
||||
// OpenRouter (the OpenAI-direct account's quota can be exhausted — 429
|
||||
// insufficient_quota). Bare model names keep the OpenAI-direct path unchanged.
|
||||
const client: BeamOpenAiClient = args.model.includes('/')
|
||||
? (() => {
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bare = args.model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model: args.model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[args.model] ?? OPENAI_PRICING[bare] ?? { inputPerMillion: 0.25, outputPerMillion: 2.0 },
|
||||
timeoutMs: 300_000, maxRetries: 2, reasoningEffort: 'minimal',
|
||||
});
|
||||
})()
|
||||
: createBeamOpenAiClient({
|
||||
model: args.model, pricing: OPENAI_PRICING[args.model],
|
||||
timeoutMs: 300_000, maxRetries: 2, reasoningEffort: 'minimal',
|
||||
});
|
||||
|
||||
console.log(`[ledger] model=${args.model} chunkMsgs=${args.chunkMsgs} concurrency=${args.concurrency} budget=$${args.budget} convs=${args.convs.length}`);
|
||||
let totalCost = 0;
|
||||
|
||||
for (const conv of args.convs) {
|
||||
const ledgerPath = path.join(OUT_DIR, `conv${conv}.ledger.txt`);
|
||||
const donePath = path.join(OUT_DIR, `conv${conv}.done.json`);
|
||||
if (args.resume && fs.existsSync(donePath) && fs.existsSync(ledgerPath)) {
|
||||
console.log(`[ledger] conv ${conv}: done (skip)`);
|
||||
continue;
|
||||
}
|
||||
const chatPath = path.join(BEAM_CHATS, '1M', String(conv), 'chat.json');
|
||||
if (!fs.existsSync(chatPath)) { console.warn(`[ledger] conv ${conv}: no chat.json (skip)`); continue; }
|
||||
|
||||
const sessions = loadSessions(conv);
|
||||
// Build the flat list of (sessionIdx, chunkIdx, date, msgs) jobs.
|
||||
interface Job { si: number; ci: number; date: string; msgs: Msg[]; }
|
||||
const jobs: Job[] = [];
|
||||
sessions.forEach((s, si) => chunk(s.messages, args.chunkMsgs).forEach((c, ci) => jobs.push({ si, ci, date: s.date, msgs: c })));
|
||||
|
||||
console.log(`[ledger] conv ${conv}: ${sessions.length} sessions, ${jobs.length} chunks → extracting...`);
|
||||
let convCost = 0, verifiedSum = 0, totalSum = 0, failures = 0;
|
||||
|
||||
const results = await mapLimit(jobs, args.concurrency, async (job) => {
|
||||
const cp = cachePath(conv, job.si, job.ci);
|
||||
if (fs.existsSync(cp)) {
|
||||
try { return JSON.parse(fs.readFileSync(cp, 'utf-8')) as ChunkResult; } catch { /* re-extract */ }
|
||||
}
|
||||
if (totalCost + convCost >= args.budget) return { lines: [], verified: 0, total: 0, costUsd: 0, failure: 'budget' } as ChunkResult;
|
||||
const res = await extractChunk(client, job.date, job.msgs);
|
||||
convCost += res.costUsd;
|
||||
if (!res.failure) fs.writeFileSync(cp, JSON.stringify(res));
|
||||
return res;
|
||||
});
|
||||
|
||||
// Assemble the ledger in job order (session-ordered, chunk-ordered → date-ordered).
|
||||
const allLines: string[] = [];
|
||||
for (const r of results) {
|
||||
allLines.push(...r.lines);
|
||||
verifiedSum += r.verified; totalSum += r.total;
|
||||
if (r.failure && r.failure !== 'budget') failures++;
|
||||
}
|
||||
if (totalCost + convCost >= args.budget && results.some(r => r.failure === 'budget')) {
|
||||
console.warn(`[ledger] conv ${conv}: BUDGET hit mid-conv — partial cache written, NOT marking done. Rerun with higher --budget --resume.`);
|
||||
totalCost += convCost;
|
||||
break;
|
||||
}
|
||||
// Chunk failures are NOT cached, so a rerun retries only them. Do not mark
|
||||
// done and do not overwrite an existing ledger with a partial one — leave the
|
||||
// prior good ledger (if any) untouched and let a rerun complete the conv.
|
||||
if (failures > 0) {
|
||||
console.warn(`[ledger] conv ${conv}: ${failures} chunk failures — NOT marking done; rerun to retry`);
|
||||
totalCost += convCost;
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.writeFileSync(ledgerPath, allLines.join('\n') + '\n');
|
||||
const approxTokens = Math.ceil((allLines.join('\n').length) / 4);
|
||||
const stats = {
|
||||
conv, sessions: sessions.length, chunks: jobs.length, messages: sessions.reduce((n, s) => n + s.messages.length, 0),
|
||||
lines: allLines.length, approx_tokens: approxTokens,
|
||||
quote_verified: totalSum ? +(verifiedSum / totalSum).toFixed(4) : 0,
|
||||
quote_verified_pct: totalSum ? +(100 * verifiedSum / totalSum).toFixed(1) : 0,
|
||||
chunk_failures: failures, cost_usd: +convCost.toFixed(4), model: args.model,
|
||||
};
|
||||
fs.writeFileSync(donePath, JSON.stringify(stats, null, 2));
|
||||
totalCost += convCost;
|
||||
console.log(`[ledger] conv ${conv}: ${allLines.length} lines, ~${approxTokens} tok, quote-verified ${stats.quote_verified_pct}% (${verifiedSum}/${totalSum}), ${failures} chunk-failures, $${convCost.toFixed(3)} | running $${totalCost.toFixed(2)}`);
|
||||
if (totalCost >= args.budget) { console.warn(`[ledger] budget $${args.budget} reached — stopping.`); break; }
|
||||
}
|
||||
console.log(`[ledger] DONE. total spend $${totalCost.toFixed(3)}`);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-build-ledger] FATAL:', e); process.exit(1); });
|
||||
114
benchmarks/harness/scripts/beam-compose-matched50.ts
Normal file
114
benchmarks/harness/scripts/beam-compose-matched50.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* Compose the E6 matched-50 headline number — general, multi-iteration.
|
||||
* Sources in precedence order (later overrides earlier for the CURRENT-CODE
|
||||
* config = latest prompt per ability). Also reports a best-per-ability variant
|
||||
* (max ability-mean across all iterations — post-hoc dev-set selection).
|
||||
*
|
||||
* pilot — all 10 abilities, OpenAI-direct gpt-5.
|
||||
* iter2-REJUDGED — 6 changed abilities, OpenRouter gpt-5.
|
||||
* iter3 — preference, temporal (OpenRouter gpt-5 inline).
|
||||
* iter4 — temporal, knowledge_update, preference, event_ordering.
|
||||
*/
|
||||
|
||||
const RESULTS = 'D:/Projects/waggle-os/benchmarks/results/beam/';
|
||||
const OUT = 'beam-1m-e6-ledger-composed-matched50.json';
|
||||
const REJUDGE_JSON = 'beam-1m-e6-ledger-rejudge-OR.json';
|
||||
|
||||
// precedence low -> high
|
||||
const SOURCES = [
|
||||
{ name: 'pilot', file: 'beam-1m-e6-ledger-pilot-anthropic-claude-sonnet-4.6.jsonl' },
|
||||
{ name: 'iter2-OR', file: 'beam-1m-e6-ledger-iter2-REJUDGED.jsonl' },
|
||||
{ name: 'iter3', file: 'beam-1m-e6-ledger-iter3-anthropic-claude-sonnet-4.6.jsonl' },
|
||||
{ name: 'iter4', file: 'beam-1m-e6-ledger-iter4-anthropic-claude-sonnet-4.6.jsonl' },
|
||||
{ name: 'iter5', file: 'beam-1m-e6-ledger-iter5-anthropic-claude-sonnet-4.6.jsonl' },
|
||||
{ name: 'iter6', file: 'beam-1m-e6-ledger-iter6-anthropic-claude-sonnet-4.6.jsonl' },
|
||||
];
|
||||
const ALL = ['abstention', 'contradiction_resolution', 'event_ordering', 'information_extraction', 'instruction_following', 'knowledge_update', 'multi_session_reasoning', 'preference_following', 'summarization', 'temporal_reasoning'];
|
||||
|
||||
const REFS: [string, number][] = [
|
||||
['baseline (no read-time stack)', 0.5533],
|
||||
['best read-time stack', 0.6198],
|
||||
['pilot (iter1, all-direct judge)', 0.6825],
|
||||
['iter2-composed', 0.7073],
|
||||
['iter3-composed', 0.7323],
|
||||
['Eywa on same-50 (MATCH target)', 0.7704],
|
||||
['clear-SOTA target', 0.80],
|
||||
];
|
||||
|
||||
type Row = { instance_id: string; memory_ability: string; score: number };
|
||||
const mean = (xs: number[]): number => xs.reduce((s, x) => s + x, 0) / xs.length;
|
||||
|
||||
function loadIfExists(f: string): Row[] {
|
||||
const p = RESULTS + f;
|
||||
if (!fs.existsSync(p)) return [];
|
||||
return fs.readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l) as Row);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
// per-source, per-ability mean + rows
|
||||
const srcAbilityMean: Record<string, Record<string, number>> = {};
|
||||
const srcAbilityRows: Record<string, Record<string, Row[]>> = {};
|
||||
for (const s of SOURCES) {
|
||||
const rows = loadIfExists(s.file);
|
||||
const byAb: Record<string, Row[]> = {};
|
||||
for (const r of rows) (byAb[r.memory_ability] ??= []).push(r);
|
||||
srcAbilityRows[s.name] = byAb;
|
||||
srcAbilityMean[s.name] = {};
|
||||
for (const a of Object.keys(byAb)) srcAbilityMean[s.name][a] = mean(byAb[a].map(r => r.score));
|
||||
}
|
||||
|
||||
const iter1OR = (JSON.parse(fs.readFileSync(RESULTS + REJUDGE_JSON, 'utf-8')) as { iter1_OR_ability_means: Record<string, number> }).iter1_OR_ability_means;
|
||||
|
||||
// current-code: latest source (highest precedence) that has the ability
|
||||
const chosen: Record<string, { src: string; rows: Row[]; mean: number }> = {};
|
||||
for (const a of ALL) {
|
||||
for (let i = SOURCES.length - 1; i >= 0; i--) {
|
||||
const s = SOURCES[i].name;
|
||||
if (srcAbilityRows[s][a]?.length) { chosen[a] = { src: s, rows: srcAbilityRows[s][a], mean: srcAbilityMean[s][a] }; break; }
|
||||
}
|
||||
}
|
||||
// best-per-ability: max ability-mean across sources
|
||||
const best: Record<string, { src: string; mean: number }> = {};
|
||||
for (const a of ALL) {
|
||||
let bv = -1, bs = '';
|
||||
for (const s of SOURCES) { const m = srcAbilityMean[s.name][a]; if (m !== undefined && m > bv) { bv = m; bs = s.name; } }
|
||||
best[a] = { src: bs, mean: bv };
|
||||
}
|
||||
|
||||
const composedRows: Row[] = ALL.flatMap(a => chosen[a].rows);
|
||||
const currentMicro = mean(composedRows.map(r => r.score));
|
||||
const currentMacro = mean(ALL.map(a => chosen[a].mean));
|
||||
const bestMacro = mean(ALL.map(a => best[a].mean));
|
||||
|
||||
console.log(`composed rows: ${composedRows.length} (expect 50)`);
|
||||
console.log('\nability current src best bestSrc iter1-OR');
|
||||
for (const a of ALL) {
|
||||
const i1 = iter1OR[a] !== undefined ? iter1OR[a].toFixed(3) : ' - ';
|
||||
console.log(`${a.padEnd(27)} ${chosen[a].mean.toFixed(3)} ${chosen[a].src.padEnd(9)} ${best[a].mean.toFixed(3)} ${best[a].src.padEnd(9)} ${i1}`);
|
||||
}
|
||||
|
||||
console.log(`\nCOMPOSED (current code) micro=${currentMicro.toFixed(4)} macro=${currentMacro.toFixed(4)}`);
|
||||
console.log(`COMPOSED (best-per-ability) macro=${bestMacro.toFixed(4)}`);
|
||||
console.log('\nvs reference (current-code micro):');
|
||||
for (const [name, val] of REFS) {
|
||||
const d = currentMicro - val;
|
||||
console.log(` ${name.padEnd(34)} ${val.toFixed(4)} Δ=${(d >= 0 ? '+' : '') + d.toFixed(4)} ${currentMicro >= val ? 'REACHED' : 'short'}`);
|
||||
}
|
||||
console.log(`\nMATCH >=0.7704: current ${currentMicro >= 0.7704 ? 'YES' : 'NO'} | best ${bestMacro >= 0.7704 ? 'YES' : 'NO'} CLEAR >=0.80: current ${currentMicro >= 0.80 ? 'YES' : 'NO'} | best ${bestMacro >= 0.80 ? 'YES' : 'NO'}`);
|
||||
|
||||
fs.writeFileSync(RESULTS + OUT, JSON.stringify({
|
||||
composed_current_code_micro: currentMicro, composed_current_code_macro: currentMacro,
|
||||
composed_best_per_ability_macro: bestMacro,
|
||||
current_code: Object.fromEntries(ALL.map(a => [a, { mean: chosen[a].mean, src: chosen[a].src }])),
|
||||
best_per_ability: Object.fromEntries(ALL.map(a => [a, best[a]])),
|
||||
per_source_ability_means: srcAbilityMean,
|
||||
iter1_OR_ability_means: iter1OR,
|
||||
reached: { match_current: currentMicro >= 0.7704, match_best: bestMacro >= 0.7704, clear_current: currentMicro >= 0.80 },
|
||||
reference_points: Object.fromEntries(REFS),
|
||||
generated_at: new Date().toISOString(),
|
||||
}, null, 2));
|
||||
console.log('\nwrote', RESULTS + OUT);
|
||||
}
|
||||
main();
|
||||
207
benchmarks/harness/scripts/beam-e2-analyze.ts
Normal file
207
benchmarks/harness/scripts/beam-e2-analyze.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E2 analysis — assemble the 2×2 store×prompt table, decompose main effects
|
||||
* and interaction, attach the confound classification, and write
|
||||
* beam-e2-FINAL.json.
|
||||
*
|
||||
* Metrics per cell: mean nugget score (0..1) and pass rate (judgment==PASS).
|
||||
* Uncertainty: conversation-cluster bootstrap. The 70 questions are 2 per
|
||||
* conversation × 35 conversations; resampling INDEPENDENT questions would
|
||||
* understate variance because the two questions from one conversation share a
|
||||
* store. So we resample the 35 conversation clusters with replacement (B=10000)
|
||||
* and recompute every statistic on each resample.
|
||||
*
|
||||
* Cells:
|
||||
* A raw v2 (retain dated turns + conflict-aware prompt)
|
||||
* B raw incumbent (retain dated turns + prefer-most-recent prompt)
|
||||
* C reconciled v2 (collapsed current-state store + conflict-aware)
|
||||
* D reconciled incumbent (collapsed current-state store + prefer-recent)
|
||||
*
|
||||
* STORE main effect = mean(raw {A,B}) − mean(reconciled {C,D})
|
||||
* PROMPT main effect = mean(v2 {A,C}) − mean(incumbent {B,D})
|
||||
* INTERACTION = (A−B) − (C−D) [ = does the prompt gap depend on store ]
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const outDir = path.resolve(path.dirname(here), '..', '..', 'results', 'beam');
|
||||
|
||||
interface Row {
|
||||
instance_id: string;
|
||||
conv: number;
|
||||
score: number;
|
||||
judgment: string;
|
||||
}
|
||||
|
||||
function load(file: string): Row[] {
|
||||
const p = path.join(outDir, file);
|
||||
if (!fs.existsSync(p)) throw new Error(`missing ${file}`);
|
||||
return fs.readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).map(l => {
|
||||
const r = JSON.parse(l);
|
||||
return { instance_id: r.instance_id, conv: r.conv, score: r.score, judgment: r.judgment };
|
||||
});
|
||||
}
|
||||
|
||||
const CELL_FILES: Record<string, string> = {
|
||||
A: 'beam-e2-cellA.jsonl', B: 'beam-e2-cellB.jsonl',
|
||||
C: 'beam-e2-cellC.jsonl', D: 'beam-e2-cellD.jsonl',
|
||||
};
|
||||
|
||||
function mean(xs: number[]): number { return xs.reduce((s, x) => s + x, 0) / xs.length; }
|
||||
function passRate(rows: Row[]): number { return rows.filter(r => r.judgment === 'PASS').length / rows.length; }
|
||||
function meanScore(rows: Row[]): number { return mean(rows.map(r => r.score)); }
|
||||
|
||||
// Align all cells to a common instance_id ordering so cluster resampling picks
|
||||
// the SAME conversation across cells.
|
||||
function main(): void {
|
||||
const cells: Record<string, Row[]> = {};
|
||||
for (const [c, f] of Object.entries(CELL_FILES)) cells[c] = load(f);
|
||||
|
||||
const ids = cells.A.map(r => r.instance_id);
|
||||
const convOf: Record<string, number> = {};
|
||||
for (const r of cells.A) convOf[r.instance_id] = r.conv;
|
||||
|
||||
// index each cell by instance_id for aligned lookup
|
||||
const byId: Record<string, Record<string, Row>> = {};
|
||||
for (const [c, rows] of Object.entries(cells)) {
|
||||
byId[c] = {};
|
||||
for (const r of rows) byId[c][r.instance_id] = r;
|
||||
}
|
||||
for (const c of Object.keys(cells)) {
|
||||
for (const id of ids) if (!byId[c][id]) throw new Error(`cell ${c} missing ${id}`);
|
||||
}
|
||||
|
||||
const convs = [...new Set(ids.map(id => convOf[id]))];
|
||||
const idsByConv: Record<number, string[]> = {};
|
||||
for (const id of ids) (idsByConv[convOf[id]] ??= []).push(id);
|
||||
|
||||
// point estimates
|
||||
const point: Record<string, { mean: number; pass: number; n: number }> = {};
|
||||
for (const c of Object.keys(cells)) {
|
||||
point[c] = { mean: meanScore(cells[c]), pass: passRate(cells[c]), n: cells[c].length };
|
||||
}
|
||||
|
||||
// statistic vector computed from a set of instance ids on a given metric
|
||||
const cellStat = (c: string, sampleIds: string[], metric: 'mean' | 'pass'): number => {
|
||||
const rows = sampleIds.map(id => byId[c][id]);
|
||||
return metric === 'mean' ? meanScore(rows) : passRate(rows);
|
||||
};
|
||||
|
||||
const B = 10000;
|
||||
// seeded RNG (mulberry32) for reproducibility
|
||||
let seed = 0x9e3779b9;
|
||||
const rng = (): number => {
|
||||
seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
|
||||
// accumulate bootstrap distributions
|
||||
const dist: Record<string, number[]> = {};
|
||||
const push = (k: string, v: number) => (dist[k] ??= []).push(v);
|
||||
|
||||
for (let b = 0; b < B; b++) {
|
||||
// resample conversation clusters with replacement
|
||||
const sampleIds: string[] = [];
|
||||
for (let i = 0; i < convs.length; i++) {
|
||||
const conv = convs[Math.floor(rng() * convs.length)];
|
||||
sampleIds.push(...idsByConv[conv]);
|
||||
}
|
||||
for (const metric of ['mean', 'pass'] as const) {
|
||||
const A = cellStat('A', sampleIds, metric);
|
||||
const Bc = cellStat('B', sampleIds, metric);
|
||||
const C = cellStat('C', sampleIds, metric);
|
||||
const D = cellStat('D', sampleIds, metric);
|
||||
push(`A_${metric}`, A); push(`B_${metric}`, Bc); push(`C_${metric}`, C); push(`D_${metric}`, D);
|
||||
push(`store_${metric}`, (A + Bc) / 2 - (C + D) / 2); // raw − reconciled
|
||||
push(`prompt_${metric}`, (A + C) / 2 - (Bc + D) / 2); // v2 − incumbent
|
||||
push(`interaction_${metric}`, (A - Bc) - (C - D)); // prompt gap: raw − reconciled
|
||||
push(`prompt_within_raw_${metric}`, A - Bc);
|
||||
push(`prompt_within_recon_${metric}`, C - D);
|
||||
push(`store_within_v2_${metric}`, A - C);
|
||||
push(`store_within_incumbent_${metric}`, Bc - D);
|
||||
}
|
||||
}
|
||||
|
||||
const ci = (k: string): { lo: number; hi: number; se: number } => {
|
||||
const xs = [...dist[k]].sort((a, b) => a - b);
|
||||
const lo = xs[Math.floor(0.025 * xs.length)];
|
||||
const hi = xs[Math.floor(0.975 * xs.length)];
|
||||
const m = mean(xs);
|
||||
const se = Math.sqrt(mean(xs.map(x => (x - m) ** 2)));
|
||||
return { lo, hi, se };
|
||||
};
|
||||
|
||||
// confound (optional)
|
||||
let confound: unknown = null;
|
||||
const confP = path.join(outDir, 'beam-e2-confound.json');
|
||||
if (fs.existsSync(confP)) confound = JSON.parse(fs.readFileSync(confP, 'utf-8'));
|
||||
|
||||
const round = (x: number) => Math.round(x * 10000) / 10000;
|
||||
const fmtCi = (k: string) => { const c = ci(k); return { lo: round(c.lo), hi: round(c.hi), se: round(c.se) }; };
|
||||
|
||||
const final = {
|
||||
experiment: 'E2 — BEAM 2×2 store×prompt causal ablation (contradiction_resolution)',
|
||||
n_questions: cells.A.length,
|
||||
n_conversations: convs.length,
|
||||
bootstrap: { method: 'conversation-cluster', B, seed_rng: 'mulberry32' },
|
||||
external_anchor: { published_cellA_subset_mean: 0.5875, published_cellA_subset_pass: 0.8714, mem0_mean: 0.3571 },
|
||||
design: {
|
||||
A: { store: 'raw', prompt: 'v2' },
|
||||
B: { store: 'raw', prompt: 'incumbent' },
|
||||
C: { store: 'reconciled', prompt: 'v2' },
|
||||
D: { store: 'reconciled', prompt: 'incumbent' },
|
||||
},
|
||||
cells: Object.fromEntries(Object.keys(cells).map(c => [c, {
|
||||
mean: round(point[c].mean), pass: round(point[c].pass), n: point[c].n,
|
||||
mean_ci: fmtCi(`${c}_mean`), pass_ci: fmtCi(`${c}_pass`),
|
||||
}])),
|
||||
effects: {
|
||||
mean: {
|
||||
store_raw_minus_reconciled: { point: round((point.A.mean + point.B.mean) / 2 - (point.C.mean + point.D.mean) / 2), ci: fmtCi('store_mean') },
|
||||
prompt_v2_minus_incumbent: { point: round((point.A.mean + point.C.mean) / 2 - (point.B.mean + point.D.mean) / 2), ci: fmtCi('prompt_mean') },
|
||||
interaction: { point: round((point.A.mean - point.B.mean) - (point.C.mean - point.D.mean)), ci: fmtCi('interaction_mean') },
|
||||
prompt_within_raw: { point: round(point.A.mean - point.B.mean), ci: fmtCi('prompt_within_raw_mean') },
|
||||
prompt_within_reconciled: { point: round(point.C.mean - point.D.mean), ci: fmtCi('prompt_within_recon_mean') },
|
||||
store_within_v2: { point: round(point.A.mean - point.C.mean), ci: fmtCi('store_within_v2_mean') },
|
||||
store_within_incumbent: { point: round(point.B.mean - point.D.mean), ci: fmtCi('store_within_incumbent_mean') },
|
||||
},
|
||||
pass: {
|
||||
store_raw_minus_reconciled: { point: round((point.A.pass + point.B.pass) / 2 - (point.C.pass + point.D.pass) / 2), ci: fmtCi('store_pass') },
|
||||
prompt_v2_minus_incumbent: { point: round((point.A.pass + point.C.pass) / 2 - (point.B.pass + point.D.pass) / 2), ci: fmtCi('prompt_pass') },
|
||||
interaction: { point: round((point.A.pass - point.B.pass) - (point.C.pass - point.D.pass)), ci: fmtCi('interaction_pass') },
|
||||
prompt_within_raw: { point: round(point.A.pass - point.B.pass), ci: fmtCi('prompt_within_raw_pass') },
|
||||
prompt_within_reconciled: { point: round(point.C.pass - point.D.pass), ci: fmtCi('prompt_within_recon_pass') },
|
||||
store_within_v2: { point: round(point.A.pass - point.C.pass), ci: fmtCi('store_within_v2_pass') },
|
||||
store_within_incumbent: { point: round(point.B.pass - point.D.pass), ci: fmtCi('store_within_incumbent_pass') },
|
||||
},
|
||||
},
|
||||
confound,
|
||||
};
|
||||
|
||||
const outP = path.join(outDir, 'beam-e2-FINAL.json');
|
||||
fs.writeFileSync(outP, JSON.stringify(final, null, 2) + '\n', 'utf-8');
|
||||
|
||||
// console summary
|
||||
console.log('\n=== E2 2×2 (contradiction_resolution, n=' + cells.A.length + ') ===');
|
||||
console.log('cell mean pass');
|
||||
for (const c of ['A', 'B', 'C', 'D']) {
|
||||
console.log(`${c} ${final.design[c as 'A'].store.padEnd(11)}${final.design[c as 'A'].prompt.padEnd(10)} ${point[c].mean.toFixed(4)} ${(point[c].pass * 100).toFixed(1)}%`);
|
||||
}
|
||||
const e = final.effects;
|
||||
console.log('\nMAIN EFFECTS (mean nugget):');
|
||||
console.log(` STORE (raw−recon): ${e.mean.store_raw_minus_reconciled.point.toFixed(4)} 95%CI[${e.mean.store_raw_minus_reconciled.ci.lo},${e.mean.store_raw_minus_reconciled.ci.hi}]`);
|
||||
console.log(` PROMPT (v2−incumb): ${e.mean.prompt_v2_minus_incumbent.point.toFixed(4)} 95%CI[${e.mean.prompt_v2_minus_incumbent.ci.lo},${e.mean.prompt_v2_minus_incumbent.ci.hi}]`);
|
||||
console.log(` INTERACTION: ${e.mean.interaction.point.toFixed(4)} 95%CI[${e.mean.interaction.ci.lo},${e.mean.interaction.ci.hi}]`);
|
||||
console.log('MAIN EFFECTS (pass rate):');
|
||||
console.log(` STORE (raw−recon): ${(e.pass.store_raw_minus_reconciled.point * 100).toFixed(1)}pp 95%CI[${(e.pass.store_raw_minus_reconciled.ci.lo * 100).toFixed(1)},${(e.pass.store_raw_minus_reconciled.ci.hi * 100).toFixed(1)}]`);
|
||||
console.log(` PROMPT (v2−incumb): ${(e.pass.prompt_v2_minus_incumbent.point * 100).toFixed(1)}pp 95%CI[${(e.pass.prompt_v2_minus_incumbent.ci.lo * 100).toFixed(1)},${(e.pass.prompt_v2_minus_incumbent.ci.hi * 100).toFixed(1)}]`);
|
||||
console.log(` INTERACTION: ${(e.pass.interaction.point * 100).toFixed(1)}pp 95%CI[${(e.pass.interaction.ci.lo * 100).toFixed(1)},${(e.pass.interaction.ci.hi * 100).toFixed(1)}]`);
|
||||
console.log(`\n→ ${outP}`);
|
||||
}
|
||||
|
||||
main();
|
||||
393
benchmarks/harness/scripts/beam-e2-cells.ts
Normal file
393
benchmarks/harness/scripts/beam-e2-cells.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E2 — BEAM 2×2 store×prompt causal ablation on the contradiction ability.
|
||||
*
|
||||
* Isolates whether our +23pp BEAM contradiction win comes from (a) retaining raw
|
||||
* dated turns in the store, (b) the conflict-aware v2 answer prompt, or their
|
||||
* interaction. Four cells over the SAME 70 contradiction_resolution questions:
|
||||
*
|
||||
* | cell | store | prompt |
|
||||
* | A | raw dated turns (ours) | conflict-aware v2 | published run (reused)
|
||||
* | B | raw dated turns (ours) | incumbent-base (v1) | this script
|
||||
* | C | reconciled current-only | conflict-aware v2 | this script
|
||||
* | D | reconciled current-only | incumbent-base (v1) | this script
|
||||
*
|
||||
* Retrieval is held CONSTANT across all cells: OUR retriever, top-30, regenerated
|
||||
* locally (deterministic) and cached once. Cells B/C/D read the identical cache.
|
||||
* The "store" dimension changes ONLY whether the retrieved dated turns are passed
|
||||
* raw (B) or first collapsed by a read-time reconciliation pass (C/D). The
|
||||
* "prompt" dimension changes ONLY the rule set (v2 vs v1), dating held constant.
|
||||
*
|
||||
* Reconciliation is a read-time SIMULATION of write-time reconciliation: a single
|
||||
* gpt-5-mini ADD/UPDATE/DELETE pass over the top-30 dated turns → a current-state
|
||||
* dated fact list where the later statement wins and no contradiction survives.
|
||||
* It is cached per instance_id so cells C and D answer a byte-identical store.
|
||||
*
|
||||
* Modes (one command each; resumable):
|
||||
* --mode retrieval build beam-e2-retrieval.json (needs ollama; no API spend)
|
||||
* --mode repro-a re-answer N cell-A questions over the cache, compare scores
|
||||
* --mode cellB answer incumbent over dated raw turns → beam-e2-cellB.jsonl
|
||||
* --mode reconcile build beam-e2-reconciled.json (gpt-5-mini)
|
||||
* --mode cellC answer v2 over reconciled store → beam-e2-cellC.jsonl
|
||||
* --mode cellD answer incumbent over reconciled store → beam-e2-cellD.jsonl
|
||||
* --mode confound classify every cell's answers surface-both/pick-latest/abstain
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import process from 'node:process';
|
||||
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import type { SearchResult } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient } from '../src/beam-openai-client.js';
|
||||
import type { BeamOpenAiClient } from '../src/beam-openai-client.js';
|
||||
import {
|
||||
buildAnswerGenerationPrompt, // v1 / incumbent-base rule set
|
||||
buildAnswerGenerationPromptV2, // conflict-aware v2 rule set
|
||||
judgeQuestion,
|
||||
} from '../src/beam-nugget-judge.js';
|
||||
import type { BeamLlmResult } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap, renderMemories } from '../src/beam-date-map.js';
|
||||
|
||||
const ABILITY = 'contradiction_resolution';
|
||||
const TOP_K = 30;
|
||||
const CONVS = Array.from({ length: 35 }, (_, i) => i + 1);
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const beamChats = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const mindsDir = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
const RETRIEVAL_CACHE = path.join(outDir, 'beam-e2-retrieval.json');
|
||||
const RECONCILED_CACHE = path.join(outDir, 'beam-e2-reconciled.json');
|
||||
const PUBLISHED_A = path.join(outDir, 'beam-1m-FULL700-gpt5-retv2.jsonl');
|
||||
|
||||
interface Question {
|
||||
instanceId: string; conv: number; gopId: string; question: string; rubric: string[];
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Load only the contradiction_resolution questions for a conv, in dataset order. */
|
||||
function loadContradictionQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(beamChats, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
const questions = data[ABILITY];
|
||||
if (!Array.isArray(questions)) return [];
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({
|
||||
instanceId: `beam_1M_${conv}_${ABILITY}_q${qi}`,
|
||||
conv, gopId: `beam_${conv}`, question: q, rubric: extractRubric(pq),
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function allQuestions(): Question[] {
|
||||
return CONVS.flatMap(loadContradictionQuestions);
|
||||
}
|
||||
|
||||
function mindPath(conv: number): string { return path.join(mindsDir, `beam_1M_${conv}.mind`); }
|
||||
|
||||
/** Oldest-first (frame id asc) — mirrors the harness's memoriesFromResults. */
|
||||
function memoriesFromResults(results: readonly SearchResult[]): string[] {
|
||||
return [...results].sort((a, b) => a.frame.id - b.frame.id).map(r => r.frame.content);
|
||||
}
|
||||
|
||||
// ── Retrieval cache (deterministic, no API) ──────────────────────────────────
|
||||
|
||||
interface RetrievalEntry { instanceId: string; conv: number; question: string; rubric: string[]; datedMemories: string[]; }
|
||||
|
||||
async function buildRetrievalCache(): Promise<void> {
|
||||
const embedder = createOllamaEmbedder();
|
||||
const byConv = new Map<number, Question[]>();
|
||||
for (const q of allQuestions()) {
|
||||
if (!byConv.has(q.conv)) byConv.set(q.conv, []);
|
||||
byConv.get(q.conv)!.push(q);
|
||||
}
|
||||
const cache: Record<string, RetrievalEntry> = {};
|
||||
let total = 0;
|
||||
for (const conv of CONVS) {
|
||||
const qs = byConv.get(conv) ?? [];
|
||||
if (qs.length === 0) continue;
|
||||
const substrate = createSubstrate({ dbPath: mindPath(conv), embedder });
|
||||
try {
|
||||
const dateMap = buildConvDateMap(path.join(beamChats, '1M', String(conv), 'chat.json'));
|
||||
for (const q of qs) {
|
||||
const results = await substrate.search.search(q.question, { limit: TOP_K, gopId: q.gopId });
|
||||
const memories = memoriesFromResults(results);
|
||||
const datedMemories = renderMemories(memories, dateMap, 'v2');
|
||||
cache[q.instanceId] = { instanceId: q.instanceId, conv, question: q.question, rubric: q.rubric, datedMemories };
|
||||
total++;
|
||||
process.stdout.write(` [conv ${conv}] ${q.instanceId} → ${datedMemories.length} dated turns\n`);
|
||||
}
|
||||
} finally { substrate.close(); }
|
||||
}
|
||||
fs.writeFileSync(RETRIEVAL_CACHE, JSON.stringify(cache, null, 0) + '\n', 'utf-8');
|
||||
console.log(`\n[retrieval] cached ${total} questions → ${RETRIEVAL_CACHE}`);
|
||||
}
|
||||
|
||||
function loadRetrievalCache(): Record<string, RetrievalEntry> {
|
||||
if (!fs.existsSync(RETRIEVAL_CACHE)) { console.error(`[e2] missing ${RETRIEVAL_CACHE}; run --mode retrieval first`); process.exit(2); }
|
||||
return JSON.parse(fs.readFileSync(RETRIEVAL_CACHE, 'utf-8')) as Record<string, RetrievalEntry>;
|
||||
}
|
||||
|
||||
// ── Reconciliation cache (gpt-5-mini; simulate write-time ADD/UPDATE/DELETE) ──
|
||||
|
||||
const RECONCILE_SYSTEM =
|
||||
'You maintain a running fact store from a user\'s chat history, mem0-style. You are given dated ' +
|
||||
'conversation excerpts in chronological order (oldest first). Produce the CURRENT state of the ' +
|
||||
'user\'s facts as a bulleted list. Apply ADD/UPDATE/DELETE semantics: when a later statement ' +
|
||||
'changes or contradicts an earlier one, KEEP ONLY the later (current) value and discard the ' +
|
||||
'earlier one — the superseded value must NOT appear anywhere. Each surviving fact is a single ' +
|
||||
'line prefixed with the date it was last affirmed: [YYYY-MM-DD] fact. Do NOT include ' +
|
||||
'contradictions, history, or "previously X now Y" phrasing — only the current resolved state. ' +
|
||||
'Output ONLY the bulleted list, one fact per line.';
|
||||
|
||||
function buildReconcilePrompt(datedMemories: string[]): string {
|
||||
const body = datedMemories.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
return `## Dated conversation excerpts (oldest first)\n${body}\n\n## Current-state fact list (later statement wins, no contradiction survives):`;
|
||||
}
|
||||
|
||||
interface ReconciledEntry { instanceId: string; reconciledMemories: string[]; }
|
||||
|
||||
function readCache(p: string): Record<string, ReconciledEntry> {
|
||||
if (!fs.existsSync(p)) return {};
|
||||
try { return JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, ReconciledEntry>; }
|
||||
catch { console.warn(`[reconcile] ${path.basename(p)} unparseable — treating as empty`); return {}; }
|
||||
}
|
||||
|
||||
/** Reconcile a conv-range shard into its own cache file. Skips ids already
|
||||
* present in the MAIN cache (from an earlier partial run) or this shard's file,
|
||||
* so parallel shards never redo each other's or the prior run's work. */
|
||||
async function buildReconciledCache(convRange: number[] | null, cachePath: string): Promise<void> {
|
||||
const retrieval = loadRetrievalCache();
|
||||
const client = createBeamOpenAiClient({ model: 'gpt-5-mini' });
|
||||
const shard = readCache(cachePath);
|
||||
const mainDone = new Set(Object.keys(readCache(RECONCILED_CACHE)));
|
||||
const inRange = (conv: number): boolean => !convRange || convRange.includes(conv);
|
||||
let cost = 0, done = 0;
|
||||
const ids = Object.keys(retrieval).sort();
|
||||
for (const id of ids) {
|
||||
const entry = retrieval[id];
|
||||
if (!inRange(entry.conv)) continue;
|
||||
if (shard[id] || mainDone.has(id)) continue;
|
||||
const res = await client.chat({ system: RECONCILE_SYSTEM, user: buildReconcilePrompt(entry.datedMemories), maxTokens: 4096 });
|
||||
cost += res.costUsd;
|
||||
const lines = res.text.split('\n').map(l => l.replace(/^\s*[-*•]\s*/, '').trim()).filter(Boolean);
|
||||
shard[id] = { instanceId: id, reconciledMemories: lines };
|
||||
fs.writeFileSync(cachePath, JSON.stringify(shard, null, 0) + '\n', 'utf-8');
|
||||
done++;
|
||||
process.stdout.write(` reconciled ${id}: ${entry.datedMemories.length} turns → ${lines.length} facts $${cost.toFixed(3)}\n`);
|
||||
}
|
||||
console.log(`\n[reconcile] shard ${path.basename(cachePath)} +${done} (${Object.keys(shard).length} in shard) cost=$${cost.toFixed(4)}`);
|
||||
}
|
||||
|
||||
/** Merge the main cache + all beam-e2-reconciled.shard-*.json into the main cache. */
|
||||
function mergeReconciledShards(): void {
|
||||
const merged = readCache(RECONCILED_CACHE);
|
||||
for (const f of fs.readdirSync(outDir)) {
|
||||
if (!/^beam-e2-reconciled\.shard-.*\.json$/.test(f)) continue;
|
||||
const shard = readCache(path.join(outDir, f));
|
||||
for (const [id, v] of Object.entries(shard)) merged[id] = v;
|
||||
}
|
||||
fs.writeFileSync(RECONCILED_CACHE, JSON.stringify(merged, null, 0) + '\n', 'utf-8');
|
||||
const retrieval = loadRetrievalCache();
|
||||
const missing = Object.keys(retrieval).filter(id => !merged[id]);
|
||||
console.log(`[reconcile-merge] ${Object.keys(merged).length}/${Object.keys(retrieval).length} reconciled → ${RECONCILED_CACHE}`);
|
||||
if (missing.length) console.warn(`[reconcile-merge] MISSING ${missing.length}: ${missing.slice(0, 10).join(', ')}${missing.length > 10 ? '…' : ''}`);
|
||||
}
|
||||
|
||||
function loadReconciledCache(): Record<string, ReconciledEntry> {
|
||||
if (!fs.existsSync(RECONCILED_CACHE)) { console.error(`[e2] missing ${RECONCILED_CACHE}; run --mode reconcile first`); process.exit(2); }
|
||||
return JSON.parse(fs.readFileSync(RECONCILED_CACHE, 'utf-8')) as Record<string, ReconciledEntry>;
|
||||
}
|
||||
|
||||
// ── Answer + judge cells ─────────────────────────────────────────────────────
|
||||
|
||||
type Store = 'raw' | 'reconciled';
|
||||
type Prompt = 'v2' | 'incumbent';
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function buildPrompt(prompt: Prompt, question: string, memories: string[]): string {
|
||||
return prompt === 'v2'
|
||||
? buildAnswerGenerationPromptV2(question, memories)
|
||||
: buildAnswerGenerationPrompt(question, memories);
|
||||
}
|
||||
|
||||
async function runCell(cellLabel: string, store: Store, prompt: Prompt, outPath: string, budget: number): Promise<void> {
|
||||
const retrieval = loadRetrievalCache();
|
||||
const reconciled = store === 'reconciled' ? loadReconciledCache() : null;
|
||||
const client = createBeamOpenAiClient({ model: 'gpt-5' });
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const r = JSON.parse(t) as { instance_id?: string }; if (r.instance_id) doneIds.add(r.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
console.log(`[${cellLabel}] resume: ${doneIds.size} already answered`);
|
||||
}
|
||||
const out = fs.createWriteStream(outPath, { flags: 'a' });
|
||||
const ids = Object.keys(retrieval).sort();
|
||||
let cost = 0, n = 0;
|
||||
for (const id of ids) {
|
||||
if (doneIds.has(id)) continue;
|
||||
if (cost >= budget) { console.warn(`[${cellLabel}] budget $${budget} hit`); break; }
|
||||
const entry = retrieval[id];
|
||||
const memories = store === 'raw' ? entry.datedMemories : (reconciled![id]?.reconciledMemories ?? []);
|
||||
const ans = await client.chat({ system: '', user: buildPrompt(prompt, entry.question, memories), maxTokens: 4096 });
|
||||
cost += ans.costUsd;
|
||||
const answer = stripAns(ans.text);
|
||||
const { judgement, llmResults } = await judgeQuestion(
|
||||
client, { question: entry.question, rubric: entry.rubric, memoryAbility: ABILITY, answer },
|
||||
);
|
||||
for (const r of llmResults) cost += r.costUsd;
|
||||
out.write(JSON.stringify({
|
||||
instance_id: id, conv: entry.conv, memory_ability: ABILITY, question: entry.question,
|
||||
answer, score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores,
|
||||
n_nuggets: entry.rubric.length, cell: cellLabel, store, prompt, top_k: TOP_K,
|
||||
n_memories: memories.length,
|
||||
note: store === 'reconciled' ? 'read-time simulation of write-time reconciliation' : undefined,
|
||||
}) + '\n');
|
||||
n++;
|
||||
process.stdout.write(` [${cellLabel}] ${id} score=${judgement.score.toFixed(2)} $${cost.toFixed(3)}\n`);
|
||||
}
|
||||
out.end();
|
||||
console.log(`\n[${cellLabel}] answered_now=${n} cost=$${cost.toFixed(4)} → ${outPath}`);
|
||||
}
|
||||
|
||||
// ── Cell-A reproduction spot check ───────────────────────────────────────────
|
||||
|
||||
async function reproA(nSample: number): Promise<void> {
|
||||
const retrieval = loadRetrievalCache();
|
||||
const publishedA = new Map<string, number>();
|
||||
for (const line of fs.readFileSync(PUBLISHED_A, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
const r = JSON.parse(t) as { instance_id: string; memory_ability: string; score: number };
|
||||
if (r.memory_ability === ABILITY && !publishedA.has(r.instance_id)) publishedA.set(r.instance_id, r.score);
|
||||
}
|
||||
const client = createBeamOpenAiClient({ model: 'gpt-5' });
|
||||
const ids = Object.keys(retrieval).sort().slice(0, nSample);
|
||||
let cost = 0;
|
||||
const rows: Array<{ id: string; repro: number; published: number }> = [];
|
||||
for (const id of ids) {
|
||||
const entry = retrieval[id];
|
||||
const ans = await client.chat({ system: '', user: buildAnswerGenerationPromptV2(entry.question, entry.datedMemories), maxTokens: 4096 });
|
||||
cost += ans.costUsd;
|
||||
const answer = stripAns(ans.text);
|
||||
const { judgement, llmResults } = await judgeQuestion(
|
||||
client, { question: entry.question, rubric: entry.rubric, memoryAbility: ABILITY, answer },
|
||||
);
|
||||
for (const r of llmResults) cost += r.costUsd;
|
||||
rows.push({ id, repro: judgement.score, published: publishedA.get(id) ?? NaN });
|
||||
process.stdout.write(` repro ${id}: repro=${judgement.score.toFixed(2)} published=${(publishedA.get(id) ?? NaN).toFixed(2)} $${cost.toFixed(3)}\n`);
|
||||
}
|
||||
const meanRepro = rows.reduce((s, r) => s + r.repro, 0) / rows.length;
|
||||
const meanPub = rows.reduce((s, r) => s + r.published, 0) / rows.length;
|
||||
console.log(`\n[repro-a] n=${rows.length} mean repro=${meanRepro.toFixed(4)} vs published=${meanPub.toFixed(4)} cost=$${cost.toFixed(4)}`);
|
||||
}
|
||||
|
||||
// ── Confound classification: surface-both vs pick-latest vs abstain ──────────
|
||||
|
||||
const CONFOUND_SYSTEM =
|
||||
'You classify how an assistant answer handled a question about a fact the user stated ' +
|
||||
'inconsistently over time. Respond with STRICT JSON: {"label":"<one of>"} where <one of> is:\n' +
|
||||
'- surface_both: the answer explicitly flags that the stored information is contradictory / ' +
|
||||
'conflicting, OR presents more than one of the conflicting values (optionally asking the user which is correct).\n' +
|
||||
'- pick_latest: the answer commits to a single value/state as the current answer without flagging any contradiction.\n' +
|
||||
'- abstain: the answer declines, saying it does not have enough information.\n' +
|
||||
'Judge ONLY the answer text\'s behaviour, not correctness.';
|
||||
|
||||
async function classifyConfound(cellFile: string): Promise<Record<string, number> | null> {
|
||||
const p = path.join(outDir, cellFile);
|
||||
if (!fs.existsSync(p)) { console.log(`[confound] ${cellFile}: missing`); return null; }
|
||||
const client = createBeamOpenAiClient({ model: 'gpt-5-mini' });
|
||||
const seen = new Set<string>();
|
||||
const counts: Record<string, number> = { surface_both: 0, pick_latest: 0, abstain: 0, other: 0 };
|
||||
let cost = 0;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
const r = JSON.parse(t) as { instance_id: string; question: string; answer: string };
|
||||
if (seen.has(r.instance_id)) continue; seen.add(r.instance_id);
|
||||
const res = await client.chat({
|
||||
system: CONFOUND_SYSTEM,
|
||||
user: `QUESTION:\n${r.question}\n\nANSWER:\n${r.answer}`,
|
||||
jsonMode: true, maxTokens: 200,
|
||||
});
|
||||
cost += res.costUsd;
|
||||
let label = 'other';
|
||||
try {
|
||||
const o = JSON.parse(res.text) as { label?: string };
|
||||
if (o.label && ['surface_both', 'pick_latest', 'abstain'].includes(o.label)) label = o.label;
|
||||
} catch { /* keep other */ }
|
||||
counts[label] = (counts[label] ?? 0) + 1;
|
||||
}
|
||||
const n = seen.size;
|
||||
console.log(`[confound] ${cellFile}: n=${n} surface_both=${counts.surface_both} pick_latest=${counts.pick_latest} abstain=${counts.abstain} other=${counts.other} cost=$${cost.toFixed(3)}`);
|
||||
return counts;
|
||||
}
|
||||
|
||||
async function runConfound(): Promise<void> {
|
||||
const results: Record<string, Record<string, number> | null> = {};
|
||||
for (const [cell, file] of [['A','beam-e2-cellA.jsonl'],['B','beam-e2-cellB.jsonl'],['C','beam-e2-cellC.jsonl'],['D','beam-e2-cellD.jsonl']] as const) {
|
||||
results[cell] = await classifyConfound(file);
|
||||
}
|
||||
fs.writeFileSync(path.join(outDir, 'beam-e2-confound.json'), JSON.stringify(results, null, 2) + '\n', 'utf-8');
|
||||
console.log(`\n[confound] → ${path.join(outDir, 'beam-e2-confound.json')}`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const argv = process.argv.slice(2);
|
||||
const mode = argv[argv.indexOf('--mode') + 1] ?? '';
|
||||
const budgetArg = argv.indexOf('--budget');
|
||||
const budget = budgetArg >= 0 ? parseFloat(argv[budgetArg + 1]) : 30;
|
||||
const nArg = argv.indexOf('--n');
|
||||
const nSample = nArg >= 0 ? parseInt(argv[nArg + 1], 10) : 5;
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
switch (mode) {
|
||||
case 'retrieval': await buildRetrievalCache(); break;
|
||||
case 'repro-a': await reproA(nSample); break;
|
||||
case 'cellA': await runCell('A', 'raw', 'v2', path.join(outDir, 'beam-e2-cellA.jsonl'), budget); break;
|
||||
case 'cellB': await runCell('B', 'raw', 'incumbent', path.join(outDir, 'beam-e2-cellB.jsonl'), budget); break;
|
||||
case 'reconcile': {
|
||||
const convArg = argv.indexOf('--convs');
|
||||
const convRange = convArg >= 0
|
||||
? argv[convArg + 1].split(',').flatMap(part => {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { const r: number[] = []; for (let i = +m[1]; i <= +m[2]; i++) r.push(i); return r; }
|
||||
return [parseInt(part, 10)];
|
||||
})
|
||||
: null;
|
||||
const cacheArg = argv.indexOf('--cache');
|
||||
const cachePath = cacheArg >= 0 ? path.join(outDir, argv[cacheArg + 1]) : RECONCILED_CACHE;
|
||||
await buildReconciledCache(convRange, cachePath);
|
||||
break;
|
||||
}
|
||||
case 'reconcile-merge': mergeReconciledShards(); break;
|
||||
case 'cellC': await runCell('C', 'reconciled', 'v2', path.join(outDir, 'beam-e2-cellC.jsonl'), budget); break;
|
||||
case 'cellD': await runCell('D', 'reconciled', 'incumbent', path.join(outDir, 'beam-e2-cellD.jsonl'), budget); break;
|
||||
case 'confound': await runConfound(); break;
|
||||
default: console.error(`unknown --mode "${mode}" (retrieval|repro-a|cellB|reconcile|cellC|cellD)`); process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
99
benchmarks/harness/scripts/beam-rejudge.ts
Normal file
99
benchmarks/harness/scripts/beam-rejudge.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Standalone BEAM re-judge: take an existing answers jsonl (produced by
|
||||
* beam-run-1m — rows carry `question`, `answer`, `memory_ability`, and
|
||||
* `nugget_scores[].nugget` = the gold rubric), and RE-SCORE the SAME answer
|
||||
* texts with a chosen judge model. This isolates judge-model effects from
|
||||
* answerer effects: the answers never change, only the judge does.
|
||||
*
|
||||
* Usage:
|
||||
* node --import tsx scripts/beam-rejudge.ts \
|
||||
* --answers results/beam/E2-sonnet-answers.jsonl \
|
||||
* --judge-model gpt-5 \
|
||||
* --out results/beam/E2-sonnet-answers.judged-gpt5.jsonl
|
||||
*
|
||||
* The judge is the SAME transport-agnostic judgeQuestion used by beam-run-1m,
|
||||
* so scoring is byte-identical to the in-run judge — only the LLM differs.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createBeamOpenAiClient } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion, type BeamLlmResult } from '../src/beam-nugget-judge.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics, type BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
|
||||
interface AnswerRow {
|
||||
instance_id: string;
|
||||
conv?: number;
|
||||
memory_ability: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
nugget_scores: Array<{ nugget: string; score: number; reason: string }>;
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const argv = process.argv.slice(2);
|
||||
let answers = '';
|
||||
let judgeModel = 'gpt-5';
|
||||
let out = '';
|
||||
let computeTau = false;
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i];
|
||||
const next = argv[i + 1];
|
||||
if (f === '--answers' && next) { answers = path.resolve(next); i++; }
|
||||
else if (f === '--judge-model' && next) { judgeModel = next; i++; }
|
||||
else if (f === '--out' && next) { out = path.resolve(next); i++; }
|
||||
else if (f === '--tau') { computeTau = true; }
|
||||
}
|
||||
if (!answers) { console.error('[beam-rejudge] --answers <path> required'); process.exit(2); }
|
||||
if (!out) out = answers.replace(/\.jsonl$/, `.judged-${judgeModel.replace(/[^a-z0-9]+/gi, '')}.jsonl`);
|
||||
return { answers, judgeModel, out, computeTau };
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
const judge = createBeamOpenAiClient({ model: args.judgeModel });
|
||||
const rows: AnswerRow[] = fs.readFileSync(args.answers, 'utf-8')
|
||||
.split('\n').map(l => l.trim()).filter(Boolean)
|
||||
.map(l => JSON.parse(l) as AnswerRow);
|
||||
console.log(`[beam-rejudge] ${rows.length} answers judge=${args.judgeModel} -> ${path.basename(args.out)}`);
|
||||
|
||||
const outStream = fs.createWriteStream(args.out, { flags: 'w' });
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
let costUsd = 0;
|
||||
const acc = (r: BeamLlmResult): void => { costUsd += r.costUsd; };
|
||||
|
||||
for (const row of rows) {
|
||||
const rubric = (row.nugget_scores ?? []).map(n => n.nugget);
|
||||
const { judgement, llmResults } = await judgeQuestion(
|
||||
judge,
|
||||
{ question: row.question, rubric, memoryAbility: row.memory_ability, answer: row.answer },
|
||||
{ computeTau: args.computeTau },
|
||||
);
|
||||
for (const r of llmResults) acc(r);
|
||||
perQuestion.push({ instanceId: row.instance_id, memoryAbility: row.memory_ability, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) });
|
||||
outStream.write(JSON.stringify({
|
||||
instance_id: row.instance_id,
|
||||
conv: row.conv,
|
||||
memory_ability: row.memory_ability,
|
||||
question: row.question,
|
||||
answer: row.answer,
|
||||
score: judgement.score,
|
||||
judgment: judgement.judgment,
|
||||
nugget_scores: judgement.nuggetScores,
|
||||
n_nuggets: rubric.length,
|
||||
judge_model: args.judgeModel,
|
||||
}) + '\n');
|
||||
process.stdout.write(` ${row.memory_ability.padEnd(24)} score=${judgement.score.toFixed(2)} $${costUsd.toFixed(3)}\n`);
|
||||
}
|
||||
outStream.end();
|
||||
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
console.log(`\n════════ BEAM re-judge (${args.judgeModel}) ════════`);
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cost=$${costUsd.toFixed(4)}`);
|
||||
const summaryPath = args.out.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({ judgeModel: args.judgeModel, answers: path.basename(args.answers), costUsd, metrics }, null, 2));
|
||||
console.log(`jsonl: ${args.out}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exit(1); });
|
||||
422
benchmarks/harness/scripts/beam-run-belief.ts
Normal file
422
benchmarks/harness/scripts/beam-run-belief.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* BEAM 1M — the `belief` cell (E3). ADDITIVE belief-store overlay on the winning
|
||||
* retrieval config.
|
||||
*
|
||||
* WHAT IT IS. The best BEAM config is cell=retrieval, prompt=v2, top_k=30 over
|
||||
* the raw dated turns in minds-1M (headline 0.6482/74.0% @ gpt-5). The prior
|
||||
* `hive_mind_ipb` cell created P/B belief frames but NEVER injected them into the
|
||||
* answer prompt (belief theater). This cell wires the REAL belief store in:
|
||||
*
|
||||
* 1. Retrieve the SAME raw dated turns from minds-1M (top_k=30, v2 date-stamped)
|
||||
* — byte-for-byte the baseline answer context. UNCHANGED. Detail is still
|
||||
* carried by the raw turns (E1: we still need them for instruction/preference).
|
||||
* 2. Retrieve the query-relevant distilled facts from minds-1M-obs (k-belief),
|
||||
* then run the ACTUAL supersede/consolidation code over them:
|
||||
* - detectSupersessionChains (LLM: same-attribute value-over-time chains)
|
||||
* - detectEntityGroups (LLM: enumerable member sets)
|
||||
* - applyConsolidation (emits the current-value P-frames + set B-frames)
|
||||
* applyConsolidation is run inside a ROLLED-BACK SQLite transaction so the
|
||||
* shared obs mind on disk is never mutated; we read the returned frames only.
|
||||
* 3. Fold the returned P/B frame contents into a "# CURRENT VALUES" belief block
|
||||
* and inject it into buildAnswerGenerationPromptV2 as a clearly-delimited
|
||||
* section BEFORE the raw turns (new optional `beliefsBlock` param; the prompt
|
||||
* is byte-identical to v2 when the block is empty).
|
||||
*
|
||||
* This replicates the LongMemEval "current values" injection mechanism (the
|
||||
* validated SOTA lever), NOT the e2-cells.ts gpt-5-mini belief *simulation* (a
|
||||
* losing arm). Detection uses a cheap model (--detect-model, default gpt-5-mini)
|
||||
* as the ConsolidationLlm transport; the graded ANSWER + JUDGE stay on the
|
||||
* canonical models.
|
||||
*
|
||||
* MODELS. --model = answerer (gpt-5 for the isolation pilot; anthropic/claude-
|
||||
* sonnet-4.6 for the stacked headline — routed through OpenRouter by
|
||||
* createBeamOpenAiClient). --judge-model = judge (default gpt-5, canonical/
|
||||
* comparable to our 64.82 and Eywa's 82.85 under the same judge). Every answer
|
||||
* row is recorded with nugget_scores so a later Sonnet-judge (Eywa protocol) pass
|
||||
* via beam-rejudge.ts is possible.
|
||||
*
|
||||
* RESUMABLE. Append-JSONL; on --resume, already-answered instance_ids are skipped.
|
||||
* --instance-ids <file> restricts to an exact allowlist (reuse matched50.txt).
|
||||
* --budget caps spend with a hard stop.
|
||||
*
|
||||
* Usage:
|
||||
* # pilot (isolation): gpt-5 answerer + gpt-5 judge, matched-50
|
||||
* tsx benchmarks/harness/scripts/beam-run-belief.ts --model gpt-5 \
|
||||
* --instance-ids benchmarks/harness/scripts/matched50.txt --budget 12 --resume
|
||||
* # headline: belief + Sonnet-4.6 answerer, gpt-5 judge, full-700
|
||||
* tsx benchmarks/harness/scripts/beam-run-belief.ts --model anthropic/claude-sonnet-4.6 \
|
||||
* --judge-model gpt-5 --convs 1-35 --budget 90 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import process from 'node:process';
|
||||
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import {
|
||||
detectSupersessionChains, detectEntityGroups, applyConsolidation,
|
||||
type ConsolidationLlm, type Observation, type MemoryFrame,
|
||||
} from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import type { Substrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { buildAnswerGenerationPromptV2, judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import type { BeamLlmResult } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap, renderMemories } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
|
||||
interface Question {
|
||||
instanceId: string;
|
||||
conv: number;
|
||||
gopId: string;
|
||||
memoryAbility: string;
|
||||
question: string;
|
||||
rubric: string[];
|
||||
}
|
||||
|
||||
interface Args {
|
||||
model: string;
|
||||
judgeModel: string;
|
||||
detectModel: string;
|
||||
topK: number;
|
||||
kBelief: number;
|
||||
budget: number;
|
||||
resume: boolean;
|
||||
convs: number[];
|
||||
beamChats: string;
|
||||
rawMindsDir: string;
|
||||
obsMindsDir: string;
|
||||
instanceIds: Set<string> | null;
|
||||
outPath: string | null;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const a: Args = {
|
||||
model: 'gpt-5',
|
||||
judgeModel: 'gpt-5',
|
||||
detectModel: 'gpt-5-mini',
|
||||
topK: 30,
|
||||
kBelief: 60,
|
||||
budget: 12,
|
||||
resume: false,
|
||||
convs: parseConvSpec('1-35'),
|
||||
beamChats: path.resolve(repoRoot, '..', 'BEAM', 'chats'),
|
||||
rawMindsDir: path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M'),
|
||||
obsMindsDir: path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M-obs'),
|
||||
instanceIds: null,
|
||||
outPath: null,
|
||||
tag: 'belief',
|
||||
};
|
||||
let judgeExplicit = false;
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; judgeExplicit = true; i++; }
|
||||
else if (f === '--detect-model' && next) { a.detectModel = next; i++; }
|
||||
else if (f === '--top-k' && next) { a.topK = parseInt(next, 10); i++; }
|
||||
else if (f === '--k-belief' && next) { a.kBelief = parseInt(next, 10); i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
const ids = fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean);
|
||||
a.instanceIds = new Set(ids); i++;
|
||||
}
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
}
|
||||
// Default: judge with the answerer's model unless a judge model was named.
|
||||
if (!judgeExplicit) a.judgeModel = a.model;
|
||||
return a;
|
||||
}
|
||||
|
||||
// ── Question loading (identical scheme to beam-run-1m.ts) ────────────────────
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(beamChats: string, conv: number): Question[] {
|
||||
const pqPath = path.join(beamChats, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({
|
||||
instanceId: `beam_1M_${conv}_${category}_q${qi}`,
|
||||
conv, gopId: `beam_${conv}`, memoryAbility: category,
|
||||
question: q, rubric: extractRubric(pq),
|
||||
});
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mindPath(mindsDir: string, conv: number): string {
|
||||
return path.join(mindsDir, `beam_1M_${conv}.mind`);
|
||||
}
|
||||
function isIngested(mindsDir: string, conv: number): boolean {
|
||||
return fs.existsSync(path.join(mindsDir, `beam_1M_${conv}.done.json`)) && fs.existsSync(mindPath(mindsDir, conv));
|
||||
}
|
||||
function chatJsonPath(beamChats: string, conv: number): string {
|
||||
return path.join(beamChats, '1M', String(conv), 'chat.json');
|
||||
}
|
||||
function memoriesFromResults(results: readonly { frame: { id: number; content: string } }[]): string[] {
|
||||
return [...results].sort((a, b) => a.frame.id - b.frame.id).map(r => r.frame.content);
|
||||
}
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
function approxTokens(s: string): number { return Math.max(1, Math.ceil(s.length / 4)); }
|
||||
|
||||
/** Build a client. gpt/o-series → OpenAI (createBeamOpenAiClient). Claude ids
|
||||
* (e.g. anthropic/claude-sonnet-4.6) → OpenRouter's OpenAI-compatible endpoint
|
||||
* with OPENROUTER_API_KEY. Isolated here so the shared client stays untouched. */
|
||||
function makeClient(model: string): BeamOpenAiClient {
|
||||
if (/claude|anthropic/i.test(model)) {
|
||||
loadDotEnv();
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey) throw new Error('OPENROUTER_API_KEY not found in environment or .env (required for Claude answerer).');
|
||||
const pricing = OPENAI_PRICING[model] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 };
|
||||
return new BeamOpenAiClient({ model, apiKey, baseUrl: 'https://openrouter.ai/api/v1', pricing });
|
||||
}
|
||||
return createBeamOpenAiClient({ model });
|
||||
}
|
||||
|
||||
// ── Belief block (REAL supersede/consolidation) ──────────────────────────────
|
||||
|
||||
const ROLLBACK = Symbol('belief-rollback');
|
||||
|
||||
/** B-frame content is JSON {description, references}; return the description
|
||||
* (`label (N members)`), falling back to the raw string if it isn't JSON. */
|
||||
function bframeDescription(content: string): string {
|
||||
try {
|
||||
const o = JSON.parse(content) as { description?: unknown };
|
||||
if (o && typeof o.description === 'string') return o.description;
|
||||
} catch { /* not JSON — use raw */ }
|
||||
return content;
|
||||
}
|
||||
|
||||
interface BeliefBlock { block: string | null; nChains: number; nGroups: number; nRetrieved: number }
|
||||
|
||||
/**
|
||||
* Build the consolidated "# CURRENT VALUES" block for a question from the obs
|
||||
* (distilled-fact) mind. Retrieves the query-relevant facts, detects supersession
|
||||
* chains + enumerable groups with the injected ConsolidationLlm, then runs the
|
||||
* REAL applyConsolidation inside a rolled-back transaction so the shared mind on
|
||||
* disk is untouched — we consume only the returned P/B frames.
|
||||
*/
|
||||
async function buildBeliefBlock(
|
||||
obsSub: Substrate, gopId: string, question: string, detectLlm: ConsolidationLlm, kBelief: number,
|
||||
): Promise<BeliefBlock> {
|
||||
const results = await obsSub.search.search(question, { limit: kBelief, gopId });
|
||||
if (results.length < 2) return { block: null, nChains: 0, nGroups: 0, nRetrieved: results.length };
|
||||
|
||||
const obs: Observation[] = results.map(r => ({
|
||||
id: r.frame.id,
|
||||
content: r.frame.content,
|
||||
created_at: String(r.frame.created_at ?? ''),
|
||||
}));
|
||||
|
||||
const [chains, groups] = await Promise.all([
|
||||
detectSupersessionChains(obs, detectLlm),
|
||||
detectEntityGroups(obs, detectLlm),
|
||||
]);
|
||||
if (chains.length === 0 && groups.length === 0) {
|
||||
return { block: null, nChains: 0, nGroups: 0, nRetrieved: results.length };
|
||||
}
|
||||
|
||||
// Real consolidation, thrown away on disk: BEGIN → applyConsolidation → ROLLBACK.
|
||||
const raw = obsSub.db.getDatabase();
|
||||
let pframes: MemoryFrame[] = [];
|
||||
let bframes: MemoryFrame[] = [];
|
||||
try {
|
||||
raw.transaction(() => {
|
||||
const res = applyConsolidation(obsSub.frames, chains, groups, gopId);
|
||||
pframes = res.pframes;
|
||||
bframes = res.bframes;
|
||||
throw ROLLBACK; // discard all writes; we already captured the returned frames
|
||||
})();
|
||||
} catch (e) {
|
||||
if (e !== ROLLBACK) throw e;
|
||||
}
|
||||
|
||||
// P-frame content is the clean `[current] attr: value (as of date)` line.
|
||||
// B-frame content is a JSON blob {description, references}; surface the
|
||||
// human-readable `description` (`label (N members)`), never the raw JSON.
|
||||
const values = pframes.map(f => String(f.content).replace(/^\[current\]\s*/, '').trim()).filter(Boolean);
|
||||
const sets = bframes.map(f => bframeDescription(String(f.content))).map(s => s.trim()).filter(Boolean);
|
||||
if (values.length === 0 && sets.length === 0) {
|
||||
return { block: null, nChains: chains.length, nGroups: groups.length, nRetrieved: results.length };
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (values.length) {
|
||||
parts.push(
|
||||
'CURRENT VALUES (consolidated from the user\'s whole history — each line is the LATEST known ' +
|
||||
'value of a fact that CHANGED over time; when a raw memory below conflicts with one of these, ' +
|
||||
'trust the value here):\n' + values.map(v => `- ${v}`).join('\n'),
|
||||
);
|
||||
}
|
||||
if (sets.length) {
|
||||
parts.push(
|
||||
'ENUMERABLE SETS (complete member counts inferred across all sessions — use these when asked ' +
|
||||
'to count or list every item of a kind):\n' + sets.map(s => `- ${s}`).join('\n'),
|
||||
);
|
||||
}
|
||||
return { block: parts.join('\n\n'), nChains: chains.length, nGroups: groups.length, nRetrieved: results.length };
|
||||
}
|
||||
|
||||
// ── Run ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function run(args: Args): Promise<void> {
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}-topk${args.topK}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[belief] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (doneIds.size === 0 && fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[belief] WARNING: ${path.basename(outPath)} exists; appending WITHOUT --resume may duplicate rows.`);
|
||||
}
|
||||
|
||||
const answerClient = makeClient(args.model);
|
||||
const judgeClient = args.judgeModel === args.model ? answerClient : makeClient(args.judgeModel);
|
||||
const detectClient = makeClient(args.detectModel);
|
||||
const embedder = createOllamaEmbedder();
|
||||
|
||||
let detectCost = 0;
|
||||
const detectLlm: ConsolidationLlm = async (system, user) => {
|
||||
const r = await detectClient.chat({ system, user, jsonMode: true, maxTokens: 1200 });
|
||||
detectCost += r.costUsd;
|
||||
return r.text;
|
||||
};
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const answerPromptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0, budgetStopped = false;
|
||||
let beliefNonEmpty = 0, chainsTotal = 0, groupsTotal = 0;
|
||||
const outStream = fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(c => isIngested(args.rawMindsDir, c) && isIngested(args.obsMindsDir, c));
|
||||
console.log(`[belief] answer=${args.model} judge=${args.judgeModel} detect=${args.detectModel} top_k=${args.topK} k_belief=${args.kBelief} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(args.beamChats, conv)
|
||||
.filter(q => !doneIds.has(q.instanceId) && (!args.instanceIds || args.instanceIds.has(q.instanceId)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const rawSub = createSubstrate({ dbPath: mindPath(args.rawMindsDir, conv), embedder });
|
||||
const obsSub = createSubstrate({ dbPath: mindPath(args.obsMindsDir, conv), embedder });
|
||||
const dateMap = buildConvDateMap(chatJsonPath(args.beamChats, conv));
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost + detectCost;
|
||||
if (spent >= args.budget) { budgetStopped = true; console.warn(`[belief] budget $${args.budget} hit ($${spent.toFixed(2)})`); break; }
|
||||
|
||||
// 1) belief block from the obs mind (real supersede/consolidation).
|
||||
const bel = await buildBeliefBlock(obsSub, q.gopId, q.question, detectLlm, args.kBelief);
|
||||
if (bel.block) beliefNonEmpty++;
|
||||
chainsTotal += bel.nChains; groupsTotal += bel.nGroups;
|
||||
|
||||
// 2) SAME raw dated turns as the baseline retrieval cell (top_k=30, v2).
|
||||
const results = await rawSub.search.search(q.question, { limit: args.topK, gopId: q.gopId });
|
||||
const memories = memoriesFromResults(results);
|
||||
const display = renderMemories(memories, dateMap, 'v2');
|
||||
const prompt = buildAnswerGenerationPromptV2(q.question, display, undefined, bel.block ?? undefined);
|
||||
answerPromptToks.push(approxTokens(prompt));
|
||||
|
||||
const ans = await answerClient.chat({ system: '', user: prompt, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
const answer = stripAns(ans.text);
|
||||
|
||||
// 3) judge (canonical).
|
||||
const { judgement, llmResults } = await judgeQuestion(
|
||||
judgeClient,
|
||||
{ question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer },
|
||||
{},
|
||||
);
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) });
|
||||
outStream.write(JSON.stringify({
|
||||
instance_id: q.instanceId, conv, memory_ability: q.memoryAbility, question: q.question,
|
||||
answer, score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores,
|
||||
n_nuggets: q.rubric.length, cell: 'belief', prompt: 'v2', top_k: args.topK, k_belief: args.kBelief,
|
||||
belief_used: !!bel.block, belief_chains: bel.nChains, belief_groups: bel.nGroups,
|
||||
answer_model: args.model, judge_model: args.judgeModel, detect_model: args.detectModel,
|
||||
...(bel.block ? { belief_block: bel.block } : {}),
|
||||
}) + '\n');
|
||||
const flag = bel.block ? `bel(${bel.nChains}c/${bel.nGroups}g)` : 'bel(—)';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${flag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost + detectCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally {
|
||||
rawSub.close();
|
||||
obsSub.close();
|
||||
}
|
||||
}
|
||||
outStream.end();
|
||||
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = answerPromptToks.length ? Math.round(answerPromptToks.reduce((s, x) => s + x, 0) / answerPromptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost + detectCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'belief', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
detect_model: args.detectModel, prompt: 'v2', top_k: args.topK, k_belief: args.kBelief,
|
||||
minds_dir: 'minds-1M (answer) + minds-1M-obs (belief)',
|
||||
mean_answer_prompt_tokens: meanTok,
|
||||
belief_nonempty: beliefNonEmpty, answered_now: perQuestion.length,
|
||||
chains_total: chainsTotal, groups_total: groupsTotal,
|
||||
budgetStopped,
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
cost: { total_usd: totalCost, answer_usd: answerCost, judge_usd: judgeCost, detect_usd: detectCost },
|
||||
}, null, 2) + '\n', 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M — belief ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`belief block non-empty on ${beliefNonEmpty}/${perQuestion.length} questions (chains=${chainsTotal} groups=${groupsTotal})`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)} detect=$${detectCost.toFixed(3)}) answered_now=${perQuestion.length} budgetStopped=${budgetStopped}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
|
||||
run(parseArgs()).catch(err => { console.error('[beam-run-belief] FATAL:', err); process.exit(1); });
|
||||
373
benchmarks/harness/scripts/beam-run-ledger-e7L1.ts
Normal file
373
benchmarks/harness/scripts/beam-run-ledger-e7L1.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* P3 — Whole-Ledger serving runner (E6). Serves the COMPLETE per-conversation
|
||||
* Evidence Ledger (P1) + STATE sections (P2) as the answer context, instead of a
|
||||
* top-k retrieval excerpt. This is the decisive test of the deep-dive thesis:
|
||||
* the 4 whole-history STATE abilities (abstention / contradiction / temporal /
|
||||
* event_ordering) are unanswerable from <3%-coverage top-k, but trivial over a
|
||||
* complete dated store.
|
||||
*
|
||||
* PER QUESTION:
|
||||
* - cacheableSystem = [framing + CURRENT VALUES + CONTRADICTION RECORDS + full
|
||||
* dated ledger] — a STABLE per-conversation prefix. With Anthropic prompt
|
||||
* caching (via OpenRouter cache_control) the 20 questions of one conversation
|
||||
* share ONE cache write; the rest are cheap cache reads. Questions are
|
||||
* processed GROUPED BY CONVERSATION to keep the cache warm.
|
||||
* - user message = ability-general instruction (gold-blind: keyed off the
|
||||
* dataset's memory_ability metadata, NOT gold) + question. Detail abilities
|
||||
* also get top-N raw dated turns from minds-1M (keeps the retrieval wins);
|
||||
* STATE abilities get the ledger ONLY (abstention needs a store it can trust
|
||||
* is complete — retrieval always surfaces something tangential).
|
||||
* - answerer: anthropic/claude-sonnet-4.6 (via OpenRouter — identical to the
|
||||
* E4-combo-sonnet baseline, for a fair comparison). Judge: canonical gpt-5.
|
||||
*
|
||||
* RESUMABLE: append-JSONL + skip-done (--resume). --instance-ids allowlist,
|
||||
* --abilities filter, --budget hard-cap. --smoke prints contexts (no judge).
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger.ts --smoke --convs 1 --instance-ids scripts/smoke3.txt
|
||||
* tsx scripts/beam-run-ledger.ts --instance-ids scripts/matched50.txt --budget 14 --tag e6-ledger-pilot --resume
|
||||
* tsx scripts/beam-run-ledger.ts --convs 1-35 --budget 60 --tag e6-ledger-FULL700 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
// E7-L1: abilities that must NOT see the P2 CURRENT VALUES overlay. The overlay injects a
|
||||
// superseded/revised value ("CURRENT = March 18; history: March 15 -> March 18") that biases
|
||||
// duration/aggregation answers toward the wrong anchor when gold wants the original value.
|
||||
// These abilities get the dated ledger (+ CONTRADICTION RECORDS) but not CURRENT VALUES.
|
||||
const NO_CV_ABILITIES = new Set(['temporal_reasoning', 'multi_session_reasoning', 'event_ordering']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e6-ledger', outPath: null,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Drop the `=== CURRENT VALUES ===` block from a state.txt, keep CONTRADICTION RECORDS. */
|
||||
function stripCurrentValues(state: string): string {
|
||||
const idx = state.indexOf('=== CONTRADICTION RECORDS ===');
|
||||
if (idx >= 0) return state.slice(idx).trim();
|
||||
// No contradiction section present: drop the whole overlay.
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the cacheable system prefix: framing + STATE sections + full ledger.
|
||||
* E7-L1: `includeCurrentValues=false` serves the dated ledger + CONTRADICTION RECORDS but
|
||||
* omits the CURRENT VALUES overlay (and its framing line), for the NO_CV_ABILITIES.
|
||||
*/
|
||||
function buildLedgerPrefix(conv: number, includeCurrentValues = true): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const fullState = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
if (includeCurrentValues) {
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${fullState}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
const state = stripCurrentValues(fullState);
|
||||
const stateBlock = state ? `${state}\n\n` : '';
|
||||
const framingSection = state
|
||||
? `Lines are ordered by date (oldest first). One consolidated section precedes the ledger:
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
`
|
||||
: `Lines are ordered by date (oldest first).
|
||||
|
||||
`;
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. ${framingSection}${stateBlock}=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
const answerClient = makeClient(args.model, true);
|
||||
const judgeClient = makeClient(args.judgeModel, false);
|
||||
const embedder = createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = args.smoke ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
// E7-L1: two cacheable prefix variants per conv. Full (CURRENT VALUES ON) for protected
|
||||
// abilities; stripped (CURRENT VALUES OFF) for NO_CV_ABILITIES. Built once and reused so
|
||||
// within-conv prompt caching is preserved (≤2 cache writes/conv).
|
||||
const prefixFull = buildLedgerPrefix(conv, true);
|
||||
const prefixNoCV = buildLedgerPrefix(conv, false);
|
||||
const prefixTokFull = Math.ceil(prefixFull.length / 4);
|
||||
const prefixTokNoCV = Math.ceil(prefixNoCV.length / 4);
|
||||
const rawSub = createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, prefix full ~${prefixTokFull} tok / noCV ~${prefixTokNoCV} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E7-L1: select the cacheable prefix — CURRENT VALUES OFF for NO_CV_ABILITIES.
|
||||
const usesCV = !NO_CV_ABILITIES.has(q.memoryAbility);
|
||||
const prefix = usesCV ? prefixFull : prefixNoCV;
|
||||
const prefixTok = usesCV ? prefixTokFull : prefixTokNoCV;
|
||||
|
||||
const instruction = abilityInstruction(q.memoryAbility);
|
||||
const userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
456
benchmarks/harness/scripts/beam-run-ledger-e8.ts
Normal file
456
benchmarks/harness/scripts/beam-run-ledger-e8.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8 — Two-pass extract-then-compute + micro-edit bundle (pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeA.txt --tag e8A-twopass --judge-model openai/gpt-5 --budget 18 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8 additions ────────────────────────────
|
||||
const TWO_PASS_ABILITIES = new Set(['temporal_reasoning', 'multi_session_reasoning']);
|
||||
|
||||
/** E8 pass-1: exhaustive extraction of dated candidates / value inventory. */
|
||||
function extractionInstruction(ability: string): string {
|
||||
if (ability === 'temporal_reasoning') {
|
||||
return `This is a TEMPORAL question. This is PASS 1 of 2 — EXTRACTION ONLY. Do NOT answer the question yet.
|
||||
|
||||
From the evidence ledger above, extract EVERY line that could serve as a date anchor for the events named in the question below.
|
||||
|
||||
METHOD:
|
||||
1. Name each event/item the question asks about (both sides of the comparison).
|
||||
2. For each, quote EVERY dated ledger line that mentions it — ALL occurrences: first mentions, plans, schedules, bookings, confirmations, reschedules, and actual completions. Missing an occurrence is worse than including a marginal one.
|
||||
3. Output format, one line per candidate: [YYYY-MM-DD] <fact as stated in the ledger> (<verbatim quote>). Group by named event, date-ordered within each group.
|
||||
4. Mark explicitly: (a) which lines state when the event ACTUALLY happened or was confirmed, vs merely planned/discussed; (b) any REVISED value or date (original → revised, both with their dates).
|
||||
5. If the two sides of the question have different granularity (e.g. one is a trip, the other a booking for that trip), list candidates for BOTH.
|
||||
Stop after the inventory. No computation, no answer.`;
|
||||
}
|
||||
return `This question reasons ACROSS multiple sessions. This is PASS 1 of 2 — EXTRACTION ONLY. Do NOT answer the question yet.
|
||||
|
||||
From the evidence ledger above, extract EVERY dated line relevant to the items the question below names.
|
||||
|
||||
METHOD:
|
||||
1. Name each item/topic the question asks about.
|
||||
2. For each item, quote EVERY dated line that states a fact about it — values, numbers, prices, durations, counts, allocations, decisions, specifications, configurations, and stated outcomes — each with its date: [YYYY-MM-DD] <fact> (<verbatim quote>). Mark REVISED values explicitly (original → revised).
|
||||
3. If the question asks for a total / count / aggregate: ALSO quote lines for RELATED but NOT-asked items under a separate OUT-OF-SCOPE heading (so they can be excluded deliberately later), and note explicitly when a line is a RUNNING TOTAL or an UPDATE of an already-listed item (updates supersede, they do not add).
|
||||
4. If the question asks to optimize / compare / prioritize: quote the lines recording the CURRENT state of each item (latest-dated values, exact tools, versions, measured numbers, stated goals) so the answer can be grounded in them.
|
||||
Stop after the inventory. No analysis, no answer.`;
|
||||
}
|
||||
|
||||
/** E8 pass-2 message: scratchpad + selection/aggregation rules + E6 clause verbatim + question. */
|
||||
function buildTwoPassAnswerMessage(ability: string, question: string, scratchpad: string, rawBlock: string): string {
|
||||
if (ability === 'temporal_reasoning') {
|
||||
return `You previously extracted the following candidate date anchors from the ledger:
|
||||
|
||||
=== EXTRACTED CANDIDATES ===
|
||||
${scratchpad.trim()}
|
||||
=== END EXTRACTED CANDIDATES ===
|
||||
|
||||
Now answer the QUESTION below using these candidates (the full ledger remains available above — if a needed anchor is missing from the extraction, add it and say so in one line).
|
||||
|
||||
SELECTION RULES:
|
||||
- Resolve WHICH occurrence each side of the question refers to BEFORE computing: for "when the event took place / was completed / was held" use the ACTUAL/confirmed date; for "when I planned / scheduled / decided / booked" use the date of that planning utterance; for "first X" use the EARLIEST occurrence of that same named item, for "last X" the LATEST — never substitute a different event.
|
||||
- If a value or date was REVISED and the question does not ask about the change or for the current/latest value, use the ORIGINAL value for that named item; if the question is about the change, the two revisions ARE the two anchors.
|
||||
- Discard candidates belonging to a different event than the one named.
|
||||
|
||||
UNIT RULE: state the duration in the EXACT unit the question asks for — "how many days" → "N days"; "how many weeks" → "N weeks"; "how many months" → "N months and M days" (never a bare "approximately N months"); "how much time" → give months AND days when the span exceeds a month. ALWAYS also state the exact range "from <date> till <date>". Compute the plain calendar difference between the two anchor dates.
|
||||
|
||||
${abilityInstruction('temporal_reasoning')}
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
ANSWER:`;
|
||||
}
|
||||
return `You previously extracted the following value inventory from the ledger:
|
||||
|
||||
=== EXTRACTED VALUE INVENTORY ===
|
||||
${scratchpad.trim()}
|
||||
=== END EXTRACTED VALUE INVENTORY ===
|
||||
|
||||
Now answer the QUESTION below using this inventory (the full ledger remains available above — if a component is missing from the extraction, add it and say so in one line).
|
||||
|
||||
AGGREGATION RULES:
|
||||
- SCOPE first: name exactly which items the question asks about; EXCLUDE everything under OUT-OF-SCOPE explicitly (one line: "excluded: X, Y").
|
||||
- For each in-scope item pick ONE value: the ORIGINALLY-stated figure, unless the question asks for the current/latest value or about the change itself.
|
||||
- Do NOT double-count: a running total or update SUPERSEDES earlier partial figures for the same item — take the item's single applicable value, never the sum of its updates.
|
||||
- Compute the aggregate explicitly: list each component with its value, then the final total.
|
||||
- If the question asks to OPTIMIZE / COMPARE / PRIORITIZE rather than aggregate: ground every claim in the extracted CURRENT-state values (latest-dated) and be specific to the user's exact tools, versions, and measured numbers.
|
||||
${rawBlock}
|
||||
|
||||
${abilityInstruction('multi_session_reasoning')}
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const twoPass = TWO_PASS_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
const extractMsg = twoPass ? `${extractionInstruction(q.memoryAbility)}\n\nQUESTION: ${q.question}` : '';
|
||||
let userMsg = twoPass ? '' : `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(((twoPass ? extractMsg : userMsg).length) / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | twoPass=${twoPass} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (twoPass) {
|
||||
console.log(`── PASS-1 (extract) ──\n${extractMsg}`);
|
||||
console.log(`── PASS-2 (template, scratchpad placeholder) ──\n${buildTwoPassAnswerMessage(q.memoryAbility, q.question, '<PASS1_SCRATCHPAD>', rawBlock)}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && twoPass) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (two-pass; printing pass-1 only, no call)`);
|
||||
console.log(`── PASS-1 ──\n${extractMsg}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let extractCostUsd = 0, extractLatencyMs = 0, passes = 1, extractText = '';
|
||||
if (twoPass) {
|
||||
const ext = await answerClient.chat({ system: '', user: extractMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
extractCostUsd = ext.costUsd; extractLatencyMs = ext.latencyMs; passes = 2; extractText = ext.text;
|
||||
answerCost += ext.costUsd;
|
||||
if (ext.cacheReadTokens || ext.cacheCreationTokens) { cacheReadSum += ext.cacheReadTokens ?? 0; cacheCreateSum += ext.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
userMsg = buildTwoPassAnswerMessage(q.memoryAbility, q.question, ext.text, rawBlock);
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, extract_cost_usd: +extractCostUsd.toFixed(4), extract_latency_ms: extractLatencyMs,
|
||||
extract_scratchpad: extractText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
423
benchmarks/harness/scripts/beam-run-ledger-e8a2.ts
Normal file
423
benchmarks/harness/scripts/beam-run-ledger-e8a2.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8A2 — Repair-pass (answer-then-repair, Eywa pattern); base E8 two-pass + micro-edit bundle (pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8a2.ts --instance-ids scripts/e8-probeA.txt --tag e8A2-repair --judge-model openai/gpt-5 --budget 18 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['temporal_reasoning', 'multi_session_reasoning']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- ANCHORS: for every event the question names, the anchor must be the date the event ACTUALLY happened / was confirmed — not the date it was merely mentioned, planned, or scheduled. Exception: if the question asks about the planning/scheduling/booking action itself, that action's own date IS the anchor. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.
|
||||
- REVISIONS: if a value or date was revised and the question does not ask about the change or the latest value, use the ORIGINAL for that named item; if it asks about the change, the two revisions ARE the two anchors.
|
||||
- UNIT: answer in the EXACT unit requested ("how many days" -> "N days"; "how many weeks" -> "N weeks"; "how many months" -> "N months and M days", never a bare "approximately N months"). For any span, also state the range "from <date> till <date>" and compute the plain calendar difference.
|
||||
- SCOPE and AGGREGATION: include every in-scope item across sessions, nothing out of scope; a running total or update SUPERSEDES earlier partial figures for the same item (never sum updates); re-check any arithmetic. For compare/optimize/prioritize questions, ground every claim in CURRENT (latest-dated) values, exact tools, versions, measured numbers.
|
||||
- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
423
benchmarks/harness/scripts/beam-run-ledger-e8d.ts
Normal file
423
benchmarks/harness/scripts/beam-run-ledger-e8d.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8D — Gold-convention temporal repair (probeD; AMENDMENT D, pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8d.ts --instance-ids scripts/e8-probeD.txt --tag e8D-convention --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['temporal_reasoning']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- ANCHORS (gold convention): anchor every named event on the date the event itself OCCURS or is SCHEDULED to occur — the date stated together with the event — not the date it was merely mentioned. "Planned / agreed / scheduled / decided to X" anchors on X's PLANNED-FOR date ("[2024-01-10] planned a self-test for January 25" -> January 25; "[2024-03-03] agreed to cook moussaka on March 12" -> March 12). "Started preparing for test/quiz T" where the line says "preparing for my test on <date>" anchors on <date>. For events the question describes as actually happening ("celebrated", "attended", "cooked", "reached", "held"), use the date the ledger reports the event ACTUALLY happened (check later retrospective lines); if only a scheduled date exists, use the scheduled date. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.
|
||||
- REVISIONS: if a value or date was revised and the question does not ask about the change or the latest value, use the ORIGINAL for that named item; if it asks about the change, the two revisions ARE the two anchors.
|
||||
- UNIT: answer in the EXACT unit requested ("how many days" -> "N days"; "how many weeks" -> "N weeks"; "how many months" -> "N months and M days", never a bare "approximately N months"). For any span, also state the range "from <date> till <date>" and compute the plain calendar difference.
|
||||
- SCOPE and AGGREGATION: include every in-scope item across sessions, nothing out of scope; a running total or update SUPERSEDES earlier partial figures for the same item (never sum updates); re-check any arithmetic. For compare/optimize/prioritize questions, ground every claim in CURRENT (latest-dated) values, exact tools, versions, measured numbers.
|
||||
- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
429
benchmarks/harness/scripts/beam-run-ledger-e8f.ts
Normal file
429
benchmarks/harness/scripts/beam-run-ledger-e8f.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8F — Combined full-700 spliced run (AMENDMENT E: B guard + C rule + D temporal + L2 event + multi repair; pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8d.ts --instance-ids scripts/e8-probeD.txt --tag e8D-convention --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['temporal_reasoning', 'multi_session_reasoning', 'event_ordering']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
const anchorsLine = ability === 'temporal_reasoning'
|
||||
? `- ANCHORS (gold convention): anchor every named event on the date the event itself OCCURS or is SCHEDULED to occur — the date stated together with the event — not the date it was merely mentioned. "Planned / agreed / scheduled / decided to X" anchors on X's PLANNED-FOR date ("[2024-01-10] planned a self-test for January 25" -> January 25; "[2024-03-03] agreed to cook moussaka on March 12" -> March 12). "Started preparing for test/quiz T" where the line says "preparing for my test on <date>" anchors on <date>. For events the question describes as actually happening ("celebrated", "attended", "cooked", "reached", "held"), use the date the ledger reports the event ACTUALLY happened (check later retrospective lines); if only a scheduled date exists, use the scheduled date. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.`
|
||||
: `- ANCHORS: for every event the question names, the anchor must be the date the event ACTUALLY happened / was confirmed — not the date it was merely mentioned, planned, or scheduled. Exception: if the question asks about the planning/scheduling/booking action itself, that action's own date IS the anchor. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.`;
|
||||
const orderBlock = ability === 'event_ordering'
|
||||
? `- ORDER: place every event the question asks about in true chronological order using ledger dates; re-check each placement individually. If a date was revised or corrected, order by the CORRECTED date. Every step the question names must appear in the sequence — a missing step is an error even when the rest are ordered correctly.\n\n`
|
||||
: '';
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
${anchorsLine}
|
||||
- REVISIONS: if a value or date was revised and the question does not ask about the change or the latest value, use the ORIGINAL for that named item; if it asks about the change, the two revisions ARE the two anchors.
|
||||
- UNIT: answer in the EXACT unit requested ("how many days" -> "N days"; "how many weeks" -> "N weeks"; "how many months" -> "N months and M days", never a bare "approximately N months"). For any span, also state the range "from <date> till <date>" and compute the plain calendar difference.
|
||||
- SCOPE and AGGREGATION: include every in-scope item across sessions, nothing out of scope; a running total or update SUPERSEDES earlier partial figures for the same item (never sum updates); re-check any arithmetic. For compare/optimize/prioritize questions, ground every claim in CURRENT (latest-dated) values, exact tools, versions, measured numbers.
|
||||
${orderBlock}- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
422
benchmarks/harness/scripts/beam-run-ledger-e8g.ts
Normal file
422
benchmarks/harness/scripts/beam-run-ledger-e8g.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8G — Summarization coverage-repair (probeF; AMENDMENT F, pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8g.ts --instance-ids scripts/e8-probeF.txt --tag e8G-sumrepair --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['summarization']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- COVERAGE: silently enumerate EVERY distinct component, stage, and topic area the ledger records (e.g. model training/fine-tuning, containerization, orchestration, frontend state, database/schema, caching, authentication/security, deployment/scaling, error handling, monitoring, plus any project-specific areas), then check the draft against that inventory. ADD every missing component with its ledger specifics; never drop a correct one already present.
|
||||
- SPECIFICITY: each component must carry the ledger's exact tools, libraries, versions, numbers, prices, dates, causes, and outcomes — name them explicitly, not generically.
|
||||
- BALANCE: later-stage and secondary topics must be covered as fully as the dominant one; do not let one theme crowd out the rest.
|
||||
- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
422
benchmarks/harness/scripts/beam-run-ledger-e8h.ts
Normal file
422
benchmarks/harness/scripts/beam-run-ledger-e8h.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8A2 — Repair-pass (answer-then-repair, Eywa pattern); base E8 two-pass + micro-edit bundle (pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8a2.ts --instance-ids scripts/e8-probeA.txt --tag e8A2-repair --judge-model openai/gpt-5 --budget 18 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['knowledge_update']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- LATEST: find the LAST dated ledger line that states a value for the EXACT item the question asks about; later dates supersede earlier ones; a plan, target, or intention is not a value until the ledger states it as done/achieved.
|
||||
- VARIANT-MATCH: match the exact variant the question names (dosage, version, price tier, role, metric, frequency); never import a sibling variant's value.
|
||||
- CURRENT-VALUES DISTRUST: the CURRENT VALUES section can conflate variants or lag behind; verify the value against the dated ledger lines themselves — the most recent dated line wins any conflict.
|
||||
- EVIDENCE: every stated value must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
422
benchmarks/harness/scripts/beam-run-ledger-e8h2.ts
Normal file
422
benchmarks/harness/scripts/beam-run-ledger-e8h2.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8A2 — Repair-pass (answer-then-repair, Eywa pattern); base E8 two-pass + micro-edit bundle (pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8a2.ts --instance-ids scripts/e8-probeA.txt --tag e8A2-repair --judge-model openai/gpt-5 --budget 18 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['knowledge_update']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- REFERENT-FIRST: identify the EXACT thing the question asks about (the named cache, the actual session duration, the specific group's active membership, the achieved count). Only ledger lines about THAT EXACT referent are candidates; topically-adjacent lines (other caches, other groups, workshop attendance, code examples, availability) are DISQUALIFIED even when more recent.
|
||||
- ACHIEVED-ONLY: plans, targets, intentions, availability, and recommendations are not values; only lines stating the value as set / done / reported count.
|
||||
- WITHIN-REFERENT RECENCY: among the surviving candidate lines about the exact referent, the latest-dated one wins; recency never crosses referent boundaries.
|
||||
- EVIDENCE: every stated value must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
429
benchmarks/harness/scripts/beam-run-ledger-e8i.ts
Normal file
429
benchmarks/harness/scripts/beam-run-ledger-e8i.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8F — Combined full-700 spliced run (AMENDMENT E: B guard + C rule + D temporal + L2 event + multi repair; pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8d.ts --instance-ids scripts/e8-probeD.txt --tag e8D-convention --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['temporal_reasoning', 'multi_session_reasoning', 'event_ordering']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
const anchorsLine = ability === 'temporal_reasoning'
|
||||
? `- ANCHORS (gold convention): anchor every named event on the date the event itself OCCURS or is SCHEDULED to occur — the date stated together with the event — not the date it was merely mentioned. "Planned / agreed / scheduled / decided to X" anchors on X's PLANNED-FOR date ("[2024-01-10] planned a self-test for January 25" -> January 25; "[2024-03-03] agreed to cook moussaka on March 12" -> March 12). "Started preparing for test/quiz T" where the line says "preparing for my test on <date>" anchors on <date>. For events the question describes as actually happening ("celebrated", "attended", "cooked", "reached", "held"), use the date the ledger reports the event ACTUALLY happened (check later retrospective lines); if only a scheduled date exists, use the scheduled date. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.`
|
||||
: `- ANCHORS: for every event the question names, the anchor must be the date the event ACTUALLY happened / was confirmed — not the date it was merely mentioned, planned, or scheduled. Exception: if the question asks about the planning/scheduling/booking action itself, that action's own date IS the anchor. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.`;
|
||||
const orderBlock = ability === 'event_ordering'
|
||||
? `- ORDER: place every event the question asks about in true chronological order using ledger dates; re-check each placement individually. If a date was revised or corrected, order by the CORRECTED date. Every step the question names must appear in the sequence — a missing step is an error even when the rest are ordered correctly.\n\n`
|
||||
: '';
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
${anchorsLine}
|
||||
- REVISIONS: if a value or date was revised and the question does not ask about the change or the latest value, use the ORIGINAL for that named item; if it asks about the change, the two revisions ARE the two anchors.
|
||||
- UNIT: answer in the EXACT unit requested ("how many days" -> "N days"; "how many weeks" -> "N weeks"; "how many months" -> "N months and M days", never a bare "approximately N months"). For any span, also state the range "from <date> till <date>" and compute the plain calendar difference.
|
||||
- SCOPE and AGGREGATION: include every in-scope item across sessions, nothing out of scope; a running total or update SUPERSEDES earlier partial figures for the same item (never sum updates); re-check any arithmetic. For compare/optimize/prioritize questions, ground every claim in CURRENT (latest-dated) values, exact tools, versions, measured numbers.
|
||||
${orderBlock}- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
424
benchmarks/harness/scripts/beam-run-ledger-e8j.ts
Normal file
424
benchmarks/harness/scripts/beam-run-ledger-e8j.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8F — Combined full-700 spliced run (AMENDMENT E: B guard + C rule + D temporal + L2 event + multi repair; pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8d.ts --instance-ids scripts/e8-probeD.txt --tag e8D-convention --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['temporal_reasoning']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e8H-rollback-splice-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** probeJ pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- MILESTONE ENDPOINTS: when the question asks when a milestone was reached/achieved (hit N sign-ups, reached X% accuracy, completed Y tasks), anchor the EARLIEST dated ledger line that reports the milestone as met, and use THAT LINE's own date stamp. An embedded "by <date>" is a bound, not an exact date — do not anchor on it.
|
||||
- FIRST/LAST BATCH SCOPE: for "first X ... last X" over a recurring scheduled event, scope both endpoints to the earliest planning batch (the first utterance that schedules a set of occurrences); the "last" is the last occurrence within that same batch, NOT the latest occurrence anywhere in the ledger.
|
||||
- STATED SPANS: if the ledger states the asked span or duration explicitly (e.g. "the 4-day delay"), quote that stated value instead of computing a span between two report dates.
|
||||
- REVISIONS-ACTUAL: when the question asks when something ACTUALLY happened or took place, use the final revised date, not the originally scheduled one.
|
||||
- RANGE PHRASE: if the answer involves a span, lead with the count and unit ("N days" / "N weeks") and also state both endpoints as "from <date> till <date>".
|
||||
- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
423
benchmarks/harness/scripts/beam-run-ledger-e8k.ts
Normal file
423
benchmarks/harness/scripts/beam-run-ledger-e8k.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8F — Combined full-700 spliced run (AMENDMENT E: B guard + C rule + D temporal + L2 event + multi repair; pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8d.ts --instance-ids scripts/e8-probeD.txt --tag e8D-convention --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['multi_session_reasoning']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e8H-rollback-splice-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** probeJ pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- FINAL VALUE PER ITEM: for each DISTINCT named item the question asks about (each stall type, each routine, each booking, each fee, each purchase), find its FINAL value — the latest-dated line that states or updates THAT item supersedes every earlier value for the same item, including cancellations, revisions, and corrections.
|
||||
- SUM DISTINCT ITEMS: for total/combined questions, arithmetically sum the FINAL values of the distinct items and show every component. Never add two values that are revisions of the SAME item.
|
||||
- COUNT ITEMS, NOT CATEGORIES: for "how many different X" questions, count the distinct concrete items the user actually mentioned or did, not abstract categories, unless the question explicitly asks for categories.
|
||||
- STATE THE TOTAL: lead with the exact total/count in the exact unit requested, then the components.
|
||||
- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
422
benchmarks/harness/scripts/beam-run-ledger-e8l.ts
Normal file
422
benchmarks/harness/scripts/beam-run-ledger-e8l.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8F — Combined full-700 spliced run (AMENDMENT E: B guard + C rule + D temporal + L2 event + multi repair; pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8d.ts --instance-ids scripts/e8-probeD.txt --tag e8D-convention --judge-model openai/gpt-5 --budget 15 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['event_ordering']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e8H-rollback-splice-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** probeJ pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- FIRST-MENTION ANCHORS: "when did I first mention/bring up X" anchors the EARLIEST dated line where that aspect appears, even if only in passing — not the later detailed discussion.
|
||||
- CARDINALITY MATCH: list only the genuinely distinct aspects the question's scope covers — prefer the FEWEST items that exactly cover it. Do not pad with invented sub-phases, merged restatements, or granular micro-steps. Each item = one named aspect + its first-mentioned date, one line each.
|
||||
- ORDER: place every item in true chronological order using ledger dates; re-check each placement individually. If a date was revised or corrected, order by the CORRECTED date. Every aspect the question names must appear exactly once — a missing aspect is an error even when the rest are ordered correctly.
|
||||
- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
426
benchmarks/harness/scripts/beam-run-ledger-e8l2.ts
Normal file
426
benchmarks/harness/scripts/beam-run-ledger-e8l2.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E8L2 — Repair-pass extended to event_ordering (pre-registered AMENDMENT L2); base E8A2 + micro-edit bundle (pre-registered, see
|
||||
* KorroResearch/benchmarks/E8-PRE-REGISTRATION.md). Derived from E6-locked
|
||||
* beam-run-ledger.ts; everything not listed below is byte-identical to E6.
|
||||
*
|
||||
* CHANGES vs E6-locked:
|
||||
* 1. TWO-PASS for temporal_reasoning + multi_session_reasoning:
|
||||
* pass 1 = exhaustive dated-candidate / value-inventory extraction from the
|
||||
* ledger (no answering); pass 2 = selection + computation over that
|
||||
* scratchpad, E6 clause included verbatim. Attacks the dominant E7 loss:
|
||||
* wrong instance/anchor selection & aggregation-scope errors (NOT retrieval).
|
||||
* 2. Temporal UNIT-RULE in pass 2 (answer in the question's exact unit +
|
||||
* explicit "from <d1> till <d2>" range) — recovers right-range/wrong-unit
|
||||
* half-losses (e.g. "155 days" vs gold "5 months").
|
||||
* 3. Summarization NEVER-ABSTAIN guard (E8 append) — fixes the two known
|
||||
* full-0 wrongful abstentions (beam_1M_27_summarization_q0/q1).
|
||||
* 4. Abstention STRICT-EVIDENCE rule (E8 append) — answer only when a verbatim
|
||||
* ledger quote directly states the exact asked detail; attacks the 11/12
|
||||
* full-0 confabulations. Regression risk on info_extraction: watched.
|
||||
* 5. --dry-run flag: builds all messages, prints them, NO API calls, NO writes.
|
||||
*
|
||||
* PROTECTED (byte-identical to E6-locked): cacheable prefix incl. CURRENT
|
||||
* VALUES + CONTRADICTION RECORDS; all other ability clauses; event_ordering
|
||||
* single-pass (probe control); raw-excerpt serving; judge; models.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger-e8.ts --dry-run --instance-ids scripts/e8-probeA.txt
|
||||
* tsx scripts/beam-run-ledger-e8l2.ts --instance-ids scripts/e8-probeL2.txt --tag e8L2-eorepair --judge-model openai/gpt-5 --budget 18 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeB.txt --tag e8B-summguard --judge-model openai/gpt-5 --budget 3 --resume
|
||||
* tsx scripts/beam-run-ledger-e8.ts --instance-ids scripts/e8-probeC.txt --tag e8C-abstain --judge-model openai/gpt-5 --budget 7 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e8', outPath: null, dryRun: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--dry-run') { a.dryRun = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── E8A2 repair-pass ────────────────────────────
|
||||
const REPAIR_ABILITIES = new Set(['temporal_reasoning', 'multi_session_reasoning', 'event_ordering']);
|
||||
const E6_DRAFTS_PATH = path.join(repoRoot, 'benchmarks', 'results', 'beam', 'beam-1m-e6-ledger-FULL700-anthropic-claude-sonnet-4.6.jsonl');
|
||||
|
||||
/** E6 full-700 draft answers, loaded once: instance_id -> answer. */
|
||||
function loadE6Drafts(p: string): Map<string, string> {
|
||||
const m = new Map<string, string>();
|
||||
if (!fs.existsSync(p)) return m;
|
||||
for (const line of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try { const r = JSON.parse(line); if (r.instance_id && r.answer) m.set(r.instance_id, r.answer); } catch { /* skip bad line */ }
|
||||
}
|
||||
return m;
|
||||
}
|
||||
const e6Drafts = loadE6Drafts(E6_DRAFTS_PATH);
|
||||
|
||||
/** A2 pass-2: verify draft against the ledger, correct silently, output ONLY the final answer. */
|
||||
function buildRepairMessage(ability: string, question: string, draft: string): string {
|
||||
const orderBlock = ability === 'event_ordering'
|
||||
? '- ORDER: place every event the question asks about in true chronological order using ledger dates; re-check each placement individually. If a date was revised or corrected, order by the CORRECTED date. Every step the question names must appear in the sequence — a missing step is an error even when the rest are ordered correctly.\n'
|
||||
: '';
|
||||
return `You previously answered the QUESTION below from the evidence ledger. Your draft answer:
|
||||
|
||||
=== DRAFT ANSWER ===
|
||||
${draft.trim()}
|
||||
=== END DRAFT ANSWER ===
|
||||
|
||||
VERIFY the draft against the evidence ledger above, then produce the FINAL answer.
|
||||
|
||||
CHECKLIST (work silently):
|
||||
- ANCHORS: for every event the question names, the anchor must be the date the event ACTUALLY happened / was confirmed — not the date it was merely mentioned, planned, or scheduled. Exception: if the question asks about the planning/scheduling/booking action itself, that action's own date IS the anchor. For "first X" use the EARLIEST occurrence of that same named item; "last X" the LATEST.
|
||||
- REVISIONS: if a value or date was revised and the question does not ask about the change or the latest value, use the ORIGINAL for that named item; if it asks about the change, the two revisions ARE the two anchors.
|
||||
- UNIT: answer in the EXACT unit requested ("how many days" -> "N days"; "how many weeks" -> "N weeks"; "how many months" -> "N months and M days", never a bare "approximately N months"). For any span, also state the range "from <date> till <date>" and compute the plain calendar difference.
|
||||
- SCOPE and AGGREGATION: include every in-scope item across sessions, nothing out of scope; a running total or update SUPERSEDES earlier partial figures for the same item (never sum updates); re-check any arithmetic. For compare/optimize/prioritize questions, ground every claim in CURRENT (latest-dated) values, exact tools, versions, measured numbers.
|
||||
${orderBlock}- EVIDENCE: every specific claim must trace to a ledger line; correct or remove anything unsupported.
|
||||
|
||||
If the draft is already correct, return it (tightened wording allowed).
|
||||
Output ONLY the final answer to the question. No verification notes, no "the draft is correct", no checklist, no process narration.
|
||||
|
||||
QUESTION: ${question}
|
||||
|
||||
FINAL ANSWER:`;
|
||||
}
|
||||
|
||||
const SUMM_NEVER_ABSTAIN = `CRITICAL: The ledger above is the COMPLETE conversation record — you ALWAYS have enough material to summarize. NEVER respond with "${ABSTAIN_SENTINEL}" or any other refusal on a summary question. If one component seems thin, summarize what the ledger records for it and continue covering the others.`;
|
||||
|
||||
const ABSTAIN_QUOTE_RULE = `STRICT EVIDENCE TEST before answering: locate the ledger line whose VERBATIM QUOTE directly states the EXACT detail asked (the exact configuration value, the exact list of items, the exact named number or version). Material that is merely about the same system, vendor, or topic does NOT count. If no line directly states that specific detail, output the abstention sentence — do NOT synthesize a plausible-looking answer from neighboring facts.`;
|
||||
|
||||
/** E8 wrapper: E6 clauses byte-identical; appends only for summarization + abstention. */
|
||||
function abilityInstructionE8(ability: string): string {
|
||||
const base = abilityInstruction(ability);
|
||||
if (ability === 'summarization') return `${base}\n\n${SUMM_NEVER_ABSTAIN}`;
|
||||
if (ability === 'abstention') return `${base}\n\n${ABSTAIN_QUOTE_RULE}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
// E8: --dry-run builds no clients (zero API surface).
|
||||
const answerClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.model, true);
|
||||
const judgeClient = args.dryRun ? (null as unknown as BeamOpenAiClient) : makeClient(args.judgeModel, false);
|
||||
const embedder = args.dryRun ? (null as unknown as ReturnType<typeof createOllamaEmbedder>) : createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = (args.smoke || args.dryRun) ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = args.dryRun ? (null as unknown as ReturnType<typeof createSubstrate>) : createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = args.dryRun ? (null as unknown as ReturnType<typeof buildConvDateMap>) : buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (!args.dryRun && args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// E8: two-pass abilities get an extraction pass first; the pass-2
|
||||
// message is built after pass 1. All other abilities: E6 verbatim.
|
||||
const repair = REPAIR_ABILITIES.has(q.memoryAbility);
|
||||
const instruction = abilityInstructionE8(q.memoryAbility);
|
||||
let userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(`\n════ DRY conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | repair=${repair} | draft=${e6Drafts.has(q.instanceId) ? 'e6' : 'live'} | rawExcerpts=${DETAIL_ABILITIES.has(q.memoryAbility) ? 'skipped-dry' : (rawBlock ? 'yes' : 'no')}`);
|
||||
if (repair) {
|
||||
console.log(`── PASS-2 (repair template, draft placeholder) ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
} else {
|
||||
console.log(`── single-pass user msg ──\n${userMsg}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke && repair) {
|
||||
console.log(`\n════ SMOKE conv ${conv} · ${q.memoryAbility} · ${q.instanceId} (repair; printing repair template, no call)`);
|
||||
console.log(`── PASS-2 ──\n${buildRepairMessage(q.memoryAbility, q.question, '<DRAFT_ANSWER>')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
let repairCostUsd = 0, repairLatencyMs = 0, passes = 1, draftText = '', draftSource = '';
|
||||
if (repair) {
|
||||
draftText = e6Drafts.get(q.instanceId) ?? '';
|
||||
draftSource = draftText ? 'e6' : 'live';
|
||||
if (!draftText) {
|
||||
const d0 = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
repairCostUsd += d0.costUsd; repairLatencyMs += d0.latencyMs; draftText = stripAns(d0.text);
|
||||
answerCost += d0.costUsd;
|
||||
if (d0.cacheReadTokens || d0.cacheCreationTokens) { cacheReadSum += d0.cacheReadTokens ?? 0; cacheCreateSum += d0.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
}
|
||||
userMsg = buildRepairMessage(q.memoryAbility, q.question, draftText);
|
||||
passes = 2;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
}
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
passes, repair_cost_usd: +repairCostUsd.toFixed(4), repair_latency_ms: repairLatencyMs,
|
||||
draft_source: draftSource, draft_answer: draftText,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { if (rawSub) rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke && !args.dryRun) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
330
benchmarks/harness/scripts/beam-run-ledger.ts
Normal file
330
benchmarks/harness/scripts/beam-run-ledger.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* P3 — Whole-Ledger serving runner (E6). Serves the COMPLETE per-conversation
|
||||
* Evidence Ledger (P1) + STATE sections (P2) as the answer context, instead of a
|
||||
* top-k retrieval excerpt. This is the decisive test of the deep-dive thesis:
|
||||
* the 4 whole-history STATE abilities (abstention / contradiction / temporal /
|
||||
* event_ordering) are unanswerable from <3%-coverage top-k, but trivial over a
|
||||
* complete dated store.
|
||||
*
|
||||
* PER QUESTION:
|
||||
* - cacheableSystem = [framing + CURRENT VALUES + CONTRADICTION RECORDS + full
|
||||
* dated ledger] — a STABLE per-conversation prefix. With Anthropic prompt
|
||||
* caching (via OpenRouter cache_control) the 20 questions of one conversation
|
||||
* share ONE cache write; the rest are cheap cache reads. Questions are
|
||||
* processed GROUPED BY CONVERSATION to keep the cache warm.
|
||||
* - user message = ability-general instruction (gold-blind: keyed off the
|
||||
* dataset's memory_ability metadata, NOT gold) + question. Detail abilities
|
||||
* also get top-N raw dated turns from minds-1M (keeps the retrieval wins);
|
||||
* STATE abilities get the ledger ONLY (abstention needs a store it can trust
|
||||
* is complete — retrieval always surfaces something tangential).
|
||||
* - answerer: anthropic/claude-sonnet-4.6 (via OpenRouter — identical to the
|
||||
* E4-combo-sonnet baseline, for a fair comparison). Judge: canonical gpt-5.
|
||||
*
|
||||
* RESUMABLE: append-JSONL + skip-done (--resume). --instance-ids allowlist,
|
||||
* --abilities filter, --budget hard-cap. --smoke prints contexts (no judge).
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/beam-run-ledger.ts --smoke --convs 1 --instance-ids scripts/smoke3.txt
|
||||
* tsx scripts/beam-run-ledger.ts --instance-ids scripts/matched50.txt --budget 14 --tag e6-ledger-pilot --resume
|
||||
* tsx scripts/beam-run-ledger.ts --convs 1-35 --budget 60 --tag e6-ledger-FULL700 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE } from '../src/beam-multiroute.js';
|
||||
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const BEAM_CHATS = path.resolve(repoRoot, '..', 'BEAM', 'chats');
|
||||
const LEDGER_DIR = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'ledgers-1M');
|
||||
const RAW_MINDS = path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M');
|
||||
|
||||
interface Question { instanceId: string; conv: number; gopId: string; memoryAbility: string; question: string; rubric: string[]; }
|
||||
interface Args {
|
||||
model: string; judgeModel: string; budget: number; resume: boolean; smoke: boolean;
|
||||
convs: number[]; instanceIds: Set<string> | null; abilities: Set<string> | null;
|
||||
rawTurns: number; noCache: boolean; tag: string; outPath: string | null;
|
||||
}
|
||||
|
||||
const STATE_ABILITIES = new Set(['abstention', 'temporal_reasoning', 'event_ordering', 'contradiction_resolution']);
|
||||
const DETAIL_ABILITIES = new Set(['information_extraction', 'instruction_following', 'preference_following', 'knowledge_update', 'multi_session_reasoning']);
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const a: Args = {
|
||||
model: 'anthropic/claude-sonnet-4.6', judgeModel: 'gpt-5', budget: 14, resume: false, smoke: false,
|
||||
convs: parseConvSpec('1-35'), instanceIds: null, abilities: null, rawTurns: 30, noCache: false,
|
||||
tag: 'e6-ledger', outPath: null,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--raw-turns' && next) { a.rawTurns = parseInt(next, 10); i++; }
|
||||
else if (f === '--no-cache') { a.noCache = true; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
a.instanceIds = new Set(fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(conv: number): Question[] {
|
||||
const pqPath = path.join(BEAM_CHATS, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({ instanceId: `beam_1M_${conv}_${category}_q${qi}`, conv, gopId: `beam_${conv}`, memoryAbility: category, question: q, rubric: extractRubric(pq) });
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ledgerAvailable(conv: number): boolean {
|
||||
return fs.existsSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`));
|
||||
}
|
||||
|
||||
/** Assemble the cacheable system prefix: framing + STATE sections + full ledger. */
|
||||
function buildLedgerPrefix(conv: number): string {
|
||||
const ledger = fs.readFileSync(path.join(LEDGER_DIR, `conv${conv}.ledger.txt`), 'utf-8').trim();
|
||||
const statePath = path.join(LEDGER_DIR, `conv${conv}.state.txt`);
|
||||
const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8').trim()
|
||||
: '=== CURRENT VALUES ===\n(none)\n\n=== CONTRADICTION RECORDS ===\n(none)';
|
||||
return `You are an AI assistant answering questions about a user, using a COMPLETE evidence ledger derived from your entire prior conversation history with them.
|
||||
|
||||
The ledger below is the COMPLETE and ONLY record of that conversation history. Every fact in it was extracted from the actual conversation; each line is dated [YYYY-MM-DD], states one fact, and carries a verbatim source quote in parentheses. Lines are ordered by date (oldest first). Two consolidated sections precede the ledger:
|
||||
- CURRENT VALUES: the latest value of any attribute/decision/preference that changed over time.
|
||||
- CONTRADICTION RECORDS: statements that were later reversed, denied, or that conflict.
|
||||
|
||||
${state}
|
||||
|
||||
=== EVIDENCE LEDGER (complete, date-ordered) ===
|
||||
${ledger}`;
|
||||
}
|
||||
|
||||
function abilityInstruction(ability: string): string {
|
||||
switch (ability) {
|
||||
case 'abstention':
|
||||
return `The evidence ledger above is the COMPLETE record of everything in this conversation. If the answer to the question is genuinely NOT present anywhere in the ledger, respond with EXACTLY this sentence and nothing else: "${ABSTAIN_SENTINEL}" Do NOT guess, infer unstated facts, or use any outside knowledge. Only answer if the ledger actually contains the information.`;
|
||||
case 'temporal_reasoning':
|
||||
return `This is a TEMPORAL question. Every ledger line carries a [YYYY-MM-DD] stamp and the ledger is date-ordered. Find every relevant date, then answer in the FORM the question asks for — anchored on the ledger stamps:
|
||||
- DURATION / SPAN ("how long", "how many days/weeks/months between A and B", total time on something): COMPUTE it and commit to a single answer like "42 days — from 2024-03-01 to 2024-04-12". Do not hedge with ranges.
|
||||
- RELATIVE OFFSET or ORDERING ("how many days before/after X", "the day before", "the week after", "which came first"): identify the specific action that actually falls on the side the question asks (for "before X" it must be dated EARLIER than X; for "after X" LATER), and answer in the question's own form (e.g. "the day before — booked 2024-04-19, one day before the 2024-04-20 symposium"). Discard candidate dates on the wrong side of X or that belong to a different event; do NOT force an unrelated multi-day span when the answer is a simple relative offset.
|
||||
- SPECIFIC DATE ("when did X happen"): give that date.
|
||||
When several ledger lines give different candidate dates for the same event, use the date the event ACTUALLY happened or was confirmed (an exam sat, a milestone completed, a session held), NOT the first time it was merely mentioned, planned, or scheduled. For "by the time X took place" / "by my exam" phrasing, anchor on when X actually occurred. IMPORTANT: when the question asks how long before/after an event you did something that is itself scheduled FOR a specific date (a booked trip, a reserved flight, a planned session), and the wording is ambiguous between when you ARRANGED it and the date it is scheduled FOR, anchor the offset on the SCHEDULED/target date of that thing, not the date you arranged it — e.g. a flight reserved on May 2 for departure on May 20, asked how many days before a May 21 conference it departs, is "the day before" (May 20), not eighteen days (the reservation date).`;
|
||||
case 'event_ordering':
|
||||
return `This is an ORDERING question — treat it as an EXHAUSTIVE, ordered walkthrough of the WHOLE conversation timeline. The question asks for a specific number of items "in order". (1) Span the ENTIRE date range from the EARLIEST to the LATEST relevant ledger date — distribute your items across the full timeline; do NOT cluster them all in the early period. Later topics (e.g. authentication, security/TLS, fine-tuning, deployment/scaling, streaming, tessellation/visualization tools) count as much as early ones. (2) Present them in strict CHRONOLOGICAL order using the [YYYY-MM-DD] stamps, oldest first, each with its date. (3) For each item, name the SPECIFIC tools, libraries, versions, techniques, or topics the ledger records (e.g. exact software or method names) — the grader checks for specific named topics, not generic descriptions. Cover the distinct sub-topics comprehensively; do not repeat one theme across items. METHOD: first mentally partition the whole timeline into DISTINCT topical PHASES (each a different sub-topic or project stage), then make each of the N requested items a DIFFERENT phase — never spend two items on the same theme. Deliberately reserve items for the LATER-stage phases, which are easy to drop: common ones include authentication / role-based access control, security / TLS configuration, transformer- or LLM-based streaming integration, streaming-performance / chunk-size tuning, database-schema changes, model fine-tuning, and deployment / scaling — include each such phase as its own item whenever the ledger records it. Label every item with its distinct phase name and its date. Give EACH distinct technique, tool, method, model, or library its OWN separate item (e.g. two differently-named tools, or two distinct calculation techniques, are two items — never merge them), and do not let one dominant theme crowd out the smaller distinct sub-topics.`;
|
||||
case 'contradiction_resolution':
|
||||
return `This is a CONTRADICTION question. These questions almost always hinge on a PLANTED DENIAL: somewhere in the ledger the user explicitly says they NEVER did / didn't / haven't / never actually / never completed / never attended / never studied / never implemented the very thing the question asks about — even though OTHER lines show they clearly DID do it. Before answering, do ALL of this:
|
||||
1. Search the ENTIRE ledger for any statement that NEGATES the asked activity (scan for "never", "didn't", "haven't", "never actually", "never completed/attended/studied/implemented/practiced"). There is very likely exactly one such line — quote it VERBATIM with its date.
|
||||
2. State the affirmative evidence that they DID do it, with specifics (dates, numbers, names) and its date.
|
||||
3. Explicitly say the information is CONTRADICTORY, present BOTH conflicting statements side by side each WITH its date, and note which is more recent and which appears correct.
|
||||
Also consult the CONTRADICTION RECORDS section above. Do NOT just answer "yes" from the affirmative history — you MUST surface the "never" denial; failing to find it is the main way this question is failed.`;
|
||||
case 'summarization':
|
||||
return `This is a SUMMARY / overview question. Be EXHAUSTIVE. METHOD: first identify ALL the distinct COMPONENTS, STAGES, and TOPIC AREAS discussed across the entire timeline — each distinct service/module, infrastructure layer, model/ML step, frontend piece, data layer, and ops concern — then write a structured section for EACH. Do NOT let the dominant topic (the one discussed most) crowd out the later or secondary components: the grader checks coverage of the WHOLE process, so a summary that is deep on one area but omits whole stages scores poorly. When present in the ledger, explicitly cover each of: model training / fine-tuning and hyperparameter tuning, containerization (Docker / Docker Compose), orchestration (Kubernetes), frontend state management, database schema and connection issues, caching strategies, authentication / security, deployment / scaling, error handling, and monitoring. For each item include the specific details the ledger records — tools, libraries, versions, numbers, prices, dates, causes, and outcomes. Prefer complete, clause-dense, structured coverage over brevity; do not omit minor items or whole components.`;
|
||||
case 'knowledge_update':
|
||||
return `This question asks for the user's CURRENT / most-recent value, setting, or decision. The CURRENT VALUES section is a helpful summary but may be incomplete or may list only a GENERAL attribute. If the question names a SPECIFIC variant (e.g. the "recent translations" cache vs the general translation cache, one specific module vs the whole project, a named sub-setting), find the latest-dated ledger line matching THAT EXACT variant — it may differ from CURRENT VALUES. Scan the latest-dated relevant ledger lines directly, report the most recent value, and note it superseded any earlier one. Do not blindly copy CURRENT VALUES if a more specific or more recent ledger line answers the exact question asked; do not report an outdated value as current. IMPORTANT: the latest value can appear in a LATER entry (even one on the SAME date) that says the value was "extended / changed / updated / increased / reduced to" a new number — scan to the very last relevant entry and report that superseding value, not the first one stated. E.g. if a rate limit is first stated as 100/s and a later same-day entry raises it to 250/s, the current value is 250/s. CAUTION: CURRENT VALUES is a DERIVED summary and can CONFLATE two similar-but-distinct settings — e.g. a general translation/response cache versus a specifically-named cache like the "recent translations" cache — and may therefore report a number that actually belongs to the OTHER setting. When the question targets a specific named item, do NOT trust CURRENT VALUES; instead locate the value from the ledger lines that are part of THAT item's own discussion thread (the ones that name or directly continue that exact item), and report the latest such value even if CURRENT VALUES and other similarly-named items show a different number.`;
|
||||
case 'preference_following':
|
||||
return `This is a PREFERENCE-FOLLOWING question. The user has previously stated a preferred METHOD, approach, style, tool, or convention for how they want this kind of task DONE or EXPLAINED (e.g. prefers vector-algebra derivations over trigonometry, prefers a specific library or framework, prefers step-by-step derivations, a particular notation, or explicitly wants to AVOID some approach/tool). This is NOT a "latest value" question — it is about honoring how they like things done. FIRST scan the ledger AND the supporting raw excerpts for the user's stated preference relevant to THIS question (if it changed over time, use the most recently stated preference). THEN actually ANSWER the question by USING and COMPLYING WITH that preferred method — solve it their way, use exactly the tool/technique/notation they prefer, and avoid any approach they said they dislike or want to avoid. Do not merely name the preference, and do not give a generically-correct answer that ignores their stated way of doing things. If the user has an ESTABLISHED tool, library, or version for this exact task, give optimizations and configuration SPECIFIC to that exact tool/version (its own options, flags, and tuning knobs), and do NOT suggest switching to, adding, or showing code for any alternative library that serves the same purpose — stay entirely within their chosen tool.`;
|
||||
case 'instruction_following':
|
||||
return `This is an INSTRUCTION-FOLLOWING question. The user has previously given a STANDING INSTRUCTION or preference about HOW answers on this topic must be formatted or what they must ALWAYS include (e.g. always cite version numbers and protocol versions like OAuth 2.0 / TLS 1.3, always include export steps and file-format options, always explain how to save or share outputs, always give step-by-step derivations, always use diagrams). Search the ledger AND the supporting raw excerpts for any such standing instruction relevant to this question, then COMPLY with it: include those specific required elements in your answer, not just the bare facts. Answer completely — name exact tools, versions, and the how-to steps the user asked you to always provide.`;
|
||||
case 'multi_session_reasoning':
|
||||
return `This question reasons ACROSS multiple sessions — scan the ENTIRE ledger and cross-reference related facts across different dates. If it asks HOW MANY distinct types / kinds / categories / use-cases of something, first GROUP the individual mentions into distinct CATEGORIES and count the CATEGORIES, not every individual instance — the intended answer is usually a small number (about 3-5), so do NOT inflate the count by listing every occurrence separately. If it asks you to optimize or compare, be specific: name the exact tools/versions and cover every relevant technique the ledger records (e.g. leverage the specific library's own strengths, schema indexing, cache-before-DB checks, robust queue with backoff). Include exact names, dates, numbers, and versions.`;
|
||||
default:
|
||||
return `Answer accurately and completely using the ledger. Scan all relevant lines, cross-reference across dates, and include exact names, dates, numbers, and versions.`;
|
||||
}
|
||||
}
|
||||
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
|
||||
function makeClient(model: string, forCaching: boolean): BeamOpenAiClient {
|
||||
// Any provider-prefixed model ("anthropic/*", "openai/*") is routed through
|
||||
// OpenRouter. The judge uses "openai/gpt-5" here because the OpenAI-direct
|
||||
// account's gpt-5 quota can be exhausted; OpenRouter is the same model at the
|
||||
// same price. Pricing lookup strips the provider prefix (openai/gpt-5 -> gpt-5).
|
||||
if (model.includes('/')) {
|
||||
loadDotEnv();
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
if (!key) throw new Error('OPENROUTER_API_KEY required for provider-routed models');
|
||||
const bareModel = model.replace(/^[^/]+\//, '');
|
||||
return new BeamOpenAiClient({
|
||||
model, apiKey: key, baseUrl: 'https://openrouter.ai/api/v1',
|
||||
pricing: OPENAI_PRICING[model] ?? OPENAI_PRICING[bareModel] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 },
|
||||
timeoutMs: 300_000, maxRetries: 3,
|
||||
});
|
||||
}
|
||||
return createBeamOpenAiClient({ model, pricing: OPENAI_PRICING[model], timeoutMs: 300_000, maxRetries: 2 });
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
loadDotEnv();
|
||||
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[ledger-run] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[ledger-run] WARNING: ${path.basename(outPath)} exists; appending without --resume may duplicate.`);
|
||||
}
|
||||
|
||||
const answerClient = makeClient(args.model, true);
|
||||
const judgeClient = makeClient(args.judgeModel, false);
|
||||
const embedder = createOllamaEmbedder();
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const promptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0;
|
||||
let cacheReadSum = 0, cacheCreateSum = 0, cachedCalls = 0;
|
||||
let budgetStopped = false;
|
||||
const outStream = args.smoke ? null : fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(ledgerAvailable);
|
||||
console.log(`[ledger-run] answer=${args.model} judge=${args.judgeModel} rawTurns=${args.rawTurns} cache=${args.noCache ? 'OFF' : 'ON'} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(conv).filter(q =>
|
||||
!doneIds.has(q.instanceId) &&
|
||||
(!args.instanceIds || args.instanceIds.has(q.instanceId)) &&
|
||||
(!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const prefix = buildLedgerPrefix(conv);
|
||||
const prefixTok = Math.ceil(prefix.length / 4);
|
||||
const rawSub = createSubstrate({ dbPath: path.join(RAW_MINDS, `beam_1M_${conv}.mind`), embedder });
|
||||
const dateMap = buildConvDateMap(path.join(BEAM_CHATS, '1M', String(conv), 'chat.json'));
|
||||
console.log(`[ledger-run] conv ${conv}: ${questions.length} q, ledger prefix ~${prefixTok} tok`);
|
||||
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost;
|
||||
if (spent >= args.budget) { console.warn(`[ledger-run] budget $${args.budget} reached ($${spent.toFixed(2)}) — stopping.`); budgetStopped = true; break; }
|
||||
|
||||
// Raw dated turns only for detail abilities (abstention/state stay ledger-only).
|
||||
let rawBlock = '';
|
||||
if (args.rawTurns > 0 && DETAIL_ABILITIES.has(q.memoryAbility)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, { ...DEFAULT_MULTIROUTE, topN: args.rawTurns });
|
||||
if (ctx.displayStrings.length) {
|
||||
rawBlock = `\n\nSUPPORTING RAW EXCERPTS (verbatim conversation turns, for extra detail — the ledger above remains the complete record):\n` +
|
||||
ctx.displayStrings.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
const instruction = abilityInstruction(q.memoryAbility);
|
||||
const userMsg = `${instruction}${rawBlock}\n\nAnswer the question using ONLY the evidence ledger (and any excerpts shown above). Be specific — include exact names, dates, numbers, and versions from the ledger.\n\nQUESTION: ${q.question}\n\nANSWER:`;
|
||||
promptToks.push(prefixTok + Math.ceil(userMsg.length / 4));
|
||||
|
||||
if (args.smoke) {
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`prefix ~${prefixTok} tok | rawExcerpts=${rawBlock ? 'yes' : 'no'} | instruction=${instruction.slice(0, 70)}...`);
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
console.log(`\n──── ANSWER (${ans.latencyMs}ms, cacheRead=${ans.cacheReadTokens ?? 0} cacheCreate=${ans.cacheCreationTokens ?? 0} $${ans.costUsd.toFixed(4)}) ────`);
|
||||
console.log(stripAns(ans.text).slice(0, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
const ans = await answerClient.chat({ system: '', user: userMsg, cacheableSystem: args.noCache ? undefined : prefix, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
if (ans.cacheReadTokens || ans.cacheCreationTokens) { cacheReadSum += ans.cacheReadTokens ?? 0; cacheCreateSum += ans.cacheCreationTokens ?? 0; cachedCalls++; }
|
||||
const answer = stripAns(ans.text) || ABSTAIN_SENTINEL;
|
||||
|
||||
const { judgement, llmResults } = await judgeQuestion(judgeClient, { question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer }, { computeTau: false });
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) } as BeamQuestionResult);
|
||||
outStream!.write(JSON.stringify({
|
||||
instance_id: q.instanceId, memory_ability: q.memoryAbility, question: q.question, answer,
|
||||
score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores, n_nuggets: q.rubric.length,
|
||||
ledger_prefix_tokens: prefixTok, raw_excerpts: !!rawBlock,
|
||||
cache_read_tokens: ans.cacheReadTokens ?? 0, cache_creation_tokens: ans.cacheCreationTokens ?? 0,
|
||||
answer_model: args.model, judge_model: args.judgeModel, answer_latency_ms: ans.latencyMs,
|
||||
}) + '\n');
|
||||
const cacheFlag = (ans.cacheReadTokens ?? 0) > 0 ? `cR=${ans.cacheReadTokens}` : (ans.cacheCreationTokens ?? 0) > 0 ? `cW=${ans.cacheCreationTokens}` : 'cache—';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} ${cacheFlag.padEnd(12)} score=${judgement.score.toFixed(2)} $${(answerCost + judgeCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally { rawSub.close(); }
|
||||
}
|
||||
if (outStream) outStream.end();
|
||||
|
||||
if (!args.smoke) {
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = promptToks.length ? Math.round(promptToks.reduce((s, x) => s + x, 0) / promptToks.length) : 0;
|
||||
const totalCost = answerCost + judgeCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'ledger', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
raw_turns: args.rawTurns, caching: !args.noCache, mean_prompt_tokens: meanTok, answered_now: perQuestion.length,
|
||||
cache_read_total: cacheReadSum, cache_creation_total: cacheCreateSum, cached_calls: cachedCalls, budgetStopped,
|
||||
answer_cost: +answerCost.toFixed(4), judge_cost: +judgeCost.toFixed(4), total_cost: +totalCost.toFixed(4),
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M · Evidence-Ledger serving ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`cache: reads=${cacheReadSum} creates=${cacheCreateSum} over ${cachedCalls} cached calls`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)} judge=$${judgeCost.toFixed(3)}) answered_now=${perQuestion.length} meanPromptTok=${meanTok}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('[beam-run-ledger] FATAL:', e); process.exit(1); });
|
||||
521
benchmarks/harness/scripts/beam-run-multiroute.ts
Normal file
521
benchmarks/harness/scripts/beam-run-multiroute.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* BEAM 1M — the `multiroute` cell (E4). Deterministic multi-route dated
|
||||
* retrieval (vector + timeline + entity, RRF-fused) + ability-gated belief
|
||||
* overlay, on top of the winning answer path.
|
||||
*
|
||||
* PER QUESTION:
|
||||
* 1. Multi-route context (src/beam-multiroute.ts): Route V (vector top-kVec)
|
||||
* + Route T (broad dated timeline, parseDateWindow filter, coverage rank)
|
||||
* + Route E (per-entity FTS across sessions), RRF-fused → top-N, DATED,
|
||||
* chronological. Replaces the baseline single top-30 similarity context.
|
||||
* 2. Ability-gated belief block (E3 buildBeliefBlock, real supersede/
|
||||
* consolidation, rolled-back txn, gpt-5-mini detect): injected ONLY for
|
||||
* --belief-abilities (default knowledge_update,abstention,
|
||||
* contradiction_resolution,event_ordering). Gate is by GOLD ability — an
|
||||
* oracle gate for architecture isolation (a deployed system would use the
|
||||
* measured gpt-5 classifier).
|
||||
* 3. Answer: buildAnswerGenerationPromptV2(q, multiRouteDisplay, undefined,
|
||||
* beliefBlock?) → answerer (gpt-5 for isolation; anthropic/claude-sonnet-4.6
|
||||
* for the headline, via OpenRouter).
|
||||
* 4. Judge: canonical gpt-5 nugget judge; full nugget_scores recorded per row
|
||||
* so a later Sonnet-judge (Eywa protocol) pass via beam-rejudge.ts works.
|
||||
*
|
||||
* RESUMABLE: append-JSONL + skip-done (--resume). --instance-ids <file> exact
|
||||
* allowlist. --abilities restricts the question set. --budget hard-caps spend.
|
||||
*
|
||||
* Usage:
|
||||
* # per-route smoke (print context, no spend):
|
||||
* tsx scripts/beam-run-multiroute.ts --smoke --convs 1 --abilities temporal_reasoning
|
||||
* # pilot (matched-50, gpt-5 answer + gpt-5 judge):
|
||||
* tsx scripts/beam-run-multiroute.ts --model gpt-5 \
|
||||
* --instance-ids scripts/matched50.txt --budget 12 --resume
|
||||
* # headline full-700 (multi-route + gated belief + Sonnet answerer, gpt-5 judge):
|
||||
* tsx scripts/beam-run-multiroute.ts --model anthropic/claude-sonnet-4.6 \
|
||||
* --judge-model gpt-5 --convs 1-35 --budget 90 --resume
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import process from 'node:process';
|
||||
|
||||
import { createOllamaEmbedder } from '@waggle/core';
|
||||
import {
|
||||
detectSupersessionChains, detectEntityGroups, applyConsolidation,
|
||||
type ConsolidationLlm, type Observation, type MemoryFrame,
|
||||
} from '@waggle/core';
|
||||
import { createSubstrate } from '../src/substrate.js';
|
||||
import type { Substrate } from '../src/substrate.js';
|
||||
import { createBeamOpenAiClient, BeamOpenAiClient, OPENAI_PRICING, loadDotEnv } from '../src/beam-openai-client.js';
|
||||
import { buildAnswerGenerationPromptV2, buildRepairPrompt, judgeQuestion } from '../src/beam-nugget-judge.js';
|
||||
import type { BeamLlmResult } from '../src/beam-nugget-judge.js';
|
||||
import { buildConvDateMap } from '../src/beam-date-map.js';
|
||||
import { computeBeamMetrics, formatBeamMetrics } from '../src/beam-metrics.js';
|
||||
import type { BeamQuestionResult } from '../src/beam-metrics.js';
|
||||
import { buildMultiRouteContext, DEFAULT_MULTIROUTE, type MultiRouteOptions } from '../src/beam-multiroute.js';
|
||||
|
||||
interface Question {
|
||||
instanceId: string;
|
||||
conv: number;
|
||||
gopId: string;
|
||||
memoryAbility: string;
|
||||
question: string;
|
||||
rubric: string[];
|
||||
}
|
||||
|
||||
interface Args {
|
||||
model: string;
|
||||
judgeModel: string;
|
||||
detectModel: string;
|
||||
budget: number;
|
||||
resume: boolean;
|
||||
smoke: boolean;
|
||||
convs: number[];
|
||||
beamChats: string;
|
||||
rawMindsDir: string;
|
||||
obsMindsDir: string;
|
||||
instanceIds: Set<string> | null;
|
||||
abilities: Set<string> | null;
|
||||
beliefAbilities: Set<string>;
|
||||
noBelief: boolean;
|
||||
route: MultiRouteOptions;
|
||||
kBelief: number;
|
||||
outPath: string | null;
|
||||
tag: string;
|
||||
repair: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_BELIEF_ABILITIES = ['knowledge_update', 'abstention', 'contradiction_resolution', 'event_ordering'];
|
||||
|
||||
function parseConvSpec(spec: string): number[] {
|
||||
const out = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const m = part.match(/^(\d+)-(\d+)$/);
|
||||
if (m) { for (let i = +m[1]; i <= +m[2]; i++) out.add(i); }
|
||||
else if (/^\d+$/.test(part.trim())) out.add(+part.trim());
|
||||
}
|
||||
return [...out].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const argv = process.argv.slice(2);
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const a: Args = {
|
||||
model: 'gpt-5',
|
||||
judgeModel: 'gpt-5',
|
||||
detectModel: 'gpt-5-mini',
|
||||
budget: 12,
|
||||
resume: false,
|
||||
smoke: false,
|
||||
convs: parseConvSpec('1-35'),
|
||||
beamChats: path.resolve(repoRoot, '..', 'BEAM', 'chats'),
|
||||
rawMindsDir: path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M'),
|
||||
obsMindsDir: path.join(repoRoot, 'benchmarks', 'data', 'beam', 'minds-1M-obs'),
|
||||
instanceIds: null,
|
||||
abilities: null,
|
||||
beliefAbilities: new Set(DEFAULT_BELIEF_ABILITIES),
|
||||
noBelief: false,
|
||||
route: { ...DEFAULT_MULTIROUTE },
|
||||
kBelief: 60,
|
||||
outPath: null,
|
||||
tag: 'multiroute',
|
||||
repair: false,
|
||||
};
|
||||
let judgeExplicit = false;
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const f = argv[i]; const next = argv[i + 1];
|
||||
if (f === '--model' && next) { a.model = next; i++; }
|
||||
else if (f === '--judge-model' && next) { a.judgeModel = next; judgeExplicit = true; i++; }
|
||||
else if (f === '--detect-model' && next) { a.detectModel = next; i++; }
|
||||
else if (f === '--budget' && next) { a.budget = parseFloat(next); i++; }
|
||||
else if (f === '--resume') { a.resume = true; }
|
||||
else if (f === '--smoke') { a.smoke = true; }
|
||||
else if (f === '--no-belief') { a.noBelief = true; }
|
||||
else if (f === '--repair') { a.repair = true; }
|
||||
else if (f === '--convs' && next) { a.convs = parseConvSpec(next); i++; }
|
||||
else if (f === '--tag' && next) { a.tag = next; i++; }
|
||||
else if (f === '--k-vec' && next) { a.route.kVec = parseInt(next, 10); i++; }
|
||||
else if (f === '--k-wide' && next) { a.route.kWide = parseInt(next, 10); i++; }
|
||||
else if (f === '--per-entity' && next) { a.route.perEntity = parseInt(next, 10); i++; }
|
||||
else if (f === '--cap-per-date' && next) { a.route.capPerDate = parseInt(next, 10); i++; }
|
||||
else if (f === '--top-n' && next) { a.route.topN = parseInt(next, 10); i++; }
|
||||
else if (f === '--k-belief' && next) { a.kBelief = parseInt(next, 10); i++; }
|
||||
else if (f === '--belief-abilities' && next) { a.beliefAbilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--abilities' && next) { a.abilities = new Set(next.split(',').map(s => s.trim()).filter(Boolean)); i++; }
|
||||
else if (f === '--instance-ids' && next) {
|
||||
const ids = fs.readFileSync(path.resolve(next), 'utf-8').split('\n').map(s => s.trim()).filter(Boolean);
|
||||
a.instanceIds = new Set(ids); i++;
|
||||
}
|
||||
else if (f === '--out' && next) { a.outPath = path.resolve(next); i++; }
|
||||
}
|
||||
if (!judgeExplicit) a.judgeModel = a.model;
|
||||
return a;
|
||||
}
|
||||
|
||||
// ── Question loading (identical scheme to beam-run-belief.ts) ────────────────
|
||||
|
||||
function extractRubric(pq: Record<string, unknown>): string[] {
|
||||
const raw = pq.rubric;
|
||||
if (Array.isArray(raw)) return raw.map(String).map(s => s.trim()).filter(Boolean);
|
||||
if (raw && typeof raw === 'object') {
|
||||
const n = (raw as Record<string, unknown>).nuggets;
|
||||
if (Array.isArray(n)) return n.map(String).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (raw) return [String(raw).trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadConvQuestions(beamChats: string, conv: number): Question[] {
|
||||
const pqPath = path.join(beamChats, '1M', String(conv), 'probing_questions', 'probing_questions.json');
|
||||
if (!fs.existsSync(pqPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(pqPath, 'utf-8')) as Record<string, Record<string, unknown>[]>;
|
||||
const out: Question[] = [];
|
||||
for (const [category, questions] of Object.entries(data)) {
|
||||
if (!Array.isArray(questions)) continue;
|
||||
questions.forEach((pq, qi) => {
|
||||
const q = typeof pq.question === 'string' ? pq.question : '';
|
||||
if (!q) return;
|
||||
out.push({
|
||||
instanceId: `beam_1M_${conv}_${category}_q${qi}`,
|
||||
conv, gopId: `beam_${conv}`, memoryAbility: category,
|
||||
question: q, rubric: extractRubric(pq),
|
||||
});
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mindPath(mindsDir: string, conv: number): string {
|
||||
return path.join(mindsDir, `beam_1M_${conv}.mind`);
|
||||
}
|
||||
function isIngested(mindsDir: string, conv: number): boolean {
|
||||
return fs.existsSync(path.join(mindsDir, `beam_1M_${conv}.done.json`)) && fs.existsSync(mindPath(mindsDir, conv));
|
||||
}
|
||||
function chatJsonPath(beamChats: string, conv: number): string {
|
||||
return path.join(beamChats, '1M', String(conv), 'chat.json');
|
||||
}
|
||||
function stripAns(text: string): string {
|
||||
return text.includes('ANSWER:') ? text.split('ANSWER:').pop()!.trim() : text.trim();
|
||||
}
|
||||
function approxTokens(s: string): number { return Math.max(1, Math.ceil(s.length / 4)); }
|
||||
|
||||
/** gpt/o-series → OpenAI; Claude ids → OpenRouter. */
|
||||
function makeClient(model: string): BeamOpenAiClient {
|
||||
if (/claude|anthropic/i.test(model)) {
|
||||
loadDotEnv();
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey) throw new Error('OPENROUTER_API_KEY not found in environment or .env (required for Claude answerer).');
|
||||
const pricing = OPENAI_PRICING[model] ?? { inputPerMillion: 3.0, outputPerMillion: 15.0 };
|
||||
// 180s (vs the 60s default): the repair pass on long summaries feeds the whole
|
||||
// draft back in and asks for an exhaustive rewrite — generation can exceed 60s
|
||||
// and abort to an empty completion (observed on summarization in the E5 smoke).
|
||||
// Extending the timeout is strictly safe (only waits longer) and also helps long drafts.
|
||||
return new BeamOpenAiClient({ model, apiKey, baseUrl: 'https://openrouter.ai/api/v1', pricing, timeoutMs: 180_000 });
|
||||
}
|
||||
return createBeamOpenAiClient({ model });
|
||||
}
|
||||
|
||||
// ── Belief block (REAL supersede/consolidation) — from beam-run-belief.ts ────
|
||||
|
||||
const ROLLBACK = Symbol('belief-rollback');
|
||||
|
||||
function bframeDescription(content: string): string {
|
||||
try {
|
||||
const o = JSON.parse(content) as { description?: unknown };
|
||||
if (o && typeof o.description === 'string') return o.description;
|
||||
} catch { /* raw */ }
|
||||
return content;
|
||||
}
|
||||
|
||||
interface BeliefBlock { block: string | null; nChains: number; nGroups: number }
|
||||
|
||||
async function buildBeliefBlock(
|
||||
obsSub: Substrate, gopId: string, question: string, detectLlm: ConsolidationLlm, kBelief: number,
|
||||
): Promise<BeliefBlock> {
|
||||
const results = await obsSub.search.search(question, { limit: kBelief, gopId });
|
||||
if (results.length < 2) return { block: null, nChains: 0, nGroups: 0 };
|
||||
const obs: Observation[] = results.map(r => ({ id: r.frame.id, content: r.frame.content, created_at: String(r.frame.created_at ?? '') }));
|
||||
const [chains, groups] = await Promise.all([detectSupersessionChains(obs, detectLlm), detectEntityGroups(obs, detectLlm)]);
|
||||
if (chains.length === 0 && groups.length === 0) return { block: null, nChains: 0, nGroups: 0 };
|
||||
|
||||
const raw = obsSub.db.getDatabase();
|
||||
let pframes: MemoryFrame[] = [];
|
||||
let bframes: MemoryFrame[] = [];
|
||||
try {
|
||||
raw.transaction(() => {
|
||||
const res = applyConsolidation(obsSub.frames, chains, groups, gopId);
|
||||
pframes = res.pframes; bframes = res.bframes;
|
||||
throw ROLLBACK;
|
||||
})();
|
||||
} catch (e) { if (e !== ROLLBACK) throw e; }
|
||||
|
||||
const values = pframes.map(f => String(f.content).replace(/^\[current\]\s*/, '').trim()).filter(Boolean);
|
||||
const sets = bframes.map(f => bframeDescription(String(f.content))).map(s => s.trim()).filter(Boolean);
|
||||
if (values.length === 0 && sets.length === 0) return { block: null, nChains: chains.length, nGroups: groups.length };
|
||||
|
||||
const parts: string[] = [];
|
||||
if (values.length) {
|
||||
parts.push(
|
||||
'CURRENT VALUES (consolidated from the user\'s whole history — each line is the LATEST known ' +
|
||||
'value of a fact that CHANGED over time; when a raw memory below conflicts with one of these, ' +
|
||||
'trust the value here):\n' + values.map(v => `- ${v}`).join('\n'),
|
||||
);
|
||||
}
|
||||
if (sets.length) {
|
||||
parts.push(
|
||||
'ENUMERABLE SETS (complete member counts inferred across all sessions — use these when asked ' +
|
||||
'to count or list every item of a kind):\n' + sets.map(s => `- ${s}`).join('\n'),
|
||||
);
|
||||
}
|
||||
return { block: parts.join('\n\n'), nChains: chains.length, nGroups: groups.length };
|
||||
}
|
||||
|
||||
// ── Smoke (print context, no spend) ──────────────────────────────────────────
|
||||
|
||||
async function smoke(args: Args): Promise<void> {
|
||||
const embedder = createOllamaEmbedder();
|
||||
const convs = args.convs.filter(c => isIngested(args.rawMindsDir, c));
|
||||
console.log(`[smoke] multi-route context (no LLM). route=${JSON.stringify(args.route)}`);
|
||||
for (const conv of convs) {
|
||||
const rawSub = createSubstrate({ dbPath: mindPath(args.rawMindsDir, conv), embedder });
|
||||
const dateMap = buildConvDateMap(chatJsonPath(args.beamChats, conv));
|
||||
try {
|
||||
const questions = loadConvQuestions(args.beamChats, conv)
|
||||
.filter(q => (!args.instanceIds || args.instanceIds.has(q.instanceId)) && (!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
for (const q of questions.slice(0, 4)) {
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, args.route);
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`entities: [${ctx.entities.join(' | ')}] window: ${ctx.dateWindow ? ctx.dateWindow.label : '—'}`);
|
||||
console.log(`routes: V=${ctx.nVec} T=${ctx.nTimeline} E=${ctx.nEntity} → fused=${ctx.nFused} (dated=${ctx.nDated})`);
|
||||
console.log('── context (first 12 lines) ──');
|
||||
for (const line of ctx.displayStrings.slice(0, 12)) console.log(' ' + line.slice(0, 140));
|
||||
if (ctx.displayStrings.length > 12) console.log(` … +${ctx.displayStrings.length - 12} more`);
|
||||
}
|
||||
} finally { rawSub.close(); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Repair smoke (draft vs repaired, side by side; DOES spend) ────────────────
|
||||
// Triggered by `--smoke --repair`. Real draft+repair model calls, NO judge.
|
||||
// Cap the number of questions with `--convs`/`--abilities`/`--instance-ids`;
|
||||
// prints at most the first `maxQ` questions (default 3 → ≤6 model calls).
|
||||
|
||||
async function repairSmoke(args: Args, maxQ = 3): Promise<void> {
|
||||
const embedder = createOllamaEmbedder();
|
||||
const answerClient = makeClient(args.model);
|
||||
const detectClient = makeClient(args.detectModel);
|
||||
let detectCost = 0, answerCost = 0, repairCost = 0;
|
||||
const detectLlm: ConsolidationLlm = async (system, user) => {
|
||||
const r = await detectClient.chat({ system, user, jsonMode: true, maxTokens: 1200 });
|
||||
detectCost += r.costUsd; return r.text;
|
||||
};
|
||||
const convs = args.convs.filter(c => isIngested(args.rawMindsDir, c));
|
||||
console.log(`[repair-smoke] answer=${args.model} detect=${args.detectModel} route=${JSON.stringify(args.route)} belief=${args.noBelief ? 'OFF' : [...args.beliefAbilities].join('+')}`);
|
||||
let shown = 0;
|
||||
for (const conv of convs) {
|
||||
if (shown >= maxQ) break;
|
||||
const rawSub = createSubstrate({ dbPath: mindPath(args.rawMindsDir, conv), embedder });
|
||||
const obsAvailable = !args.noBelief && isIngested(args.obsMindsDir, conv);
|
||||
const obsSub = obsAvailable ? createSubstrate({ dbPath: mindPath(args.obsMindsDir, conv), embedder }) : null;
|
||||
const dateMap = buildConvDateMap(chatJsonPath(args.beamChats, conv));
|
||||
try {
|
||||
const questions = loadConvQuestions(args.beamChats, conv)
|
||||
.filter(q => (!args.instanceIds || args.instanceIds.has(q.instanceId)) && (!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
for (const q of questions) {
|
||||
if (shown >= maxQ) break;
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, args.route);
|
||||
let bel: BeliefBlock = { block: null, nChains: 0, nGroups: 0 };
|
||||
if (!args.noBelief && args.beliefAbilities.has(q.memoryAbility) && obsSub) {
|
||||
bel = await buildBeliefBlock(obsSub, q.gopId, q.question, detectLlm, args.kBelief);
|
||||
}
|
||||
const prompt = buildAnswerGenerationPromptV2(q.question, ctx.displayStrings, undefined, bel.block ?? undefined);
|
||||
const ans = await answerClient.chat({ system: '', user: prompt, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
const draftAnswer = stripAns(ans.text);
|
||||
const repairPrompt = buildRepairPrompt(q.question, ctx.displayStrings, draftAnswer, undefined, bel.block ?? undefined);
|
||||
const rep = await answerClient.chat({ system: '', user: repairPrompt, maxTokens: 8192 });
|
||||
repairCost += rep.costUsd;
|
||||
const repaired = stripAns(rep.text);
|
||||
console.log(`\n════ conv ${conv} · ${q.memoryAbility} · ${q.instanceId}`);
|
||||
console.log(`Q: ${q.question}`);
|
||||
console.log(`ctx: fused=${ctx.nFused} dated=${ctx.nDated} belief=${bel.block ? `${bel.nChains}c/${bel.nGroups}g` : '—'}`);
|
||||
console.log(`\n──── DRAFT ────\n${draftAnswer}`);
|
||||
console.log(`\n──── REPAIRED ────\n${repaired}`);
|
||||
console.log(`\n[changed=${repaired !== draftAnswer}] running cost=$${(answerCost + repairCost + detectCost).toFixed(4)}`);
|
||||
shown++;
|
||||
}
|
||||
} finally { rawSub.close(); if (obsSub) obsSub.close(); }
|
||||
}
|
||||
console.log(`\n[repair-smoke] done — ${shown} question(s), cost=$${(answerCost + repairCost + detectCost).toFixed(4)} (answer=$${answerCost.toFixed(4)} repair=$${repairCost.toFixed(4)} detect=$${detectCost.toFixed(4)})`);
|
||||
}
|
||||
|
||||
// ── Run ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function run(args: Args): Promise<void> {
|
||||
const here = url.fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(here), '..', '..', '..');
|
||||
const outDir = path.join(repoRoot, 'benchmarks', 'results', 'beam');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const modelSlug = args.model.replace(/[^a-z0-9.]+/gi, '-');
|
||||
const outPath = args.outPath ?? path.join(outDir, `beam-1m-${args.tag}-${modelSlug}.jsonl`);
|
||||
|
||||
const doneIds = new Set<string>();
|
||||
if (fs.existsSync(outPath)) {
|
||||
for (const line of fs.readFileSync(outPath, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
try { const row = JSON.parse(t) as { instance_id?: string }; if (args.resume && row.instance_id) doneIds.add(row.instance_id); } catch { /* skip */ }
|
||||
}
|
||||
if (args.resume) console.log(`[multiroute] resume: ${doneIds.size} already answered in ${path.basename(outPath)}`);
|
||||
else if (fs.readFileSync(outPath, 'utf-8').trim()) console.warn(`[multiroute] WARNING: ${path.basename(outPath)} exists; appending WITHOUT --resume may duplicate rows.`);
|
||||
}
|
||||
|
||||
const answerClient = makeClient(args.model);
|
||||
const judgeClient = args.judgeModel === args.model ? answerClient : makeClient(args.judgeModel);
|
||||
const detectClient = makeClient(args.detectModel);
|
||||
const embedder = createOllamaEmbedder();
|
||||
|
||||
let detectCost = 0;
|
||||
const detectLlm: ConsolidationLlm = async (system, user) => {
|
||||
const r = await detectClient.chat({ system, user, jsonMode: true, maxTokens: 1200 });
|
||||
detectCost += r.costUsd;
|
||||
return r.text;
|
||||
};
|
||||
|
||||
const perQuestion: BeamQuestionResult[] = [];
|
||||
const answerPromptToks: number[] = [];
|
||||
let answerCost = 0, judgeCost = 0, budgetStopped = false;
|
||||
let beliefNonEmpty = 0, beliefEligible = 0;
|
||||
let repairCost = 0, repairChanged = 0, repairAbstainToAnswer = 0, repairAnswerToAbstain = 0;
|
||||
const ABSTAIN_SENTINEL = "I don't have enough information to answer this question.";
|
||||
const isAbstain = (s: string) => s.trim().toLowerCase().startsWith("i don't have enough information");
|
||||
const outStream = fs.createWriteStream(outPath, { flags: 'a' });
|
||||
|
||||
const convs = args.convs.filter(c => isIngested(args.rawMindsDir, c));
|
||||
console.log(`[multiroute] answer=${args.model} judge=${args.judgeModel} detect=${args.detectModel} route=${JSON.stringify(args.route)} belief=${args.noBelief ? 'OFF' : [...args.beliefAbilities].join('+')} kBelief=${args.kBelief} budget=$${args.budget} convs=${convs.length}${args.instanceIds ? ` allowlist=${args.instanceIds.size}` : ''}`);
|
||||
|
||||
for (const conv of convs) {
|
||||
if (budgetStopped) break;
|
||||
const questions = loadConvQuestions(args.beamChats, conv)
|
||||
.filter(q => !doneIds.has(q.instanceId)
|
||||
&& (!args.instanceIds || args.instanceIds.has(q.instanceId))
|
||||
&& (!args.abilities || args.abilities.has(q.memoryAbility)));
|
||||
if (questions.length === 0) continue;
|
||||
|
||||
const rawSub = createSubstrate({ dbPath: mindPath(args.rawMindsDir, conv), embedder });
|
||||
const obsAvailable = !args.noBelief && isIngested(args.obsMindsDir, conv);
|
||||
const obsSub = obsAvailable ? createSubstrate({ dbPath: mindPath(args.obsMindsDir, conv), embedder }) : null;
|
||||
const dateMap = buildConvDateMap(chatJsonPath(args.beamChats, conv));
|
||||
try {
|
||||
for (const q of questions) {
|
||||
const spent = answerCost + judgeCost + detectCost;
|
||||
if (spent >= args.budget) { budgetStopped = true; console.warn(`[multiroute] budget $${args.budget} hit ($${spent.toFixed(2)})`); break; }
|
||||
|
||||
// 1) multi-route dated context.
|
||||
const ctx = await buildMultiRouteContext(rawSub, q.gopId, q.question, dateMap, args.route);
|
||||
|
||||
// 2) ability-gated belief block.
|
||||
let bel: BeliefBlock = { block: null, nChains: 0, nGroups: 0 };
|
||||
const beliefGated = !args.noBelief && args.beliefAbilities.has(q.memoryAbility);
|
||||
if (beliefGated && obsSub) {
|
||||
beliefEligible++;
|
||||
bel = await buildBeliefBlock(obsSub, q.gopId, q.question, detectLlm, args.kBelief);
|
||||
if (bel.block) beliefNonEmpty++;
|
||||
}
|
||||
|
||||
// 3) answer (draft).
|
||||
const prompt = buildAnswerGenerationPromptV2(q.question, ctx.displayStrings, undefined, bel.block ?? undefined);
|
||||
answerPromptToks.push(approxTokens(prompt));
|
||||
const ans = await answerClient.chat({ system: '', user: prompt, maxTokens: 4096 });
|
||||
answerCost += ans.costUsd;
|
||||
const draftAnswer = stripAns(ans.text);
|
||||
|
||||
// 3b) repair pass (E5 self-correction, gold-blind). Same model, same
|
||||
// context; the repaired answer is what gets judged. Draft kept below.
|
||||
let answer = draftAnswer;
|
||||
if (args.repair) {
|
||||
const repairPrompt = buildRepairPrompt(q.question, ctx.displayStrings, draftAnswer, undefined, bel.block ?? undefined);
|
||||
// Repair asks for EXHAUSTIVE coverage → longer output than the draft, and
|
||||
// Sonnet-via-OpenRouter returns an empty HTTP-200 completion when its budget
|
||||
// is exhausted (this client treats non-gpt5 as non-reasoning, no auto-expand).
|
||||
// Give the repair pass more headroom so summaries don't come back empty.
|
||||
const rep = await answerClient.chat({ system: '', user: repairPrompt, maxTokens: 8192 });
|
||||
repairCost += rep.costUsd;
|
||||
const repaired = stripAns(rep.text);
|
||||
if (repaired) answer = repaired; // empty repair → keep the draft (safe fallback)
|
||||
if (answer !== draftAnswer) repairChanged++;
|
||||
const dAbs = isAbstain(draftAnswer), rAbs = isAbstain(answer);
|
||||
if (dAbs && !rAbs) repairAbstainToAnswer++;
|
||||
if (!dAbs && rAbs) repairAnswerToAbstain++;
|
||||
}
|
||||
|
||||
// 4) judge (canonical) — on the repaired answer when --repair, else draft.
|
||||
const { judgement, llmResults } = await judgeQuestion(
|
||||
judgeClient,
|
||||
{ question: q.question, rubric: q.rubric, memoryAbility: q.memoryAbility, answer },
|
||||
{},
|
||||
);
|
||||
for (const r of llmResults) judgeCost += r.costUsd;
|
||||
|
||||
perQuestion.push({ instanceId: q.instanceId, memoryAbility: q.memoryAbility, score: judgement.score, ...(judgement.error ? { error: judgement.error } : {}) });
|
||||
outStream.write(JSON.stringify({
|
||||
instance_id: q.instanceId, conv, memory_ability: q.memoryAbility, question: q.question,
|
||||
answer, score: judgement.score, judgment: judgement.judgment, nugget_scores: judgement.nuggetScores,
|
||||
n_nuggets: q.rubric.length, cell: 'multiroute', prompt: 'v2',
|
||||
route: args.route, entities: ctx.entities, date_window: ctx.dateWindow ? ctx.dateWindow.label : null,
|
||||
n_vec: ctx.nVec, n_timeline: ctx.nTimeline, n_entity: ctx.nEntity, n_fused: ctx.nFused, n_dated: ctx.nDated,
|
||||
belief_gated: beliefGated, belief_used: !!bel.block, belief_chains: bel.nChains, belief_groups: bel.nGroups,
|
||||
answer_model: args.model, judge_model: args.judgeModel, detect_model: args.detectModel,
|
||||
...(bel.block ? { belief_block: bel.block } : {}),
|
||||
repair: args.repair, ...(args.repair ? { draft_answer: draftAnswer, repair_changed: answer !== draftAnswer } : {}),
|
||||
}) + '\n');
|
||||
const flag = bel.block ? `bel(${bel.nChains}c/${bel.nGroups}g)` : (beliefGated ? 'bel(—)' : 'bel(gate)');
|
||||
const repFlag = args.repair ? (answer !== draftAnswer ? ' rep✎' : ' rep=') : '';
|
||||
process.stdout.write(` [conv ${conv}] ${q.memoryAbility.padEnd(24)} E=${String(ctx.nEntity).padStart(2)} fuse=${String(ctx.nFused).padStart(2)} ${flag.padEnd(11)}${repFlag} score=${judgement.score.toFixed(2)} $${(answerCost + repairCost + judgeCost + detectCost).toFixed(3)}\n`);
|
||||
}
|
||||
} finally {
|
||||
rawSub.close();
|
||||
if (obsSub) obsSub.close();
|
||||
}
|
||||
}
|
||||
outStream.end();
|
||||
|
||||
const metrics = computeBeamMetrics(perQuestion);
|
||||
const meanTok = answerPromptToks.length ? Math.round(answerPromptToks.reduce((s, x) => s + x, 0) / answerPromptToks.length) : 0;
|
||||
const totalCost = answerCost + repairCost + judgeCost + detectCost;
|
||||
const summaryPath = outPath.replace(/\.jsonl$/, '.summary.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({
|
||||
run: {
|
||||
cell: 'multiroute', dataset: 'beam-1m', answer_model: args.model, judge_model: args.judgeModel,
|
||||
detect_model: args.detectModel, prompt: 'v2', route: args.route, k_belief: args.kBelief,
|
||||
belief_abilities: args.noBelief ? [] : [...args.beliefAbilities],
|
||||
minds_dir: 'minds-1M (answer) + minds-1M-obs (belief)',
|
||||
mean_answer_prompt_tokens: meanTok,
|
||||
belief_eligible: beliefEligible, belief_nonempty: beliefNonEmpty, answered_now: perQuestion.length,
|
||||
repair: args.repair,
|
||||
...(args.repair ? { repair_changed: repairChanged, repair_abstain_to_answer: repairAbstainToAnswer, repair_answer_to_abstain: repairAnswerToAbstain } : {}),
|
||||
budgetStopped,
|
||||
},
|
||||
metrics: { overall_avg_score: metrics.overall.avgScore, overall_pass_rate_pct: metrics.overall.accuracy, by_ability: metrics.byAbility },
|
||||
cost: { total_usd: totalCost, answer_usd: answerCost, repair_usd: repairCost, judge_usd: judgeCost, detect_usd: detectCost },
|
||||
}, null, 2) + '\n', 'utf-8');
|
||||
|
||||
console.log('\n════════ BEAM 1M — multiroute ════════');
|
||||
console.log(formatBeamMetrics(metrics));
|
||||
console.log(`belief eligible ${beliefEligible}, non-empty ${beliefNonEmpty}`);
|
||||
if (args.repair) console.log(`repair: ON — changed ${repairChanged}/${perQuestion.length}, abstain→answer ${repairAbstainToAnswer}, answer→abstain ${repairAnswerToAbstain}`);
|
||||
console.log(`cost=$${totalCost.toFixed(4)} (answer=$${answerCost.toFixed(3)}${args.repair ? ` repair=$${repairCost.toFixed(3)}` : ''} judge=$${judgeCost.toFixed(3)} detect=$${detectCost.toFixed(3)}) answered_now=${perQuestion.length} budgetStopped=${budgetStopped}`);
|
||||
console.log(`jsonl: ${outPath}`);
|
||||
console.log(`summary: ${summaryPath}`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs();
|
||||
if (args.smoke && args.repair) { await repairSmoke(args); return; }
|
||||
if (args.smoke) { await smoke(args); return; }
|
||||
await run(args);
|
||||
}
|
||||
|
||||
main().catch(err => { console.error('[beam-run-multiroute] FATAL:', err); process.exit(1); });
|
||||
137
benchmarks/harness/scripts/e1-aggregate.ts
Normal file
137
benchmarks/harness/scripts/e1-aggregate.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E1 aggregation — Eywa answers @ OUR gpt-5 judge vs Eywa self-judge.
|
||||
* Reads e1-eywa-ourjudge.jsonl + the original Eywa JSON, emits console + markdown.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
|
||||
const EYWA_JSON =
|
||||
'D:/Projects/KorroResearch/benchmarks/eywa-artifacts/eywa-beam-sonnet46-answers.json';
|
||||
const OUT_JSONL =
|
||||
'D:/Projects/KorroResearch/benchmarks/eywa-artifacts/e1-eywa-ourjudge.jsonl';
|
||||
|
||||
interface EywaRecord { id: string; cat: string; eywaNugget: number[]; }
|
||||
interface OutRecord {
|
||||
id: string; cat: string; our_raw_score: number;
|
||||
our_nugget_scores: number[]; eywa_raw: number;
|
||||
}
|
||||
|
||||
const OUR_OWN_HEADLINE = 0.6482; // our own answers @ our gpt-5 judge
|
||||
|
||||
function mean(xs: number[]): number { return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0; }
|
||||
function pct(x: number): string { return (x * 100).toFixed(2) + '%'; }
|
||||
|
||||
const eywaRecords = JSON.parse(fs.readFileSync(EYWA_JSON, 'utf-8')) as EywaRecord[];
|
||||
const eywaById = new Map(eywaRecords.map(r => [r.id, r]));
|
||||
|
||||
const out: OutRecord[] = [];
|
||||
for (const line of fs.readFileSync(OUT_JSONL, 'utf-8').split('\n')) {
|
||||
const t = line.trim(); if (!t) continue;
|
||||
out.push(JSON.parse(t) as OutRecord);
|
||||
}
|
||||
// dedup by id (last wins)
|
||||
const outById = new Map(out.map(r => [r.id, r]));
|
||||
const rows = [...outById.values()];
|
||||
|
||||
console.log(`[agg] scored records: ${rows.length}/700`);
|
||||
|
||||
// Overall
|
||||
const ourOverall = mean(rows.map(r => r.our_raw_score));
|
||||
const eywaOverall = mean(rows.map(r => r.eywa_raw));
|
||||
|
||||
// Per-category
|
||||
const cats = [...new Set(rows.map(r => r.cat))].sort();
|
||||
interface CatStat { cat: string; n: number; our: number; eywa: number; delta: number; }
|
||||
const catStats: CatStat[] = cats.map(cat => {
|
||||
const rs = rows.filter(r => r.cat === cat);
|
||||
const our = mean(rs.map(r => r.our_raw_score));
|
||||
const eywa = mean(rs.map(r => r.eywa_raw));
|
||||
return { cat, n: rs.length, our, eywa, delta: our - eywa };
|
||||
});
|
||||
|
||||
// Per-nugget agreement (align our_nugget_scores to eywaNugget by index)
|
||||
let nTotal = 0, nOurLess = 0, nOurMore = 0, nEqual = 0;
|
||||
let sumOur = 0, sumEywa = 0;
|
||||
for (const r of rows) {
|
||||
const e = eywaById.get(r.id);
|
||||
if (!e) continue;
|
||||
const our = r.our_nugget_scores;
|
||||
const eyw = e.eywaNugget;
|
||||
const k = Math.min(our.length, eyw.length);
|
||||
for (let i = 0; i < k; i++) {
|
||||
nTotal++;
|
||||
sumOur += our[i]; sumEywa += eyw[i];
|
||||
if (our[i] < eyw[i]) nOurLess++;
|
||||
else if (our[i] > eyw[i]) nOurMore++;
|
||||
else nEqual++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Console report ──
|
||||
console.log('\n=== OVERALL ===');
|
||||
console.log(`Eywa answers @ OUR gpt-5 judge : ${pct(ourOverall)}`);
|
||||
console.log(`Eywa self-judge (Sonnet 4.6) : ${pct(eywaOverall)} (published headline 81.45%)`);
|
||||
console.log(`Self-judge inflation delta : ${((eywaOverall - ourOverall) * 100).toFixed(2)} pp`);
|
||||
console.log(`\nHONEST SAME-JUDGE LEADERBOARD (gpt-5 nugget judge):`);
|
||||
console.log(` Eywa answers @ gpt-5 judge : ${pct(ourOverall)}`);
|
||||
console.log(` OUR answers @ gpt-5 judge : ${pct(OUR_OWN_HEADLINE)}`);
|
||||
console.log(` Eywa - ours = ${((ourOverall - OUR_OWN_HEADLINE) * 100).toFixed(2)} pp`);
|
||||
|
||||
console.log('\n=== PER CATEGORY ===');
|
||||
console.log('cat'.padEnd(28), 'n'.padStart(4), 'ourJ%'.padStart(8), 'eywaSelf%'.padStart(10), 'delta_pp'.padStart(9));
|
||||
for (const c of catStats.sort((a, b) => a.delta - b.delta)) {
|
||||
console.log(
|
||||
c.cat.padEnd(28), String(c.n).padStart(4),
|
||||
pct(c.our).padStart(8), pct(c.eywa).padStart(10),
|
||||
((c.delta) * 100).toFixed(2).padStart(9),
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n=== PER-NUGGET AGREEMENT ===');
|
||||
console.log(`total nuggets compared: ${nTotal}`);
|
||||
console.log(`our < eywa (we stricter): ${nOurLess} (${pct(nOurLess / nTotal)})`);
|
||||
console.log(`our > eywa (we lenient) : ${nOurMore} (${pct(nOurMore / nTotal)})`);
|
||||
console.log(`equal : ${nEqual} (${pct(nEqual / nTotal)})`);
|
||||
console.log(`mean(our) - mean(eywa) per nugget: ${((sumOur - sumEywa) / nTotal).toFixed(4)}`);
|
||||
|
||||
// ── Markdown ──
|
||||
const md: string[] = [];
|
||||
md.push('# E1 — Eywa BEAM answers re-judged with OUR canonical gpt-5 nugget judge\n');
|
||||
md.push(`_Judge: gpt-5 (model id \`gpt-5\`), verbatim mem0 graded nugget prompt from \`src/beam-nugget-judge.ts\` — the SAME judge behind our published 0.6482 headline. Per-question score = mean of 0/0.5/1 nugget scores._\n`);
|
||||
md.push(`_Scored ${rows.length}/700 Eywa Sonnet-4.6 answers over ${nTotal} rubric nuggets._\n`);
|
||||
md.push('## Headline\n');
|
||||
md.push('| Metric | Value |');
|
||||
md.push('|---|---|');
|
||||
md.push(`| Eywa answers @ **our gpt-5 judge** | **${pct(ourOverall)}** |`);
|
||||
md.push(`| Eywa self-judge (Sonnet 4.6), our recompute | ${pct(eywaOverall)} |`);
|
||||
md.push(`| Eywa published headline | 81.45% |`);
|
||||
md.push(`| **Self-judge inflation** (eywa-self − our-judge) | **${((eywaOverall - ourOverall) * 100).toFixed(2)} pp** |`);
|
||||
md.push('');
|
||||
md.push('## Honest same-judge leaderboard (both @ gpt-5 nugget judge)\n');
|
||||
md.push('| System | Answers @ gpt-5 judge |');
|
||||
md.push('|---|---|');
|
||||
md.push(`| Eywa (Sonnet 4.6 answers) | **${pct(ourOverall)}** |`);
|
||||
md.push(`| Ours (published) | ${pct(OUR_OWN_HEADLINE)} |`);
|
||||
md.push(`| Gap (Eywa − ours) | ${((ourOverall - OUR_OWN_HEADLINE) * 100).toFixed(2)} pp |`);
|
||||
md.push('');
|
||||
md.push('## Per-category (n=70 each)\n');
|
||||
md.push('| Category | n | Our gpt-5 judge % | Eywa self-judge % | Delta (pp) |');
|
||||
md.push('|---|---|---|---|---|');
|
||||
for (const c of catStats.sort((a, b) => a.delta - b.delta)) {
|
||||
md.push(`| ${c.cat} | ${c.n} | ${pct(c.our)} | ${pct(c.eywa)} | ${(c.delta * 100).toFixed(2)} |`);
|
||||
}
|
||||
md.push('');
|
||||
md.push('_Delta = our-judge − eywa-self-judge. Large negative delta = Eywa self-judge inflated that category. Small delta = self-judge was honest there._\n');
|
||||
md.push('## Per-nugget judge agreement\n');
|
||||
md.push(`- Total nuggets compared: ${nTotal}`);
|
||||
md.push(`- Our score **< **Eywa (we stricter / leniency in their favor): ${nOurLess} (${pct(nOurLess / nTotal)})`);
|
||||
md.push(`- Our score **>** Eywa (we more lenient): ${nOurMore} (${pct(nOurMore / nTotal)})`);
|
||||
md.push(`- Equal: ${nEqual} (${pct(nEqual / nTotal)})`);
|
||||
md.push(`- mean(our) − mean(eywa) per nugget: **${((sumOur - sumEywa) / nTotal).toFixed(4)}**`);
|
||||
md.push('');
|
||||
|
||||
fs.writeFileSync(
|
||||
'D:/Projects/KorroResearch/benchmarks/eywa-artifacts/E1-eywa-answers-ourjudge.md',
|
||||
md.join('\n'),
|
||||
);
|
||||
console.log('\n[agg] wrote E1-eywa-answers-ourjudge.md');
|
||||
143
benchmarks/harness/scripts/e1-judge-eywa.ts
Normal file
143
benchmarks/harness/scripts/e1-judge-eywa.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* E1 — Re-judge Eywa's published BEAM answers with OUR canonical gpt-5 nugget judge.
|
||||
*
|
||||
* Loads Eywa's 700 Sonnet-4.6 answers (same 700 BEAM-1M questions/rubrics as ours)
|
||||
* and re-scores each with the IDENTICAL graded nugget judge behind our published
|
||||
* headline (0.6482): src/beam-nugget-judge.ts `judgeSingleNugget` over gpt-5.
|
||||
*
|
||||
* Per-question score = arithmetic mean of nugget scores (0/0.5/1) — exactly the
|
||||
* mem0 headline metric. No tau blend (auxiliary diagnostic only, not headline).
|
||||
*
|
||||
* Resumable: appends JSONL, skips ids already scored.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/e1-judge-eywa.ts --limit 5 # smoke
|
||||
* tsx scripts/e1-judge-eywa.ts # full 700
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import { createBeamOpenAiClient } from '../src/beam-openai-client.js';
|
||||
import { judgeSingleNugget } from '../src/beam-nugget-judge.js';
|
||||
|
||||
const EYWA_JSON =
|
||||
'D:/Projects/KorroResearch/benchmarks/eywa-artifacts/eywa-beam-sonnet46-answers.json';
|
||||
const OUT_JSONL =
|
||||
'D:/Projects/KorroResearch/benchmarks/eywa-artifacts/e1-eywa-ourjudge.jsonl';
|
||||
|
||||
interface EywaRecord {
|
||||
id: string;
|
||||
di: number;
|
||||
cat: string;
|
||||
q: string;
|
||||
gold: string;
|
||||
rubric: string[];
|
||||
answer: string;
|
||||
eywaNugget: number[];
|
||||
eywaJudgeScore: number;
|
||||
eywaVerdict: string;
|
||||
}
|
||||
|
||||
interface OutRecord {
|
||||
id: string;
|
||||
cat: string;
|
||||
our_raw_score: number;
|
||||
our_nugget_scores: number[];
|
||||
eywa_raw: number;
|
||||
our_nugget_reasons: string[];
|
||||
}
|
||||
|
||||
function mean(xs: number[]): number {
|
||||
return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const limIdx = args.indexOf('--limit');
|
||||
const limit = limIdx >= 0 ? Number(args[limIdx + 1]) : Infinity;
|
||||
|
||||
const records = JSON.parse(fs.readFileSync(EYWA_JSON, 'utf-8')) as EywaRecord[];
|
||||
console.log(`[e1] loaded ${records.length} Eywa records`);
|
||||
|
||||
const done = new Set<string>();
|
||||
if (fs.existsSync(OUT_JSONL)) {
|
||||
for (const line of fs.readFileSync(OUT_JSONL, 'utf-8').split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t) continue;
|
||||
try {
|
||||
const r = JSON.parse(t) as { id?: string };
|
||||
if (r.id) done.add(r.id);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
console.log(`[e1] resume: ${done.size} already scored`);
|
||||
}
|
||||
|
||||
const client = createBeamOpenAiClient({ model: 'gpt-5' });
|
||||
const out = fs.createWriteStream(OUT_JSONL, { flags: 'a' });
|
||||
|
||||
const concIdx = args.indexOf('--conc');
|
||||
const CONCURRENCY = concIdx >= 0 ? Number(args[concIdx + 1]) : 10;
|
||||
const ceilIdx = args.indexOf('--ceiling');
|
||||
const BUDGET_CEILING = ceilIdx >= 0 ? Number(args[ceilIdx + 1]) : 15; // hard stop guard
|
||||
|
||||
let costUsd = 0;
|
||||
let processed = 0;
|
||||
let stopped = false;
|
||||
const todo = records.filter(r => !done.has(r.id)).slice(0, limit === Infinity ? undefined : limit);
|
||||
console.log(`[e1] scoring ${todo.length} records with gpt-5 nugget judge (conc=${CONCURRENCY}, ceiling=$${BUDGET_CEILING})`);
|
||||
|
||||
async function scoreRecord(rec: EywaRecord): Promise<void> {
|
||||
const nuggetScores: number[] = [];
|
||||
const nuggetReasons: string[] = [];
|
||||
for (const nugget of rec.rubric) {
|
||||
const ns = await judgeSingleNugget(client, rec.q, nugget, rec.answer);
|
||||
costUsd += ns.result.costUsd;
|
||||
nuggetScores.push(ns.score);
|
||||
nuggetReasons.push(ns.reason);
|
||||
}
|
||||
const ourRaw = mean(nuggetScores);
|
||||
const eywaRaw = mean(rec.eywaNugget);
|
||||
const outRec: OutRecord = {
|
||||
id: rec.id,
|
||||
cat: rec.cat,
|
||||
our_raw_score: Math.round(ourRaw * 1e4) / 1e4,
|
||||
our_nugget_scores: nuggetScores,
|
||||
eywa_raw: Math.round(eywaRaw * 1e4) / 1e4,
|
||||
our_nugget_reasons: nuggetReasons,
|
||||
};
|
||||
out.write(JSON.stringify(outRec) + '\n');
|
||||
processed++;
|
||||
if (processed <= 5 || processed % 25 === 0) {
|
||||
console.log(
|
||||
`[e1] ${processed}/${todo.length} ${rec.id} ${rec.cat} ` +
|
||||
`our=${ourRaw.toFixed(3)} eywa=${eywaRaw.toFixed(3)} $${costUsd.toFixed(3)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Simple concurrency pool over the todo queue.
|
||||
let cursor = 0;
|
||||
async function worker(): Promise<void> {
|
||||
while (cursor < todo.length && !stopped) {
|
||||
if (costUsd >= BUDGET_CEILING) {
|
||||
stopped = true;
|
||||
console.error(`[e1] BUDGET CEILING $${BUDGET_CEILING} hit at $${costUsd.toFixed(3)} — stopping.`);
|
||||
break;
|
||||
}
|
||||
const idx = cursor++;
|
||||
await scoreRecord(todo[idx]);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
|
||||
|
||||
out.end();
|
||||
console.log(`[e1] DONE processed=${processed}/${todo.length} cost=$${costUsd.toFixed(4)} stopped=${stopped}`);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('[e1] FATAL', err);
|
||||
process.exit(1);
|
||||
});
|
||||
140
benchmarks/harness/scripts/e7-gate140.txt
Normal file
140
benchmarks/harness/scripts/e7-gate140.txt
Normal file
@@ -0,0 +1,140 @@
|
||||
beam_1M_1_abstention_q0
|
||||
beam_1M_1_abstention_q1
|
||||
beam_1M_1_contradiction_resolution_q0
|
||||
beam_1M_1_contradiction_resolution_q1
|
||||
beam_1M_1_event_ordering_q0
|
||||
beam_1M_1_event_ordering_q1
|
||||
beam_1M_1_information_extraction_q0
|
||||
beam_1M_1_information_extraction_q1
|
||||
beam_1M_1_instruction_following_q0
|
||||
beam_1M_1_instruction_following_q1
|
||||
beam_1M_1_knowledge_update_q0
|
||||
beam_1M_1_knowledge_update_q1
|
||||
beam_1M_1_multi_session_reasoning_q0
|
||||
beam_1M_1_multi_session_reasoning_q1
|
||||
beam_1M_1_preference_following_q0
|
||||
beam_1M_1_preference_following_q1
|
||||
beam_1M_1_summarization_q0
|
||||
beam_1M_1_summarization_q1
|
||||
beam_1M_1_temporal_reasoning_q0
|
||||
beam_1M_1_temporal_reasoning_q1
|
||||
beam_1M_9_abstention_q0
|
||||
beam_1M_9_abstention_q1
|
||||
beam_1M_9_contradiction_resolution_q0
|
||||
beam_1M_9_contradiction_resolution_q1
|
||||
beam_1M_9_event_ordering_q0
|
||||
beam_1M_9_event_ordering_q1
|
||||
beam_1M_9_information_extraction_q0
|
||||
beam_1M_9_information_extraction_q1
|
||||
beam_1M_9_instruction_following_q0
|
||||
beam_1M_9_instruction_following_q1
|
||||
beam_1M_9_knowledge_update_q0
|
||||
beam_1M_9_knowledge_update_q1
|
||||
beam_1M_9_multi_session_reasoning_q0
|
||||
beam_1M_9_multi_session_reasoning_q1
|
||||
beam_1M_9_preference_following_q0
|
||||
beam_1M_9_preference_following_q1
|
||||
beam_1M_9_summarization_q0
|
||||
beam_1M_9_summarization_q1
|
||||
beam_1M_9_temporal_reasoning_q0
|
||||
beam_1M_9_temporal_reasoning_q1
|
||||
beam_1M_10_abstention_q0
|
||||
beam_1M_10_abstention_q1
|
||||
beam_1M_10_contradiction_resolution_q0
|
||||
beam_1M_10_contradiction_resolution_q1
|
||||
beam_1M_10_event_ordering_q0
|
||||
beam_1M_10_event_ordering_q1
|
||||
beam_1M_10_information_extraction_q0
|
||||
beam_1M_10_information_extraction_q1
|
||||
beam_1M_10_instruction_following_q0
|
||||
beam_1M_10_instruction_following_q1
|
||||
beam_1M_10_knowledge_update_q0
|
||||
beam_1M_10_knowledge_update_q1
|
||||
beam_1M_10_multi_session_reasoning_q0
|
||||
beam_1M_10_multi_session_reasoning_q1
|
||||
beam_1M_10_preference_following_q0
|
||||
beam_1M_10_preference_following_q1
|
||||
beam_1M_10_summarization_q0
|
||||
beam_1M_10_summarization_q1
|
||||
beam_1M_10_temporal_reasoning_q0
|
||||
beam_1M_10_temporal_reasoning_q1
|
||||
beam_1M_13_abstention_q0
|
||||
beam_1M_13_abstention_q1
|
||||
beam_1M_13_contradiction_resolution_q0
|
||||
beam_1M_13_contradiction_resolution_q1
|
||||
beam_1M_13_event_ordering_q0
|
||||
beam_1M_13_event_ordering_q1
|
||||
beam_1M_13_information_extraction_q0
|
||||
beam_1M_13_information_extraction_q1
|
||||
beam_1M_13_instruction_following_q0
|
||||
beam_1M_13_instruction_following_q1
|
||||
beam_1M_13_knowledge_update_q0
|
||||
beam_1M_13_knowledge_update_q1
|
||||
beam_1M_13_multi_session_reasoning_q0
|
||||
beam_1M_13_multi_session_reasoning_q1
|
||||
beam_1M_13_preference_following_q0
|
||||
beam_1M_13_preference_following_q1
|
||||
beam_1M_13_summarization_q0
|
||||
beam_1M_13_summarization_q1
|
||||
beam_1M_13_temporal_reasoning_q0
|
||||
beam_1M_13_temporal_reasoning_q1
|
||||
beam_1M_15_abstention_q0
|
||||
beam_1M_15_abstention_q1
|
||||
beam_1M_15_contradiction_resolution_q0
|
||||
beam_1M_15_contradiction_resolution_q1
|
||||
beam_1M_15_event_ordering_q0
|
||||
beam_1M_15_event_ordering_q1
|
||||
beam_1M_15_information_extraction_q0
|
||||
beam_1M_15_information_extraction_q1
|
||||
beam_1M_15_instruction_following_q0
|
||||
beam_1M_15_instruction_following_q1
|
||||
beam_1M_15_knowledge_update_q0
|
||||
beam_1M_15_knowledge_update_q1
|
||||
beam_1M_15_multi_session_reasoning_q0
|
||||
beam_1M_15_multi_session_reasoning_q1
|
||||
beam_1M_15_preference_following_q0
|
||||
beam_1M_15_preference_following_q1
|
||||
beam_1M_15_summarization_q0
|
||||
beam_1M_15_summarization_q1
|
||||
beam_1M_15_temporal_reasoning_q0
|
||||
beam_1M_15_temporal_reasoning_q1
|
||||
beam_1M_23_abstention_q0
|
||||
beam_1M_23_abstention_q1
|
||||
beam_1M_23_contradiction_resolution_q0
|
||||
beam_1M_23_contradiction_resolution_q1
|
||||
beam_1M_23_event_ordering_q0
|
||||
beam_1M_23_event_ordering_q1
|
||||
beam_1M_23_information_extraction_q0
|
||||
beam_1M_23_information_extraction_q1
|
||||
beam_1M_23_instruction_following_q0
|
||||
beam_1M_23_instruction_following_q1
|
||||
beam_1M_23_knowledge_update_q0
|
||||
beam_1M_23_knowledge_update_q1
|
||||
beam_1M_23_multi_session_reasoning_q0
|
||||
beam_1M_23_multi_session_reasoning_q1
|
||||
beam_1M_23_preference_following_q0
|
||||
beam_1M_23_preference_following_q1
|
||||
beam_1M_23_summarization_q0
|
||||
beam_1M_23_summarization_q1
|
||||
beam_1M_23_temporal_reasoning_q0
|
||||
beam_1M_23_temporal_reasoning_q1
|
||||
beam_1M_27_abstention_q0
|
||||
beam_1M_27_abstention_q1
|
||||
beam_1M_27_contradiction_resolution_q0
|
||||
beam_1M_27_contradiction_resolution_q1
|
||||
beam_1M_27_event_ordering_q0
|
||||
beam_1M_27_event_ordering_q1
|
||||
beam_1M_27_information_extraction_q0
|
||||
beam_1M_27_information_extraction_q1
|
||||
beam_1M_27_instruction_following_q0
|
||||
beam_1M_27_instruction_following_q1
|
||||
beam_1M_27_knowledge_update_q0
|
||||
beam_1M_27_knowledge_update_q1
|
||||
beam_1M_27_multi_session_reasoning_q0
|
||||
beam_1M_27_multi_session_reasoning_q1
|
||||
beam_1M_27_preference_following_q0
|
||||
beam_1M_27_preference_following_q1
|
||||
beam_1M_27_summarization_q0
|
||||
beam_1M_27_summarization_q1
|
||||
beam_1M_27_temporal_reasoning_q0
|
||||
beam_1M_27_temporal_reasoning_q1
|
||||
42
benchmarks/harness/scripts/e7-gate42.txt
Normal file
42
benchmarks/harness/scripts/e7-gate42.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
beam_1M_1_temporal_reasoning_q0
|
||||
beam_1M_1_temporal_reasoning_q1
|
||||
beam_1M_1_multi_session_reasoning_q0
|
||||
beam_1M_1_multi_session_reasoning_q1
|
||||
beam_1M_1_event_ordering_q0
|
||||
beam_1M_1_event_ordering_q1
|
||||
beam_1M_9_temporal_reasoning_q0
|
||||
beam_1M_9_temporal_reasoning_q1
|
||||
beam_1M_9_multi_session_reasoning_q0
|
||||
beam_1M_9_multi_session_reasoning_q1
|
||||
beam_1M_9_event_ordering_q0
|
||||
beam_1M_9_event_ordering_q1
|
||||
beam_1M_10_temporal_reasoning_q0
|
||||
beam_1M_10_temporal_reasoning_q1
|
||||
beam_1M_10_multi_session_reasoning_q0
|
||||
beam_1M_10_multi_session_reasoning_q1
|
||||
beam_1M_10_event_ordering_q0
|
||||
beam_1M_10_event_ordering_q1
|
||||
beam_1M_13_temporal_reasoning_q0
|
||||
beam_1M_13_temporal_reasoning_q1
|
||||
beam_1M_13_multi_session_reasoning_q0
|
||||
beam_1M_13_multi_session_reasoning_q1
|
||||
beam_1M_13_event_ordering_q0
|
||||
beam_1M_13_event_ordering_q1
|
||||
beam_1M_15_temporal_reasoning_q0
|
||||
beam_1M_15_temporal_reasoning_q1
|
||||
beam_1M_15_multi_session_reasoning_q0
|
||||
beam_1M_15_multi_session_reasoning_q1
|
||||
beam_1M_15_event_ordering_q0
|
||||
beam_1M_15_event_ordering_q1
|
||||
beam_1M_23_temporal_reasoning_q0
|
||||
beam_1M_23_temporal_reasoning_q1
|
||||
beam_1M_23_multi_session_reasoning_q0
|
||||
beam_1M_23_multi_session_reasoning_q1
|
||||
beam_1M_23_event_ordering_q0
|
||||
beam_1M_23_event_ordering_q1
|
||||
beam_1M_27_temporal_reasoning_q0
|
||||
beam_1M_27_temporal_reasoning_q1
|
||||
beam_1M_27_multi_session_reasoning_q0
|
||||
beam_1M_27_multi_session_reasoning_q1
|
||||
beam_1M_27_event_ordering_q0
|
||||
beam_1M_27_event_ordering_q1
|
||||
140
benchmarks/harness/scripts/e8-heldout140.txt
Normal file
140
benchmarks/harness/scripts/e8-heldout140.txt
Normal file
@@ -0,0 +1,140 @@
|
||||
beam_1M_4_abstention_q0
|
||||
beam_1M_4_abstention_q1
|
||||
beam_1M_4_contradiction_resolution_q0
|
||||
beam_1M_4_contradiction_resolution_q1
|
||||
beam_1M_4_event_ordering_q0
|
||||
beam_1M_4_event_ordering_q1
|
||||
beam_1M_4_information_extraction_q0
|
||||
beam_1M_4_information_extraction_q1
|
||||
beam_1M_4_instruction_following_q0
|
||||
beam_1M_4_instruction_following_q1
|
||||
beam_1M_4_knowledge_update_q0
|
||||
beam_1M_4_knowledge_update_q1
|
||||
beam_1M_4_multi_session_reasoning_q0
|
||||
beam_1M_4_multi_session_reasoning_q1
|
||||
beam_1M_4_preference_following_q0
|
||||
beam_1M_4_preference_following_q1
|
||||
beam_1M_4_summarization_q0
|
||||
beam_1M_4_summarization_q1
|
||||
beam_1M_4_temporal_reasoning_q0
|
||||
beam_1M_4_temporal_reasoning_q1
|
||||
beam_1M_5_abstention_q0
|
||||
beam_1M_5_abstention_q1
|
||||
beam_1M_5_contradiction_resolution_q0
|
||||
beam_1M_5_contradiction_resolution_q1
|
||||
beam_1M_5_event_ordering_q0
|
||||
beam_1M_5_event_ordering_q1
|
||||
beam_1M_5_information_extraction_q0
|
||||
beam_1M_5_information_extraction_q1
|
||||
beam_1M_5_instruction_following_q0
|
||||
beam_1M_5_instruction_following_q1
|
||||
beam_1M_5_knowledge_update_q0
|
||||
beam_1M_5_knowledge_update_q1
|
||||
beam_1M_5_multi_session_reasoning_q0
|
||||
beam_1M_5_multi_session_reasoning_q1
|
||||
beam_1M_5_preference_following_q0
|
||||
beam_1M_5_preference_following_q1
|
||||
beam_1M_5_summarization_q0
|
||||
beam_1M_5_summarization_q1
|
||||
beam_1M_5_temporal_reasoning_q0
|
||||
beam_1M_5_temporal_reasoning_q1
|
||||
beam_1M_11_abstention_q0
|
||||
beam_1M_11_abstention_q1
|
||||
beam_1M_11_contradiction_resolution_q0
|
||||
beam_1M_11_contradiction_resolution_q1
|
||||
beam_1M_11_event_ordering_q0
|
||||
beam_1M_11_event_ordering_q1
|
||||
beam_1M_11_information_extraction_q0
|
||||
beam_1M_11_information_extraction_q1
|
||||
beam_1M_11_instruction_following_q0
|
||||
beam_1M_11_instruction_following_q1
|
||||
beam_1M_11_knowledge_update_q0
|
||||
beam_1M_11_knowledge_update_q1
|
||||
beam_1M_11_multi_session_reasoning_q0
|
||||
beam_1M_11_multi_session_reasoning_q1
|
||||
beam_1M_11_preference_following_q0
|
||||
beam_1M_11_preference_following_q1
|
||||
beam_1M_11_summarization_q0
|
||||
beam_1M_11_summarization_q1
|
||||
beam_1M_11_temporal_reasoning_q0
|
||||
beam_1M_11_temporal_reasoning_q1
|
||||
beam_1M_14_abstention_q0
|
||||
beam_1M_14_abstention_q1
|
||||
beam_1M_14_contradiction_resolution_q0
|
||||
beam_1M_14_contradiction_resolution_q1
|
||||
beam_1M_14_event_ordering_q0
|
||||
beam_1M_14_event_ordering_q1
|
||||
beam_1M_14_information_extraction_q0
|
||||
beam_1M_14_information_extraction_q1
|
||||
beam_1M_14_instruction_following_q0
|
||||
beam_1M_14_instruction_following_q1
|
||||
beam_1M_14_knowledge_update_q0
|
||||
beam_1M_14_knowledge_update_q1
|
||||
beam_1M_14_multi_session_reasoning_q0
|
||||
beam_1M_14_multi_session_reasoning_q1
|
||||
beam_1M_14_preference_following_q0
|
||||
beam_1M_14_preference_following_q1
|
||||
beam_1M_14_summarization_q0
|
||||
beam_1M_14_summarization_q1
|
||||
beam_1M_14_temporal_reasoning_q0
|
||||
beam_1M_14_temporal_reasoning_q1
|
||||
beam_1M_17_abstention_q0
|
||||
beam_1M_17_abstention_q1
|
||||
beam_1M_17_contradiction_resolution_q0
|
||||
beam_1M_17_contradiction_resolution_q1
|
||||
beam_1M_17_event_ordering_q0
|
||||
beam_1M_17_event_ordering_q1
|
||||
beam_1M_17_information_extraction_q0
|
||||
beam_1M_17_information_extraction_q1
|
||||
beam_1M_17_instruction_following_q0
|
||||
beam_1M_17_instruction_following_q1
|
||||
beam_1M_17_knowledge_update_q0
|
||||
beam_1M_17_knowledge_update_q1
|
||||
beam_1M_17_multi_session_reasoning_q0
|
||||
beam_1M_17_multi_session_reasoning_q1
|
||||
beam_1M_17_preference_following_q0
|
||||
beam_1M_17_preference_following_q1
|
||||
beam_1M_17_summarization_q0
|
||||
beam_1M_17_summarization_q1
|
||||
beam_1M_17_temporal_reasoning_q0
|
||||
beam_1M_17_temporal_reasoning_q1
|
||||
beam_1M_18_abstention_q0
|
||||
beam_1M_18_abstention_q1
|
||||
beam_1M_18_contradiction_resolution_q0
|
||||
beam_1M_18_contradiction_resolution_q1
|
||||
beam_1M_18_event_ordering_q0
|
||||
beam_1M_18_event_ordering_q1
|
||||
beam_1M_18_information_extraction_q0
|
||||
beam_1M_18_information_extraction_q1
|
||||
beam_1M_18_instruction_following_q0
|
||||
beam_1M_18_instruction_following_q1
|
||||
beam_1M_18_knowledge_update_q0
|
||||
beam_1M_18_knowledge_update_q1
|
||||
beam_1M_18_multi_session_reasoning_q0
|
||||
beam_1M_18_multi_session_reasoning_q1
|
||||
beam_1M_18_preference_following_q0
|
||||
beam_1M_18_preference_following_q1
|
||||
beam_1M_18_summarization_q0
|
||||
beam_1M_18_summarization_q1
|
||||
beam_1M_18_temporal_reasoning_q0
|
||||
beam_1M_18_temporal_reasoning_q1
|
||||
beam_1M_28_abstention_q0
|
||||
beam_1M_28_abstention_q1
|
||||
beam_1M_28_contradiction_resolution_q0
|
||||
beam_1M_28_contradiction_resolution_q1
|
||||
beam_1M_28_event_ordering_q0
|
||||
beam_1M_28_event_ordering_q1
|
||||
beam_1M_28_information_extraction_q0
|
||||
beam_1M_28_information_extraction_q1
|
||||
beam_1M_28_instruction_following_q0
|
||||
beam_1M_28_instruction_following_q1
|
||||
beam_1M_28_knowledge_update_q0
|
||||
beam_1M_28_knowledge_update_q1
|
||||
beam_1M_28_multi_session_reasoning_q0
|
||||
beam_1M_28_multi_session_reasoning_q1
|
||||
beam_1M_28_preference_following_q0
|
||||
beam_1M_28_preference_following_q1
|
||||
beam_1M_28_summarization_q0
|
||||
beam_1M_28_summarization_q1
|
||||
beam_1M_28_temporal_reasoning_q0
|
||||
beam_1M_28_temporal_reasoning_q1
|
||||
28
benchmarks/harness/scripts/e8-probeA.txt
Normal file
28
benchmarks/harness/scripts/e8-probeA.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
beam_1M_1_temporal_reasoning_q0
|
||||
beam_1M_1_temporal_reasoning_q1
|
||||
beam_1M_1_multi_session_reasoning_q0
|
||||
beam_1M_1_multi_session_reasoning_q1
|
||||
beam_1M_9_temporal_reasoning_q0
|
||||
beam_1M_9_temporal_reasoning_q1
|
||||
beam_1M_9_multi_session_reasoning_q0
|
||||
beam_1M_9_multi_session_reasoning_q1
|
||||
beam_1M_10_temporal_reasoning_q0
|
||||
beam_1M_10_temporal_reasoning_q1
|
||||
beam_1M_10_multi_session_reasoning_q0
|
||||
beam_1M_10_multi_session_reasoning_q1
|
||||
beam_1M_13_temporal_reasoning_q0
|
||||
beam_1M_13_temporal_reasoning_q1
|
||||
beam_1M_13_multi_session_reasoning_q0
|
||||
beam_1M_13_multi_session_reasoning_q1
|
||||
beam_1M_15_temporal_reasoning_q0
|
||||
beam_1M_15_temporal_reasoning_q1
|
||||
beam_1M_15_multi_session_reasoning_q0
|
||||
beam_1M_15_multi_session_reasoning_q1
|
||||
beam_1M_23_temporal_reasoning_q0
|
||||
beam_1M_23_temporal_reasoning_q1
|
||||
beam_1M_23_multi_session_reasoning_q0
|
||||
beam_1M_23_multi_session_reasoning_q1
|
||||
beam_1M_27_temporal_reasoning_q0
|
||||
beam_1M_27_temporal_reasoning_q1
|
||||
beam_1M_27_multi_session_reasoning_q0
|
||||
beam_1M_27_multi_session_reasoning_q1
|
||||
2
benchmarks/harness/scripts/e8-probeB.txt
Normal file
2
benchmarks/harness/scripts/e8-probeB.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
beam_1M_27_summarization_q0
|
||||
beam_1M_27_summarization_q1
|
||||
14
benchmarks/harness/scripts/e8-probeC.txt
Normal file
14
benchmarks/harness/scripts/e8-probeC.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
beam_1M_1_abstention_q0
|
||||
beam_1M_1_abstention_q1
|
||||
beam_1M_9_abstention_q0
|
||||
beam_1M_9_abstention_q1
|
||||
beam_1M_10_abstention_q0
|
||||
beam_1M_10_abstention_q1
|
||||
beam_1M_13_abstention_q0
|
||||
beam_1M_13_abstention_q1
|
||||
beam_1M_15_abstention_q0
|
||||
beam_1M_15_abstention_q1
|
||||
beam_1M_23_abstention_q0
|
||||
beam_1M_23_abstention_q1
|
||||
beam_1M_27_abstention_q0
|
||||
beam_1M_27_abstention_q1
|
||||
14
benchmarks/harness/scripts/e8-probeD.txt
Normal file
14
benchmarks/harness/scripts/e8-probeD.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
beam_1M_1_temporal_reasoning_q0
|
||||
beam_1M_1_temporal_reasoning_q1
|
||||
beam_1M_9_temporal_reasoning_q0
|
||||
beam_1M_9_temporal_reasoning_q1
|
||||
beam_1M_10_temporal_reasoning_q0
|
||||
beam_1M_10_temporal_reasoning_q1
|
||||
beam_1M_13_temporal_reasoning_q0
|
||||
beam_1M_13_temporal_reasoning_q1
|
||||
beam_1M_15_temporal_reasoning_q0
|
||||
beam_1M_15_temporal_reasoning_q1
|
||||
beam_1M_23_temporal_reasoning_q0
|
||||
beam_1M_23_temporal_reasoning_q1
|
||||
beam_1M_27_temporal_reasoning_q0
|
||||
beam_1M_27_temporal_reasoning_q1
|
||||
12
benchmarks/harness/scripts/e8-probeF.txt
Normal file
12
benchmarks/harness/scripts/e8-probeF.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
beam_1M_1_summarization_q0
|
||||
beam_1M_1_summarization_q1
|
||||
beam_1M_9_summarization_q0
|
||||
beam_1M_9_summarization_q1
|
||||
beam_1M_10_summarization_q0
|
||||
beam_1M_10_summarization_q1
|
||||
beam_1M_13_summarization_q0
|
||||
beam_1M_13_summarization_q1
|
||||
beam_1M_15_summarization_q0
|
||||
beam_1M_15_summarization_q1
|
||||
beam_1M_23_summarization_q0
|
||||
beam_1M_23_summarization_q1
|
||||
14
benchmarks/harness/scripts/e8-probeG.txt
Normal file
14
benchmarks/harness/scripts/e8-probeG.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
beam_1M_1_knowledge_update_q0
|
||||
beam_1M_1_knowledge_update_q1
|
||||
beam_1M_9_knowledge_update_q0
|
||||
beam_1M_9_knowledge_update_q1
|
||||
beam_1M_10_knowledge_update_q0
|
||||
beam_1M_10_knowledge_update_q1
|
||||
beam_1M_13_knowledge_update_q0
|
||||
beam_1M_13_knowledge_update_q1
|
||||
beam_1M_15_knowledge_update_q0
|
||||
beam_1M_15_knowledge_update_q1
|
||||
beam_1M_23_knowledge_update_q0
|
||||
beam_1M_23_knowledge_update_q1
|
||||
beam_1M_27_knowledge_update_q0
|
||||
beam_1M_27_knowledge_update_q1
|
||||
70
benchmarks/harness/scripts/e8-probeJ.txt
Normal file
70
benchmarks/harness/scripts/e8-probeJ.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
beam_1M_1_temporal_reasoning_q0
|
||||
beam_1M_1_temporal_reasoning_q1
|
||||
beam_1M_2_temporal_reasoning_q0
|
||||
beam_1M_2_temporal_reasoning_q1
|
||||
beam_1M_3_temporal_reasoning_q0
|
||||
beam_1M_3_temporal_reasoning_q1
|
||||
beam_1M_4_temporal_reasoning_q0
|
||||
beam_1M_4_temporal_reasoning_q1
|
||||
beam_1M_5_temporal_reasoning_q0
|
||||
beam_1M_5_temporal_reasoning_q1
|
||||
beam_1M_6_temporal_reasoning_q0
|
||||
beam_1M_6_temporal_reasoning_q1
|
||||
beam_1M_7_temporal_reasoning_q0
|
||||
beam_1M_7_temporal_reasoning_q1
|
||||
beam_1M_8_temporal_reasoning_q0
|
||||
beam_1M_8_temporal_reasoning_q1
|
||||
beam_1M_9_temporal_reasoning_q0
|
||||
beam_1M_9_temporal_reasoning_q1
|
||||
beam_1M_10_temporal_reasoning_q0
|
||||
beam_1M_10_temporal_reasoning_q1
|
||||
beam_1M_11_temporal_reasoning_q0
|
||||
beam_1M_11_temporal_reasoning_q1
|
||||
beam_1M_12_temporal_reasoning_q0
|
||||
beam_1M_12_temporal_reasoning_q1
|
||||
beam_1M_13_temporal_reasoning_q0
|
||||
beam_1M_13_temporal_reasoning_q1
|
||||
beam_1M_14_temporal_reasoning_q0
|
||||
beam_1M_14_temporal_reasoning_q1
|
||||
beam_1M_15_temporal_reasoning_q0
|
||||
beam_1M_15_temporal_reasoning_q1
|
||||
beam_1M_16_temporal_reasoning_q0
|
||||
beam_1M_16_temporal_reasoning_q1
|
||||
beam_1M_17_temporal_reasoning_q0
|
||||
beam_1M_17_temporal_reasoning_q1
|
||||
beam_1M_18_temporal_reasoning_q0
|
||||
beam_1M_18_temporal_reasoning_q1
|
||||
beam_1M_19_temporal_reasoning_q0
|
||||
beam_1M_19_temporal_reasoning_q1
|
||||
beam_1M_20_temporal_reasoning_q0
|
||||
beam_1M_20_temporal_reasoning_q1
|
||||
beam_1M_21_temporal_reasoning_q0
|
||||
beam_1M_21_temporal_reasoning_q1
|
||||
beam_1M_22_temporal_reasoning_q0
|
||||
beam_1M_22_temporal_reasoning_q1
|
||||
beam_1M_23_temporal_reasoning_q0
|
||||
beam_1M_23_temporal_reasoning_q1
|
||||
beam_1M_24_temporal_reasoning_q0
|
||||
beam_1M_24_temporal_reasoning_q1
|
||||
beam_1M_25_temporal_reasoning_q0
|
||||
beam_1M_25_temporal_reasoning_q1
|
||||
beam_1M_26_temporal_reasoning_q0
|
||||
beam_1M_26_temporal_reasoning_q1
|
||||
beam_1M_27_temporal_reasoning_q0
|
||||
beam_1M_27_temporal_reasoning_q1
|
||||
beam_1M_28_temporal_reasoning_q0
|
||||
beam_1M_28_temporal_reasoning_q1
|
||||
beam_1M_29_temporal_reasoning_q0
|
||||
beam_1M_29_temporal_reasoning_q1
|
||||
beam_1M_30_temporal_reasoning_q0
|
||||
beam_1M_30_temporal_reasoning_q1
|
||||
beam_1M_31_temporal_reasoning_q0
|
||||
beam_1M_31_temporal_reasoning_q1
|
||||
beam_1M_32_temporal_reasoning_q0
|
||||
beam_1M_32_temporal_reasoning_q1
|
||||
beam_1M_33_temporal_reasoning_q0
|
||||
beam_1M_33_temporal_reasoning_q1
|
||||
beam_1M_34_temporal_reasoning_q0
|
||||
beam_1M_34_temporal_reasoning_q1
|
||||
beam_1M_35_temporal_reasoning_q0
|
||||
beam_1M_35_temporal_reasoning_q1
|
||||
70
benchmarks/harness/scripts/e8-probeK.txt
Normal file
70
benchmarks/harness/scripts/e8-probeK.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
beam_1M_1_multi_session_reasoning_q0
|
||||
beam_1M_1_multi_session_reasoning_q1
|
||||
beam_1M_2_multi_session_reasoning_q0
|
||||
beam_1M_2_multi_session_reasoning_q1
|
||||
beam_1M_3_multi_session_reasoning_q0
|
||||
beam_1M_3_multi_session_reasoning_q1
|
||||
beam_1M_4_multi_session_reasoning_q0
|
||||
beam_1M_4_multi_session_reasoning_q1
|
||||
beam_1M_5_multi_session_reasoning_q0
|
||||
beam_1M_5_multi_session_reasoning_q1
|
||||
beam_1M_6_multi_session_reasoning_q0
|
||||
beam_1M_6_multi_session_reasoning_q1
|
||||
beam_1M_7_multi_session_reasoning_q0
|
||||
beam_1M_7_multi_session_reasoning_q1
|
||||
beam_1M_8_multi_session_reasoning_q0
|
||||
beam_1M_8_multi_session_reasoning_q1
|
||||
beam_1M_9_multi_session_reasoning_q0
|
||||
beam_1M_9_multi_session_reasoning_q1
|
||||
beam_1M_10_multi_session_reasoning_q0
|
||||
beam_1M_10_multi_session_reasoning_q1
|
||||
beam_1M_11_multi_session_reasoning_q0
|
||||
beam_1M_11_multi_session_reasoning_q1
|
||||
beam_1M_12_multi_session_reasoning_q0
|
||||
beam_1M_12_multi_session_reasoning_q1
|
||||
beam_1M_13_multi_session_reasoning_q0
|
||||
beam_1M_13_multi_session_reasoning_q1
|
||||
beam_1M_14_multi_session_reasoning_q0
|
||||
beam_1M_14_multi_session_reasoning_q1
|
||||
beam_1M_15_multi_session_reasoning_q0
|
||||
beam_1M_15_multi_session_reasoning_q1
|
||||
beam_1M_16_multi_session_reasoning_q0
|
||||
beam_1M_16_multi_session_reasoning_q1
|
||||
beam_1M_17_multi_session_reasoning_q0
|
||||
beam_1M_17_multi_session_reasoning_q1
|
||||
beam_1M_18_multi_session_reasoning_q0
|
||||
beam_1M_18_multi_session_reasoning_q1
|
||||
beam_1M_19_multi_session_reasoning_q0
|
||||
beam_1M_19_multi_session_reasoning_q1
|
||||
beam_1M_20_multi_session_reasoning_q0
|
||||
beam_1M_20_multi_session_reasoning_q1
|
||||
beam_1M_21_multi_session_reasoning_q0
|
||||
beam_1M_21_multi_session_reasoning_q1
|
||||
beam_1M_22_multi_session_reasoning_q0
|
||||
beam_1M_22_multi_session_reasoning_q1
|
||||
beam_1M_23_multi_session_reasoning_q0
|
||||
beam_1M_23_multi_session_reasoning_q1
|
||||
beam_1M_24_multi_session_reasoning_q0
|
||||
beam_1M_24_multi_session_reasoning_q1
|
||||
beam_1M_25_multi_session_reasoning_q0
|
||||
beam_1M_25_multi_session_reasoning_q1
|
||||
beam_1M_26_multi_session_reasoning_q0
|
||||
beam_1M_26_multi_session_reasoning_q1
|
||||
beam_1M_27_multi_session_reasoning_q0
|
||||
beam_1M_27_multi_session_reasoning_q1
|
||||
beam_1M_28_multi_session_reasoning_q0
|
||||
beam_1M_28_multi_session_reasoning_q1
|
||||
beam_1M_29_multi_session_reasoning_q0
|
||||
beam_1M_29_multi_session_reasoning_q1
|
||||
beam_1M_30_multi_session_reasoning_q0
|
||||
beam_1M_30_multi_session_reasoning_q1
|
||||
beam_1M_31_multi_session_reasoning_q0
|
||||
beam_1M_31_multi_session_reasoning_q1
|
||||
beam_1M_32_multi_session_reasoning_q0
|
||||
beam_1M_32_multi_session_reasoning_q1
|
||||
beam_1M_33_multi_session_reasoning_q0
|
||||
beam_1M_33_multi_session_reasoning_q1
|
||||
beam_1M_34_multi_session_reasoning_q0
|
||||
beam_1M_34_multi_session_reasoning_q1
|
||||
beam_1M_35_multi_session_reasoning_q0
|
||||
beam_1M_35_multi_session_reasoning_q1
|
||||
70
benchmarks/harness/scripts/e8-probeL.txt
Normal file
70
benchmarks/harness/scripts/e8-probeL.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
beam_1M_1_event_ordering_q0
|
||||
beam_1M_1_event_ordering_q1
|
||||
beam_1M_2_event_ordering_q0
|
||||
beam_1M_2_event_ordering_q1
|
||||
beam_1M_3_event_ordering_q0
|
||||
beam_1M_3_event_ordering_q1
|
||||
beam_1M_4_event_ordering_q0
|
||||
beam_1M_4_event_ordering_q1
|
||||
beam_1M_5_event_ordering_q0
|
||||
beam_1M_5_event_ordering_q1
|
||||
beam_1M_6_event_ordering_q0
|
||||
beam_1M_6_event_ordering_q1
|
||||
beam_1M_7_event_ordering_q0
|
||||
beam_1M_7_event_ordering_q1
|
||||
beam_1M_8_event_ordering_q0
|
||||
beam_1M_8_event_ordering_q1
|
||||
beam_1M_9_event_ordering_q0
|
||||
beam_1M_9_event_ordering_q1
|
||||
beam_1M_10_event_ordering_q0
|
||||
beam_1M_10_event_ordering_q1
|
||||
beam_1M_11_event_ordering_q0
|
||||
beam_1M_11_event_ordering_q1
|
||||
beam_1M_12_event_ordering_q0
|
||||
beam_1M_12_event_ordering_q1
|
||||
beam_1M_13_event_ordering_q0
|
||||
beam_1M_13_event_ordering_q1
|
||||
beam_1M_14_event_ordering_q0
|
||||
beam_1M_14_event_ordering_q1
|
||||
beam_1M_15_event_ordering_q0
|
||||
beam_1M_15_event_ordering_q1
|
||||
beam_1M_16_event_ordering_q0
|
||||
beam_1M_16_event_ordering_q1
|
||||
beam_1M_17_event_ordering_q0
|
||||
beam_1M_17_event_ordering_q1
|
||||
beam_1M_18_event_ordering_q0
|
||||
beam_1M_18_event_ordering_q1
|
||||
beam_1M_19_event_ordering_q0
|
||||
beam_1M_19_event_ordering_q1
|
||||
beam_1M_20_event_ordering_q0
|
||||
beam_1M_20_event_ordering_q1
|
||||
beam_1M_21_event_ordering_q0
|
||||
beam_1M_21_event_ordering_q1
|
||||
beam_1M_22_event_ordering_q0
|
||||
beam_1M_22_event_ordering_q1
|
||||
beam_1M_23_event_ordering_q0
|
||||
beam_1M_23_event_ordering_q1
|
||||
beam_1M_24_event_ordering_q0
|
||||
beam_1M_24_event_ordering_q1
|
||||
beam_1M_25_event_ordering_q0
|
||||
beam_1M_25_event_ordering_q1
|
||||
beam_1M_26_event_ordering_q0
|
||||
beam_1M_26_event_ordering_q1
|
||||
beam_1M_27_event_ordering_q0
|
||||
beam_1M_27_event_ordering_q1
|
||||
beam_1M_28_event_ordering_q0
|
||||
beam_1M_28_event_ordering_q1
|
||||
beam_1M_29_event_ordering_q0
|
||||
beam_1M_29_event_ordering_q1
|
||||
beam_1M_30_event_ordering_q0
|
||||
beam_1M_30_event_ordering_q1
|
||||
beam_1M_31_event_ordering_q0
|
||||
beam_1M_31_event_ordering_q1
|
||||
beam_1M_32_event_ordering_q0
|
||||
beam_1M_32_event_ordering_q1
|
||||
beam_1M_33_event_ordering_q0
|
||||
beam_1M_33_event_ordering_q1
|
||||
beam_1M_34_event_ordering_q0
|
||||
beam_1M_34_event_ordering_q1
|
||||
beam_1M_35_event_ordering_q0
|
||||
beam_1M_35_event_ordering_q1
|
||||
14
benchmarks/harness/scripts/e8-probeL2.txt
Normal file
14
benchmarks/harness/scripts/e8-probeL2.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
beam_1M_1_event_ordering_q0
|
||||
beam_1M_1_event_ordering_q1
|
||||
beam_1M_9_event_ordering_q0
|
||||
beam_1M_9_event_ordering_q1
|
||||
beam_1M_10_event_ordering_q0
|
||||
beam_1M_10_event_ordering_q1
|
||||
beam_1M_13_event_ordering_q0
|
||||
beam_1M_13_event_ordering_q1
|
||||
beam_1M_15_event_ordering_q0
|
||||
beam_1M_15_event_ordering_q1
|
||||
beam_1M_23_event_ordering_q0
|
||||
beam_1M_23_event_ordering_q1
|
||||
beam_1M_27_event_ordering_q0
|
||||
beam_1M_27_event_ordering_q1
|
||||
50
benchmarks/harness/scripts/matched50.txt
Normal file
50
benchmarks/harness/scripts/matched50.txt
Normal file
@@ -0,0 +1,50 @@
|
||||
beam_1M_10_abstention_q0
|
||||
beam_1M_10_abstention_q1
|
||||
beam_1M_10_contradiction_resolution_q0
|
||||
beam_1M_10_contradiction_resolution_q1
|
||||
beam_1M_10_event_ordering_q0
|
||||
beam_1M_10_event_ordering_q1
|
||||
beam_1M_10_information_extraction_q0
|
||||
beam_1M_10_information_extraction_q1
|
||||
beam_1M_10_instruction_following_q0
|
||||
beam_1M_10_instruction_following_q1
|
||||
beam_1M_10_knowledge_update_q0
|
||||
beam_1M_10_knowledge_update_q1
|
||||
beam_1M_10_multi_session_reasoning_q0
|
||||
beam_1M_10_multi_session_reasoning_q1
|
||||
beam_1M_10_preference_following_q0
|
||||
beam_1M_10_preference_following_q1
|
||||
beam_1M_10_summarization_q0
|
||||
beam_1M_10_summarization_q1
|
||||
beam_1M_10_temporal_reasoning_q0
|
||||
beam_1M_10_temporal_reasoning_q1
|
||||
beam_1M_11_abstention_q0
|
||||
beam_1M_11_contradiction_resolution_q0
|
||||
beam_1M_11_event_ordering_q0
|
||||
beam_1M_11_information_extraction_q0
|
||||
beam_1M_11_instruction_following_q0
|
||||
beam_1M_11_knowledge_update_q0
|
||||
beam_1M_11_multi_session_reasoning_q0
|
||||
beam_1M_11_preference_following_q0
|
||||
beam_1M_11_summarization_q0
|
||||
beam_1M_11_temporal_reasoning_q0
|
||||
beam_1M_1_abstention_q0
|
||||
beam_1M_1_abstention_q1
|
||||
beam_1M_1_contradiction_resolution_q0
|
||||
beam_1M_1_contradiction_resolution_q1
|
||||
beam_1M_1_event_ordering_q0
|
||||
beam_1M_1_event_ordering_q1
|
||||
beam_1M_1_information_extraction_q0
|
||||
beam_1M_1_information_extraction_q1
|
||||
beam_1M_1_instruction_following_q0
|
||||
beam_1M_1_instruction_following_q1
|
||||
beam_1M_1_knowledge_update_q0
|
||||
beam_1M_1_knowledge_update_q1
|
||||
beam_1M_1_multi_session_reasoning_q0
|
||||
beam_1M_1_multi_session_reasoning_q1
|
||||
beam_1M_1_preference_following_q0
|
||||
beam_1M_1_preference_following_q1
|
||||
beam_1M_1_summarization_q0
|
||||
beam_1M_1_summarization_q1
|
||||
beam_1M_1_temporal_reasoning_q0
|
||||
beam_1M_1_temporal_reasoning_q1
|
||||
281
benchmarks/harness/src/beam-multiroute.ts
Normal file
281
benchmarks/harness/src/beam-multiroute.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* BEAM 1M — multi-route dated retrieval (E4 retrieval-architecture lever).
|
||||
*
|
||||
* Eywa's BEAM lead is dominated by COVERAGE / CHRONOLOGICAL-ORDER / CROSS-SESSION
|
||||
* retrieval, which a single top-30 similarity route cannot serve. This module
|
||||
* builds a deterministic 3-route retrieval over the DATED raw-turn store and
|
||||
* fuses it with RRF:
|
||||
*
|
||||
* Route V (vector) : substrate.search.search(q, {limit:kVec}) — existing.
|
||||
* Route T (timeline) : broad similarity fetch (kWide), dated via dateMap;
|
||||
* parseDateWindow(q) hard-filters to an explicit period
|
||||
* when present; ranked by DATE-STRATIFIED COVERAGE
|
||||
* (≤capPerDate turns/date, chronological) so breadth —
|
||||
* not similarity density — drives event_ordering / summ.
|
||||
* Route E (entity) : deterministic query-entity extraction → per-entity
|
||||
* keywordSearch (FTS, phrase-quoted, conv-scoped) → ALL
|
||||
* cross-session mentions of each entity (multi_session).
|
||||
*
|
||||
* FUSE: RRF (k=60) over the three ranked frame-id lists → top-N. Then EVERY
|
||||
* survivor is dated via the dateMap and rendered oldest→newest as
|
||||
* "[YYYY-MM-DD] role: content" (undated turns appended last, unbracketed).
|
||||
* Raw turns are kept verbatim (we still win instruction/preference on detail).
|
||||
*
|
||||
* WHY dateMap and not `created_at`: BEAM ingest writes every turn with
|
||||
* created_at = ingest time (all frames share one date), so the substrate's
|
||||
* built-in since/until temporal filter cannot separate turns by conversation
|
||||
* date. The dateMap (chat.json time_anchor) is the only real per-turn date.
|
||||
*
|
||||
* Pure retrieval + string assembly; the only I/O is substrate.search (local
|
||||
* ollama embeddings + SQLite). No LLM calls here.
|
||||
*/
|
||||
|
||||
import type { SearchResult } from '@waggle/core';
|
||||
import { parseDateWindow } from '@waggle/core';
|
||||
import type { Substrate } from './substrate.js';
|
||||
|
||||
const RRF_K = 60;
|
||||
|
||||
// ── Entity extraction (deterministic, query-side) ────────────────────────────
|
||||
|
||||
/** Question words / generic tokens that are never useful retrieval entities. */
|
||||
const STOP = new Set([
|
||||
'what', 'when', 'which', 'where', 'who', 'whom', 'whose', 'why', 'how', 'did',
|
||||
'do', 'does', 'have', 'has', 'had', 'was', 'were', 'is', 'are', 'am', 'the',
|
||||
'a', 'an', 'my', 'your', 'our', 'their', 'his', 'her', 'its', 'i', 'you', 'we',
|
||||
'they', 'it', 'this', 'that', 'these', 'those', 'and', 'or', 'but', 'for', 'to',
|
||||
'of', 'in', 'on', 'at', 'by', 'with', 'from', 'about', 'into', 'over', 'after',
|
||||
'before', 'between', 'during', 'since', 'until', 'ago', 'many', 'much', 'any',
|
||||
'some', 'all', 'each', 'every', 'been', 'be', 'being', 'will', 'would', 'can',
|
||||
'could', 'should', 'may', 'might', 'must', 'me', 'us', 'them', 'if', 'then',
|
||||
'there', 'here', 'now', 'ever', 'never', 'still', 'yet', 'also', 'just', 'so',
|
||||
'than', 'as', 'up', 'out', 'down', 'off', 'no', 'not', 'yes',
|
||||
// BEAM probing-question instruction / filler / quantifier words — these recur
|
||||
// verbatim ("Mention ONLY and ONLY ten items", "in order", "list every ...")
|
||||
// and are never useful retrieval entities.
|
||||
'mention', 'only', 'list', 'name', 'named', 'items', 'item', 'order', 'ordered',
|
||||
'progress', 'progressed', 'discussion', 'discussions', 'discussed', 'discuss',
|
||||
'say', 'said', 'tell', 'give', 'provide', 'describe', 'summarize', 'summary',
|
||||
'overview', 'account', 'sequence', 'chronological', 'versions', 'version',
|
||||
'thing', 'things', 'stuff', 'first', 'second', 'third', 'fourth', 'fifth', 'last',
|
||||
'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
|
||||
'eleven', 'twelve', 'both', 'entire', 'whole', 'total', 'number', 'count',
|
||||
'happened', 'occur', 'occurred', 'time', 'times', 'date', 'dates', 'day', 'days',
|
||||
'week', 'weeks', 'month', 'months', 'year', 'years',
|
||||
'considering', 'using', 'given', 'based', 'regarding', 'concerning',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract distinctive entities from a question: quoted spans, version/tech
|
||||
* tokens (contain a digit or dot, e.g. "React 18.2", "v6.1.0", "port 4000"),
|
||||
* lowercase alnum tokens with a digit, and runs of Capitalized words
|
||||
* (proper-noun phrases). Deduplicated case-insensitively, capped.
|
||||
*/
|
||||
export function extractQueryEntities(question: string, cap = 6): string[] {
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (raw: string): void => {
|
||||
const t = raw.trim().replace(/[.,;:?!]+$/, '').trim();
|
||||
if (t.length < 3) return;
|
||||
const key = t.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
// Skip pure stopwords / question words.
|
||||
if (STOP.has(key)) return;
|
||||
seen.add(key);
|
||||
out.push(t);
|
||||
};
|
||||
|
||||
// 1) Quoted spans (highest precision).
|
||||
for (const m of question.matchAll(/["'“”‘’]([^"'“”‘’]{2,60})["'“”‘’]/g)) push(m[1]);
|
||||
|
||||
// 2) Version / tech tokens: a token containing a digit (React18, 18.2, v6.1.0,
|
||||
// 4000, PostgreSQL14) OR a dotted identifier. Keep an adjacent Capitalized
|
||||
// word as a two-word unit ("React 18.2", "PostgreSQL 14", "port 4000").
|
||||
const words = question.split(/\s+/);
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const w = words[i].replace(/[(),;:?!]+$/g, '').replace(/^[(),;:?!]+/g, '');
|
||||
if (/\d/.test(w) && /[A-Za-z0-9.]/.test(w) && !/^\d{4}$/.test(w) === true) {
|
||||
const prev = i > 0 ? words[i - 1].replace(/[(),;:?!]+$/g, '') : '';
|
||||
if (prev && (/^[A-Z]/.test(prev) || /^(port|version|v|node|python|react|postgres|postgresql)$/i.test(prev)) && !STOP.has(prev.toLowerCase())) {
|
||||
push(`${prev} ${w}`);
|
||||
}
|
||||
push(w);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Runs of Capitalized words (proper-noun phrases), ignoring the leading
|
||||
// sentence-initial capital by only taking runs of length>=1 that aren't a
|
||||
// lone stopword. Multi-word runs are kept whole AND their head token.
|
||||
const capRun = /([A-Z][A-Za-z0-9+.#-]*(?:\s+[A-Z][A-Za-z0-9+.#-]*)*)/g;
|
||||
for (const m of question.matchAll(capRun)) {
|
||||
const phrase = m[1].trim();
|
||||
const toks = phrase.split(/\s+/);
|
||||
// Drop a leading sentence-initial single-cap common word (e.g. "What").
|
||||
if (toks.length === 1) {
|
||||
if (!STOP.has(toks[0].toLowerCase()) && toks[0].length >= 4) push(toks[0]);
|
||||
continue;
|
||||
}
|
||||
// Multi-word proper-noun phrase.
|
||||
const filtered = toks.filter((t, idx) => !(idx === 0 && STOP.has(t.toLowerCase())));
|
||||
if (filtered.length >= 2) push(filtered.join(' '));
|
||||
else if (filtered.length === 1 && filtered[0].length >= 4) push(filtered[0]);
|
||||
}
|
||||
|
||||
// 4) lowercase alnum tokens that carry a digit (e.g. "franc6", "gpt4") — rare
|
||||
// but distinctive; plain lowercase words are left to Route V/T.
|
||||
for (const w of words) {
|
||||
const t = w.replace(/[(),;:?!.]+$/g, '');
|
||||
if (/^[a-z][a-z0-9.-]*\d[a-z0-9.-]*$/i.test(t)) push(t);
|
||||
}
|
||||
|
||||
return out.slice(0, cap);
|
||||
}
|
||||
|
||||
// ── RRF fusion ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** RRF-fuse ranked id lists → id → score. Higher = better. */
|
||||
export function rrfFuse(lists: number[][], k = RRF_K): Map<number, number> {
|
||||
const scores = new Map<number, number>();
|
||||
for (const list of lists) {
|
||||
list.forEach((id, rank) => {
|
||||
scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank));
|
||||
});
|
||||
}
|
||||
return scores;
|
||||
}
|
||||
|
||||
// ── Timeline route (date-stratified coverage) ────────────────────────────────
|
||||
|
||||
interface DatedFrame { id: number; content: string; date: string }
|
||||
|
||||
/** Rank a candidate set for COVERAGE: keep ≤capPerDate per date, ordered
|
||||
* chronologically (oldest→newest). Returns frame ids in coverage order. */
|
||||
function coverageRank(dated: DatedFrame[], capPerDate: number, limit: number): number[] {
|
||||
const sorted = [...dated].sort((a, b) => (a.date === b.date ? a.id - b.id : a.date < b.date ? -1 : 1));
|
||||
const perDate = new Map<string, number>();
|
||||
const kept: number[] = [];
|
||||
for (const e of sorted) {
|
||||
const n = perDate.get(e.date) ?? 0;
|
||||
if (n >= capPerDate) continue;
|
||||
perDate.set(e.date, n + 1);
|
||||
kept.push(e.id);
|
||||
if (kept.length >= limit) break;
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
// ── Public: build the fused, dated, chronological context ────────────────────
|
||||
|
||||
export interface MultiRouteOptions {
|
||||
kVec: number; // Route V similarity limit (baseline parity, 30)
|
||||
kWide: number; // Route T broad similarity fetch (150)
|
||||
perEntity: number; // Route E per-entity FTS limit (20)
|
||||
capPerDate: number;// Route T coverage stratification cap (2)
|
||||
topN: number; // fused frames kept for the answer context (45)
|
||||
}
|
||||
|
||||
export const DEFAULT_MULTIROUTE: MultiRouteOptions = {
|
||||
kVec: 30, kWide: 150, perEntity: 20, capPerDate: 2, topN: 45,
|
||||
};
|
||||
|
||||
export interface MultiRouteResult {
|
||||
/** Dated display strings, oldest→newest, ready for buildAnswerGenerationPromptV2. */
|
||||
displayStrings: string[];
|
||||
/** Diagnostics. */
|
||||
entities: string[];
|
||||
dateWindow: { since: string; until: string; label: string } | null;
|
||||
nVec: number;
|
||||
nTimeline: number;
|
||||
nEntity: number;
|
||||
nFused: number;
|
||||
nDated: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all three routes over one conversation's raw-turn mind and return the
|
||||
* fused, dated, chronologically-ordered answer context. `dateMap` maps exact
|
||||
* frame content ("role: text") → ISO date (from chat.json time_anchor).
|
||||
*/
|
||||
export async function buildMultiRouteContext(
|
||||
sub: Substrate,
|
||||
gopId: string,
|
||||
question: string,
|
||||
dateMap: Map<string, string> | null,
|
||||
opts: MultiRouteOptions = DEFAULT_MULTIROUTE,
|
||||
): Promise<MultiRouteResult> {
|
||||
const frameById = new Map<number, { id: number; content: string }>();
|
||||
const record = (r: SearchResult): void => { frameById.set(r.frame.id, { id: r.frame.id, content: r.frame.content }); };
|
||||
const dateOf = (content: string): string => dateMap?.get(content) ?? '';
|
||||
|
||||
// Route V — vector (baseline parity).
|
||||
const vec = await sub.search.search(question, { limit: opts.kVec, gopId });
|
||||
vec.forEach(record);
|
||||
const Lv = vec.map(r => r.frame.id);
|
||||
|
||||
// Route T — timeline: broad fetch, date-window filter, coverage rank.
|
||||
const wide = await sub.search.search(question, { limit: opts.kWide, gopId });
|
||||
wide.forEach(record);
|
||||
const window = parseDateWindow(question);
|
||||
let widedated: DatedFrame[] = wide.map(r => ({ id: r.frame.id, content: r.frame.content, date: dateOf(r.frame.content) }));
|
||||
if (window) {
|
||||
const inWin = widedated.filter(e => e.date && e.date >= window.since && e.date <= window.until);
|
||||
// Only apply the window if it actually retains evidence; else keep the broad
|
||||
// set (a mis-parse must never empty the context).
|
||||
if (inWin.length > 0) widedated = inWin;
|
||||
}
|
||||
const Lt = coverageRank(widedated, opts.capPerDate, opts.kWide);
|
||||
|
||||
// Route E — entity: per-entity FTS across the whole conversation.
|
||||
const entities = extractQueryEntities(question);
|
||||
const entityHits = new Map<number, number>(); // id → #entities that matched it
|
||||
for (const ent of entities) {
|
||||
// Phrase-quote multi-word entities so keywordSearch does a phrase match
|
||||
// (it passes a query containing a quote through to FTS unchanged).
|
||||
const q = /\s/.test(ent) ? `"${ent.replace(/"/g, '')}"` : ent;
|
||||
let ids: number[] = [];
|
||||
try { ids = await sub.search.keywordSearch(q, opts.perEntity, gopId); } catch { ids = []; }
|
||||
for (const id of ids) entityHits.set(id, (entityHits.get(id) ?? 0) + 1);
|
||||
// Materialise contents for ids not yet seen by V/T.
|
||||
const missing = ids.filter(id => !frameById.has(id));
|
||||
if (missing.length) {
|
||||
const rawdb = sub.db.getDatabase();
|
||||
const ph = missing.map(() => '?').join(',');
|
||||
const rows = rawdb.prepare(`SELECT id, content FROM memory_frames WHERE id IN (${ph})`).all(...missing) as Array<{ id: number; content: string }>;
|
||||
for (const row of rows) frameById.set(row.id, row);
|
||||
}
|
||||
}
|
||||
// Le ranked by (most entities matched, then id) — frames mentioning more of
|
||||
// the question's entities rank higher.
|
||||
const Le = [...entityHits.entries()].sort((a, b) => (b[1] - a[1]) || (a[0] - b[0])).map(e => e[0]);
|
||||
|
||||
// FUSE — RRF over the three ranked lists → top-N.
|
||||
const fused = rrfFuse([Lv, Lt, Le]);
|
||||
const fusedIds = [...fused.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(e => e[0])
|
||||
.slice(0, opts.topN);
|
||||
|
||||
// DATE + chronological render: dated turns oldest→newest, undated appended last.
|
||||
const entries = fusedIds
|
||||
.map(id => frameById.get(id))
|
||||
.filter((f): f is { id: number; content: string } => !!f)
|
||||
.map(f => ({ id: f.id, content: f.content, date: dateOf(f.content) }));
|
||||
entries.sort((a, b) => {
|
||||
if (a.date && b.date) return a.date === b.date ? a.id - b.id : a.date < b.date ? -1 : 1;
|
||||
if (a.date) return -1; // dated before undated
|
||||
if (b.date) return 1;
|
||||
return a.id - b.id;
|
||||
});
|
||||
const displayStrings = entries.map(e => (e.date ? `[${e.date}] ${e.content}` : e.content));
|
||||
|
||||
return {
|
||||
displayStrings,
|
||||
entities,
|
||||
dateWindow: window,
|
||||
nVec: Lv.length,
|
||||
nTimeline: Lt.length,
|
||||
nEntity: Le.length,
|
||||
nFused: fusedIds.length,
|
||||
nDated: entries.filter(e => e.date).length,
|
||||
};
|
||||
}
|
||||
@@ -31,6 +31,11 @@ export interface BeamLlmResult {
|
||||
latencyMs: number;
|
||||
/** null = OK; otherwise a short failure classification. */
|
||||
failureMode: string | null;
|
||||
/** Prompt-caching economics (Anthropic via OpenRouter): tokens served from
|
||||
* cache (cheap) and tokens written to cache (surcharged). Absent when the
|
||||
* provider/route reports no cache usage. */
|
||||
cacheReadTokens?: number;
|
||||
cacheCreationTokens?: number;
|
||||
}
|
||||
|
||||
export interface BeamLlm {
|
||||
@@ -138,7 +143,7 @@ ANSWER:`;
|
||||
* negation/"never happened" rule is added. All other rules are verbatim v1.
|
||||
* Pair with date-stamped `memories` ("[YYYY-MM-DD] role: ...") so the
|
||||
* contradiction rule can surface each statement with its date. */
|
||||
export function buildAnswerGenerationPromptV2(question: string, memories: string[], outline?: string): string {
|
||||
export function buildAnswerGenerationPromptV2(question: string, memories: string[], outline?: string, beliefsBlock?: string): string {
|
||||
const memoriesText =
|
||||
memories.length === 0
|
||||
? '(No memories available)'
|
||||
@@ -146,6 +151,12 @@ export function buildAnswerGenerationPromptV2(question: string, memories: string
|
||||
// `outline` is a generic pre-labeled preamble: the CALLER builds the labeled
|
||||
// block(s) (timeline, standing directives, ...) and this just inserts them.
|
||||
const outlineBlock = outline ? `\n${outline}\n` : '';
|
||||
// `beliefsBlock` is an ADDITIVE consolidated "current values" section built by
|
||||
// the belief cell (real supersede/consolidation code). Placed BEFORE the raw
|
||||
// turns so the model prefers the latest known value on a conflict, while the
|
||||
// raw dated turns remain for detail. When absent the prompt is BYTE-IDENTICAL
|
||||
// to the original v2 (both `beliefsSection` and `outlineBlock` collapse to '').
|
||||
const beliefsSection = beliefsBlock ? `\n${beliefsBlock}\n` : '';
|
||||
return `You are an AI assistant with access to stored memories from prior conversations with a user.
|
||||
Use these memories to answer the following question as accurately and completely as possible.
|
||||
|
||||
@@ -162,7 +173,7 @@ IMPORTANT RULES:
|
||||
10. Do NOT invent or assume information that isn't in the memories.
|
||||
|
||||
QUESTION: ${question}
|
||||
${outlineBlock}
|
||||
${beliefsSection}${outlineBlock}
|
||||
RETRIEVED MEMORIES:
|
||||
${memoriesText}
|
||||
|
||||
@@ -283,6 +294,54 @@ ${memoriesText}
|
||||
ANSWER:`;
|
||||
}
|
||||
|
||||
/** Answer-REPAIR prompt (E5 self-correction lever). Test-time, gold-blind second
|
||||
* pass: it sees ONLY the question, the SAME retrieved dated context the drafter
|
||||
* saw, and the draft answer — NO rubric, NO gold, NO nuggets. It rewrites the
|
||||
* draft into a more complete, better-grounded answer:
|
||||
* - fill coverage gaps (enumerate every relevant item/date/clause the context
|
||||
* supports that the draft omitted) — BEAM grades on nugget coverage;
|
||||
* - correct chronology / temporal anchors (event date, not mention date);
|
||||
* - strip any claim the context does not support (no new facts);
|
||||
* - PRESERVE correct abstentions — if the context genuinely lacks the answer,
|
||||
* keep the exact sentinel and do not invent to look complete.
|
||||
* Mirrors the V2 signature (optional outline + beliefsBlock) so the repair pass
|
||||
* is grounded in the identical context the drafter received. */
|
||||
export function buildRepairPrompt(
|
||||
question: string,
|
||||
memories: string[],
|
||||
draftAnswer: string,
|
||||
outline?: string,
|
||||
beliefsBlock?: string,
|
||||
): string {
|
||||
const memoriesText =
|
||||
memories.length === 0
|
||||
? '(No memories available)'
|
||||
: memories.map((m, i) => `${i + 1}. ${m}`).join('\n');
|
||||
const outlineBlock = outline ? `\n${outline}\n` : '';
|
||||
const beliefsSection = beliefsBlock ? `\n${beliefsBlock}\n` : '';
|
||||
return `You are a meticulous reviewer improving a draft answer to a question. You are given the question, the stored memories (dated, from prior conversations with the user) that were available, and a DRAFT ANSWER written from those memories. Your job is to return a single IMPROVED ANSWER that is more complete and better grounded — nothing else.
|
||||
|
||||
REPAIR RULES:
|
||||
1. Ground everything ONLY in the provided memories (and any consolidated values/timeline shown). Do NOT add any fact, name, date, number, or claim that is not supported by the provided context. You have no outside knowledge of this user.
|
||||
2. Improve COVERAGE: re-scan ALL memories and add every relevant item the draft missed. If the question asks for a summary, overview, account of a process/journey, or a list/ordering of events, be EXHAUSTIVE — enumerate every relevant topic, project, event, tool, version, number, date, cause, outcome, and sub-step the memories support. Do not drop minor items.
|
||||
3. Fix accuracy: correct any wrong or unsupported statement in the draft. For dates and durations, anchor on the date the event actually happened or is scheduled FOR (not the date it was merely mentioned); present events in chronological order.
|
||||
4. Handle contradictions honestly: if the memories contain conflicting statements relevant to the question, present each conflicting statement (with its date when shown) rather than silently picking one.
|
||||
5. Remove hallucinations: delete anything in the draft that the memories do not support.
|
||||
6. PRESERVE CORRECT ABSTENTION: if the memories genuinely do not contain the information asked, do NOT invent an answer to look more complete — return exactly: "I don't have enough information to answer this question." Conversely, if the memories DO support an answer but the draft wrongly abstained, replace the abstention with the supported answer.
|
||||
7. Keep everything in the draft that is already correct and supported; this is a revision, not a rewrite from scratch.
|
||||
8. Output ONLY the improved answer text — no preamble, no explanation of your changes, no mention of "the draft".
|
||||
|
||||
QUESTION: ${question}
|
||||
${beliefsSection}${outlineBlock}
|
||||
RETRIEVED MEMORIES:
|
||||
${memoriesText}
|
||||
|
||||
DRAFT ANSWER:
|
||||
${draftAnswer}
|
||||
|
||||
IMPROVED ANSWER:`;
|
||||
}
|
||||
|
||||
/** Fact-extraction prompt for event_ordering (mem0 `get_beam_fact_extraction_prompt`). */
|
||||
export function buildFactExtractionPrompt(response: string): string {
|
||||
return `Extract all distinct events or facts mentioned in the following response,
|
||||
@@ -551,12 +610,30 @@ export async function judgeQuestion(
|
||||
}
|
||||
|
||||
const nuggetScores: NuggetScore[] = [];
|
||||
let judgeFailures = 0;
|
||||
for (const nugget of input.rubric) {
|
||||
const ns = await judgeSingleNugget(llm, input.question, nugget, input.answer);
|
||||
llmResults.push(ns.result);
|
||||
// A transport failure (http_429/timeout/etc.) or an empty completion means
|
||||
// the JUDGE broke — not that the answer scored 0. Recording 0 here would
|
||||
// silently corrupt the metric, so flag it and let the caller retry / hard-fail.
|
||||
if (ns.result.failureMode || ns.result.text.trim() === '') judgeFailures++;
|
||||
nuggetScores.push({ nugget, score: ns.score, reason: ns.reason });
|
||||
}
|
||||
|
||||
if (judgeFailures > 0) {
|
||||
return {
|
||||
judgement: {
|
||||
score: 0,
|
||||
judgment: 'ERROR',
|
||||
nuggetScores,
|
||||
judgeCalls: nuggetScores.length,
|
||||
error: `judge transport failure on ${judgeFailures}/${nuggetScores.length} nuggets`,
|
||||
},
|
||||
llmResults,
|
||||
};
|
||||
}
|
||||
|
||||
const avg = nuggetScores.reduce((s, n) => s + n.score, 0) / nuggetScores.length;
|
||||
const judgement: QuestionJudgement = {
|
||||
score: round4(avg),
|
||||
|
||||
@@ -77,9 +77,14 @@ export const OPENAI_PRICING: Record<string, ModelPricing> = {
|
||||
};
|
||||
|
||||
/** gpt-5 / o-series reasoning models reject `max_tokens` + non-default
|
||||
* temperature, and spend completion budget on hidden reasoning tokens. */
|
||||
* temperature, and spend completion budget on hidden reasoning tokens. The
|
||||
* provider-prefixed form ("openai/gpt-5" via OpenRouter) must match too, or the
|
||||
* client wrongly uses the max_tokens path and gpt-5 truncates its JSON mid-
|
||||
* reasoning. gpt-5-chat is NOT a reasoning model, so exclude it explicitly. */
|
||||
function isReasoningModel(model: string): boolean {
|
||||
return /^(gpt-5|o\d)/.test(model.toLowerCase());
|
||||
const m = model.toLowerCase();
|
||||
if (/(^|\/)gpt-5-chat/.test(m)) return false;
|
||||
return /(^|\/)(gpt-5|o\d)/.test(m);
|
||||
}
|
||||
|
||||
// ── Client ──────────────────────────────────────────────────────────────────
|
||||
@@ -94,6 +99,11 @@ export interface BeamOpenAiClientOptions {
|
||||
pricing?: ModelPricing;
|
||||
timeoutMs?: number;
|
||||
maxRetries?: number;
|
||||
/** Optional reasoning_effort for gpt-5/o-series (e.g. 'minimal' | 'low' |
|
||||
* 'medium' | 'high'). Only sent for reasoning models; omitted by default so
|
||||
* existing callers are byte-identical. Extraction uses 'low' to cut hidden
|
||||
* reasoning tokens (latency + cost) on a mechanical task. */
|
||||
reasoningEffort?: string;
|
||||
}
|
||||
|
||||
export class BeamOpenAiClient implements BeamLlm {
|
||||
@@ -103,6 +113,7 @@ export class BeamOpenAiClient implements BeamLlm {
|
||||
private readonly pricing: ModelPricing;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly maxRetries: number;
|
||||
private readonly reasoningEffort: string | null;
|
||||
|
||||
constructor(opts: BeamOpenAiClientOptions) {
|
||||
this.model = opts.model;
|
||||
@@ -111,9 +122,21 @@ export class BeamOpenAiClient implements BeamLlm {
|
||||
this.pricing = opts.pricing ?? OPENAI_PRICING[opts.model] ?? { inputPerMillion: 0, outputPerMillion: 0 };
|
||||
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
|
||||
this.reasoningEffort = opts.reasoningEffort ?? null;
|
||||
}
|
||||
|
||||
async chat(opts: { system: string; user: string; jsonMode?: boolean; maxTokens?: number }): Promise<BeamLlmResult> {
|
||||
async chat(opts: {
|
||||
system: string;
|
||||
user: string;
|
||||
jsonMode?: boolean;
|
||||
maxTokens?: number;
|
||||
/** Large, stable text prefix to mark for prompt caching (Anthropic via
|
||||
* OpenRouter). Placed FIRST in the system message with
|
||||
* cache_control:{type:'ephemeral'} so repeated calls sharing this prefix
|
||||
* (e.g. one conversation's ledger across its 20 questions) read it from
|
||||
* cache. Ignored (sent as a plain system block) by non-caching providers. */
|
||||
cacheableSystem?: string;
|
||||
}): Promise<BeamLlmResult> {
|
||||
const started = Date.now();
|
||||
let lastFailure = 'unknown';
|
||||
const reasoning = isReasoningModel(this.model);
|
||||
@@ -130,15 +153,30 @@ export class BeamOpenAiClient implements BeamLlm {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
try {
|
||||
// System message: when a cacheable prefix is supplied, send the system
|
||||
// content as an array of parts with cache_control on the (large, stable)
|
||||
// prefix so Anthropic (via OpenRouter) serves it from cache on repeat
|
||||
// calls. Otherwise a plain string (byte-identical to prior behaviour).
|
||||
let systemContent: unknown;
|
||||
if (opts.cacheableSystem) {
|
||||
const parts: Array<Record<string, unknown>> = [
|
||||
{ type: 'text', text: opts.cacheableSystem, cache_control: { type: 'ephemeral' } },
|
||||
];
|
||||
if (opts.system) parts.push({ type: 'text', text: opts.system });
|
||||
systemContent = parts;
|
||||
} else {
|
||||
systemContent = opts.system;
|
||||
}
|
||||
const body: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: 'system', content: opts.system },
|
||||
{ role: 'system', content: systemContent },
|
||||
{ role: 'user', content: opts.user },
|
||||
],
|
||||
};
|
||||
if (reasoning) {
|
||||
body.max_completion_tokens = reasoningBudget;
|
||||
if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
|
||||
} else {
|
||||
body.temperature = 0;
|
||||
body.max_tokens = opts.maxTokens ?? 800;
|
||||
@@ -165,7 +203,13 @@ export class BeamOpenAiClient implements BeamLlm {
|
||||
|
||||
const json = (await res.json()) as {
|
||||
choices?: Array<{ message?: { content?: string } }>;
|
||||
usage?: { prompt_tokens?: number; completion_tokens?: number };
|
||||
usage?: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
prompt_tokens_details?: { cached_tokens?: number };
|
||||
cache_creation_input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
};
|
||||
};
|
||||
const text = json.choices?.[0]?.message?.content ?? '';
|
||||
// Retry-on-empty for reasoning models: HTTP-200 but no answer text means
|
||||
@@ -183,10 +227,28 @@ export class BeamOpenAiClient implements BeamLlm {
|
||||
}
|
||||
const inputTokens = json.usage?.prompt_tokens ?? approxTokens(opts.system + opts.user);
|
||||
const outputTokens = json.usage?.completion_tokens ?? approxTokens(text);
|
||||
// Prompt-caching accounting (Anthropic via OpenRouter). Providers report
|
||||
// either Anthropic-native fields (cache_creation/cache_read_input_tokens)
|
||||
// or the OpenAI-style prompt_tokens_details.cached_tokens (reads only).
|
||||
const cacheCreationTokens = json.usage?.cache_creation_input_tokens ?? 0;
|
||||
const cacheReadTokens =
|
||||
json.usage?.cache_read_input_tokens ?? json.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
||||
// `prompt_tokens` from Anthropic EXCLUDES cached-read tokens but INCLUDES
|
||||
// cache-creation tokens; from OpenAI it INCLUDES cached tokens. Compute
|
||||
// uncached input as prompt_tokens minus any cache portions already in it.
|
||||
const inRate = this.pricing.inputPerMillion / 1_000_000;
|
||||
const outRate = this.pricing.outputPerMillion / 1_000_000;
|
||||
// Cache writes are surcharged 1.25x, reads discounted to 0.1x (Anthropic).
|
||||
const uncachedInput = Math.max(0, inputTokens - cacheCreationTokens - (json.usage?.prompt_tokens_details?.cached_tokens ?? 0));
|
||||
const costUsd =
|
||||
(inputTokens / 1_000_000) * this.pricing.inputPerMillion +
|
||||
(outputTokens / 1_000_000) * this.pricing.outputPerMillion;
|
||||
return { text, inputTokens, outputTokens, costUsd, latencyMs: Date.now() - started, failureMode: null };
|
||||
uncachedInput * inRate +
|
||||
cacheCreationTokens * inRate * 1.25 +
|
||||
cacheReadTokens * inRate * 0.1 +
|
||||
outputTokens * outRate;
|
||||
return {
|
||||
text, inputTokens, outputTokens, costUsd, latencyMs: Date.now() - started, failureMode: null,
|
||||
cacheReadTokens, cacheCreationTokens,
|
||||
};
|
||||
} catch (err) {
|
||||
const name = (err as Error).name;
|
||||
lastFailure = name === 'AbortError' ? 'timeout' : `fetch_error_${name}`;
|
||||
@@ -209,6 +271,9 @@ export function createBeamOpenAiClient(opts: {
|
||||
envPath?: string;
|
||||
baseUrl?: string;
|
||||
pricing?: ModelPricing;
|
||||
timeoutMs?: number;
|
||||
maxRetries?: number;
|
||||
reasoningEffort?: string;
|
||||
}): BeamOpenAiClient {
|
||||
loadDotEnv(opts.envPath);
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
@@ -218,7 +283,10 @@ export function createBeamOpenAiClient(opts: {
|
||||
'Set it in waggle-os/.env or export it before running.',
|
||||
);
|
||||
}
|
||||
return new BeamOpenAiClient({ model: opts.model, apiKey, baseUrl: opts.baseUrl, pricing: opts.pricing });
|
||||
return new BeamOpenAiClient({
|
||||
model: opts.model, apiKey, baseUrl: opts.baseUrl, pricing: opts.pricing,
|
||||
timeoutMs: opts.timeoutMs, maxRetries: opts.maxRetries, reasoningEffort: opts.reasoningEffort,
|
||||
});
|
||||
}
|
||||
|
||||
function approxTokens(s: string): number {
|
||||
|
||||
Reference in New Issue
Block a user