Skip to content

Commit 6b72c79

Browse files
feat: full WRIT benchmark harness, 50 scenarios, tests, CI
Implement write-integrity evaluation (structured/temporal/provenance/ constraint scoring, LLM judge fallback), Neotoma adapter fixes (POST /entities/query, auth, writ_fact_id mapping), baseline fixes, markdown/JSON reports, CLI thresholds, GitHub Actions workflow, and documentation (authoring, adapters, metrics).
1 parent 1346328 commit 6b72c79

73 files changed

Lines changed: 8711 additions & 186 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmark.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: WRIT Benchmark CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 20
17+
cache: npm
18+
- run: npm install
19+
- run: npx tsc --noEmit
20+
- run: npx vitest run
21+
22+
benchmark-baseline:
23+
runs-on: ubuntu-latest
24+
needs: test
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version: 20
30+
cache: npm
31+
- run: npm install
32+
- run: npx tsx src/cli.ts --adapter baseline --modes native_memory --output-format both --output results
33+
- uses: actions/upload-artifact@v4
34+
with:
35+
name: writ-results
36+
path: results/

docs/adapters.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Implementing a WRIT Adapter
2+
3+
A WRIT adapter wraps a memory system so the benchmark can feed it conversation sessions, probe it for answers, and inspect its internal state.
4+
5+
## Interface
6+
7+
Implement the `MemoryAdapter` interface from `src/adapter.ts`:
8+
9+
```typescript
10+
interface MemoryAdapter {
11+
readonly name: string;
12+
init(): Promise<void>;
13+
processSession(session: Session): Promise<void>;
14+
probe(prompt: string, options?: ProbeOptions): Promise<ProbeResult>;
15+
getHistory(factId: string): Promise<FactHistory | null>;
16+
getStateAsOf(factId: string, timestamp: string): Promise<unknown | null>;
17+
getProvenance(factId: string): Promise<Provenance | null>;
18+
getCapabilities(): AdapterCapabilities;
19+
reset(): Promise<void>;
20+
teardown(): Promise<void>;
21+
}
22+
```
23+
24+
## Methods
25+
26+
### `init()`
27+
28+
Called once before any scenarios run. Connect to the memory system, verify health, allocate resources.
29+
30+
### `processSession(session)`
31+
32+
Feed a conversation session into the memory system. The adapter should store facts, entities, and relationships exactly as the target system would in production.
33+
34+
The `session` object contains:
35+
- `session_id`: numeric identifier
36+
- `timestamp`: ISO 8601 timestamp for when the session occurred
37+
- `messages`: array of `{ role: "user" | "assistant", content: string }`
38+
39+
Store the session's timestamp alongside any stored entities so temporal queries work correctly.
40+
41+
### `probe(prompt, options?)`
42+
43+
Ask the memory system a question. Return:
44+
- `answer`: the system's textual response
45+
- `confidence`: optional numeric confidence score (0-1)
46+
- `cited_sources`: array of source identifiers the system referenced
47+
- `abstained`: whether the system declined to answer
48+
49+
Handle three modes via `options.mode`:
50+
- `"no_memory"`: Return an empty/abstained response without consulting memory
51+
- `"native_memory"`: Query the system's actual memory
52+
- `"oracle_memory"`: Use `options.oracle_state` (a `Record<string, unknown>` of fact_id -> value) instead of the system's own memory
53+
54+
### `getHistory(factId)`
55+
56+
Return the full value history for a stored fact. If the system does not track history, return `null`.
57+
58+
The `factId` comes from `scenario.memory_events[].id`. Map this to whatever internal identifier your system uses.
59+
60+
Return a `FactHistory`:
61+
```typescript
62+
{
63+
fact_id: string;
64+
values: Array<{ value: unknown; as_of: string; source_session: number }>;
65+
current_value: unknown;
66+
}
67+
```
68+
69+
### `getStateAsOf(factId, timestamp)`
70+
71+
Reconstruct a fact's value as it was at a specific point in time. If the system does not support temporal queries, return `null`.
72+
73+
### `getProvenance(factId)`
74+
75+
Return provenance metadata: which session, which message, and who (user/assistant) introduced the fact. If the system does not track provenance, return `null`.
76+
77+
Return a `Provenance`:
78+
```typescript
79+
{
80+
fact_id: string;
81+
source_session: number;
82+
source_message_index: number;
83+
agent_or_user: string;
84+
chain: ProvenanceChainLink[];
85+
}
86+
```
87+
88+
### `getCapabilities()`
89+
90+
Declare what the adapter supports. The evaluator uses this to skip metrics the adapter cannot perform (scored as `null` / N/A, not penalized).
91+
92+
```typescript
93+
{
94+
supports_history: boolean;
95+
supports_temporal_replay: boolean;
96+
supports_provenance: boolean;
97+
supports_abstention: boolean;
98+
}
99+
```
100+
101+
### `reset()`
102+
103+
Clear all state between scenarios. Each scenario must start from a clean slate.
104+
105+
### `teardown()`
106+
107+
Clean up connections and resources after all scenarios complete.
108+
109+
## Registration
110+
111+
Add your adapter to `src/cli.ts` in the `createAdapter` switch:
112+
113+
```typescript
114+
case "my-system":
115+
return new MySystemAdapter(url);
116+
```
117+
118+
## Example: Minimal Adapter
119+
120+
```typescript
121+
import type { MemoryAdapter, AdapterCapabilities } from "../adapter.js";
122+
import type { Session, ProbeOptions, ProbeResult, FactHistory, Provenance } from "../types.js";
123+
124+
export class MyAdapter implements MemoryAdapter {
125+
readonly name = "my-system";
126+
127+
async init() { /* connect */ }
128+
129+
async processSession(session: Session) {
130+
for (const msg of session.messages) {
131+
if (msg.role === "user") {
132+
await this.storeMessage(msg.content, session.timestamp);
133+
}
134+
}
135+
}
136+
137+
async probe(prompt: string, options?: ProbeOptions): Promise<ProbeResult> {
138+
if (options?.mode === "no_memory") {
139+
return { answer: "", confidence: null, cited_sources: [], abstained: true };
140+
}
141+
const answer = await this.queryMemory(prompt);
142+
return {
143+
answer: answer ?? "",
144+
confidence: answer ? 0.9 : null,
145+
cited_sources: [],
146+
abstained: !answer,
147+
};
148+
}
149+
150+
async getHistory(_factId: string): Promise<FactHistory | null> {
151+
return null; // not supported
152+
}
153+
154+
async getStateAsOf(_factId: string, _ts: string): Promise<unknown | null> {
155+
return null; // not supported
156+
}
157+
158+
async getProvenance(_factId: string): Promise<Provenance | null> {
159+
return null; // not supported
160+
}
161+
162+
getCapabilities(): AdapterCapabilities {
163+
return {
164+
supports_history: false,
165+
supports_temporal_replay: false,
166+
supports_provenance: false,
167+
supports_abstention: false,
168+
};
169+
}
170+
171+
async reset() { /* clear state */ }
172+
async teardown() { /* close connections */ }
173+
174+
private async storeMessage(content: string, timestamp: string) { /* ... */ }
175+
private async queryMemory(prompt: string): Promise<string | null> { /* ... */ }
176+
}
177+
```
178+
179+
## Included Adapters
180+
181+
| Adapter | File | Capabilities |
182+
|---------|------|-------------|
183+
| `baseline` | `src/adapters/baseline.ts` | None (naive KV store, overwrites on update) |
184+
| `neotoma` | `src/adapters/neotoma.ts` | History, temporal replay, provenance |

0 commit comments

Comments
 (0)