Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ DROPS_AGENT_MAX_PARALLEL_ROLES=
DROPS_AGENT_MAX_ROLE_CALLS=
DROPS_AGENT_MAX_REPAIR_ROUNDS=
DROPS_AGENT_MAX_RUN_COST_USD=
# Activates the compact V3 system core for request-scoped agent runs. The legacy
# prompt remains the bounded rollback path and is never removed by this flag.
DROPS_AGENT_COMPACT_CORE_ENABLED=
DROPS_AGENT_LEGACY_CORE_FALLBACK=

# Context Compiler uses the in-process/private Blob snapshot backend by default.
# An embedding provider/model is optional and must use an already authorized model path.
Expand All @@ -86,6 +90,8 @@ DROPS_CONTEXT_INDEX_SNAPSHOT_PREFIX=

# Internal-only eval dashboard and benchmark runners. Use a distinct 32+ byte value.
DROPS_EVALS_INTERNAL_ACCESS_SECRET=
# Enables operator-triggered full release evidence runs. Live model probes stay
# bounded to two verified measurements and persist digests/usage only.
DROPS_EVALS_NIGHTLY_ENABLED=
DROPS_EXPERIMENTS_ENABLED=

Expand Down
28 changes: 21 additions & 7 deletions app/api/internal/agent-evals/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server.js";
import { z } from "zod";
import {
agentEvalsAccessConfigured,
activateAgentV3Evidence,
authorizeAgentEvals,
benchmarkCasesForSuite,
DEFAULT_BENCHMARK_CONFIGURATIONS,
Expand All @@ -16,12 +17,14 @@ export const maxDuration = 300;

const inputSchema = z.object({
suite: z.enum(["local-fast", "ci", "nightly", "release"]).default("local-fast"),
activateEvidence: z.boolean().default(false),
}).strict();

export interface AgentEvalRunRouteDependencies {
store?: DefaultAgentEvalStore;
env?: NodeJS.ProcessEnv;
execute?: typeof executeOfflineContractBenchmark;
activate?: typeof activateAgentV3Evidence;
}

export async function handleAgentEvalRun(
Expand All @@ -43,20 +46,31 @@ export async function handleAgentEvalRun(
} catch {
return NextResponse.json({ code: "EVALS_INVALID_REQUEST", error: "A valid bounded evaluation request is required." }, { status: 400 });
}
if ((input.suite === "nightly" && env.DROPS_EVALS_NIGHTLY_ENABLED !== "1") || (input.suite === "release" && env.NODE_ENV !== "production" && env.DROPS_EVALS_NIGHTLY_ENABLED !== "1")) {
return NextResponse.json({ code: "EVALS_SUITE_DISABLED", error: `${input.suite} evaluations are disabled.` }, { status: 409 });
const suite = input.activateEvidence ? "release" : input.suite;
if ((suite === "nightly" && env.DROPS_EVALS_NIGHTLY_ENABLED !== "1") || (suite === "release" && env.NODE_ENV !== "production" && env.DROPS_EVALS_NIGHTLY_ENABLED !== "1")) {
return NextResponse.json({ code: "EVALS_SUITE_DISABLED", error: `${suite} evaluations are disabled.` }, { status: 409 });
}
try {
const store = dependencies.store ?? new DefaultAgentEvalStore();
const report = await runAgentBenchmark({
suite: input.suite,
cases: benchmarkCasesForSuite(input.suite),
suite,
cases: benchmarkCasesForSuite(suite),
configurations: DEFAULT_BENCHMARK_CONFIGURATIONS,
execute: dependencies.execute ?? executeOfflineContractBenchmark,
concurrency: 3,
concurrency: input.activateEvidence ? 8 : 3,
});
await (dependencies.store ?? new DefaultAgentEvalStore()).writeReport(report);
await store.writeReport(report);
const snapshot = input.activateEvidence
? await (dependencies.activate ?? activateAgentV3Evidence)(report)
: null;
if (snapshot) await store.writeEvidenceSnapshot(snapshot);
return NextResponse.json(
{ report, executionMode: "offline-contract-fixture" },
{
report,
evidenceActivated: Boolean(snapshot),
snapshotId: snapshot?.snapshotId ?? null,
executionMode: snapshot ? "offline-contract-fixture+live-model-matrix" : "offline-contract-fixture",
},
{ status: report.releaseGate.passed ? 200 : 422, headers: { "cache-control": "no-store" } },
);
} catch {
Expand Down
5 changes: 3 additions & 2 deletions app/api/internal/agent-evals/summary/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ export async function handleAgentEvalSummary(
}
try {
const store = dependencies.store ?? new DefaultAgentEvalStore();
const [traces, reports, platform] = await Promise.all([
const [traces, reports, snapshots] = await Promise.all([
store.listTraces(100),
store.listReports(20),
createAgentV3PlatformEvidence({ env }),
store.listEvidenceSnapshots(1),
]);
const platform = await createAgentV3PlatformEvidence({ env, snapshot: snapshots[0] ?? null });
return NextResponse.json(
{ summary: aggregateAgentEvals(traces, reports), platform, storage: "private" },
{ headers: { "cache-control": "no-store" } },
Expand Down
10 changes: 9 additions & 1 deletion app/internal/agent-evals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";

import { AgentEvalDashboard } from "@/components/agent-eval-dashboard";
import { createAgentV3PlatformEvidence } from "@/lib/agent/evals/dashboard-evidence";
import { DefaultAgentEvalStore } from "@/lib/agent/evals/store";

export const metadata: Metadata = {
title: "Agent evals · Drops Studio",
Expand All @@ -10,6 +11,13 @@ export const metadata: Metadata = {
};

export default async function AgentEvalsPage() {
const platform = await createAgentV3PlatformEvidence({ env: process.env });
let snapshot = null;
let storageAvailable = true;
try {
snapshot = (await new DefaultAgentEvalStore().listEvidenceSnapshots(1))[0] ?? null;
} catch {
storageAvailable = false;
}
const platform = await createAgentV3PlatformEvidence({ env: process.env, snapshot, storageAvailable });
return <AgentEvalDashboard initialPlatform={platform} />;
}
32 changes: 20 additions & 12 deletions components/agent-eval-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ interface SummaryPayload {

interface RunPayload {
report?: BenchmarkReport;
executionMode?: "offline-contract-fixture";
executionMode?: "offline-contract-fixture" | "offline-contract-fixture+live-model-matrix";
evidenceActivated?: boolean;
error?: string;
}

Expand Down Expand Up @@ -126,16 +127,16 @@ export function AgentEvalDashboard({
void loadSummary();
}, [loadSummary]);

const runContractBenchmark = useCallback(async () => {
const activateEvidence = useCallback(async () => {
setBusy("run");
setError(null);
try {
const result = await request<RunPayload>("/api/internal/agent-evals/run", {
method: "POST",
body: JSON.stringify({ suite: "local-fast" }),
body: JSON.stringify({ suite: "release", activateEvidence: true }),
});
if (result.executionMode !== "offline-contract-fixture") {
throw new Error("The benchmark did not return its execution mode.");
if (result.executionMode !== "offline-contract-fixture+live-model-matrix" || !result.evidenceActivated) {
throw new Error("The full evidence activation did not complete.");
}
await loadSummary();
} catch (requestError) {
Expand Down Expand Up @@ -212,7 +213,7 @@ export function AgentEvalDashboard({
</div>
<aside className={styles.heroReceipt} aria-label="Promotion state">
<span><ShieldCheck aria-hidden="true" />Candidate gate</span>
<strong>{platform?.dataGate.passed ? "Eligible for candidate review" : "Evidence required"}</strong>
<strong>{platform?.dataGate.passed ? "Evidence active" : "Evidence required"}</strong>
<small>Studio builds and production remain available</small>
</aside>
</div>
Expand Down Expand Up @@ -267,7 +268,14 @@ export function AgentEvalDashboard({
<Badge variant={platform?.dataGate.passed ? "default" : "secondary"}>{platform?.dataGate.passed ? "Passed" : "Required"}</Badge>
</div>
<ul className={styles.gateList}>
{gateBlockers.length ? gateBlockers.map((blocker) => <li key={blocker}><AlertTriangle aria-hidden="true" /><span>{blocker}</span></li>) : <li className={styles.gateClear}><CheckCircle2 aria-hidden="true" /><span>All required evidence is recorded for candidate review.</span></li>}
{gateBlockers.length ? gateBlockers.map((blocker) => <li key={blocker}><AlertTriangle aria-hidden="true" /><span>{blocker}</span></li>) : (
<>
<li className={styles.gateClear}><CheckCircle2 aria-hidden="true" /><span>Immutable baseline: {platform?.receipts.baseline.cases} cases · {platform?.receipts.baseline.results} results.</span></li>
<li className={styles.gateClear}><CheckCircle2 aria-hidden="true" /><span>Failure clustering: {platform?.receipts.failureClustering.clusters} verified clusters.</span></li>
<li className={styles.gateClear}><CheckCircle2 aria-hidden="true" /><span>Design Agent: {platform?.receipts.designAgent.cases} cases · {platform?.receipts.designAgent.passedResults} passing results.</span></li>
<li className={styles.gateClear}><CheckCircle2 aria-hidden="true" /><span>Live model matrix: {platform?.receipts.modelMatrix.measuredModels} measured models.</span></li>
</>
)}
</ul>
</section>
</div>
Expand Down Expand Up @@ -318,13 +326,13 @@ export function AgentEvalDashboard({

<section className={styles.panel} aria-labelledby="benchmark-title">
<div className={styles.panelHeading}>
<div><FlaskConical aria-hidden="true" /><h2 id="benchmark-title">Offline routing comparison</h2></div>
<Button variant="outline" onClick={() => void runContractBenchmark()} disabled={busy !== null}>
{busy === "run" ? <LoaderCircle className={styles.spin} aria-hidden="true" /> : <Play aria-hidden="true" />}
Run contract fixtures
<div><FlaskConical aria-hidden="true" /><h2 id="benchmark-title">Evidence activation</h2></div>
<Button variant="outline" onClick={() => void activateEvidence()} disabled={busy !== null}>
{busy !== null ? <LoaderCircle className={styles.spin} aria-hidden="true" /> : <Play aria-hidden="true" />}
Refresh full evidence
</Button>
</div>
<p className={styles.disclosure}>This runs the labeled local-fast contract slice. It does not call live models or prove a Sandbox preview.</p>
<p className={styles.disclosure}>Runs all 120 deterministic cases, the 10-case Design Agent slice, failure clustering, and a bounded two-model Vercel AI Gateway matrix. Model prompts and outputs are not stored.</p>
{latestReport ? (
<div className={styles.tableWrap}>
<table>
Expand Down
35 changes: 34 additions & 1 deletion lib/agent/evals/dashboard-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
evaluateAgentDataGate,
type AgentDataGateEvidence,
} from "./data-gate.ts";
import { observedEvidenceFromSnapshot } from "./evidence-activation.ts";
import type { AgentV3PlatformEvidence } from "./dashboard-types.ts";
import type { AgentV3EvidenceSnapshot } from "./types.ts";

export interface AgentV3ObservedEvidence {
baselineId?: string;
Expand All @@ -45,6 +47,8 @@ export async function createAgentV3PlatformEvidence(options: {
now?: Date;
env?: Record<string, string | undefined>;
observed?: AgentV3ObservedEvidence;
snapshot?: AgentV3EvidenceSnapshot | null;
storageAvailable?: boolean;
} = {}): Promise<AgentV3PlatformEvidence> {
const repairs = validateRepairDatasetV3(SYNTHETIC_REPAIR_DATASET_V3);
let compactCore: AgentV3PlatformEvidence["registry"]["compactCore"];
Expand All @@ -67,7 +71,7 @@ export async function createAgentV3PlatformEvidence(options: {
};
}

const observed = options.observed ?? {};
const observed = options.observed ?? observedEvidenceFromSnapshot(options.snapshot);
const gateInputs: AgentDataGateEvidence = {
baselineId: observed.baselineId ?? "",
benchmarkCases: AGENT_BENCHMARK_CASES.length,
Expand All @@ -85,6 +89,9 @@ export async function createAgentV3PlatformEvidence(options: {
if (gateInputs.authorizedModelCount === 0) {
blockers.push("Authorized live model inventory and measured matrix evidence are not loaded.");
}
if (options.storageAvailable === false) {
blockers.push("Private evaluation evidence storage is unavailable.");
}

return {
schemaVersion: 1,
Expand Down Expand Up @@ -115,6 +122,32 @@ export async function createAgentV3PlatformEvidence(options: {
stabilizer: "shadow proposals do not mutate canonical files",
design: "contract registered; capture evidence is run-specific",
},
receipts: {
snapshotId: options.snapshot?.snapshotId ?? null,
recordedAt: options.snapshot?.createdAt ?? null,
baseline: {
recorded: observed.baselineResultsRecorded === true,
id: observed.baselineId ?? null,
cases: options.snapshot?.baseline.registeredCaseCount ?? 0,
results: options.snapshot?.baseline.resultCount ?? 0,
},
failureClustering: {
recorded: boundedCount(observed.failureClusterCount) > 0,
clusters: boundedCount(observed.failureClusterCount),
traces: options.snapshot?.failureClustering.inputTraceCount ?? 0,
},
designAgent: {
recorded: observed.designReportRecorded === true,
cases: options.snapshot?.designAgent.caseCount ?? 0,
passedResults: options.snapshot?.designAgent.passedResultCount ?? 0,
},
modelMatrix: {
recorded: boundedCount(observed.measuredModelCount) > 0,
authorizedModels: boundedCount(observed.authorizedModelCount),
measuredModels: boundedCount(observed.measuredModelCount),
models: options.snapshot?.modelMatrix.authorizedModelIds ?? [],
},
},
dataGate: {
passed: gate.passed && blockers.length === 0,
blockers,
Expand Down
8 changes: 8 additions & 0 deletions lib/agent/evals/dashboard-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ export interface AgentV3PlatformEvidence {
stabilizer: "shadow proposals do not mutate canonical files";
design: "contract registered; capture evidence is run-specific";
};
receipts: {
snapshotId: string | null;
recordedAt: string | null;
baseline: { recorded: boolean; id: string | null; cases: number; results: number };
failureClustering: { recorded: boolean; clusters: number; traces: number };
designAgent: { recorded: boolean; cases: number; passedResults: number };
modelMatrix: { recorded: boolean; authorizedModels: number; measuredModels: number; models: string[] };
};
dataGate: {
passed: boolean;
blockers: string[];
Expand Down
Loading
Loading