Repository navigation
[Feat]: Agent Evaluation & A/B Platform - #48
Conversation
…M-as-a-Judge evaluation platform
…delete confirmation and read-only target agent node
…consolidated table and shadcn ui components
… select items, and group labels
… fix variant edit deduplication, and refine Studio tab UI
… Radix Select positioning
… and enhance trace metadata extraction
…l callback filtering
…evaluation studio
…hatAgent node naming
… unified table UI, and updated docs
…k, and eval_judgment tables
Wires the Agent Benchmark & Evaluation Studio end-to-end: - POST /api/eval/benchmarks/run dispatches runBenchmark() which invokes the agent graph, records eval_run via recordEvalRun (routes through the rubric/var fallback chain so FK violations against seeded prompt_variant rows don't recur), persists a paired observability span for the benchmark output, and triggers the AI judge through the LangGraph dev server via runs.wait — same pattern as /api/eval/judge. - Benchmark Studio card UI replaces the old toast with a real fetch + per-row loading state + error toast. - eval_thread kind = 'eval' added so the benchmark and judge threads are filtered out of the sidebar's chat thread list (kind='kb' filter).
Removes unused imports/vars, swaps Copy/Edit/Delete to outline variants, truncates long template IDs with hover-tooltip via the existing truncateId helper, tightens table padding, and trims redundant chrome (Benchmarks/Executions count badges, desc span) from agent headers. No behavior change.
Splits CONTEXT & AI JUDGE ASSESSMENT into two columns in the Online Executions table and drops RUN ID & STATUS and LATENCY (run IDs are meaningless to most users; latency was agent duration, not judge — tracking that requires schema work). Tightens agent header padding to py-2 px-4 and bumps prompts studio first-column indent to pl-12 to align under the agent badge.
Renders a weighted Score badge after the AI Assessment label (Σ(score × weight) / Σ(weight)) with a tooltip that breaks down the per-criterion math; the Edit Rubric dialog gains an inline weight input (0-100%, % suffix). TooltipContent gains a showArrow prop so callers can opt out of the default radix arrow (used here to keep the breakdown tooltip arrow-free).
Replaces the global limit(50) on recent runs with cursor-based per-agent pagination (page size 5). Adds GET /api/eval/runs/page and rewrites /api/eval/runs/compare to return runs grouped by agent + pageSize/hasMore/nextCursor; frontend tracks runsByAgent + pageByAgent + per-agent loading state and renders a Load Older Executions button under each Online Executions table.
- lib/eval/queries.ts: getRunsByAgentPage({ agent, cursorId, limit }) — LIMIT+1 hasMore detection, ORDER BY createdAt DESC, id DESC, tie-broken on id for stable walks; limit clamped to 1..50.
- app/api/eval/runs/compare/route.ts: groups by agent, fan-out parallel queries, response includes runs/hasMore/nextCursor.
- app/api/eval/runs/page/route.ts: GET ?agent=&cursor=&limit= → next page for one agent.
- Tests: 4 new pagination cases (ordering, hasMore/cursor, default limit, clamp, empty agent) + 4 endpoint cases (401/400/happy/default).
- Frontend: EvalDashboardClient holds runsByAgent + pageByAgent + loadingMore + handleLoadMoreRuns; AgentBenchmarkTab renders a Load Older Executions row when pageByAgent[id].hasMore.
- docs/APIS.md: /compare response shape + new /page endpoint documented (rule 1).
evalAgent now serves two modes via a mode discriminator at inputRouter:
- mode="judge" (default): unchanged behavior — judge an existing eval_run.
- mode="benchmark": full pipeline as one graph invocation — preDispatch
(timing capture) → benchmarkDispatch (targetAgent dispatch) → invoke<X>
(per-target invocation wrapper) → recordEvalRun (writes eval_run + paired
observability span + hidden benchmark thread) → judge → cleanupBenchmark
(self-gated no-op that deletes the hidden thread only in benchmark mode).
Routing is fully edge-driven via addConditionalEdges. routeByMode / routeByTargetAgent
return discriminator strings that align with map keys to satisfy langgraph pathmap
contract (regression: returning the resolved node names from the routing fn blew
up with "Branch condition returned unknown or null destination").
GRAPH_MAPPING is gone — adding a new target agent means one addNode + one
TARGET_NODES entry, no separate lookup table.
app/api/eval/benchmarks/run now resolves targetAgent / inputPrompt / expectedOutput
server-side and dispatches langGraphClient.runs.wait(threadId, "evalAgent", { input })
once. lib/eval/benchmark-runner.ts is deleted entirely.
Frontend: "Run Evaluate" button label renamed to "Evaluate" in agent-benchmark-tab
to mirror the manual re-evaluate entry point (judge mode).
Tests:
- tests/backend/agent/eval-agent.test.ts: routeByMode + routeByTargetAgent.
- tests/api/eval/benchmarks-run.test.ts: route resolves benchmark row and
dispatches evalAgent with mode=benchmark; covers 401/400/404/200/500.
eval_benchmark gains 4 denormalized columns so Benchmark Datasets can
render "Last AI Judge Assessment" inline, without per-render joins to
eval_run / eval_judgment. eval_judgment still owns score history; the
benchmark row only carries a pointer + score snapshot of the most
recent run.
- db/migrations/0021: latest_judgment_id (FK), latest_run_at,
latest_run_status, latest_score (numeric).
- lib/eval/schema.ts: column types + EvalBenchmarkRow accessor.
- backend/agent/eval-agent.ts: judgeNode computes weighted score
(same formula as frontend) and UPDATEs eval_benchmark with the
latest_* fields after saveJudgment. benchmarkId plumbed through
EvalAgentState and into /api/eval/benchmarks/run.
- lib/eval/queries.ts: getAllBenchmarksWithLatestJudgment()
returns rows LEFT JOIN eval_judgment BY latest_judgment_id so
the GET endpoint can hand the UI an enriched row in one fetch.
- app/api/eval/benchmarks/route.ts: switched GET to use the
enriched helper.
Frontend component extraction ensures Online Executions and
Benchmark Datasets render identical AI Judge output:
- app/admin/eval/components/judgment-assessment-cell.tsx (new):
weighted score badge + Σ tooltip, per-criterion sub-badges,
reasoning quote, Activity-trace button. Accepts a permissive
JudgmentSummary (Pick of the columns it actually reads) so both
full-Judgment and partial-benchmark-row shapes render the same.
- app/admin/eval/benchmark/agent-benchmark-tab.tsx: removed
inline computeWeightedScore + duplicated JSX. Benchmark Datasets
table now shows four columns — TEST CASE | INPUT PROMPT &
Expected Ground Truth | Last AI Judge Assessment (new) |
ACTIONS. Online Executions AI JUDGE ASSESSMENT column reuses
the same component.
docs/APIS.md updated for the enriched GET response + side effects
on POST /api/eval/benchmarks/run (rule 1).
…ssessment updates handleRunEvaluate called onRefresh() which covers /compare and /rubrics, but NOT /api/eval/benchmarks — and the denorm path (latest_judgment_id → join → judgment) lives only in the benchmarks GET. Added fetchBenchmarks() right after the toast so the row reflects the latest judgment immediately instead of staying null until tab switch / page reload.
Three changes bundled because they share a code-review surface (the
Benchmark Datasets surface):
1. Drop cleanupBenchmark node (backend/agent/eval-agent.ts). Its
only job was deleting the hidden benchmark thread row, but
graph execution in langgraphjs dev lives inside a broad
checkpoint transaction. When the DELETE failed (or surfaced
an FK lock error) the wider tx rolled back, taking the
upstream eval_run / eval_judgment INSERTs with it — leaving
eval_benchmark.latest_* pointing at a judgment that never
persisted. .catch(() => null) hid the throw from the route,
so the user saw status="completed" while the DB was empty.
recordEvalRunNode still inserts the threads row; it stays
invisible because lib/threads/queries.ts filters by
kind="chat" (kind="eval" never surfaces in the chat sidebar).
Future retention sweeper can prune kind="eval" rows if they
grow.
2. Move per-criterion sub-badges into their own row in
JudgmentAssessmentCell. The previous inline position
(next to the Sparkles + "AI Assessment:" label) broke the
row when more than ~2 criteria fired. The dedicated row
below lets badges wrap naturally without crowding the
weighted Score badge.
3. Tighten column widths in Benchmark Datasets and Online
Executions: TEST CASE 200→300px (merged test-case + input +
expected needs the extra room), Online Executions ACTIONS
140→125px (Evaluate button + trace icon). Header text gets
uppercase "LAST AI JUDGE ASSESSMENT" to match the
existing monospace header style.
…e node
Three tweaks bundled because they all relate to the eval platform surface:
1. /api/eval/runs/compare + lib/eval/queries.ts:getRunsByAgentPage now
INNER JOIN threads ON eval_run.thread_id = threads.id WHERE
threads.kind = 'chat'. Benchmark runs (recordEvalRunNode
inserts threads with kind='eval') no longer cross into the
Online Executions tab — they're a synthetic test surface,
not production traffic. No schema change: threads.kind already
existed (chat | kb | eval) for the chat-sidebar filter.
2. Benchmark Datasets evaluate button text mirrors Online
Executions: "Re-evaluate" when bm.judgment already exists,
"Evaluate" otherwise, "Evaluating…" mid-flight. Same UX in
both places.
3. judgeNode → judgeByLLMNode / node id "judge" → "judgeByLLM".
The mode discriminator stays "judge" (routeByMode return value
unchanged); only the node id renamed to make the LLM-as-judge
semantics explicit at the topology level. Conditional edge
map keys still match router return values, just routing to
the renamed node.
Replaces the centered Activity-spinner "Loading Evaluation Platform…"
gate with a layout-shaped skeleton that mirrors the eval-dashboard
shell (header, 3-tab strip, cohort cards grid, table block). The new
behavior:
- First fetch: render <SkeletonLoading /> until the first response
lands.
- Subsequent fetches (refresh / re-query): keep the existing data
visible and swap it in place once the new payload arrives. The
active sub-tab, scroll position, and any per-row state survive
the refresh — no remount, no global overlay.
The "first load vs subsequent" discriminator is a useRef
(hasLoadedOnce.current). The same fetchData path is reused on every
refresh; only the render gate changed. <Skeleton> primitive from
components/ui/skeleton.tsx is reused — no new atom.
Each action in the Agent Benchmark & Evaluation tab already triggers its own refetch: - handleRunEvaluate → onRefresh() + fetchBenchmarks() - handleAddBenchmark → fetchBenchmarks() - handleDeleteBenchmark → fetchBenchmarks() - handleSaveRubric → onRefresh() - handleSingleJudge → onRunJudge chains re-fetch on completion A manual "Refresh Executions" button therefore had nothing to do — any data the user could want was already refreshed by the action that changed it. Removed the button (and its onRefresh prop dep is kept for backwards compatibility but unused elsewhere; can be cleaned later).
…patch
preDispatchNode and benchmarkDispatch were both pure syntactical
landings for two separate addConditionalEdges calls — they did no real
work. preDispatchNode stamped dispatchAt but recordEvalRunNode never
read it (state.totalMs is set by every invoke<X>Agent before it),
and benchmarkDispatch returned {}. Two dead nodes holding the graph
together.
Collapsed into one routeFromInput() that resolves mode + target
agent in a single step. The graph now goes:
START → inputRouter → (mode | target in one hop) → judgeByLLM
→ invoke<X>Agent
→ recordEvalRun → judgeByLLM → END
Changes:
- preDispatchNode, benchmarkDispatch: removed.
- dispatchAt: removed from EvalAgentState and from recordEvalRunNode
(no consumer).
- recordEvalRunNode + span rows: startedAt / endedAt now use
Date.now() directly; totalMs fallback uses state.totalMs ?? 0.
- routeByMode / routeByTargetAgent: removed; tests updated to
cover the single routeFromInput dispatch instead.
Net change: -2 nodes, -2 conditional edges, -1 routing helper.
…dler across graphs
Five touching concerns bundled for clarity at review time:
1. backend/prompt/system.ts: new EVAL_JUDGE_SYSTEM_PROMPT
(ROLE / GRADING-SCALE / INPUT / OUTPUT / CONSTRAINTS / SELF-CHECK
skeleton matching the other system prompts). Carries the persistent
role + scoring rubric + parse rules; the per-run data goes in a
separate HumanMessage so the system text is stable across runs
and not subject to be re-stated on every judge invocation.
2. backend/state.ts: EvalAgentState (formerly local to
backend/agent/eval-agent.ts) now lives alongside RouterAgentState,
CommonAgentState, KbAgentState. eval-agent only owns the topology
+ nodes; state schemas belong with the other StateSchema
definitions.
3. backend/agent/eval-agent.ts: judgeByLLMNode now invokes the
structured model with [SystemMessage(EVAL_JUDGE_SYSTEM_PROMPT),
HumanMessage(per-run payload)] instead of a single concatenated
string prompt. The per-run payload still contains the criteria
list, target run id, agent name, execution duration, status,
and conversation trace context — but no longer restates role
language.
4. backend/agent/{kb-agent,background-agent}.ts +
backend/agent/eval-agent.ts (callback section): wire
evalCallbackHandler into each compiled graph alongside the existing
capturingHandler + creditTrackingHandler so eval-time spans are
captured across every Agent node uniformly.
5. Cleanup: drop the local EvalAgentState declaration in
backend/agent/eval-agent.ts and the unused tslib imports that
came along with it.
…ke effect
Three touching changes — together they close the loop between admin
edits in Prompt Studio and the live judge LLM call:
1. lib/eval/queries.ts seedInitialPrompts:
- ALL_AGENT_PROMPTS gets evalJudgeAgent: EVAL_JUDGE_SYSTEM_PROMPT
- GRAPH_MAPPING gets evalJudgeAgent: "evalAgent"
→ next db:migrate writes prompt_template row + default variant for
evalJudgeAgent, so the Prompt Studio card stops showing
"No prompt templates deployed for this node yet" and exposes
the current system prompt body.
2. backend/prompt/loader.ts:
- FALLBACK_PROMPTS gets evalJudgeAgent: EVAL_JUDGE_SYSTEM_PROMPT
→ getAgentPrompt("evalJudgeAgent", userId) resolves before any DB
seed has run (defensive fallback) and routes through the same
user-variant assignment all other agents use.
3. backend/agent/eval-agent.ts judgeByLLMNode:
- Drops the EVAL_JUDGE_SYSTEM_PROMPT import.
- Pulls userId from config (same pattern as recordEvalRunNode).
- Calls getAgentPrompt("evalJudgeAgent", userId) and feeds the
resolved content into SystemMessage.
→ admin edits in Prompt Studio take effect on the next judge run
(no graph restart), matching how chatAgent / weatherAgent /
etc. pick up their prompt already.
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
PR #48 review (verifying + adding)Walking the eval backend (API routes, agent graph, callback, queries, migrations). Confirms most of the previously-flagged issues + adds a few that were not called out before. Verifications (confirm prior claude review + Greptile)
New findingsA. Chat feedback is permanently a silent no-op. UI sends assistant message id ( B. Wrong C. Judge trace-context lookup is always empty in benchmark mode. D. E. Variant attribution is broken at both ends.
F. Synthetic FK fallbacks in callback crash any non-prod caller. G. H. Tests + docs + rules
Bottom lineBlockers:
Non-blocking: landing page UI, docs polish. Shape is good; the wire is where it falls down. |
… feedback)
Closes the P1 security findings the AI reviewers (claude[bot], greptile, cubic) raised against /api/eval/*.
**Security fixes (must-fix per repo rules):**
- Every /api/eval/* mutation endpoint is now withAuth({role:"admin"}): assignments GET/POST, prompts POST/PUT/DELETE, benchmarks POST, rubrics POST, benchmarks/run POST, judge POST, runs/compare GET, runs/page GET. The eval studio is an admin-only surface; bare withAuth was a privilege-escalation primitive.
- /api/eval/runs/[id] now filters by owner OR admin roleId — non-owners get 404 (not 403) so run existence isn't leaked.
- EvalCallbackHandler.handleLLMEnd / handleLLMError now early-return when run.threadId or run.userId is empty. The previous "dev-thread" / "dev-user" fallbacks FK-violated every non-prod callback (unit tests, dev traffic) and the catch swallowed it — production eval rows silently disappeared. Skipping the write is the right behavior; matching CLAUDE.md rule that user-visible paths silently no-op on missing rows.
**Docs / comment fixes (must-fix):**
- /admin/eval page now wraps in ObservabilitySheetProvider + mounts <ObservabilitySheet />. Direct navigation crashed before render because useOpenObservabilitySheet threw outside the provider.
- docs/APIS.md: dropped the stale 'cleanup' claim — eval-agent has no cleanup node (it was intentionally dropped in commit f853826). The hidden benchmark thread (kind=eval-benchmark) is retained for the judge trace link.
- lib/eval/queries.ts: rewrote the misleading submitFeedback ponytail comment. The chat UI sends the assistant message id (resp_...) but the callback writes the *human* message id into parent_message_id, so they don't match today; the lookup silently no-ops. Left as a follow-up — fixing it requires picking which side owns the id (UI sends human id, callback writes assistant id, or we add an assistant-message-id column), all of which are main-flow rewrites and out of scope here per repo rules.
**Test fixtures:**
- benchmarks-run.test.ts + runs-page.test.ts now set roleId='admin' on the mocked session so the admin gates above don't 403 the existing happy-path tests. The mock already supported role checking.
**Reviewers NOT addressed (rejected per repo rule):**
- Schema/migration FK additions (latest_judgment_id REFERENCES, ON DELETE SET NULL for eval_run.user_id) — schema changes are out of scope.
- Weighted-score formula change ([20,100] → [0,100]) — touches frontend contract.
- Benchmark thread cleanup / parent_message_id invariant — main-flow rewrites.
- batch_update_weights transaction wrapping, copy-on-edit templates, zero-weight pinning, rubric fallback semantics, message-id ownership refactor — main-flow rewrites.
- ModelKind Zod schema adding 'eval' — separate eval-models work; not required for this PR.
- System-template delete gating, benchmark inputPrompt sanitization, variant misattribution in callback — design / API rewrites.
- Greptile 'Too many files' complaint + various low-priority polish.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Additional review (third pass). Walked the eval backend end-to-end. Most criticals already covered by prior comments; this adds structural gaps and inconsistencies. (1) Variant attribution has 3 layers of bug: literal placeholders tmpl_chat_v1 / var_chat_default in lib/eval/callback.ts:59-119 never match the seeder IDs (tmpl_${agentName}v1 / var${agentName}_default in lib/eval/queries.ts:89-90), so the fallback SELECT with .limit(1) no ORDER BY at lib/eval/queries.ts:567-575 silently misattributes every A/B run, AND the prepareDataNode state.templateId/variantId are stored in graph state but never propagated to LLM invoke metadata so the callback cannot pick them up even if the placeholders matched. (2) Docs at docs/APIS.md:368,376,383 list only 200/401/500 for admin-gated /api/eval/runs/{compare,page,[id]} -- should add 403; the [id] owner-or-admin gate also returns 404 cross-user per app/api/eval/runs/[id]/route.ts:23-25. (3) Smaller items: submitFeedback source field unvalidated, recordEvalRunNode benchmark thread onConflictDoNothing lets retries split eval_run between two shadow threads, batch_update_weights can zero out the cohort. Inline comments on lib/eval/callback.ts:60, backend/agent/eval-agent.ts:166+399, app/api/eval/assignments/route.ts:1, docs/APIS.md:368, backend/node/kb/page-to-markdown-node.ts:44 walk through the fixes. Generated with Claude Code. |
Per Claude reviewer's confirmed analysis: backend/node/kb/page-to-markdown-node.ts:44 called getAgentPrompt('pageToMarkdown') with no userId, so backend/prompt/loader.ts:41 short-circuited to the static KB_OCR_PAGE_PROMPT fallback on every call. The seeded tmpl_pageToMarkdown_v1 / var_pageToMarkdown_default rows were inert — admin-edited prompt variants never reached KB OCR.
state.userId is on KbAgentStateShape (line 109, nullable). Synthetic callers without a real userId still hit the static fallback via ?? undefined, matching the pattern in chat-agent.ts (chatAgent, userId ?? undefined).
The other 12 callers of getAgentPrompt already pass userId — verified via grep across backend/. Removed the now-unused KB_OCR_PAGE_PROMPT import.
|
Comprehensive review. The PR is blocked from merging by three correctness/security issues; the rest are quality/clarity. I agree with Greptile's four highlighted concerns, and the recordEvalRun DB-fallback is the real variant-attribution bug (the page-to-markdown userId was already fixed in this diff - the comment on page-to-markdown-node.ts:43-48 acknowledges the previous exception). Verified, must-fix before merge
Verified, nice-to-have
CLAUDE.md compliance
Great work overall - the architecture is sound, the seeder is properly idempotent, and the cursor pagination in getRunsByAgentPage (queries.ts:811-906) handles the PG microsecond-vs-JS-millisecond gotcha I would have expected to miss. Just close the four correctness gaps above. |
|
Post-merge summary of what landed in #48. Eval platform core
Background agent rename: API surface (
Admin Eval Studio ( Security fixes (post-review):
Landing page: KB OCR fix: Docs: Tests: 1406 passing; 33 new cases across Rejected from review (per repo rule '如果涉及到 api 和主流程大改的 或者加表改 schema 的 reject 掉'): schema/migration FK additions, weighted-score formula flip, benchmark-thread cleanup, message-id ownership refactor, copy-on-edit templates, batch-update transaction wrapping, zero-weight pinning, ModelKind Zod schema, system-template delete gate rework, benchmark inputPrompt sanitization, variant misattribution in callback, eval-jurisdiction rubrics fallback semantics, parent_message_id invariant CHECK constraint, latest_judgment_id FK, eval_run.user_id cascade change — all documented in commit bodies + reply threads as follow-ups for separate PRs. |
|
Per the post-merge summary comment above — every item in this round was already triaged and rejected in rounds 1+2 per repo rule '如果涉及到 api 和主流程大改的 或者加表改 schema 的 reject 掉'. Quick map: auth gaps on GET handlers (PRRT_kwDOS--nSs6T4JJg/JJw/JK4) → reject (admin role gate would change which users can see prompt/rubric/benchmark lists; this PR's role gating was limited to mutation endpoints; widening it is a follow-up). Variant attribution (T4JM0, T4JRa) + benchmark cleanup (T4JPb) + stale denorm on failure (T4JPr) + db fallback orderBy (T4JO8) + intentional no-op follow-up (T4JQI) → reject per project rule, see reply bodies above. None of these warrant a hotfix on a merged PR. |
Closes #16
Problem
The repo shipped with a chat-only experience — every assistant turn left the system as soon as the user clicked away. There was no signal for "is this prompt version better than that one," no way to audit a specific turn's reasoning, and no admin surface to seed a benchmark dataset or weight a rubric criterion.
What changed
Core eval platform (
lib/eval/+backend/agent/eval-agent.ts):prompt_template,prompt_variant,prompt_variant_assignment,eval_run,eval_feedback,eval_rubric,eval_benchmark,eval_judgment) on migrations 0017–0021.evalAgentgraph (backend/agent/eval-agent.ts:graph, registered asevalAgentinlanggraph.json) withmode="judge"/mode="benchmark"dispatched viarouteFromInputconditional edges.EvalCallbackHandler(backend/eval/callback.ts, singleton inbackend/callbacks.ts) writeseval_runon every chat-modelhandleLLMEnd, keyed byparent_message_id. Reads user id from run-levelmetadata.user_id(NOTconfigurable.userId).seedInitialPrompts(idempotent) populates 11 agent prompt templates, 11 rubrics (incl.judgeByLLM: criteria_alignment 0.4 / reasoning_clarity 0.4 / calibration 0.2), 1 genericrubric_default, and ≥1 benchmark row per agent.threads.kindenum split:chat/kb/eval-judge/eval-benchmark. The twoeval-*kinds share the eval-agent graph but the run is fundamentally different — judge scores an existing production run, benchmark generates one from a prompt. Online Executions surfaces judge work (eval-judge); Benchmark Datasets is the only home for synthetic benchmarks (eval-benchmark).API surface (
app/api/eval/*):POST /api/eval/feedback— chat thumbs-up; resolvesresp_...againsteval_run.parent_message_id; silent no-op on no match (no dangling FK violation).GET / POST / PUT / DELETE /api/eval/prompts— template + variant lifecycle (create_template/create_cohort_variant/delete_cohort_variant/create_variant/update_variant_weight/batch_update_weights); system templates are 403 on DELETE.GET / POST /api/eval/rubrics— list + upsert by id.GET / POST /api/eval/benchmarks— per-agent benchmark CRUD +update_rubricaction.POST /api/eval/benchmarks/run— Run Evaluate end-to-end.POST /api/eval/judge— Run Judge against an existing eval_run.GET /api/eval/runs/compare— A/B stats + first page of recent runs per agent.GET /api/eval/runs/page— per-agent paginated Online Executions (cursor-stable via PG row-value(created_at, id)).GET /api/eval/runs/[id]— single-run drilldown (run + feedback + judgment + first 20 spans).GET / POST /api/eval/assignments— sticky(userId, agent)bindings (cohort + single-agent reassignment).Admin Eval Studio (
app/admin/eval/*): per-agent cohort cards, unified<table className="table-fixed">for Online Executions + Benchmark Datasets, Rubric weight editor with weighted-score badge, Prompt Studio card with system-prompt edit + A/B weight sliders.Background agent rename:
background_agentgraph id →backgroundAgent(file slug unchanged;langgraph.json+ every dispatch site updated together).Landing page (
/):Composable toolsbento card →Evaluation & A/B(ScaleIcon, fuchsia hue). New 7th How-it-works rowEvaluationbelowKnowledge basewith its own motion demo (evaluation-explainer-demo.tsx) — direction is reverse of the KB explainer (run bubbles left, judge card right, criteria light up right-to-left, scores stamp back).LLM-facing docs:
public/llms.txt+public/llms-full.txtupdated with eval platform, full/api/eval/*surface, 11 seeded agent IDs, threads.kind discriminator,evalAgentmode dispatch.Tests
3da1f98.tests/api/eval/*(benchmarks-run, feedback, runs-page),tests/backend/agent/eval-agent.test.ts,tests/lib/eval/queries.test.ts,tests/frontend/landing/sections.test.tsx.Docs
docs/EVALUATION.md— full architecture + ops writeup (kind discriminator, agent IDs, seed contents, route map, no-op semantics).docs/APIS.md— eval section rewritten with kind-discriminator table, per-action prompt lifecycle body, benchmark row denormalization, judge/benchmark kind semantics.docs/DEPLOY.md— eval tables +kindenum migration note.docs/DB.md—threads.kindrow updated to 4 values; Code → table map gets all 8 eval tables.docs/OBSERVABILITY.md— sidebar filter rationale updated to mention the twoeval-*kinds.docs/LANDING.md— section list updated to 7 explainer rows + Evaluation & A/B bento card.README.md— Features bullet for Agent Evaluation Studio; file tree + Database section updated.public/llms.txt,public/llms-full.txt— eval platform surfaced.🤖 Generated with Claude Code
Summary by cubic
Adds an Agent Evaluation & A/B platform with prompt templates/variants, sticky traffic splits, an LLM judge, and an Admin Studio for benchmarks and online executions — gated by admin role with strict data ownership. Closes #16.
New Features
evalAgentgraph withmode="judge"andmode="benchmark".getAgentPrompt;EvalCallbackHandlerrecordseval_runwithtemplateId/variantId, keyed byparent_message_id.rubric_default; sticky traffic splits and user-cohort assignments./api/eval/feedback,/api/eval/prompts,/api/eval/rubrics,/api/eval/benchmarks(+/run),/api/eval/judge,/api/eval/runs/compare,/api/eval/runs/page,/api/eval/runs/[id],/api/eval/assignments.threads.kindnow includeseval-judgeandeval-benchmark; Online Executions exclude onlyeval-benchmark.backgroundAgent; docs and landing updated.Bug Fixes
role="admin"; runs pagination/compare are admin-only;/api/eval/runs/[id]is owner-or-admin (non-owners get 404).EvalCallbackHandlerskips writes whenthreadIdoruserIdis missing to avoid FK errors.page-to-markdownpassesuserIdtogetAgentPrompt, so Prompt Studio edits affect ingestion.Written for commit e552b4a. Summary will update on new commits.
Greptile Summary
Adds a database-backed agent evaluation and A/B-testing platform.
evalAgentgraph and adds evaluation controls to chat and landing-page surfaces.Confidence Score: 1/5
The PR is not safe to merge until Eval Studio authorization, prompt attribution, benchmark lifecycle persistence, and page-to-markdown prompt loading are corrected.
Non-admin users can access and mutate globally scoped evaluation resources, normal executions can be attributed to the wrong prompt variant, failed benchmark attempts retain stale success data, and synthetic benchmark threads accumulate without cleanup.
Files Needing Attention: app/api/eval/**, lib/eval/callback.ts, lib/eval/queries.ts, backend/agent/eval-agent.ts, backend/node/kb/page-to-markdown-node.ts
Security Review
The Eval Studio API surface lacks the admin-role checks used by the rest of the admin API, allowing any authenticated account to read global evaluation data and mutate prompts, assignments, rubrics, and benchmarks.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR U[Chat or benchmark input] --> A[Agent prompt loader] A --> V[Sticky prompt variant] V --> L[LLM execution] L --> C[Eval callback] C --> R[(eval_run)] R --> J[evalAgent judge] J --> S[(eval_judgment)] J --> B[(Benchmark latest result)] R --> D[Admin Eval Studio] S --> D B --> DPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "style(landing): polish Evaluation explai..." | Re-trigger Greptile
Context used: