@visual-z/xeno 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +36 -9
- package/docs/foundations.md +1 -1
- package/package.json +3 -1
- package/src/apps/cli.ts +4 -3
- package/src/apps/commands.ts +8 -0
- package/src/apps/goal-flow.ts +70 -0
- package/src/apps/render.ts +0 -2
- package/src/apps/session-hub.ts +6 -5
- package/src/apps/session-lifecycle.ts +64 -0
- package/src/apps/shell.ts +89 -34
- package/src/apps/startup.ts +32 -31
- package/src/apps/ui/app.ts +38 -6
- package/src/apps/ui/control-model.ts +18 -1
- package/src/apps/ui/surface.ts +2 -0
- package/src/apps/update-check.ts +1 -1
- package/src/core/acceptance.ts +41 -9
- package/src/core/blind-checks.ts +72 -0
- package/src/core/branch-receipt.ts +3 -1
- package/src/core/checkpoints.ts +21 -0
- package/src/core/churn.ts +2 -2
- package/src/core/completion-review.ts +18 -11
- package/src/core/continuation.ts +11 -10
- package/src/core/control-view.ts +5 -8
- package/src/core/deadline.ts +2 -1
- package/src/core/derive.ts +0 -7
- package/src/core/environment.ts +2 -2
- package/src/core/event-validation.ts +19 -6
- package/src/core/events.ts +41 -32
- package/src/core/evidence-ladder.ts +62 -0
- package/src/core/evidence-links.ts +0 -10
- package/src/core/evidence.ts +1 -2
- package/src/core/export.ts +6 -11
- package/src/core/goal.ts +200 -0
- package/src/core/history.ts +1 -11
- package/src/core/json-event.ts +0 -6
- package/src/core/outcome.ts +0 -1
- package/src/core/program-status.ts +11 -2
- package/src/core/progress.ts +14 -8
- package/src/core/project.ts +6 -16
- package/src/core/read-bounds.ts +11 -2
- package/src/core/read-observation.ts +24 -0
- package/src/core/request-budget.ts +13 -3
- package/src/core/request-state.ts +178 -19
- package/src/core/response-recovery.ts +2 -1
- package/src/core/session-entry.ts +8 -3
- package/src/core/session-exit.ts +19 -3
- package/src/core/session-format.ts +169 -0
- package/src/core/settings.ts +7 -27
- package/src/core/stall.ts +30 -7
- package/src/core/stop-report.ts +4 -3
- package/src/core/transcript.ts +22 -2
- package/src/runtime/blind-check.ts +36 -0
- package/src/runtime/counter-check.ts +74 -0
- package/src/runtime/exec-sessions.ts +11 -0
- package/src/runtime/foundation.ts +46 -8
- package/src/runtime/loop.ts +239 -430
- package/src/runtime/oauth.ts +1 -3
- package/src/runtime/provider-request.ts +2 -1
- package/src/runtime/runtime-target.ts +2 -2
- package/src/runtime/session-control.ts +4 -4
- package/src/runtime/tools.ts +44 -42
- package/src/runtime/version.ts +12 -0
- package/src/runtime/workspace-lifecycle.ts +31 -0
- package/src/store/blind-checks.ts +32 -0
- package/src/store/checkpoint-history.ts +1 -12
- package/src/store/file-index.ts +88 -7
- package/src/store/log.ts +25 -27
- package/src/store/recovery.ts +1 -3
- package/src/store/replay.ts +2 -2
- package/src/store/restore.ts +10 -202
- package/src/store/search.ts +7 -5
- package/src/store/session-archive.ts +0 -1
- package/src/store/sessions.ts +31 -33
- package/src/store/workspace-clone.ts +42 -0
- package/src/store/workspace.ts +10 -157
- package/src/tui/ui/status.ts +6 -2
- package/src/core/benchmark.ts +0 -195
- package/src/core/evaluation-coverage.ts +0 -74
- package/src/core/experiment.ts +0 -173
- package/src/core/harness-comparison.ts +0 -90
- package/src/core/harness-diagnostics.ts +0 -206
- package/src/core/native-eval.ts +0 -25
- package/src/core/policy-evaluation.ts +0 -55
- package/src/core/query-eval-contract.ts +0 -14
- package/src/core/recall-analysis.ts +0 -90
- package/src/core/recall-trial.ts +0 -153
- package/src/core/recovery-corpus.ts +0 -17
- package/src/core/recovery-evaluation.ts +0 -60
- package/src/core/tool-mode-protocol.ts +0 -25
- package/src/core/trial-lifecycle.ts +0 -14
- package/src/core/window-trial.ts +0 -65
- package/src/index.ts +0 -10
- package/src/runtime/benchmark.ts +0 -187
- package/src/runtime/harness-comparison.ts +0 -376
- package/src/runtime/harness-eval.ts +0 -462
- package/src/runtime/native-eval-fixtures.ts +0 -33
- package/src/runtime/native-eval.ts +0 -146
- package/src/runtime/query-eval-verifier.ts +0 -28
- package/src/runtime/recall-trial.ts +0 -226
- package/src/runtime/recovery-coverage.ts +0 -71
- package/src/runtime/recovery-evidence.ts +0 -76
- package/src/runtime/recovery-fixtures.ts +0 -116
- package/src/runtime/tool-mode-eval.ts +0 -146
- package/src/runtime/trial-control.ts +0 -45
- package/src/runtime/window-trial.ts +0 -141
- package/src/store/git-branch.ts +0 -35
- package/src/tui/ui/testing.ts +0 -5
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/** Shared evaluation protocol, not a production context policy (research N.10). */
|
|
2
|
-
import { shuffled } from "./experiment.ts";
|
|
3
|
-
import { canReserve, type BenchmarkUsage } from "./benchmark.ts";
|
|
4
|
-
|
|
5
|
-
export const COMPARISON_ARMS = ["xeno-windowed", "xeno-continuous", "pi"] as const;
|
|
6
|
-
export type ComparisonArm = typeof COMPARISON_ARMS[number];
|
|
7
|
-
export interface ComparisonJob { id: string; arm: ComparisonArm }
|
|
8
|
-
|
|
9
|
-
/** Null means an explicit provider-balance authorization, never zero allowance. */
|
|
10
|
-
export function comparisonCanReserve(charged: number, reservation: number, budget: number | null): boolean {
|
|
11
|
-
if (![charged, reservation, charged + reservation].every(Number.isFinite) || charged < 0 || reservation <= 0) return false;
|
|
12
|
-
return budget === null || canReserve(charged, reservation, budget);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** DeepSeek documents 402 as insufficient balance; other errors need their own diagnosis. */
|
|
16
|
-
export function comparisonFailureKind(status: number | null): "balance" | "provider" {
|
|
17
|
-
return status === 402 ? "balance" : "provider";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function comparisonSchedule(ids: readonly string[], seed: number): ComparisonJob[] {
|
|
21
|
-
if (ids.length === 0 || new Set(ids).size !== ids.length) throw new Error("empty or duplicate comparison tasks");
|
|
22
|
-
const arms = shuffled(COMPARISON_ARMS, seed);
|
|
23
|
-
return [...ids].sort().flatMap((id, i) => arms.map((_, position) => ({ id, arm: arms[(i + position) % arms.length]! })));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function jsonObject(value: unknown): Record<string, unknown> {
|
|
27
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected JSON object");
|
|
28
|
-
return value as Record<string, unknown>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Missing cache usage is unknown, never a fabricated zero or a discounted bill. */
|
|
32
|
-
export function comparisonUsage(body: unknown): BenchmarkUsage | null {
|
|
33
|
-
try {
|
|
34
|
-
const usage = jsonObject(jsonObject(body)["usage"]);
|
|
35
|
-
const prompt = usage["prompt_tokens"], output = usage["completion_tokens"];
|
|
36
|
-
const details = usage["prompt_tokens_details"];
|
|
37
|
-
const vendorCached = usage["prompt_cache_hit_tokens"];
|
|
38
|
-
const standardCached = details ? jsonObject(details)["cached_tokens"] : undefined;
|
|
39
|
-
if (vendorCached !== undefined && standardCached !== undefined && vendorCached !== standardCached) return null;
|
|
40
|
-
const cached = vendorCached ?? standardCached;
|
|
41
|
-
if (![prompt, output, cached].every((n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0)) return null;
|
|
42
|
-
const p = prompt as number, o = output as number, c = cached as number;
|
|
43
|
-
if (p === 0 || o === 0 || c > p) return null;
|
|
44
|
-
if (usage["prompt_cache_miss_tokens"] !== undefined && usage["prompt_cache_miss_tokens"] !== p - c) return null;
|
|
45
|
-
return { input_tokens: p - c, output_tokens: o, cache_read_input_tokens: c, cache_creation_input_tokens: 0 };
|
|
46
|
-
} catch { return null; }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Buffer upstream responses for one billing boundary; do not report this as TTFT. */
|
|
50
|
-
export function completionSse(value: unknown): string {
|
|
51
|
-
const body = jsonObject(value);
|
|
52
|
-
const choices = body["choices"];
|
|
53
|
-
if (!Array.isArray(choices) || choices.length !== 1) throw new Error("expected one completion");
|
|
54
|
-
const choice = jsonObject(choices[0]), message = jsonObject(choice["message"]);
|
|
55
|
-
const calls = message["tool_calls"];
|
|
56
|
-
const finish = choice["finish_reason"];
|
|
57
|
-
if (typeof finish !== "string" || !["stop", "tool_calls", "length", "content_filter"].includes(finish)) throw new Error("incomplete completion");
|
|
58
|
-
if (calls !== undefined && !Array.isArray(calls)) throw new Error("invalid tool calls");
|
|
59
|
-
const delta = { ...message, ...(Array.isArray(calls) ? { tool_calls: calls.map((call: unknown, index: number) => ({ ...jsonObject(call), index })) } : {}) };
|
|
60
|
-
const common = { id: body["id"], object: "chat.completion.chunk", created: body["created"], model: body["model"] };
|
|
61
|
-
return [
|
|
62
|
-
{ ...common, choices: [{ index: 0, delta, finish_reason: null }] },
|
|
63
|
-
{ ...common, choices: [{ index: 0, delta: {}, finish_reason: finish }] },
|
|
64
|
-
{ ...common, choices: [], usage: body["usage"] },
|
|
65
|
-
].map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("") + "data: [DONE]\n\n";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** JSONL permits U+2028 in strings. Only LF frames an event, including Pi's output. */
|
|
69
|
-
export function comparisonLines(buffer: string, chunk: string): { buffer: string; values: unknown[] } {
|
|
70
|
-
const lines = (buffer + chunk).split("\n");
|
|
71
|
-
const remainder = lines.pop()!;
|
|
72
|
-
return { buffer: remainder, values: lines.filter((line) => line.trim() !== "").map((line) => JSON.parse(line) as unknown) };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function comparisonSummary(plan: readonly ComparisonJob[], results: readonly (ComparisonJob & { status: string; correct: boolean })[]) {
|
|
76
|
-
const keys = new Set(plan.map((job) => `${job.id}/${job.arm}`)), seen = new Set<string>();
|
|
77
|
-
if (keys.size !== plan.length) throw new Error("duplicate planned comparison job");
|
|
78
|
-
for (const result of results) {
|
|
79
|
-
const key = `${result.id}/${result.arm}`;
|
|
80
|
-
if (!keys.has(key) || seen.has(key) || (result.correct && result.status !== "answered")) throw new Error("invalid comparison outcome");
|
|
81
|
-
seen.add(key);
|
|
82
|
-
}
|
|
83
|
-
return COMPARISON_ARMS.map((arm) => {
|
|
84
|
-
const planned = plan.filter((job) => job.arm === arm).length, rows = results.filter((row) => row.arm === arm);
|
|
85
|
-
const correct = rows.filter((row) => row.correct).length;
|
|
86
|
-
return { arm, planned, attempted: rows.length, answered: rows.filter((row) => row.status === "answered").length,
|
|
87
|
-
correct, missing: planned - rows.length, accuracy: rows.length ? correct / rows.length : null,
|
|
88
|
-
complete: rows.length === planned };
|
|
89
|
-
}).filter((row) => row.planned > 0);
|
|
90
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/** D11 / O.3: classify durable execution facts, never error words in tool output. */
|
|
2
|
-
import type { LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
-
import type { BenchmarkUsage } from "./benchmark.ts";
|
|
4
|
-
import { canonicalValue } from "./continuation.ts";
|
|
5
|
-
|
|
6
|
-
export type ResultClass = "success" | "invalid_input" | "cancelled" | "timeout" | "command_nonzero" |
|
|
7
|
-
"execution_failed" | "rejected" | "unclassified_failure";
|
|
8
|
-
|
|
9
|
-
export function classifyToolResult(event: ToolResultEvent): ResultClass {
|
|
10
|
-
const code = event.diagnostic?.code;
|
|
11
|
-
if (code === "invalid_input") return "invalid_input";
|
|
12
|
-
if (code === "cancelled" || event.outcome?.reason === "cancelled") return "cancelled";
|
|
13
|
-
if (code === "timeout" || event.outcome?.reason === "timeout") return "timeout";
|
|
14
|
-
if (event.effect?.kind === "command" && event.effect.exit !== 0) return "command_nonzero";
|
|
15
|
-
if (event.ok) return "success";
|
|
16
|
-
if (event.diagnostic?.execution === "not_started" ||
|
|
17
|
-
["version_mismatch", "not_found", "ambiguous_edit", "permission_denied", "environment_missing"].includes(code ?? "")) return "rejected";
|
|
18
|
-
if (code === "execution_failed" || code === "unknown_effect" || event.outcome?.reason === "execution_error") return "execution_failed";
|
|
19
|
-
return "unclassified_failure";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface DiagnosticRequest {
|
|
23
|
-
request: number;
|
|
24
|
-
node: number | null;
|
|
25
|
-
usage: BenchmarkUsage | null;
|
|
26
|
-
responseModel: string | null;
|
|
27
|
-
failure: string | null;
|
|
28
|
-
firstDeltaMs: number | null;
|
|
29
|
-
wallMs: number | null;
|
|
30
|
-
prefixUnchanged: boolean | null;
|
|
31
|
-
sharedInputBytes: number | null;
|
|
32
|
-
frozenSha256: string | null;
|
|
33
|
-
/** Raw terminal counts can remain known when a cache breakdown is absent. */
|
|
34
|
-
reportedPromptTokens?: number | null;
|
|
35
|
-
reportedOutputTokens?: number | null;
|
|
36
|
-
}
|
|
37
|
-
export interface ToolCounters {
|
|
38
|
-
calls: number; providerCalls: number; invocations: number; nestedInvocations: number;
|
|
39
|
-
results: number; pairedCalls: number; orphanCalls: number; orphanResults: number;
|
|
40
|
-
duplicateProviderCalls: number; duplicateInvocations: number; duplicateResults: number;
|
|
41
|
-
repeatedProviderInputs: number; repeatedInvocationInputs: number;
|
|
42
|
-
classes: Record<ResultClass, number>;
|
|
43
|
-
diagnosticCodes: Record<string, number>;
|
|
44
|
-
commandExits: Record<string, number>;
|
|
45
|
-
expectedNonzeroCommands: number;
|
|
46
|
-
undeclaredNonzeroCommands: number;
|
|
47
|
-
}
|
|
48
|
-
const classes = (): Record<ResultClass, number> => ({ success: 0, invalid_input: 0, cancelled: 0, timeout: 0,
|
|
49
|
-
command_nonzero: 0, execution_failed: 0, rejected: 0, unclassified_failure: 0 });
|
|
50
|
-
const counters = (): ToolCounters => ({ calls: 0, providerCalls: 0, invocations: 0, nestedInvocations: 0,
|
|
51
|
-
results: 0, pairedCalls: 0, orphanCalls: 0, orphanResults: 0, duplicateProviderCalls: 0,
|
|
52
|
-
duplicateInvocations: 0, duplicateResults: 0, repeatedProviderInputs: 0, repeatedInvocationInputs: 0,
|
|
53
|
-
classes: classes(), diagnosticCodes: {}, commandExits: {}, expectedNonzeroCommands: 0, undeclaredNonzeroCommands: 0 });
|
|
54
|
-
function add(record: Record<string, number>, key: string): void { record[key] = (record[key] ?? 0) + 1; }
|
|
55
|
-
const validCount = (value: number): boolean => Number.isSafeInteger(value) && value >= 0;
|
|
56
|
-
const reportedCount = (value: number | null | undefined): number | null => typeof value === "number" && validCount(value) ? value : null;
|
|
57
|
-
const validTime = (value: number | null): number | null => value !== null && Number.isFinite(value) && value >= 0 ? value : null;
|
|
58
|
-
function observedUsage(usage: BenchmarkUsage | null): BenchmarkUsage | null {
|
|
59
|
-
return usage && [usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens,
|
|
60
|
-
usage.cache_creation_input_tokens].every(validCount) ? usage : null;
|
|
61
|
-
}
|
|
62
|
-
function quantile(values: readonly number[], q: number): number | null {
|
|
63
|
-
return values.length ? values.toSorted((a, b) => a - b)[Math.max(0, Math.ceil(values.length * q) - 1)]! : null;
|
|
64
|
-
}
|
|
65
|
-
interface Call {
|
|
66
|
-
name: string; provider: boolean; invocation: boolean; nested: boolean;
|
|
67
|
-
providerInput: string | null; invocationInput: string | null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Inputs and outputs remain private. Equal-input counts use canonical original
|
|
72
|
-
* model arguments and immutable admitted-input hashes separately (O.3 / Pi A).
|
|
73
|
-
* Expected nonzero exits need an evaluator declaration; an exit alone cannot
|
|
74
|
-
* prove a command was accidental, and model prose cannot establish intent.
|
|
75
|
-
*/
|
|
76
|
-
export function summarizeHarnessDiagnostics(input: {
|
|
77
|
-
events: readonly LogEvent[]; requests: readonly DiagnosticRequest[];
|
|
78
|
-
expectedNonzeroCallIds?: readonly string[];
|
|
79
|
-
}) {
|
|
80
|
-
const tools: Record<string, ToolCounters> = {};
|
|
81
|
-
const tool = (name: string): ToolCounters => tools[name] ??= counters();
|
|
82
|
-
const calls = new Map<string, Call>(), results = new Map<string, ToolResultEvent[]>(), seenEvents = new Set<string>();
|
|
83
|
-
const guidance: Record<string, number> = {}, cuts: Record<string, number> = {}, cleanupProcessExits: Record<string, number> = {};
|
|
84
|
-
let duplicateEvents = 0, interruptions = 0;
|
|
85
|
-
for (const event of input.events) {
|
|
86
|
-
if (seenEvents.has(event.id)) { duplicateEvents++; continue; }
|
|
87
|
-
seenEvents.add(event.id);
|
|
88
|
-
if (event.t === "guidance") add(guidance, event.reason);
|
|
89
|
-
if (event.t === "cut") add(cuts, event.reason);
|
|
90
|
-
if (event.t === "interrupt") interruptions++;
|
|
91
|
-
if (event.t === "process_exit") add(cleanupProcessExits, String(event.exit));
|
|
92
|
-
if (event.t === "assistant") for (const block of event.blocks) {
|
|
93
|
-
if (block.type !== "tool_use") continue;
|
|
94
|
-
const existing = calls.get(block.call_id), stats = tool(block.name);
|
|
95
|
-
if (existing?.provider) stats.duplicateProviderCalls++;
|
|
96
|
-
calls.set(block.call_id, { name: existing?.name ?? block.name, provider: true,
|
|
97
|
-
invocation: existing?.invocation ?? false, nested: existing?.nested ?? false,
|
|
98
|
-
providerInput: canonicalValue(block.input), invocationInput: existing?.invocationInput ?? null });
|
|
99
|
-
}
|
|
100
|
-
if (event.t === "tool_invocation") {
|
|
101
|
-
const existing = calls.get(event.call_id), stats = tool(event.name);
|
|
102
|
-
if (existing?.invocation) stats.duplicateInvocations++;
|
|
103
|
-
calls.set(event.call_id, { name: existing?.name ?? event.name, provider: existing?.provider ?? false,
|
|
104
|
-
invocation: true, nested: event.parent_call_id !== undefined,
|
|
105
|
-
providerInput: existing?.providerInput ?? null, invocationInput: event.input_ref.sha256 });
|
|
106
|
-
}
|
|
107
|
-
if (event.t === "tool_result") {
|
|
108
|
-
const found = results.get(event.call_id) ?? [];
|
|
109
|
-
found.push(event); results.set(event.call_id, found);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const providerInputs = new Map<string, Set<string>>(), invocationInputs = new Map<string, Set<string>>();
|
|
113
|
-
const repeat = (groups: Map<string, Set<string>>, name: string, key: string | null): boolean => {
|
|
114
|
-
if (key === null) return false;
|
|
115
|
-
const seen = groups.get(name) ?? new Set<string>(), repeated = seen.has(key);
|
|
116
|
-
seen.add(key); groups.set(name, seen); return repeated;
|
|
117
|
-
};
|
|
118
|
-
for (const [id, call] of calls) {
|
|
119
|
-
const stats = tool(call.name); stats.calls++;
|
|
120
|
-
if (call.provider) stats.providerCalls++;
|
|
121
|
-
if (call.invocation) stats.invocations++;
|
|
122
|
-
if (call.nested) stats.nestedInvocations++;
|
|
123
|
-
if (repeat(providerInputs, call.name, call.providerInput)) stats.repeatedProviderInputs++;
|
|
124
|
-
if (repeat(invocationInputs, call.name, call.invocationInput)) stats.repeatedInvocationInputs++;
|
|
125
|
-
if (results.has(id)) stats.pairedCalls++; else stats.orphanCalls++;
|
|
126
|
-
}
|
|
127
|
-
const expected = new Set(input.expectedNonzeroCallIds ?? []);
|
|
128
|
-
for (const [id, found] of results) {
|
|
129
|
-
const call = calls.get(id), stats = tool(call?.name ?? "<orphan>");
|
|
130
|
-
stats.duplicateResults += Math.max(0, found.length - 1);
|
|
131
|
-
for (const event of found) {
|
|
132
|
-
stats.results++;
|
|
133
|
-
if (!call) stats.orphanResults++;
|
|
134
|
-
stats.classes[classifyToolResult(event)]++;
|
|
135
|
-
if (event.diagnostic) add(stats.diagnosticCodes, event.diagnostic.code);
|
|
136
|
-
if (event.effect?.kind === "command") {
|
|
137
|
-
add(stats.commandExits, String(event.effect.exit));
|
|
138
|
-
if (event.effect.exit !== 0) {
|
|
139
|
-
if (expected.has(id)) stats.expectedNonzeroCommands++; else stats.undeclaredNonzeroCommands++;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const requestIds = new Set<number>(), requests: {
|
|
145
|
-
request: number; node: number | null; usageKnown: boolean; promptTokens: number | null;
|
|
146
|
-
cachedTokens: number | null; cacheWriteTokens: number | null; outputTokens: number | null;
|
|
147
|
-
cacheHitRate: number | null; responseModel: string | null; failure: string | null;
|
|
148
|
-
firstDeltaMs: number | null; wallMs: number | null; prefixUnchanged: boolean | null; sharedInputBytes: number | null;
|
|
149
|
-
}[] = [];
|
|
150
|
-
const models: Record<string, number> = {};
|
|
151
|
-
const usage = { knownRequests: 0, unknownRequests: 0, requestsWithCacheReads: 0, promptTokens: 0, cachedTokens: 0,
|
|
152
|
-
cacheWriteTokens: 0, outputTokens: 0, promptKnownRequests: 0, promptUnknownRequests: 0,
|
|
153
|
-
cacheCoveredPromptTokens: 0, cacheCoverageRate: null as number | null,
|
|
154
|
-
observedCacheHitRate: null as number | null, cacheHitRate: null as number | null };
|
|
155
|
-
const prefix = { sameNodeComparisons: 0, sameNodeMutations: 0, boundaryComparisons: 0, boundaryMutations: 0,
|
|
156
|
-
unknownComparisons: 0, sameNodeFrozenMutations: 0, boundaryFrozenMutations: 0 };
|
|
157
|
-
let previous: DiagnosticRequest | undefined, duplicateRequests = 0;
|
|
158
|
-
for (const request of input.requests.toSorted((a, b) => a.request - b.request)) {
|
|
159
|
-
if (requestIds.has(request.request)) { duplicateRequests++; continue; }
|
|
160
|
-
requestIds.add(request.request);
|
|
161
|
-
const observed = observedUsage(request.usage);
|
|
162
|
-
const prompt = reportedCount(request.reportedPromptTokens) ?? (observed ?
|
|
163
|
-
observed.input_tokens + observed.cache_read_input_tokens + observed.cache_creation_input_tokens : null);
|
|
164
|
-
const output = reportedCount(request.reportedOutputTokens) ?? observed?.output_tokens ?? null;
|
|
165
|
-
if (prompt !== null) { usage.promptKnownRequests++; usage.promptTokens += prompt; } else usage.promptUnknownRequests++;
|
|
166
|
-
if (output !== null) usage.outputTokens += output;
|
|
167
|
-
if (observed) {
|
|
168
|
-
usage.knownRequests++; usage.cacheCoveredPromptTokens += prompt!; usage.cachedTokens += observed.cache_read_input_tokens;
|
|
169
|
-
usage.cacheWriteTokens += observed.cache_creation_input_tokens;
|
|
170
|
-
if (observed.cache_read_input_tokens > 0) usage.requestsWithCacheReads++;
|
|
171
|
-
} else usage.unknownRequests++;
|
|
172
|
-
if (request.responseModel !== null) add(models, request.responseModel);
|
|
173
|
-
if (previous) {
|
|
174
|
-
if (previous.node === null || request.node === null || request.prefixUnchanged === null) prefix.unknownComparisons++;
|
|
175
|
-
else if (previous.node === request.node) { prefix.sameNodeComparisons++; if (!request.prefixUnchanged) prefix.sameNodeMutations++; }
|
|
176
|
-
else { prefix.boundaryComparisons++; if (!request.prefixUnchanged) prefix.boundaryMutations++; }
|
|
177
|
-
if (previous.frozenSha256 !== null && request.frozenSha256 !== null && previous.frozenSha256 !== request.frozenSha256) {
|
|
178
|
-
if (previous.node === request.node && request.node !== null) prefix.sameNodeFrozenMutations++;
|
|
179
|
-
else if (previous.node !== null && request.node !== null) prefix.boundaryFrozenMutations++;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
previous = request;
|
|
183
|
-
requests.push({ request: request.request, node: request.node, usageKnown: observed !== null, promptTokens: prompt,
|
|
184
|
-
cachedTokens: observed?.cache_read_input_tokens ?? null, cacheWriteTokens: observed?.cache_creation_input_tokens ?? null,
|
|
185
|
-
outputTokens: output, cacheHitRate: observed && prompt ? observed.cache_read_input_tokens / prompt : null,
|
|
186
|
-
responseModel: request.responseModel, failure: request.failure, firstDeltaMs: validTime(request.firstDeltaMs),
|
|
187
|
-
wallMs: validTime(request.wallMs), prefixUnchanged: request.prefixUnchanged, sharedInputBytes: request.sharedInputBytes });
|
|
188
|
-
}
|
|
189
|
-
usage.observedCacheHitRate = usage.cacheCoveredPromptTokens > 0 ? usage.cachedTokens / usage.cacheCoveredPromptTokens : null;
|
|
190
|
-
usage.cacheCoverageRate = usage.promptUnknownRequests === 0 && usage.promptTokens > 0 ? usage.cacheCoveredPromptTokens / usage.promptTokens : null;
|
|
191
|
-
usage.cacheHitRate = usage.unknownRequests === 0 && usage.promptTokens > 0 ? usage.cachedTokens / usage.promptTokens : null;
|
|
192
|
-
const delta = requests.flatMap(r => r.firstDeltaMs === null ? [] : [r.firstDeltaMs]);
|
|
193
|
-
const totals = counters();
|
|
194
|
-
for (const stats of Object.values(tools)) {
|
|
195
|
-
for (const key of Object.keys(totals) as (keyof ToolCounters)[]) {
|
|
196
|
-
const value = stats[key];
|
|
197
|
-
if (typeof value === "number") (totals[key] as number) += value;
|
|
198
|
-
}
|
|
199
|
-
for (const key of Object.keys(stats.classes) as ResultClass[]) totals.classes[key] += stats.classes[key];
|
|
200
|
-
for (const key of Object.keys(stats.commandExits)) totals.commandExits[key] = (totals.commandExits[key] ?? 0) + stats.commandExits[key]!;
|
|
201
|
-
for (const key of Object.keys(stats.diagnosticCodes)) totals.diagnosticCodes[key] = (totals.diagnosticCodes[key] ?? 0) + stats.diagnosticCodes[key]!;
|
|
202
|
-
}
|
|
203
|
-
return { eventCount: seenEvents.size, duplicateEvents, tools, totals, guidance, cuts, interruptions, cleanupProcessExits,
|
|
204
|
-
requestCount: requests.length, duplicateRequests, models, usage, prefix, requests,
|
|
205
|
-
firstDeltaMs: { samples: delta.length, missing: requests.length - delta.length, p50: quantile(delta, 0.5), p95: quantile(delta, 0.95) } };
|
|
206
|
-
}
|
package/src/core/native-eval.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/** Q.20: native subscription measurements are standard API price equivalents, never invoices. */
|
|
2
|
-
import { measuredCharge, requestReservation, type BenchmarkPrice, type BenchmarkUsage } from "./benchmark.ts";
|
|
3
|
-
|
|
4
|
-
// Verified Luna short-context tier. Bound wire bytes conservatively before dispatch;
|
|
5
|
-
// crossing this tier needs a new protocol, not silent use of the cheaper price.
|
|
6
|
-
export const NATIVE_EVAL_INPUT_LIMIT = 272000;
|
|
7
|
-
export function nativeUsage(response: unknown): BenchmarkUsage | null {
|
|
8
|
-
if (!response || typeof response !== "object") return null;
|
|
9
|
-
const raw = (response as Record<string, unknown>)["usage"];
|
|
10
|
-
if (!raw || typeof raw !== "object") return null;
|
|
11
|
-
const u = raw as Record<string, unknown>, details = u["input_tokens_details"] as Record<string, unknown> | undefined;
|
|
12
|
-
const input = u["input_tokens"], output = u["output_tokens"], cached = details?.["cached_tokens"] ?? 0, created = details?.["cache_write_tokens"] ?? 0;
|
|
13
|
-
if (![input, output, cached, created].every(n => typeof n === "number" && Number.isSafeInteger(n) && n >= 0) ||
|
|
14
|
-
Number(input) <= 0 || Number(output) <= 0 || Number(input) > NATIVE_EVAL_INPUT_LIMIT || Number(cached) + Number(created) > Number(input)) return null;
|
|
15
|
-
return { input_tokens: Number(input) - Number(cached) - Number(created), output_tokens: Number(output),
|
|
16
|
-
cache_read_input_tokens: Number(cached), cache_creation_input_tokens: Number(created) };
|
|
17
|
-
}
|
|
18
|
-
export function nativeReservation(bytes: number, modelMaxOutput: number, price: BenchmarkPrice): number {
|
|
19
|
-
if (bytes + 4096 > NATIVE_EVAL_INPUT_LIMIT || price.cacheWrite === undefined) throw Error("native eval price tier or cache-write rate is not covered");
|
|
20
|
-
return requestReservation(bytes, modelMaxOutput, { ...price, input: Math.max(price.input, price.cacheWrite) });
|
|
21
|
-
}
|
|
22
|
-
export function nativeMeasured(response: unknown, price: BenchmarkPrice): number | null {
|
|
23
|
-
const usage = nativeUsage(response);
|
|
24
|
-
return usage && price.cacheWrite !== undefined ? measuredCharge(usage, price) : null;
|
|
25
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** N.7: repetitions stay attached to their task; they are not independent questions. */
|
|
2
|
-
import { type ContextPolicy } from "./context-policy.ts";
|
|
3
|
-
import { shuffled } from "./experiment.ts";
|
|
4
|
-
|
|
5
|
-
// N.7 is a frozen three-arm protocol; new production policies cannot change it.
|
|
6
|
-
const EVALUATED_POLICIES: readonly ContextPolicy[] = ["segmented", "continuous", "adaptive"];
|
|
7
|
-
|
|
8
|
-
export interface PolicyJob { id: string; policy: ContextPolicy; repeat: number }
|
|
9
|
-
export function policyJobKey(job: PolicyJob): string { return JSON.stringify([job.id, job.policy, job.repeat]); }
|
|
10
|
-
export function policySchedule(ids: readonly string[], repetitions: number, seed: number): PolicyJob[] {
|
|
11
|
-
if (ids.length === 0 || new Set(ids).size !== ids.length || !Number.isSafeInteger(repetitions) || repetitions < 1) throw new Error("invalid policy trial design");
|
|
12
|
-
const policies = shuffled(EVALUATED_POLICIES, seed);
|
|
13
|
-
const jobs: PolicyJob[] = [];
|
|
14
|
-
let block = 0;
|
|
15
|
-
for (let repeat = 0; repeat < repetitions; repeat++) for (const id of shuffled([...ids].sort(), seed + repeat + 1)) {
|
|
16
|
-
for (let position = 0; position < policies.length; position++) jobs.push({ id, repeat, policy: policies[(block + position) % policies.length]! });
|
|
17
|
-
block++;
|
|
18
|
-
}
|
|
19
|
-
return jobs;
|
|
20
|
-
}
|
|
21
|
-
export interface PolicyObservation extends PolicyJob {
|
|
22
|
-
status: string; correct: boolean; peakRateUsd: number; reservedUnknownUsd: number;
|
|
23
|
-
requests: number; cuts: number; incompleteHandoffs: number; wallMs: number;
|
|
24
|
-
usage: { prompt_tokens: number; cache_read_input_tokens: number; output_tokens: number };
|
|
25
|
-
}
|
|
26
|
-
export function summarizePolicies(plan: readonly PolicyJob[], results: readonly PolicyObservation[]) {
|
|
27
|
-
const planned = new Set(plan.map(policyJobKey)), observed = new Set<string>();
|
|
28
|
-
if (planned.size !== plan.length) throw new Error("duplicate planned policy job");
|
|
29
|
-
for (const result of results) {
|
|
30
|
-
const key = policyJobKey(result);
|
|
31
|
-
if (!planned.has(key) || observed.has(key) || (result.correct && result.status !== "answered")) throw new Error("invalid policy result");
|
|
32
|
-
observed.add(key);
|
|
33
|
-
}
|
|
34
|
-
return [...new Set(plan.map((job) => job.policy))].sort().map((policy) => {
|
|
35
|
-
const schedule = plan.filter((job) => job.policy === policy), group = results.filter((row) => row.policy === policy);
|
|
36
|
-
const sum = (read: (row: PolicyObservation) => number) => group.reduce((n, row) => n + read(row), 0);
|
|
37
|
-
const correct = group.filter((row) => row.correct).length;
|
|
38
|
-
const peakRateUsd = sum((row) => row.peakRateUsd), reservedUnknownUsd = sum((row) => row.reservedUnknownUsd);
|
|
39
|
-
const promptTokens = sum((row) => row.usage.prompt_tokens), cachedTokens = sum((row) => row.usage.cache_read_input_tokens);
|
|
40
|
-
return { policy, planned: schedule.length, attempted: group.length, missing: schedule.length - group.length,
|
|
41
|
-
independentTasks: new Set(schedule.map((job) => job.id)).size, correct,
|
|
42
|
-
accuracy: group.length ? correct / group.length : null, complete: group.length === schedule.length,
|
|
43
|
-
plannedSolvedFraction: schedule.length ? correct / schedule.length : null,
|
|
44
|
-
peakRateUsd, reservedUnknownUsd, allAttemptCostPerSuccessUsd: correct ? (peakRateUsd + reservedUnknownUsd) / correct : null,
|
|
45
|
-
promptTokens, cachedTokens, outputTokens: sum((row) => row.usage.output_tokens),
|
|
46
|
-
cacheHitRate: promptTokens ? cachedTokens / promptTokens : null,
|
|
47
|
-
requests: sum((row) => row.requests), cuts: sum((row) => row.cuts), incompleteHandoffs: sum((row) => row.incompleteHandoffs),
|
|
48
|
-
wallMs: sum((row) => row.wallMs),
|
|
49
|
-
tasks: [...new Set(schedule.map((job) => job.id))].map((id) => ({ id,
|
|
50
|
-
planned: schedule.filter((job) => job.id === id).length,
|
|
51
|
-
attempted: group.filter((row) => row.id === id).length,
|
|
52
|
-
correct: group.filter((row) => row.id === id && row.correct).length })),
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** Q.21: task-specific shape advice cannot reveal the independent numeric oracle. */
|
|
2
|
-
import type { ToolModeTask } from "./tool-mode-protocol.ts";
|
|
3
|
-
|
|
4
|
-
export function queryEvalContract(task: ToolModeTask, paths: readonly string[], answerSha256: string) {
|
|
5
|
-
const columns = task === "tool-small" ? ["amount"] : task === "tool-batch" ? ["total"] : ["team", "total"];
|
|
6
|
-
const rowCount = task === "tool-aggregate" ? 4 : 1;
|
|
7
|
-
const operation = task === "tool-small" ? "select amount; return one row with only amount" : task === "tool-batch"
|
|
8
|
-
? "sum amount across all records as total; return one row with only total per source"
|
|
9
|
-
: "keep enabled=true, group by team, sum amount as total, and sort by team ascending; return four rows with columns team,total in that order";
|
|
10
|
-
const instruction = "Use query on each of " + paths.join(", ") + " to " + operation +
|
|
11
|
-
'. Write compact JSON with a trailing newline to answer.txt: [{"path":"data-0.jsonl","rows":[...]}], one item per source in the listed order. ' +
|
|
12
|
-
"Preserve all fixture inputs and run bun run gate.";
|
|
13
|
-
return { instruction, columns, rowCount, paths: [...paths], answerSha256 };
|
|
14
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-registered F2 analysis (docs/tasks/bench-recall-pilot.md). Pure.
|
|
3
|
-
*
|
|
4
|
-
* The unit is a (seed, ratio) block: arms in one block face byte-identical
|
|
5
|
-
* pages and questions, blocks are independent, stages within a run are not.
|
|
6
|
-
*/
|
|
7
|
-
export interface RecallStageRow {
|
|
8
|
-
stage: number; kind: "recent" | "recall" | "latest"; distance: number;
|
|
9
|
-
visibleThroughout: boolean | null; verdict: { ok: boolean; reason: string }; status: string;
|
|
10
|
-
}
|
|
11
|
-
export interface RecallRunRow {
|
|
12
|
-
arm: string; ratio: number; seed: number; planned: number; unrun: number; boundaries: number;
|
|
13
|
-
knownCost: number; reservedUnknown: number; stages: RecallStageRow[]; modelFiles: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Two-sided exact sign test; ties are dropped before counting. */
|
|
17
|
-
export function signTest(differences: readonly number[]): { positive: number; negative: number; ties: number; p: number } {
|
|
18
|
-
const positive = differences.filter((d) => d > 0).length, negative = differences.filter((d) => d < 0).length;
|
|
19
|
-
const n = positive + negative, k = Math.min(positive, negative);
|
|
20
|
-
let tail = 0;
|
|
21
|
-
for (let i = 0; i <= k; i++) tail += binomial(n, i) / 2 ** n;
|
|
22
|
-
return { positive, negative, ties: differences.length - n, p: n === 0 ? 1 : Math.min(1, 2 * tail) };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function binomial(n: number, k: number): number {
|
|
26
|
-
let out = 1;
|
|
27
|
-
for (let i = 1; i <= k; i++) out = out * (n - k + i) / i;
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function median(values: readonly number[]): number | null {
|
|
32
|
-
if (values.length === 0) return null;
|
|
33
|
-
const sorted = [...values].sort((a, b) => a - b), mid = Math.floor(sorted.length / 2);
|
|
34
|
-
return sorted.length % 2 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Non-recent accuracy over planned questions: an unrun stage counts as wrong, never as missing. */
|
|
38
|
-
export function memoryAccuracy(run: RecallRunRow, planned: readonly RecallStageRow["kind"][]): number | null {
|
|
39
|
-
const asked = planned.filter((kind) => kind !== "recent").length;
|
|
40
|
-
if (asked === 0) return null;
|
|
41
|
-
return run.stages.filter((row) => row.kind !== "recent" && row.verdict.ok).length / asked;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface BlockDifference { seed: number; ratio: number; a: number | null; b: number | null; difference: number | null }
|
|
45
|
-
|
|
46
|
-
export function pairedBlocks(runs: readonly RecallRunRow[], a: string, b: string, ratios: readonly number[],
|
|
47
|
-
plannedKinds: (seed: number, ratio: number) => readonly RecallStageRow["kind"][]): BlockDifference[] {
|
|
48
|
-
const blocks = [...new Set(runs.filter((run) => ratios.includes(run.ratio)).map((run) => `${run.seed}/${run.ratio}`))].sort();
|
|
49
|
-
return blocks.map((key) => {
|
|
50
|
-
const [seed, ratio] = key.split("/").map(Number) as [number, number];
|
|
51
|
-
const kinds = plannedKinds(seed, ratio);
|
|
52
|
-
const find = (arm: string) => runs.find((run) => run.arm === arm && run.seed === seed && run.ratio === ratio);
|
|
53
|
-
const ra = find(a), rb = find(b);
|
|
54
|
-
const accA = ra ? memoryAccuracy(ra, kinds) : null, accB = rb ? memoryAccuracy(rb, kinds) : null;
|
|
55
|
-
return { seed, ratio, a: accA, b: accB, difference: accA === null || accB === null ? null : accA - accB };
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function compare(blocks: readonly BlockDifference[]) {
|
|
60
|
-
// A block missing either arm is reported, never silently imputed.
|
|
61
|
-
const complete = blocks.filter((block) => block.difference !== null).map((block) => block.difference!);
|
|
62
|
-
return { blocks: blocks.length, complete: complete.length, medianDifference: median(complete), ...signTest(complete) };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Pre-registered decision: p < 0.05 and median difference ≥ 10 points. */
|
|
66
|
-
export function decide(primary: ReturnType<typeof compare>): "advantage" | "no_advantage_measured" {
|
|
67
|
-
return primary.complete > 0 && primary.p < 0.05 && (primary.medianDifference ?? 0) >= 0.10 ? "advantage" : "no_advantage_measured";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function describeArm(runs: readonly RecallRunRow[], arm: string, ratio: number) {
|
|
71
|
-
const rows = runs.filter((run) => run.arm === arm && run.ratio === ratio);
|
|
72
|
-
const stages = rows.flatMap((run) => run.stages), memory = stages.filter((row) => row.kind !== "recent");
|
|
73
|
-
const evicted = memory.filter((row) => row.visibleThroughout === false);
|
|
74
|
-
const latest = stages.filter((row) => row.kind === "latest");
|
|
75
|
-
const correct = stages.filter((row) => row.verdict.ok).length;
|
|
76
|
-
const cost = rows.reduce((sum, run) => sum + run.knownCost, 0);
|
|
77
|
-
const rate = (n: number, d: number) => d === 0 ? null : n / d;
|
|
78
|
-
return {
|
|
79
|
-
arm, ratio, runs: rows.length, stages: stages.length, unrun: rows.reduce((n, run) => n + run.unrun, 0),
|
|
80
|
-
recentAccuracy: rate(stages.filter((row) => row.kind === "recent" && row.verdict.ok).length, stages.filter((row) => row.kind === "recent").length),
|
|
81
|
-
memoryAccuracy: rate(memory.filter((row) => row.verdict.ok).length, memory.length),
|
|
82
|
-
evictionRate: rate(evicted.length, memory.length),
|
|
83
|
-
evictedAccuracy: rate(evicted.filter((row) => row.verdict.ok).length, evicted.length),
|
|
84
|
-
staleLatest: rate(latest.filter((row) => row.verdict.reason === "stale").length, latest.length),
|
|
85
|
-
boundaries: rows.reduce((n, run) => n + run.boundaries, 0),
|
|
86
|
-
cost, costPerCorrect: correct === 0 ? null : cost / correct,
|
|
87
|
-
reservedUnknown: rows.reduce((n, run) => n + run.reservedUnknown, 0),
|
|
88
|
-
runsWithModelFiles: rows.filter((run) => run.modelFiles.length > 0).length,
|
|
89
|
-
};
|
|
90
|
-
}
|
package/src/core/recall-trial.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F2 delayed recall: information observed exactly once, asked about later.
|
|
3
|
-
*
|
|
4
|
-
* The runner deletes each page when its stage ends, so the workspace holds no
|
|
5
|
-
* durable copy; only what the harness kept (a log, a summary, the model's own
|
|
6
|
-
* notes) can answer a later question. This isolates the claim the harness makes
|
|
7
|
-
* -- lossless history beats lossy summaries when a detail's relevance is only
|
|
8
|
-
* revealed later -- from the F1 ledger task, where state lives on disk and any
|
|
9
|
-
* file-capable harness can continue. Modelled on MemoryAgentBench's incremental
|
|
10
|
-
* chunk injection (accurate retrieval + conflict resolution); none of its data
|
|
11
|
-
* or scoring is reused. Pure: no I/O, clock or ambient randomness.
|
|
12
|
-
*/
|
|
13
|
-
import { seededRandom } from "./experiment.ts";
|
|
14
|
-
|
|
15
|
-
export type RecallKind = "recent" | "recall" | "latest";
|
|
16
|
-
export interface RecallEvent { ticket: string; code: string; action: "assign" | "reassign" }
|
|
17
|
-
export interface RecallQuestion {
|
|
18
|
-
stage: number; kind: RecallKind; ticket: string; expected: string;
|
|
19
|
-
/** Page holding the expected code; distance is stage - source. */
|
|
20
|
-
source: number;
|
|
21
|
-
/** Codes the ticket held earlier; answering one of them is a stale answer. */
|
|
22
|
-
stale: string[];
|
|
23
|
-
}
|
|
24
|
-
export interface RecallPage { stage: number; path: string; answerPath: string; body: string; events: RecallEvent[]; question: RecallQuestion }
|
|
25
|
-
export interface RecallFixture { seed: number; disclose: boolean; contract: string; pages: RecallPage[] }
|
|
26
|
-
export interface RecallOptions {
|
|
27
|
-
stages: number; eventsPerPage: number; paddingChars: number;
|
|
28
|
-
/**
|
|
29
|
-
* Whether the contract says later stages may ask about earlier pages.
|
|
30
|
-
* Default false: disclosed, a diligent model copies every event into a notes
|
|
31
|
-
* file and the task collapses into F1 (state on disk). Undisclosed is the
|
|
32
|
-
* case the claim is about -- relevance revealed only after the fact.
|
|
33
|
-
*/
|
|
34
|
-
disclose?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Log-like filler at roughly 4 chars/token: hex filler tokenizes ~2x denser,
|
|
38
|
-
// which would double the cost of the same nominal length for no realism gain.
|
|
39
|
-
const LEVELS = ["INFO", "INFO", "INFO", "DEBUG", "WARN"];
|
|
40
|
-
const SERVICES = ["api", "worker", "scheduler", "cache", "gateway", "billing", "search", "auth"];
|
|
41
|
-
const MESSAGES = ["heartbeat ok", "request served", "cache refreshed", "retrying upstream call", "job finished",
|
|
42
|
-
"connection pooled", "config reloaded", "slow query logged", "queue drained", "health check passed"];
|
|
43
|
-
|
|
44
|
-
const pad = (n: number, width: number) => String(n).padStart(width, "0");
|
|
45
|
-
|
|
46
|
-
export function recallFixture(seed: number, options: RecallOptions): RecallFixture {
|
|
47
|
-
const { stages, eventsPerPage, paddingChars } = options, disclose = options.disclose ?? false;
|
|
48
|
-
if (!Number.isSafeInteger(seed) || !Number.isSafeInteger(stages) || stages < 1 || stages > 256 ||
|
|
49
|
-
!Number.isSafeInteger(eventsPerPage) || eventsPerPage < 2 || eventsPerPage > 200 ||
|
|
50
|
-
!Number.isSafeInteger(paddingChars) || paddingChars < 0 || paddingChars > 4_000_000) throw new RangeError("invalid recall fixture dimensions");
|
|
51
|
-
const random = seededRandom(seed), int = (n: number) => Math.floor(random() * n);
|
|
52
|
-
const pick = <T>(xs: readonly T[]): T => xs[int(xs.length)]!;
|
|
53
|
-
const used = new Set<string>();
|
|
54
|
-
const fresh = (make: () => string) => { for (;;) { const v = make(); if (!used.has(v)) { used.add(v); return v; } } };
|
|
55
|
-
const hex = (n: number) => Array.from({ length: n }, () => "0123456789abcdef"[int(16)]).join("");
|
|
56
|
-
// Current state and each ticket's history, replayed exactly as a reader would.
|
|
57
|
-
const history = new Map<string, { stage: number; code: string }[]>();
|
|
58
|
-
const asked = new Set<string>();
|
|
59
|
-
const pages: RecallPage[] = [];
|
|
60
|
-
for (let stage = 0; stage < stages; stage++) {
|
|
61
|
-
const earlier = [...history.keys()];
|
|
62
|
-
// A quarter of events after page 0 reassign an earlier ticket (conflicts).
|
|
63
|
-
const reassign = stage === 0 ? 0 : Math.min(earlier.length, Math.floor(eventsPerPage / 4));
|
|
64
|
-
const events: RecallEvent[] = [];
|
|
65
|
-
const targets = new Set<string>();
|
|
66
|
-
while (targets.size < reassign) targets.add(pick(earlier));
|
|
67
|
-
for (const ticket of targets) events.push({ ticket, code: fresh(() => `K-${hex(12)}`), action: "reassign" });
|
|
68
|
-
while (events.length < eventsPerPage) events.push({ ticket: fresh(() => `T-${pad(10000 + int(90000), 5)}`), code: fresh(() => `K-${hex(12)}`), action: "assign" });
|
|
69
|
-
// Shuffle so reassignments are not always first on the page.
|
|
70
|
-
for (let i = events.length - 1; i > 0; i--) { const j = int(i + 1); [events[i], events[j]] = [events[j]!, events[i]!]; }
|
|
71
|
-
for (const event of events) {
|
|
72
|
-
const past = history.get(event.ticket) ?? [];
|
|
73
|
-
past.push({ stage, code: event.code }); history.set(event.ticket, past);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const question = chooseQuestion(stage, history, asked, int);
|
|
77
|
-
asked.add(question.ticket);
|
|
78
|
-
const lines: string[] = [];
|
|
79
|
-
for (let usedChars = 0, second = 0; usedChars < paddingChars; second++) {
|
|
80
|
-
const line = `2026-09-28T${pad(Math.floor(second / 3600) % 24, 2)}:${pad(Math.floor(second / 60) % 60, 2)}:${pad(second % 60, 2)}Z ` +
|
|
81
|
-
`${pick(LEVELS)} ${pick(SERVICES)}-${int(8)} ${pick(MESSAGES)} latency=${int(900)}ms queue=${int(64)}`;
|
|
82
|
-
lines.push(line); usedChars += line.length + 1;
|
|
83
|
-
}
|
|
84
|
-
// Spread events through the whole page, including its tail, so a head-only
|
|
85
|
-
// preview cannot pass as a full observation (same rule as F1).
|
|
86
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
87
|
-
const at = Math.floor((i + 0.5) * lines.length / events.length);
|
|
88
|
-
lines.splice(at, 0, `EVENT ${JSON.stringify(events[i])}`);
|
|
89
|
-
}
|
|
90
|
-
pages.push({ stage, path: `incoming/page-${pad(stage, 2)}.txt`, answerPath: `answers/stage-${pad(stage, 2)}.txt`,
|
|
91
|
-
body: lines.join("\n"), events, question });
|
|
92
|
-
}
|
|
93
|
-
const contract = "This task runs in stages. Each stage delivers one page at incoming/page-NN.txt; the page is deleted when the stage ends. " +
|
|
94
|
-
"Pages contain EVENT lines (JSON) that assign or reassign a code to a ticket, mixed with unrelated log lines. " +
|
|
95
|
-
"A ticket's current code is the code from its most recent EVENT across all pages so far. " +
|
|
96
|
-
(disclose ? "Each stage asks about one ticket, which may have appeared on this page or on any earlier page. "
|
|
97
|
-
: "Each stage asks about one ticket. ") +
|
|
98
|
-
"Read the page in full with read, then write only the ticket's current code to the stage's answer file. Use only the provided tools. Reply briefly when the stage is done.";
|
|
99
|
-
return { seed, disclose, contract, pages };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Rotate kinds so every family appears; fall back when a kind has no candidate yet. */
|
|
103
|
-
function chooseQuestion(stage: number, history: Map<string, { stage: number; code: string }[]>, asked: Set<string>, int: (n: number) => number): RecallQuestion {
|
|
104
|
-
const rows = [...history.entries()].filter(([ticket]) => !asked.has(ticket)).map(([ticket, past]) => ({ ticket, past, last: past.at(-1)! }));
|
|
105
|
-
const candidates: Record<RecallKind, typeof rows> = {
|
|
106
|
-
// Asked on the page that last set it, so no retention is needed.
|
|
107
|
-
recent: rows.filter((row) => row.last.stage === stage),
|
|
108
|
-
// Set once, on an earlier page: pure retrieval of a detail seen once.
|
|
109
|
-
recall: rows.filter((row) => row.past.length === 1 && row.last.stage < stage),
|
|
110
|
-
// Changed on an earlier page: the stale code must be rejected.
|
|
111
|
-
latest: rows.filter((row) => row.past.length > 1 && row.last.stage < stage),
|
|
112
|
-
};
|
|
113
|
-
const rotation: RecallKind[] = ["recent", "recall", "latest"];
|
|
114
|
-
const preferred = rotation[stage % 3]!;
|
|
115
|
-
for (const kind of [preferred, "recall", "latest", "recent"] as RecallKind[]) {
|
|
116
|
-
const pool = candidates[kind];
|
|
117
|
-
if (pool.length === 0) continue;
|
|
118
|
-
// Pick the source page first, uniformly, so distances are spread rather
|
|
119
|
-
// than dominated by whichever page had the most unasked tickets.
|
|
120
|
-
const sources = [...new Set(pool.map((row) => row.last.stage))].sort((a, b) => a - b);
|
|
121
|
-
const source = sources[int(sources.length)]!;
|
|
122
|
-
const onSource = pool.filter((row) => row.last.stage === source);
|
|
123
|
-
const row = onSource[int(onSource.length)]!;
|
|
124
|
-
return { stage, kind, ticket: row.ticket, expected: row.last.code, source, stale: row.past.slice(0, -1).map((p) => p.code) };
|
|
125
|
-
}
|
|
126
|
-
throw new Error("recall fixture has no question candidate");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function recallPrompt(fixture: RecallFixture, stage: number): string {
|
|
130
|
-
const page = fixture.pages[stage]; if (!page) throw new RangeError("unknown recall stage");
|
|
131
|
-
return (stage === 0 ? fixture.contract + "\n\n" : "Continue under the original rules. ") +
|
|
132
|
-
`Stage ${stage}: read ${page.path} in full. Question: what is the current code of ticket ${page.question.ticket}? ` +
|
|
133
|
-
`Write only the code to ${page.answerPath}, then finish this stage.`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export type RecallVerdict = { ok: true; reason: "exact" } | { ok: false; reason: "missing" | "stale" | "other_ticket" | "wrong" };
|
|
137
|
-
|
|
138
|
-
/** Only whitespace and one wrapping quote/backtick pair are forgiven. */
|
|
139
|
-
export function verifyRecallAnswer(body: string | null, question: RecallQuestion, codes: ReadonlySet<string>): RecallVerdict {
|
|
140
|
-
if (body === null) return { ok: false, reason: "missing" };
|
|
141
|
-
let answer = body.trim();
|
|
142
|
-
const wrapped = /^(["'`])(.*)\1$/s.exec(answer);
|
|
143
|
-
if (wrapped) answer = wrapped[2]!.trim();
|
|
144
|
-
if (answer === "") return { ok: false, reason: "missing" };
|
|
145
|
-
if (answer === question.expected) return { ok: true, reason: "exact" };
|
|
146
|
-
if (question.stale.includes(answer)) return { ok: false, reason: "stale" };
|
|
147
|
-
return { ok: false, reason: codes.has(answer) ? "other_ticket" : "wrong" };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Every code any page issued, to tell a confused lookup from a fabrication. */
|
|
151
|
-
export function recallCodes(fixture: RecallFixture): Set<string> {
|
|
152
|
-
return new Set(fixture.pages.flatMap((page) => page.events.map((event) => event.code)));
|
|
153
|
-
}
|