项目文件夹

文件
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

943 行
28 KiB
TypeScript

#!/usr/bin/env bun
/**
* Conversation-compaction drift harness.
*
* Drives a synthetic multi-turn conversation with planted facts, forces a
* selected compaction strategy on a fixed cadence, and emits reproducible
* JSONL events consumed by packages/benchmarks/context-bench.
*/
import {
compactors,
findSafeCompactionBoundary,
} from "../../packages/agent/src/runtime/conversation-compactor.ts";
import {
approxCountTokens,
type CompactionArtifact,
type CompactorMessage,
type CompactorModelCall,
type CompactorTranscript,
countTranscriptTokens,
} from "../../packages/agent/src/runtime/conversation-compactor.types.ts";
const KNOWN_STRATEGIES = [
"none",
"prompt-stripping",
"naive-summary",
"structured-state",
"hierarchical-summary",
"hybrid-ledger",
] as const;
type StrategyName = (typeof KNOWN_STRATEGIES)[number];
type ReasoningEffort = "low" | "medium" | "high";
type Args = {
strategy: StrategyName;
turns: number;
compactEvery: number;
plantFacts: number;
seed: number;
output?: string;
dryRun: boolean;
agentModel: string;
judgeModel: string;
compactorModel: string;
agentReasoningEffort: ReasoningEffort;
judgeReasoningEffort: ReasoningEffort;
compactorReasoningEffort: ReasoningEffort;
probeMaxTokens: number;
realisticSystemPrompt: boolean;
withToolCalls: boolean;
};
type Fact = {
id: string;
kind: string;
expected: string;
question: string;
plantedTurn: number;
};
type ProbeEvent = {
event: "probe";
atTurn: number;
factId: string;
plantedTurn: number;
kind: string;
expected: string;
actual: string;
correct: boolean;
judgeReasoning: string;
phase: "post-compact" | "final";
};
const FACT_KINDS = [
"aws_account",
"person_name",
"address",
"code",
"book_title",
"project_codename",
"isbn",
"date_iso",
"birthday",
"flight_number",
"uuid",
"zipcode",
] as const;
function usage(): string {
return [
"usage: bun run scripts/benchmark/drift-harness.ts --strategy <name> [options]",
"",
`strategies: ${KNOWN_STRATEGIES.join(", ")}`,
"",
"options:",
" --turns <n> default 50",
" --compact-every <n> default 10",
" --plant-facts <n> default 5",
" --seed <n> default 1337",
" --output <path> write JSONL to path",
" --dry-run deterministic local model; no API calls",
" --agent-model <id> default env AGENT_MODEL or gpt-oss-120b",
" --judge-model <id> default env JUDGE_MODEL or agent model",
" --compactor-model <id> default env COMPACTOR_MODEL or agent model",
" --agent-reasoning-effort <level> low | medium | high, default medium",
" --judge-reasoning-effort <level> low | medium | high, default medium",
" --compactor-reasoning-effort <level> low | medium | high, default low",
" --probe-max-tokens <n> default 600",
" --realistic-system-prompt use a larger Eliza-style system prompt",
" --with-tool-calls add synthetic tool-call/result probes",
].join("\n");
}
function parseReasoningEffort(value: string): ReasoningEffort {
if (value === "low" || value === "medium" || value === "high") return value;
throw new Error(`invalid reasoning effort ${value}`);
}
function readFlag(argv: string[], index: number): string {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`missing value for ${argv[index]}`);
}
return value;
}
function parseArgs(argv: string[]): Args {
const agentModel = process.env.AGENT_MODEL ?? "gpt-oss-120b";
const args: Args = {
strategy: "none",
turns: 50,
compactEvery: 10,
plantFacts: 5,
seed: 1337,
dryRun: false,
agentModel,
judgeModel: process.env.JUDGE_MODEL ?? agentModel,
compactorModel: process.env.COMPACTOR_MODEL ?? agentModel,
agentReasoningEffort: "medium",
judgeReasoningEffort: "medium",
compactorReasoningEffort: "low",
probeMaxTokens: 600,
realisticSystemPrompt: false,
withToolCalls: false,
};
for (let i = 0; i < argv.length; i++) {
const flag = argv[i];
switch (flag) {
case "--help":
case "-h":
process.stdout.write(`${usage()}\n`);
process.exit(0);
break;
case "--strategy": {
const value = readFlag(argv, i);
if (!KNOWN_STRATEGIES.includes(value as StrategyName)) {
throw new Error(
`unknown strategy ${value}; expected ${KNOWN_STRATEGIES.join(", ")}`,
);
}
args.strategy = value as StrategyName;
i++;
break;
}
case "--turns":
args.turns = Number(readFlag(argv, i));
i++;
break;
case "--compact-every":
args.compactEvery = Number(readFlag(argv, i));
i++;
break;
case "--plant-facts":
args.plantFacts = Number(readFlag(argv, i));
i++;
break;
case "--seed":
args.seed = Number(readFlag(argv, i));
i++;
break;
case "--output":
args.output = readFlag(argv, i);
i++;
break;
case "--dry-run":
args.dryRun = true;
break;
case "--agent-model":
args.agentModel = readFlag(argv, i);
i++;
break;
case "--judge-model":
args.judgeModel = readFlag(argv, i);
i++;
break;
case "--compactor-model":
args.compactorModel = readFlag(argv, i);
i++;
break;
case "--agent-reasoning-effort":
args.agentReasoningEffort = parseReasoningEffort(readFlag(argv, i));
i++;
break;
case "--judge-reasoning-effort":
args.judgeReasoningEffort = parseReasoningEffort(readFlag(argv, i));
i++;
break;
case "--compactor-reasoning-effort":
args.compactorReasoningEffort = parseReasoningEffort(readFlag(argv, i));
i++;
break;
case "--probe-max-tokens":
args.probeMaxTokens = Number(readFlag(argv, i));
i++;
break;
case "--realistic-system-prompt":
args.realisticSystemPrompt = true;
break;
case "--with-tool-calls":
args.withToolCalls = true;
break;
default:
throw new Error(`unknown argument ${flag}`);
}
}
if (!Number.isInteger(args.turns) || args.turns < 1) {
throw new Error("--turns must be a positive integer");
}
if (!Number.isInteger(args.compactEvery) || args.compactEvery < 1) {
throw new Error("--compact-every must be a positive integer");
}
if (!Number.isInteger(args.plantFacts) || args.plantFacts < 0) {
throw new Error("--plant-facts must be a non-negative integer");
}
if (!Number.isInteger(args.seed)) {
throw new Error("--seed must be an integer");
}
if (!Number.isInteger(args.probeMaxTokens) || args.probeMaxTokens < 1) {
throw new Error("--probe-max-tokens must be a positive integer");
}
return args;
}
function seededInt(seed: number, index: number, modulo: number): number {
let x = (seed ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0;
x ^= x << 13;
x ^= x >>> 17;
x ^= x << 5;
return (x >>> 0) % modulo;
}
function padded(seed: number, index: number, digits: number): string {
const max = 10 ** Math.min(digits, 12);
const n = seededInt(seed, index, max);
return String(n).padStart(digits, "0").slice(0, digits);
}
function uuidFor(seed: number, index: number): string {
const parts = [8, 4, 4, 4, 12].map((digits, part) =>
padded(seed + part * 101, index, digits).replace(/[0-9]/g, (d) =>
Number(d).toString(16),
),
);
return parts.join("-");
}
function valueForKind(kind: string, seed: number, index: number): string {
const names = ["Mira Valen", "Talia Rune", "Niko Ardent", "Sana Holt"];
const books = [
"The Glass Atlas",
"Lanterns Under Ice",
"The Quiet Relay",
"Signal Garden",
];
const codenames = [
"EMBER-VAULT",
"POLARIS-NINE",
"CITRINE-LAB",
"ORBIT-MINT",
];
switch (kind) {
case "aws_account":
return padded(seed, index, 12);
case "person_name":
return names[seededInt(seed, index, names.length)];
case "address":
return `${100 + seededInt(seed, index, 899)} Meridian Ave, Unit ${1 + seededInt(seed, index + 1, 40)}`;
case "code":
return `CTX-${padded(seed, index, 6)}`;
case "book_title":
return books[seededInt(seed, index, books.length)];
case "project_codename":
return codenames[seededInt(seed, index, codenames.length)];
case "isbn":
return `978-1-${padded(seed, index, 3)}-${padded(seed, index + 1, 5)}-${seededInt(seed, index + 2, 10)}`;
case "date_iso":
return `2026-${String(1 + seededInt(seed, index, 12)).padStart(2, "0")}-${String(1 + seededInt(seed, index + 1, 28)).padStart(2, "0")}`;
case "birthday":
return `${String(1 + seededInt(seed, index, 12)).padStart(2, "0")}/${String(1 + seededInt(seed, index + 1, 28)).padStart(2, "0")}/199${seededInt(seed, index + 2, 10)}`;
case "flight_number":
return `EL${padded(seed, index, 4)}`;
case "uuid":
return uuidFor(seed, index);
case "zipcode":
return padded(seed, index, 5);
default:
return `VALUE-${padded(seed, index, 6)}`;
}
}
function questionFor(kind: string, id: string): string {
switch (kind) {
case "aws_account":
return `What AWS account number was planted for ${id}?`;
case "person_name":
return `What person name was planted for ${id}?`;
case "address":
return `What address was planted for ${id}?`;
case "code":
return `What code was planted for ${id}?`;
case "book_title":
return `What book title was planted for ${id}?`;
case "project_codename":
return `What project codename was planted for ${id}?`;
case "isbn":
return `What ISBN was planted for ${id}?`;
case "date_iso":
return `What ISO date was planted for ${id}?`;
case "birthday":
return `What birthday was planted for ${id}?`;
case "flight_number":
return `What flight number was planted for ${id}?`;
case "uuid":
return `What UUID was planted for ${id}?`;
case "zipcode":
return `What ZIP code was planted for ${id}?`;
default:
return `What value was planted for ${id}?`;
}
}
function buildFacts(args: Args): Fact[] {
const facts: Fact[] = [];
const usedTurns = new Set<number>();
for (let i = 0; i < args.plantFacts; i++) {
const id = `fact_${i + 1}`;
const kind = FACT_KINDS[(i + args.seed) % FACT_KINDS.length];
let plantedTurn = Math.max(
1,
Math.min(
args.turns,
Math.floor(((i + 1) * args.turns) / (args.plantFacts + 1)),
),
);
while (usedTurns.has(plantedTurn) && plantedTurn < args.turns)
plantedTurn++;
usedTurns.add(plantedTurn);
const expected = valueForKind(kind, args.seed, i);
facts.push({
id,
kind,
expected,
question: questionFor(kind, id),
plantedTurn,
});
}
return facts;
}
function defaultSystemPrompt(args: Args): string {
if (!args.realisticSystemPrompt) {
return "You are an Eliza agent. Track user-provided facts exactly and answer later recall probes with the exact value.";
}
return [
"You are an Eliza agent running inside elizaOS.",
"Maintain continuity across long conversations, respect tool results, and keep exact identifiers intact.",
"Available actions include REPLY, TASKS, RUNTIME, SHELL, and NONE.",
"Providers include recent messages, user profile, workspace state, runtime settings, and active integrations.",
"When a user gives a durable fact, treat the exact value as load-bearing context for later turns.",
"Never invent identifiers. If a value appears in prior conversation history, reproduce it exactly.",
"Plugin descriptions: shell command execution, dashboard runtime management, cloud usage reporting, and local workspace inspection.",
"This synthetic prompt intentionally resembles a larger production prompt so compaction has non-conversation context to preserve.",
].join("\n\n");
}
function renderTranscript(messages: CompactorMessage[]): string {
return messages
.map((message) => {
const extras: string[] = [];
if (message.toolCalls) {
extras.push(` toolCalls=${JSON.stringify(message.toolCalls)}`);
}
if (message.toolCallId) extras.push(` toolCallId=${message.toolCallId}`);
if (message.toolName) extras.push(` toolName=${message.toolName}`);
return `[${message.role}]${extras.join("")} ${message.content}`;
})
.join("\n");
}
function extractFacts(text: string): string[] {
const out: string[] = [];
for (const match of text.matchAll(/FACT\s+(fact_\d+):\s+([^\n]+)/g)) {
out.push(`${match[1]}: ${match[2].trim()}`);
}
for (const match of text.matchAll(
/\[tool_result:([^\]]+)\]\s+([^\n]+?)\s+\(turn=(\d+)\)/g,
)) {
out.push(`tool ${match[1]} turn ${match[3]}: ${match[2].trim()}`);
}
return Array.from(new Set(out));
}
function extractExpectedAnswer(text: string, fact: Fact): string | null {
const factLine = new RegExp(
`FACT\\s+${fact.id}:\\s+[^\\n]*expected=${escapeRegExp(fact.expected)}(?:\\s|$)`,
);
if (factLine.test(text)) return fact.expected;
if (text.includes(fact.expected)) return fact.expected;
return null;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function dryRunModelCall(): CompactorModelCall {
return async ({ systemPrompt, messages }) => {
const text = messages.map((message) => message.content).join("\n");
const facts = extractFacts(text);
if (
systemPrompt.includes('"facts"') &&
systemPrompt.includes("JSON object")
) {
return JSON.stringify({
facts,
decisions: [],
pending_actions: [],
forbidden_behaviors: [],
entities: {},
});
}
if (systemPrompt.includes("hybrid ledger")) {
return JSON.stringify({
state: {
facts,
decisions: [],
pending_actions: [],
forbidden_behaviors: [],
entities: {},
},
ledger: facts.map((fact, index) => ({ index, note: fact })),
});
}
return facts.length > 0
? facts.map((fact) => `Preserved ${fact}`).join("\n")
: "No durable planted facts were present in the compacted region.";
};
}
async function chatCompletion(args: {
model: string;
systemPrompt: string;
messages: CompactorMessage[];
maxOutputTokens: number;
reasoningEffort: ReasoningEffort;
}): Promise<string> {
const apiKey =
process.env.CEREBRAS_API_KEY ??
process.env.OPENAI_API_KEY ??
process.env.OPENAI_COMPAT_API_KEY;
if (!apiKey) {
throw new Error(
"real drift runs require CEREBRAS_API_KEY, OPENAI_API_KEY, or OPENAI_COMPAT_API_KEY; use --dry-run for local smoke tests",
);
}
const baseUrl =
process.env.OPENAI_COMPAT_BASE_URL ??
process.env.CEREBRAS_BASE_URL ??
"https://api.cerebras.ai/v1";
const response = await fetch(
`${baseUrl.replace(/\/$/, "")}/chat/completions`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: args.model,
messages: [
{ role: "system", content: args.systemPrompt },
...args.messages.map((message) => ({
role: message.role === "tool" ? "user" : message.role,
content: message.content,
})),
],
max_tokens: args.maxOutputTokens,
reasoning_effort: args.reasoningEffort,
}),
},
);
if (!response.ok) {
throw new Error(
`chat completion failed (${response.status}): ${await response.text()}`,
);
}
const payload = (await response.json()) as {
choices?: Array<{ message?: { content?: string } }>;
};
return payload.choices?.[0]?.message?.content?.trim() ?? "";
}
function realModelCall(args: Args): CompactorModelCall {
return ({ systemPrompt, messages, maxOutputTokens }) =>
chatCompletion({
model: args.compactorModel,
systemPrompt,
messages,
maxOutputTokens: maxOutputTokens ?? 800,
reasoningEffort: args.compactorReasoningEffort,
});
}
function promptStripCompact(
transcript: CompactorTranscript,
preserveTailMessages: number,
): CompactionArtifact {
const startedAt = Date.now();
const messages = transcript.messages;
const boundary = findSafeCompactionBoundary(messages, preserveTailMessages);
const systemPrefix = messages.filter((message, index) => {
return (
index < boundary &&
(message.role === "system" || message.role === "developer")
);
});
const protectedCount = systemPrefix.length;
const region = messages.slice(protectedCount, boundary);
const preservedTail = messages.slice(boundary);
const stripped = region
.map((message) => message.content)
.join("\n")
.replace(/\([^)]*internal thought:[^)]*\)/gi, "")
.replace(/\([^)]*actions:[^)]*\)/gi, "")
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "")
.replace(/\n{3,}/g, "\n\n")
.trim();
const replacementMessages: CompactorMessage[] = stripped
? [
{
role: "assistant",
content: `[conversation prompt-stripping]\n${stripped}`,
tags: ["compactor:prompt-stripping"],
},
]
: [];
const compacted = [...systemPrefix, ...replacementMessages, ...preservedTail];
return {
replacementMessages,
stats: {
originalMessageCount: messages.length,
compactedMessageCount: compacted.length,
originalTokens: countTranscriptTokens(transcript),
compactedTokens: countTranscriptTokens({ messages: compacted }),
latencyMs: Date.now() - startedAt,
extra: { regionSize: region.length },
},
};
}
async function applyStrategy(args: {
strategy: StrategyName;
transcript: CompactorTranscript;
callModel: CompactorModelCall;
}): Promise<{
transcript: CompactorTranscript;
stats: CompactionArtifact["stats"];
}> {
const preserveTailMessages = 6;
if (args.strategy === "none") {
const tokens = countTranscriptTokens(args.transcript);
return {
transcript: args.transcript,
stats: {
originalMessageCount: args.transcript.messages.length,
compactedMessageCount: args.transcript.messages.length,
originalTokens: tokens,
compactedTokens: tokens,
latencyMs: 0,
},
};
}
const artifact =
args.strategy === "prompt-stripping"
? promptStripCompact(args.transcript, preserveTailMessages)
: await compactors[args.strategy].compact(args.transcript, {
targetTokens: 700,
preserveTailMessages,
countTokens: approxCountTokens,
callModel: args.callModel,
summarizationModel: "drift-harness",
});
const messages = args.transcript.messages;
const systemPrefixLength = messages.findIndex(
(message) => message.role !== "system" && message.role !== "developer",
);
const prefixLength =
systemPrefixLength === -1 ? messages.length : systemPrefixLength;
const boundary = findSafeCompactionBoundary(messages, preserveTailMessages);
return {
transcript: {
messages: [
...messages.slice(0, prefixLength),
...artifact.replacementMessages,
...messages.slice(boundary),
],
metadata: args.transcript.metadata,
},
stats: artifact.stats,
};
}
function emitTurn(
lines: string[],
turn: number,
role: "user" | "assistant",
content: string,
factId?: string,
): void {
lines.push(
JSON.stringify({
event: "turn",
turn,
role,
contentLen: content.length,
tokens: approxCountTokens(content),
...(factId ? { factId } : {}),
}),
);
}
async function answerProbe(args: {
harnessArgs: Args;
transcript: CompactorTranscript;
fact: Fact;
}): Promise<string> {
const transcriptText = renderTranscript(args.transcript.messages);
if (args.harnessArgs.dryRun) {
return (
extractExpectedAnswer(transcriptText, args.fact) ?? "I don't recall."
);
}
return chatCompletion({
model: args.harnessArgs.agentModel,
systemPrompt:
"Answer the recall probe using only the supplied transcript. Return the exact value if present.",
messages: [
{
role: "user",
content: `Transcript:\n${transcriptText}\n\nProbe: ${args.fact.question}`,
},
],
maxOutputTokens: args.harnessArgs.probeMaxTokens,
reasoningEffort: args.harnessArgs.agentReasoningEffort,
});
}
async function judgeProbe(args: {
harnessArgs: Args;
fact: Fact;
actual: string;
}): Promise<{ correct: boolean; reasoning: string }> {
const exact = args.actual.includes(args.fact.expected);
if (args.harnessArgs.dryRun || exact) {
return {
correct: exact,
reasoning: exact
? "exact-match: expected substring present"
: "exact-match: expected substring missing",
};
}
const verdict = await chatCompletion({
model: args.harnessArgs.judgeModel,
systemPrompt:
'Grade whether ACTUAL contains the same exact value as EXPECTED. Respond JSON: {"correct":boolean,"reasoning":"..."}.',
messages: [
{
role: "user",
content: `EXPECTED: ${args.fact.expected}\nACTUAL: ${args.actual}`,
},
],
maxOutputTokens: 200,
reasoningEffort: args.harnessArgs.judgeReasoningEffort,
});
try {
const parsed = JSON.parse(verdict) as {
correct?: boolean;
reasoning?: string;
};
return {
correct: parsed.correct === true,
reasoning: parsed.reasoning ?? "judge-json",
};
} catch {
return {
correct: exact,
reasoning: `judge-unparseable; exact-match=${exact}`,
};
}
}
async function probeFacts(args: {
harnessArgs: Args;
transcript: CompactorTranscript;
facts: Fact[];
lines: string[];
atTurn: number;
phase: ProbeEvent["phase"];
}): Promise<ProbeEvent[]> {
const events: ProbeEvent[] = [];
for (const fact of args.facts.filter(
(item) => item.plantedTurn <= args.atTurn,
)) {
const actual = await answerProbe({
harnessArgs: args.harnessArgs,
transcript: args.transcript,
fact,
});
const judgment = await judgeProbe({
harnessArgs: args.harnessArgs,
fact,
actual,
});
const event: ProbeEvent = {
event: "probe",
atTurn: args.atTurn,
factId: fact.id,
plantedTurn: fact.plantedTurn,
kind: fact.kind,
expected: fact.expected,
actual,
correct: judgment.correct,
judgeReasoning: judgment.reasoning,
phase: args.phase,
};
args.lines.push(JSON.stringify(event));
events.push(event);
}
return events;
}
function addToolCallMessages(
messages: CompactorMessage[],
lines: string[],
turn: number,
facts: Fact[],
seed: number,
): void {
const toolName = "lookup_context";
const expected = `TOOL-${padded(seed, turn, 6)}`;
const fact: Fact = {
id: `tool_${turn}`,
kind: "tool_result",
expected,
plantedTurn: turn,
question: `What did ${toolName} return at turn ${turn}?`,
};
facts.push(fact);
const assistant: CompactorMessage = {
role: "assistant",
content: `[tool_call:${toolName}] requesting synthetic value for turn ${turn}`,
toolCalls: [{ id: `call_${turn}`, name: toolName, arguments: { turn } }],
};
const tool: CompactorMessage = {
role: "tool",
toolCallId: `call_${turn}`,
toolName,
content: `[tool_result:${toolName}] ${expected} (turn=${turn})`,
};
messages.push(assistant, tool);
emitTurn(lines, turn, "assistant", assistant.content);
lines.push(
JSON.stringify({
event: "turn",
turn,
role: "tool",
contentLen: tool.content.length,
tokens: approxCountTokens(tool.content),
factId: fact.id,
}),
);
}
async function main(): Promise<number> {
const args = parseArgs(Bun.argv.slice(2));
const facts = buildFacts(args);
const factsByTurn = new Map<number, Fact[]>();
for (const fact of facts) {
const items = factsByTurn.get(fact.plantedTurn) ?? [];
items.push(fact);
factsByTurn.set(fact.plantedTurn, items);
}
const lines: string[] = [];
const transcript: CompactorTranscript = {
messages: [{ role: "system", content: defaultSystemPrompt(args) }],
metadata: { seed: args.seed, strategy: args.strategy },
};
const callModel = args.dryRun ? dryRunModelCall() : realModelCall(args);
let totalCompactions = 0;
let totalTokensSaved = 0;
const probes: ProbeEvent[] = [];
for (let turn = 1; turn <= args.turns; turn++) {
const planted = factsByTurn.get(turn) ?? [];
const durableFacts = planted.map(
(fact) =>
`FACT ${fact.id}: kind=${fact.kind} expected=${fact.expected} question="${fact.question}"`,
);
const filler = `Turn ${turn}: continue the planning conversation and keep prior durable facts available.`;
const userContent = [filler, ...durableFacts].join("\n");
transcript.messages.push({ role: "user", content: userContent });
emitTurn(lines, turn, "user", userContent, planted[0]?.id);
const assistantContent = planted.length
? `Acknowledged ${planted.map((fact) => fact.id).join(", ")}.`
: `Acknowledged turn ${turn}.`;
transcript.messages.push({ role: "assistant", content: assistantContent });
emitTurn(lines, turn, "assistant", assistantContent);
if (args.withToolCalls && turn % 5 === 0) {
addToolCallMessages(transcript.messages, lines, turn, facts, args.seed);
}
if (turn % args.compactEvery === 0 && args.strategy !== "none") {
const result = await applyStrategy({
strategy: args.strategy,
transcript,
callModel,
});
transcript.messages = result.transcript.messages;
totalCompactions++;
totalTokensSaved += Math.max(
0,
result.stats.originalTokens - result.stats.compactedTokens,
);
lines.push(
JSON.stringify({
event: "compact",
atTurn: turn,
strategy: args.strategy,
originalTokens: result.stats.originalTokens,
compactedTokens: result.stats.compactedTokens,
latencyMs: result.stats.latencyMs,
}),
);
probes.push(
...(await probeFacts({
harnessArgs: args,
transcript,
facts,
lines,
atTurn: turn,
phase: "post-compact",
})),
);
}
}
probes.push(
...(await probeFacts({
harnessArgs: args,
transcript,
facts,
lines,
atTurn: args.turns,
phase: "final",
})),
);
const totalCorrect = probes.filter((probe) => probe.correct).length;
const perKind = new Map<string, { correct: number; total: number }>();
for (const probe of probes) {
const stats = perKind.get(probe.kind) ?? { correct: 0, total: 0 };
stats.total++;
if (probe.correct) stats.correct++;
perKind.set(probe.kind, stats);
}
const perKindAccuracy = Object.fromEntries(
Array.from(perKind.entries()).map(([kind, stats]) => [
kind,
{
correct: stats.correct,
total: stats.total,
accuracy: stats.total > 0 ? stats.correct / stats.total : 0,
},
]),
);
lines.push(
JSON.stringify({
event: "summary",
strategy: args.strategy,
overallAccuracy: probes.length > 0 ? totalCorrect / probes.length : 0,
totalCompactions,
totalTokensSaved,
totalProbes: probes.length,
totalCorrect,
seed: args.seed,
turns: args.turns,
compactEvery: args.compactEvery,
plantFacts: args.plantFacts,
valid: true,
skipped: false,
perKindAccuracy,
}),
);
const output = `${lines.join("\n")}\n`;
if (args.output) {
await Bun.write(args.output, output);
}
process.stdout.write(output);
return 0;
}
main().then(
(code) => process.exit(code),
(error: unknown) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n\n${usage()}\n`,
);
process.exit(1);
},
);