Skip to content

Ship 2 (epic): agent eval substrate, layered with framework on top per ADR 14 #20

Description

@nabinpkl

Summary

Build the agent eval substrate so just eval evals/cases/<suite>.yaml runs a real suite end-to-end against the live stack: hits /agent/ask, captures trace_ids, runs probes against otel.otel_traces, persists per-probe JSON results + a summary, and prints pass/fail counts (with a regression diff against committed baselines).

Architectural decision recorded in ADR 14. Foundation comes from ADR 13 (OTel + Langfuse) plus Ship 1.5 (mcae.* namespace + enriched gate/claim/primitive spans, commits 1177c27 and 6d81e6b).

The four invariants this ship protects

  1. A case is data, not code (YAML, loadable into a typed schema, stable IDs).
  2. A probe is a predicate over an OTel trace (pure function, no framework imports).
  3. A probe result is a structured artifact (JSON, queryable in ClickHouse, schema independent of any tool).
  4. The agent under test is invoked exactly the way production invokes it (/agent/ask over HTTP, real OTel pipeline, real mcae.* and gen_ai.* spans).

The four-layer stack

Layer 4: Framework adapter (THIN, SWAPPABLE)  ~80 LOC, pydantic_evals seam
Layer 3: Runner (FRAMEWORK-AGNOSTIC)          ~50 LOC, orchestration
Layer 2: Probes (PURE FUNCTIONS, OURS)        ~150 LOC, 5-7 probe kinds
Layer 1: Schema (CANONICAL, OURS)             ~80 LOC, pydantic models

Lower layers know nothing about higher ones. Framework swap touches only Layer 4.

Probe vocabulary (initial)

  • has_matching_span(span_name, attrs)
  • tool_called_with_args(tool_name, arg_predicates) — reads gen_ai.tool.name + mcae.primitive.input
  • claim_grounded_in(source_kind) — reads mcae.claim.source_kind, anchor for future sql_explore hybrid
  • gate_passed(kind, version?) — reads mcae.gate.verdict + mcae.gate.version
  • span_latency_p50_under(span_name, ms) — aggregates OTel Duration
  • no_span_with_status(span_name, status)
  • llm_call_used_model(model_name) — reads gen_ai.request.model

New probe kinds extend the schema's Literal and add a file in agent_service/evals/probes/.

Sub-tickets (sequential, each lands as its own commit)

Done when

  • just eval evals/cases/wallet_profile_smoke.yaml returns a summary like 5/5 cases passed, 23/23 probes passed (vs baseline: +0 -0).
  • evals/runs/<run_id>/ populated with per-probe JSON.
  • New probe kinds can be added by extending the schema Literal + dropping one file in probes/.
  • New cases are pure YAML edits.
  • Layer 4 can be swapped (rewrite pydantic_evals_adapter.py) without touching Layers 1-3.

Trigger conditions to revisit ADR 14

  • pydantic_evals introduces a feature we want that does not map cleanly to our schema.
  • pydantic_evals stagnates or pivots in a direction incompatible with our use.
  • Anthropic, OpenAI, or another credible 2026+ source publishes an agent-eval pattern materially different from this design (verify CURRENT per AGENTS.md research-dating rule).

References

  • ADR 14 — decision rationale and four invariants
  • ADR 13 — OTel substrate this ship consumes
  • AGENTS.md — library acceptance bar + research-dating rule + no-adapter-bridging rule

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestship:eval-substrateShip 2: framework-on-foundation eval substrate per ADR 14

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions