Files
PaperClipAI/tests/runner-e2e/braintrust-experiment.ts
T
DottaandPaperclip b6e690044a feat(evals): allow reviewed bounded provisional accounting (#17365)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Product E2E campaigns include model, setup, storage and
observability costs.
> - Some billing windows close after the tests and reports finish.
> - Requiring settled receipts for retained storage can block a good
campaign for weeks.
> - A reservation alone does not prove that a pending charge has an
upper bound.
> - This change permits provisional completion only with reviewed
evidence for the exact cost configuration.
> - Reports preserve unknown amounts and holds until later
reconciliation.

## Linked Issues or Issue Description

Refs: #17360, #17354

**What existing behavior does this improve?**

The Product E2E scheduled accounting gate and its retained cost reports.

**Current behavior**

Every pending receipt fails finalization. A completed test can wait
through the retained-storage billing window. The journal has dollar
reservations but no explicit qualification of their bounds.

**Proposed behavior**

Permit provisional completion when all pending charges have reviewed
full-lifecycle bounds, no requests remain in flight, no receipts
conflict, and known costs plus held amounts fit the campaign limit. Keep
all existing holds and receipt-finality rules. Missing evidence still
fails.

**Reason and benefit**

An operator can distinguish delayed but bounded billing from unbounded
or missing costs. The campaign can finish without claiming that its
invoice is settled.

**Breaking changes**

The journal accepts optional bound metadata and an invalidation record.
New reports add an optional bounded-pending count. Current readers
retain old journals and reports without granting them bounds. Older code
must not read new bound-bearing journals. The existing daily/weekly
enable gates and budget amounts stay unchanged.

**Additional context**

This implements the operator-approved provisional-cost policy for the
existing eval harness. It does not change product budgets or the
agent-eval roadmap. A targeted GitHub search found no duplicate open PR.

## What Changed

- Accounting owns a narrow evidence contract: a reviewed audit hash,
exact configuration hash and full-lifecycle scope.
- Gateway reservations retain that evidence before dispatch. Changed
limits or rates fail before dispatch. Unexpected routing, model identity
or excessive usage invalidates a bound durably.
- Operating allowances can carry separately reviewed bounds for each
cost component, including its full storage lifetime. No existing
allowance becomes qualified by default.
- Reports and Braintrust experiment metadata distinguish provisional
accounting, complete receipts, blocked accounting and unavailable
evidence. Public output omits private audit hashes.
- Scheduled finalization allows qualified provisional costs and
explicitly rejects spending overruns, including overruns with complete
receipts.
- Add regressions for legacy data, restarts, late reconciliation,
conflicts, malformed evidence, routing uncertainty and exceeded limits.
Document the evidence and migration boundaries.

## Verification

- Full offline eval-support suite: 4,062 Vitest tests passed, two
existing tests skipped; 137 Node tests passed.
- `pnpm test:e2e:runner:typecheck`: passed.
- `git diff --check`: passed.
- Read the retained hosted qualification journal with the new policy.
All six legacy pending charges remain unqualified. Known cost
`$0.421245200`, held `$19.327680000`, and committed `$19.748925200`
remain unchanged. The journal's SHA-256 is unchanged. The original
failed workflow remains failed.
- No paid models, network provider calls, receipt imports or reservation
releases were used for this verification.
- Rebased onto master `4d90634f74`; 142 targeted tests and typecheck
passed.
- Review reproduced a lost OpenAI input-total limit check when cache
details were missing (1 failed, 47 passed). Fix
`5fef9ba7c149951a51eae468614a4b89f0b3d6db` preserves unpriced provider
usage bounds. Then 178 focused tests, full offline support and typecheck
passed.
- Current-head hosted CI passed:
https://github.com/paperclipai/paperclip/actions/runs/40020746585
(attempt 2). Typecheck, build, all general/Runner/serialized tests and
browser gates are green.
- CI attempt 1 hit a PostgreSQL deadlock in unchanged chat-test cleanup;
its skipped flag restoration caused a second assertion failure. One
failed-jobs-only retry passed. Original failure evidence is preserved in
attempt 1. No paid eval campaign was rerun.
- Greptile reviewed exact head
`5fef9ba7c149951a51eae468614a4b89f0b3d6db` at 5/5; its one finding is
fixed and resolved. The optional Paperclip review is paused and supplied
no assessment.

## Risks

- Evidence hashes bind an audit to a configuration; they do not prove
the audit is true. Operators must review real provider limits, all price
tiers, full resource lifetimes and enforceable infrastructure limits
before configuring bounds.
- Provisional completion is not invoice settlement. Later billing can
reveal overruns, which must be reconciled and inspected. The ledger
cannot stop external billing.
- Existing pilot journals, configured allowances and price cards have no
new bound approval. This PR alone does not qualify or enable recurring
paid work.
- The gateway cannot qualify a canceled Braintrust request without route
confirmation. It preserves that unknown hold.
- No new dependency, secret, repository variable, workflow, schedule or
cloud resource changes.

## Model Used

OpenAI Codex, GPT-6 family. The exact deployment ID and context-window
size are not exposed in this session. Used reasoning, repository tools
and code execution. No paid evaluation model calls were made for this
change.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 13:57:57 -07:00

210 lines
16 KiB
TypeScript

import { evalAccountingStatus } from "./operations-report.js";
import { createHash } from "node:crypto";
import { buildCampaignAccounting } from "./attempt-accounting.js";
import { exportBraintrustAttemptParent, type EvalBraintrustConfig } from "./braintrust.js";
import { validateCampaignFeedbackReview } from "./feedback-dispositions.js";
import { validateEvalOperationsReport } from "./operations-report.js";
import { html, isPublicReportTimestamp } from "./report-format.js";
import type { RunnerE2ECampaign, RunnerE2EResult } from "./types.js";
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const digest = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
const identifier = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(value);
const modelName = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/@=,\[\]-]{0,255}$/.test(value);
const count = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 0;
const invalid = (): never => { throw new Error("Invalid retained experiment evidence"); };
function attemptProjection(result: RunnerE2EResult, projectId: string) {
if (result.schema !== "paperclip.runner-e2e.result/v2"
|| ![result.executionId, result.suiteId, result.profileId, result.caseId, result.provider,
result.accountingIdentity?.campaignId, result.accountingIdentity?.attemptId].every(identifier)
|| !modelName(result.model)
|| !/^[0-9a-f]{64}$/.test(result.suiteDefinitionHash ?? "") || !/^[0-9a-f]{40}$/.test(result.source?.sha ?? "")
|| !count(result.attempt) || result.attempt < 1 || !count(result.durationMs)
|| !["passed", "failed"].includes(result.status) || !["passed", "failed", "not_started"].includes(result.cleanup)
|| !["native", "legacy"].includes(result.runtimeMode) || !["local", "daytona"].includes(result.environmentId)
|| !isPublicReportTimestamp(result.startedAt) || !isPublicReportTimestamp(result.finishedAt)
|| Date.parse(result.finishedAt) < Date.parse(result.startedAt)) invalid();
const feedback = result.feedback;
if (feedback && (![feedback.complaints, feedback.suggestions, feedback.invocations?.failed, feedback.invocations?.unknown].every(count)
|| !["complete", "partial", "unavailable", "not_started"].includes(feedback.collection)
|| !["complete", "partial", "unavailable", "not_started"].includes(feedback.invocationCollection))) invalid();
const trace = result.tracing;
if (trace) {
exportBraintrustAttemptParent(trace);
if (trace.projectId !== projectId || !["pending", "accepted", "failed"].includes(trace.status)) invalid();
}
return {
input: { execution_id: result.executionId, suite_id: result.suiteId, profile_id: result.profileId,
case_id: result.caseId, provider: result.provider, model: result.model, environment: result.environmentId,
runtime: result.runtimeMode, source_revision: result.source!.sha, definition_hash: result.suiteDefinitionHash },
output: { product: result.status, cleanup: result.cleanup },
scores: { product_pass: Number(result.status === "passed"), cleanup_pass: Number(result.cleanup === "passed") },
metrics: { start: Date.parse(result.startedAt) / 1000, end: Date.parse(result.finishedAt) / 1000 },
metadata: { campaign_id: result.accountingIdentity!.campaignId, attempt_id: result.accountingIdentity!.attemptId,
attempt_number: result.attempt, wall_duration_ms: result.durationMs, grader: "tests/runner-e2e/runner.spec.ts",
feedback: feedback ? { collection: feedback.collection, invocation_collection: feedback.invocationCollection,
complaints: feedback.complaints, suggestions: feedback.suggestions,
failed_submissions: feedback.invocations.failed, unresolved_submissions: feedback.invocations.unknown } : null,
trace: trace ? { project_id: trace.projectId, root_span_id: trace.traceId, span_id: trace.spanId,
export_status: trace.status, indexed_verification: "unverified" } : null },
span_attributes: { name: result.executionId, type: "eval" },
};
}
export function buildBraintrustExperiment(projectId: string, campaign: RunnerE2ECampaign, originals: readonly RunnerE2EResult[]) {
if (!uuid.test(projectId) || campaign.schema !== "paperclip.runner-e2e.campaign/v2" || !identifier(campaign.campaignId)
|| campaign.accounting?.scope !== "retained_attempts" || !Array.isArray(campaign.accounting.attempts)
|| !count(campaign.accounting.missingAttempts) || !Array.isArray(campaign.expected)
|| campaign.expected.length === 0 || !campaign.expected.every(identifier)
|| new Set(campaign.expected).size !== campaign.expected.length || !Array.isArray(originals)
|| originals.length > 10_000 || campaign.accounting.attempts.length === 0) invalid();
if (campaign.operations) validateEvalOperationsReport(campaign.operations);
if (campaign.feedbackReview) validateCampaignFeedbackReview(campaign, campaign.feedbackReview);
const accounting = campaign.accounting!;
const operations = campaign.operations?.status !== "unavailable" ? campaign.operations : undefined;
if (operations && operations.ledgerCampaignId !== campaign.campaignId) invalid();
const candidates = new Map<string, { original: RunnerE2EResult; projection: ReturnType<typeof attemptProjection> }>();
for (const original of originals) {
const projection = attemptProjection(original, projectId);
if (!campaign.expected.includes(original.executionId)) invalid();
const receipt = buildCampaignAccounting({ results: [original] }).attempts[0]!;
const retained = accounting.attempts.find(attempt => attempt.id === receipt.id);
if (!retained || JSON.stringify(retained) !== JSON.stringify(receipt)) invalid();
const previous = candidates.get(receipt.id);
if (previous && digest(previous.projection) !== digest(projection)) invalid();
candidates.set(receipt.id, { original, projection });
}
if (candidates.size !== accounting.attempts.length) invalid();
for (const selected of campaign.results.filter(result => result.attempt > 0)) {
const id = buildCampaignAccounting({ results: [selected] }).attempts[0]!.id;
const original = candidates.get(id);
const selectedProjection = attemptProjection(selected, projectId);
if (!original || digest(original.projection.input) !== digest(selectedProjection.input)
|| digest(original.projection.metrics) !== digest(selectedProjection.metrics)
|| digest(original.projection.metadata) !== digest(selectedProjection.metadata)) invalid();
}
const events = [...candidates.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([id, { original, projection }]) => {
const cost = operations && original.accountingIdentity!.campaignId === operations.ledgerCampaignId
? operations.attempts.find(row => row.executionId === original.executionId && row.attemptId === original.accountingIdentity!.attemptId)
: undefined;
return { ...projection, id, span_id: id, root_span_id: id,
metadata: { ...projection.metadata, request_ledger: cost ? {
known_usd: cost.knownUsd, additional_held_usd: cost.heldUsd, requests: cost.requests,
unresolved_requests: cost.unresolvedRequests, journal_hash: operations!.journalHash,
} : null } };
});
const metadata = { importer: "paperclip-retained-attempts-v1", campaign_id: campaign.campaignId,
expected_execution_ids: [...campaign.expected].sort(), retained_attempts: events.length,
missing_attempts: accounting.missingAttempts,
request_ledger: operations ? { journal_hash: operations.journalHash, known_usd: operations.knownUsd,
additional_held_usd: operations.heldUsd, committed_usd: operations.committedUsd,
accounting_status: evalAccountingStatus(operations), bounded_unresolved_requests: operations.boundedUnresolvedRequests ?? 0,
requests: operations.requests, unresolved_requests: operations.unresolvedRequests, breakdown: operations.breakdown } : null,
feedback_review: campaign.feedbackReview ?? null,
accounting_note: "Ledger amounts are separate observations, not additive trace metrics. Held reservations are not charges. Requests outside the gateway and infrastructure require separate accounting." };
const payloadHash = digest({ projectId, metadata, events });
return { projectId, payloadHash, name: `${campaign.campaignId}-${payloadHash}`, metadata: { ...metadata, payload_hash: payloadHash }, events };
}
export interface BraintrustExperimentReceipt {
version: 1; projectId: string; experimentId: string; name: string; payloadHash: string;
status: "created" | "accepted"; acceptedRows: number; expectedRows: number; verification: "unverified";
evidenceHash: string; projectUrl?: string; experimentUrl?: string;
}
export function experimentEvidenceHash(campaign: RunnerE2ECampaign) {
return digest({ campaignId: campaign.campaignId, expected: [...campaign.expected].sort(),
selected: campaign.results.map(result => ({ executionId: result.executionId, accountingIdentity: result.accountingIdentity,
attempt: result.attempt, startedAt: result.attempt === 0 ? null : result.startedAt, sourceRevision: result.source?.sha ?? null,
suiteDefinitionHash: result.suiteDefinitionHash ?? null, suiteId: result.suiteId, profileId: result.profileId,
caseId: result.caseId, provider: result.provider, model: result.model, environmentId: result.environmentId,
runtimeMode: result.runtimeMode })).sort((a, b) => digest(a).localeCompare(digest(b))),
accounting: campaign.accounting, operations: campaign.operations ?? null, feedbackReview: campaign.feedbackReview ?? null });
}
function safeBraintrustUrl(value: unknown): value is string {
if (typeof value !== "string") return false;
try {
const url = new URL(value);
return url.protocol === "https:" && ["braintrust.dev", "www.braintrust.dev"].includes(url.host)
&& !url.username && !url.password && !url.search && !url.hash && url.pathname.startsWith("/app/");
} catch { return false; }
}
export function validateBraintrustExperimentReceipt(campaign: RunnerE2ECampaign, receipt: BraintrustExperimentReceipt): void {
if (!receipt || Object.keys(receipt).some(key => !["version", "projectId", "experimentId", "name", "payloadHash", "status", "acceptedRows", "expectedRows", "verification", "evidenceHash", "projectUrl", "experimentUrl"].includes(key))
|| receipt.version !== 1 || !uuid.test(receipt.projectId) || !uuid.test(receipt.experimentId)
|| receipt.evidenceHash !== experimentEvidenceHash(campaign) || !/^[a-f0-9]{64}$/.test(receipt.payloadHash)
|| receipt.name !== `${campaign.campaignId}-${receipt.payloadHash}` || !["created", "accepted"].includes(receipt.status)
|| !count(receipt.acceptedRows) || receipt.expectedRows !== campaign.accounting?.attempts.length
|| receipt.acceptedRows > receipt.expectedRows || receipt.verification !== "unverified"
|| receipt.status === "accepted" && receipt.acceptedRows !== receipt.expectedRows
|| receipt.experimentUrl !== undefined && !safeBraintrustUrl(receipt.experimentUrl)
|| receipt.projectUrl !== undefined && !safeBraintrustUrl(receipt.projectUrl)) invalid();
}
export function renderBraintrustExperiment(campaign: RunnerE2ECampaign): string {
const receipt = campaign.braintrustExperiment;
if (!receipt) return "";
validateBraintrustExperimentReceipt(campaign, receipt);
return `<section aria-label="Braintrust experiment"><h2>Braintrust experiment</h2><p>${receipt.experimentUrl
? `<a href="${html(receipt.experimentUrl)}">Open retained attempt scores in Braintrust</a>`
: `Experiment ${html(receipt.experimentId)}`}. ${receipt.acceptedRows}/${receipt.expectedRows} rows accepted.
Original product and cleanup grades are separate. Indexing requires independent verification; no agents were rerun for this import.</p></section>`;
}
async function request(config: EvalBraintrustConfig, endpoint: string, payload: unknown, transport: typeof fetch) {
const response = await transport(`https://api.braintrust.dev/v1/${endpoint}`, {
method: payload === undefined ? "GET" : "POST", redirect: "error", signal: AbortSignal.timeout(15_000),
headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
});
const reader = response.body?.getReader(), chunks: Uint8Array[] = []; let size = 0;
try {
if (!response.ok) throw new Error("rejected");
while (reader) {
const next = await reader.read(); if (next.done) break;
size += next.value.length; if (size > 1_048_576) throw new Error("oversized");
chunks.push(next.value);
}
return JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown>;
} finally { await reader?.cancel().catch(() => {}); }
}
export async function importBraintrustExperiment(config: EvalBraintrustConfig, campaign: RunnerE2ECampaign,
originals: readonly RunnerE2EResult[], persist: (receipt: BraintrustExperimentReceipt) => Promise<void>, transport: typeof fetch = fetch,
previous?: BraintrustExperimentReceipt) {
const plan = buildBraintrustExperiment(config.projectId, campaign, originals);
if (!config.apiKey?.trim() || /[\r\n]/.test(config.apiKey)) throw new Error("Missing Braintrust ingestion credential");
if (previous) {
validateBraintrustExperimentReceipt(campaign, previous);
if (previous.projectId !== plan.projectId || previous.payloadHash !== plan.payloadHash) invalid();
}
try {
const created = await request(config, "experiment", { project_id: plan.projectId, name: plan.name,
public: false, ensure_new: false, metadata: plan.metadata }, transport);
if (!uuid.test(String(created?.id)) || created.project_id !== plan.projectId || created.name !== plan.name || created.public !== false
|| (created.metadata as Record<string, unknown> | undefined)?.payload_hash !== plan.payloadHash) throw new Error("conflicting_experiment");
if (previous && previous.experimentId !== created.id) throw new Error("conflicting_experiment");
const receipt: BraintrustExperimentReceipt = previous ? { ...previous } : { version: 1, projectId: plan.projectId, experimentId: String(created.id),
name: plan.name, payloadHash: plan.payloadHash, evidenceHash: experimentEvidenceHash(campaign),
status: "created", acceptedRows: 0, expectedRows: plan.events.length, verification: "unverified" };
if (!previous) await persist({ ...receipt });
for (let offset = 0; offset < plan.events.length; offset += 100) {
const events = plan.events.slice(offset, offset + 100);
const inserted = await request(config, `experiment/${receipt.experimentId}/insert`, { events }, transport);
if (!Array.isArray(inserted?.row_ids) || JSON.stringify([...inserted.row_ids].sort()) !== JSON.stringify(events.map(event => event.id).sort())) throw new Error("missing_rows");
receipt.acceptedRows = Math.max(receipt.acceptedRows, offset + events.length);
if (receipt.acceptedRows === receipt.expectedRows) receipt.status = "accepted";
await persist({ ...receipt });
}
const summary = await request(config, `experiment/${receipt.experimentId}/summarize`, undefined, transport).catch(() => undefined);
if (summary?.experiment_name === plan.name && safeBraintrustUrl(summary.project_url) && safeBraintrustUrl(summary.experiment_url)) {
receipt.projectUrl = summary.project_url; receipt.experimentUrl = summary.experiment_url;
await persist({ ...receipt });
}
return receipt;
} catch { throw new Error("Braintrust experiment import incomplete; retain the receipt and retry identical evidence. Remote details withheld."); }
}