ADR-042: Adopt agentic-evalkit as ARP's Evaluation Framework (Sliced Migration)¶
Status: Proposed
Date: 2026-07-03
Related: agentic-v2-eval/ (in-tree package being superseded),
agentic_v2/scoring/step_scoring.py, agentic_v2/scoring/evalkit_bridge.py
(new, this slice), agentic_v2/models/llm.py, ADR-032 (extract scoring
package), ADR-033 (eval-config root resolution), ADR-023 (ExecutionKit
external-package precedent)
Context¶
agentic-v2-eval is ARP's in-tree evaluation package: rubric-YAML scoring
(Scorer), an LLM-as-judge, dataset adapters, sandboxed execution, and
multi-format reporting. It is a monorepo workspace member
([tool.uv.workspace] members in the root pyproject.toml) installed
directly into CI (.github/workflows/ci.yml).
A standalone library, agentic-evalkit, has been built independently (see
agentic-evalkit in the wider workspace) as a general-purpose evaluation
framework: typed, frozen Pydantic contracts (EvalSample,
NormalizedExecutionResult, GradeResult), a structural ExecutionTarget
protocol with callable/subprocess/HTTP adapters, composable graders
(Rubric/RubricCriterion, CompositeGrader/WeightedGrader, schema and
judge graders), dataset presets, and multi-format reporting. It enforces its
own architectural boundary with an AST-based contract test: evalkit code may
never import agentic_v2, tools, or executionkit. It has no public git
remote yet — it exists only as a local checkout.
ARP already consumes one piece of evalkit's design lineage today in reverse:
agentic_v2/models/llm.py imports LLMClientProtocol from
agentic_v2_eval.interfaces (the in-tree package), not the other way
around — agentic-v2-eval's own docs describe this as "exports
LLMClientProtocol -> consumed by agentic-workflows-v2." Any migration must
account for that existing ARP -> agentic_v2_eval dependency edge, not just
the agentic_v2 -> agentic_v2_eval scoring call sites.
We have a working precedent for absorbing an external, independently-versioned
library into ARP: ADR-023 moved executionkit from a co-located sibling path
to a published PyPI dependency ([tool.uv.sources] no longer points at a
local path; ek = ["executionkit>=0.1.0,<0.3.0"] is a normal version-pinned
extra). agentic-evalkit is earlier in that same lifecycle — pre-remote,
pre-publish — so the adoption path mirrors ADR-023's ladder but starts one
rung lower.
Decision¶
Adopt agentic-evalkit as ARP's evaluation framework, superseding
in-tree agentic-v2-eval, via a sliced migration rather than a single
cutover PR. Each slice is independently mergeable and leaves CI green.
Install-mechanism ladder¶
agentic-evalkit's installation method advances as the library matures,
mirroring the ExecutionKit precedent (ADR-023):
- Today (Slices A-B): local path dependency, dev-only. Added as an
optional
uvworkspace/path source for local development (pip install -e '../agentic-evalkit', analogous to howekwas consumed before ADR-023's PyPI cutover). Not installed in CI. - Once evalkit has a public remote (Slice C onward): pinned git
dependency.
agentic-evalkit @ git+https://github.com/<org>/agentic-evalkit@<SHA>, pinned to a specific commit SHA (not a branch) for reproducibility — the same discipline ARP already applies to its own dependency pins (ci-constraints.txt). - Once evalkit publishes to PyPI: version-range pin.
agentic-evalkit>=X.Y,<Zas a normal[project.optional-dependencies]extra, replacing the git pin — the end state ADR-023 already reached forexecutionkit.
The hard dependency invariant¶
agentic-evalkit must never import agentic_v2, tools, or executionkit.
This is enforced inside evalkit itself by its own AST-based boundary
contract test, not by anything in this repository. The practical consequence
for ARP: all adaptation logic between the two systems must live on the ARP
side of the boundary. agentic_v2/scoring/evalkit_bridge.py (this slice) is
that adapter; evalkit's public API is never modified to accommodate ARP.
Slice plan¶
- Slice A — dependency. Add
agentic-evalkitas an optional, dev-only local-path dependency. No behavior change; nothing imports it yet. - Slice B — bridge (this ADR/PR). Add
agentic_v2/scoring/evalkit_bridge.py: rubric-YAML -> evalkitRubricconversion, ascore_criteriaweighted-score function reproducing legacyScorersemantics via evalkit'sRubricmodel, and aworkflow_callable_targetfactory wrapping an ARP workflow-run coroutine as an evalkitCallableTarget. Additive only —step_scoring.pyis untouched, nothing calls the bridge yet, and evalkit stays optional (not installed in CI). - Slice C — cutover. Rewire
step_scoring.pyto callevalkit_bridge.rubric_from_yaml_dict/score_criteriainstead ofagentic_v2_eval.rubrics.load_rubric/agentic_v2_eval.scorer.Scorer, behind the same_EVAL_AVAILABLE-style guard (renamed to reflect the new optional dependency). Retire the directagentic_v2_evalimports fromagentic_v2/scoring/.agentic_v2/models/llm.py'sagentic_v2_eval.interfaces.LLMClientProtocolimport is handled separately — see Risks below. - Slice D — deprecate and delete
agentic-v2-eval. Once Slice C ships and nothing underagentic_v2/importsagentic_v2_evaldirectly,agentic-v2-evalbecomes a thin deprecated re-export shim (each public name re-exported from evalkit, with aDeprecationWarning) for one release window, then the package is deleted entirely: removed from[tool.uv.workspace] members, its directory deleted, its CI job (.github/workflows/eval-package-ci.yml) removed. - Slice E — CI cutover. Once evalkit is the only evaluation dependency,
update
.github/workflows/ci.ymlat its exactagentic-v2-evalreferences: - Line 27 (
python-smokejob):pip install -e "agentic-v2-eval/[dev]" -c ci-constraints.txt-> replaced with the evalkit install per the then-current rung of the install-mechanism ladder. - Lines 167, 170, 175 (
test-coveragejob, "Report whole-repo coverage" step): thepip install -e "agentic-v2-eval/[dev]"install, theagentic-v2-eval/testspath fed to the whole-repo pytest invocation, and the--cov=agentic-v2-eval/src/agentic_v2_evalcoverage target all need updating or removing onceagentic-v2-evalno longer exists. pyproject.toml[tool.uv.workspace] members(currently["agentic-workflows-v2", "agentic-v2-eval"]): drop"agentic-v2-eval"once Slice D deletes the package.
This ADR covers the decision and Slice A/B's scope in detail; Slices C-E are recorded here as the committed plan but implemented in their own PRs.
Why evalkit stays optional in ARP until the remote exists¶
agentic-evalkit has no public git remote today. A hard dependency on an
unpublished local checkout cannot be installed by CI (no clone target, no
package index entry), so:
- Slices A and B keep
agentic-evalkitoptional — guarded-import, mirroring the existing_EVAL_AVAILABLEpattern instep_scoring.py(EVALKIT_AVAILABLEhere, deliberately public rather than underscore-prefixed since this module's whole purpose is to be evalkit-facing). - CI for
agentic-workflows-v2does not installagentic-evalkitand must stay green without it — every public function inevalkit_bridge.pyraises a clearRuntimeError(not an import-time failure) when called with evalkit absent, andtests/test_evalkit_bridge.pyusespytest.importorskip("agentic_evalkit")at module level so the whole test file skips cleanly rather than erroring. - The optional status is lifted only once the install-mechanism ladder reaches a rung CI can actually resolve (a real remote for the git pin, or a PyPI release).
Consequences¶
step_scoring.py's graceful-degradation contract is preserved unchanged in this slice. Slice B adds a new module; it does not touchstep_scoring.py's existing_EVAL_AVAILABLEguard,AGENT_RUBRIC_MAP, or scoring behavior. Slice C is where that guard is repointed, and it must preserve the same "returnsNone/disables scoring when the dependency is absent" behaviorstep_scoring.pyalready has today — this ADR treats that as a hard requirement for Slice C's acceptance, not a nice-to-have.- Benchmark preset coverage gap. evalkit's built-in dataset presets
(
agentic_evalkit.datasets.presets) currently number far fewer than ARP's benchmark surface viatools.agents.benchmarks(HumanEval, MBPP, SWE-bench, etc.) — 7 of ARP's 9 benchmark presets have no evalkit-side equivalent yet. Those 7 stay ARP-side (continue to load through the existingtools.agents.benchmarkslazy-import path) until evalkit's preset catalog grows to cover them; this is not blocking for Slices A/B, which touch rubric/criterion scoring and workflow execution wrapping only, not dataset loading. LLMClientProtocoldoes not move.agentic_v2_eval.interfaces.LLMClientProtocolis a small, ARP-consumed structural protocol (generate_text(model_name, prompt, temperature, max_tokens, **kwargs) -> str) that predates and is orthogonal to evalkit's typed contracts (EvalSample,NormalizedExecutionResult, etc.) — it is a synchronous, string-in/string-out LLM-call shape, not an evaluation contract. It is not an ARP-internal utility as might be assumed by analogy with the rest of this migration — it is currently defined insideagentic_v2_evaland imported by ARP (agentic_v2/models/llm.py). Moving it into evalkit would importagentic_v2_eval's only remaining ARP-facing export into a library whose entire premise is zero ARP awareness, which is backwards. When Slice D deletesagentic-v2-eval,LLMClientProtocolneeds a landing spot decided explicitly (candidates: inline intoagentic_v2/models/llm.pysince ARP is its only consumer, or a small ARP-owned protocols module) — it does not default to moving intoagentic-evalkit. Flagged here so Slice D does not silently drop or mis-home it.- New module, no wiring yet.
agentic_v2/scoring/evalkit_bridge.pyandtests/test_evalkit_bridge.pyship in this slice; no existing ARP code path changes behavior. The bridge is exercised only by its own tests until Slice C. - Score parity is exact, not approximate, for the ported aggregation.
evalkit_bridge.score_criteriareproducesagentic_v2_eval.scorer.Scorer.score(...) .weighted_scorebit-for-bit (verified against the legacyScoreracross every ARP rubric YAML that definescriteria, including missing-criterion, zero-weight-criterion, and out-of-range-value edge cases). This required computing the weighted mean directly from the convertedRubric's criteria rather than driving evalkit'sCompositeGrader, becauseCompositeGrader's aggregation excludes a missing/non-definitive criterion's weight from both the numerator and denominator, while legacyScorerfixes the denominator over all criteria weights up front and excludes a missing criterion from the numerator only — the two are not interchangeable for that case. Seeevalkit_bridge.score_criteria's docstring for the full derivation. - Rubric shape is lossy in one direction. ARP's rubric YAML carries
fields evalkit's
Rubric/RubricCriterionhas no slot for: per-level rubric text (levels: {5: "...", ..., 0: "..."}, documentation only —Scorernever reads it), rubric-levelthresholds(pass/excellent/warning— a caller concern, not a per-criterion concept), andmetadata. None of these affectscore_criteria's numeric output;rubric_from_yaml_dict's docstring documents each dropped field and why.
Alternatives considered¶
- Single-PR cutover (dependency + bridge + step_scoring rewire + delete
agentic-v2-eval all at once) — rejected: too large to review safely, and
couples "add an optional dependency" with "delete a package
models/llm.pystill imports from," which would break the build mid-PR. The ADR-032 scoring-package extraction and ADR-023 EK migration both took a staged approach for the same reason; this repo's own history favors slices over big-bang migrations. - Wire
step_scoring.pydirectly to evalkit in this slice (skip the bridge) — rejected:step_scoring.py'sScorer(rubric_data).score(scores) .weighted_scorecall site is small and stable; rewriting it to speak evalkit's richer, execution-result-shaped grading API directly (rather than through an adapter) would spread evalkit-specific object construction through scoring call sites instead of concentrating it in one bridge module, making the eventualagentic-v2-evaldeletion (Slice D) touch more files instead of fewer. - Drive
score_criteriathrough evalkit'sCompositeGrader/WeightedGraderinstead of computing the weighted mean directly — rejected: as detailed in Consequences,CompositeGrader's missing-component semantics (exclude from numerator and denominator) diverge from legacyScorer's (exclude from numerator only) whenever a criterion is missing from the input scores, which would silently change scoring behavior for exactly the case Slice C's callers (e.g.step_scoring.score_step, which always scores every criterion inscorer.criteriatoday but need not forever) most need to be safe against. - Require evalkit unconditionally starting now, add it to CI immediately — rejected: evalkit has no public remote; CI cannot install an unpublished local-only checkout. This would either break CI outright or force a path-hack that only works on the author's machine — neither is acceptable for a shared repo.