Repository navigation
Conversation
An eval is a test case in test/evals/*_eval.rb. `rails test` collects only *_test.rb, so the suite never calls a model by accident, and `bin/rails active_agent:evals` runs them through the same runner. ActiveAgent::Evals::Capture runs one generation into a Replay: the answer, tokens, duration, the recorded prompt, and every tool call captured on the agent instance's tools_function (where the telemetry tool spans are), with model: and provider: overrides the way a comparison run replays, and a block form for agents run some other way. ActiveAgent::Evals::TestHelper and TestCase add run_eval, the assertions over the replay (answer present, output contains, tool called with an argument subset, tools succeeded), assert_eval_passes / score_eval against the scorer and diagnosis, run_eval_suite over a YAML suite across models, and assert_pass_rate over the Report. The framework's railtie now loads the gem's rake tasks; the empty placeholder it never loaded is gone. Refs #625 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An eval is a *_test.rb file that `rails test` collects like any other. ActiveAgent::Evals::TestCase skips its tests unless EVALS is on, so the default suite reports them as skips and calls no model. `bin/rails test:evals` switches EVALS on and hands test/evals to the Rails runner (TEST=path narrows, TESTOPTS passes through); `EVALS=1 bin/rails test` runs one file, one line, or the whole suite with the evals in. The same switch gates an RSpec suite through filter_run_excluding, documented. Replaces the _eval.rb suffix and the active_agent:evals task from the first cut: evals should live in the suite and run on demand in development or on release, not in a runner of their own. The gem's own bin/test and Rakefile leave the dummy app's sample eval out. Refs #625 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EjYedJiSYGbTMY1NbL8mdK
Contributor
Author
|
Reshaped after discussion: evals are now ordinary
Why a skip guard rather than adding Dummy app: |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #625. Draft: the shape is up for discussion before it hardens. No migration;
activeagentonly.An eval is an ordinary test under
test/evals, collected byrails testlike any other. Each one calls a model, soActiveAgent::Evals::TestCaseskips its tests unlessEVALSis on: the default suite reports them as skips and calls nothing.bin/rails test:evalsswitches them on and runstest/evalsthrough the same runner (TEST=pathnarrows,TESTOPTSpasses through);EVALS=1 bin/rails test test/evals/support_agent_test.rb:12runs one, andEVALS=1 bin/rails testruns the suite with the evals in — on demand in development, or in the release job that validates agent behaviour before a deploy.What is in it
ActiveAgent::Evals::Capture(lib/active_agent/evals/capture.rb, loaded with the module): runs oneActiveAgent::Generationinto aReplay: the last assistant turn as the answer, tokens from the response's usage, wall-clock duration, the first user turn as the recorded prompt, and every tool call captured by wrapping the agent instance'stools_function(the hook the telemetry tool spans use;baseis the telemetry-wrapped proc, so spans still fire): name, arguments (JSON-shaped), a result preview or the error, duration.model:sets the option on the instance;provider:runs an anonymous subclass withgenerate_with provider, model:, named as its parent soagent_name, the view prefixes and span names stay the parent's (the parent'sinstructions:option is carried; the rest followsgenerate_with's rule for a subclass). An exception becomes an errored Replay as underRunner;raise_errors: truere-raises. A block form takes aPromptResponse, aReplay, a Hash or a String for agents run some other way.ActiveAgent::Evals::TestHelperandTestCase(lib/active_agent/evals/test_helper.rb, required explicitly likerails/test_help):run_eval;assert_answer_present,assert_output_contains/refute_output_contains(substring, case-insensitive, or regex, viaScorer.matches_pattern?),assert_tool_called(with an argument subset) /refute_tool_called,assert_tools_succeeded, each failing with what the agent did (answer, tools called, error);assert_eval_passes/score_eval, which build a one-scenarioRunnerfromexpect:(+prompt:,criteria:,judge:,available_tools:,threshold:) and fail with the fault, the recommendation and the scores;run_eval_suite, which replays a YAML suite acrossmodels:(or the agent as configured) through the block and returns theReport;assert_pass_rate report, at_least:, which names each model below the gate and its failing scenarios.TestHelper.enabled?reads theEVALSswitch (on unless0/false/no/off);TestCaseskips every test with the command to run them when it is off.bin/rails test:evals(lib/tasks/active_agent/evals.rake): the framework's railtie now has arake_taskshook; the emptyactiveagent_tasks.rakeplaceholder it never loaded is removed. The task setsEVALSand handstest/evals(orTEST=) toRails::TestUnit::Runner.run_from_rake(rake_runon older railties); Rails'testcommand falls through to rake fortest:*names it does not define, sobin/rails test:evalsresolves.expect_with :rspec, :minitestfor the assertions,define_derived_metadata(file_path: %r{/spec/evals/})to tag the directory,filter_run_excluding eval: true unless ActiveAgent::Evals::TestHelper.enabled?. Verified in a scratch bundle; documented.test/dummy/test/evals/orders_agent_test.rbfollows the convention on the dummy app:cd test/dummy && bin/rails test→ 2 skips,bin/rails test:evals→ 2 runs pass,EVALS=1 bin/rails test test/evals/orders_agent_test.rb→ 2 runs pass,bin/rails test:evals TEST=missingaborts with a message. The gem's ownbin/testand Rakefile leavetest/dummy/test/evalsout, as the host app's sample.Tests
test/evals/capture_test.rb(12) andtest/evals/test_helper_test.rb(9), offline, onEvalsTest::OrdersAgentwith a mock provider subclass that actually dispatches a tool (test/evals/support/eval_agents.rb, templates undertest/dummy/app/views/agents/evals_test/orders/): the replay's answer, tokens, timing, prompt and metadata; tool calls with arguments, result and timing; a raising tool recorded as an error and the errored replay;raise_errors; the action template rendering; model and provider overrides, including that templates resolve under the renamed subclass and the agent class is untouched;retargetkeeping params, action and the instructions option; the block forms; every assertion's pass and failure message;assert_eval_passespassing and failing with the fault and fix;score_evalwith a prompt, criteria and threshold;run_eval_suiteas configured, under named models withproviders:, with the production-only filter and the gate's failure message; theEVALSswitch's values; aTestCasetest skipped with the run command when the switch is off and run when it is on (Minitest.run_one_method); the rake file definingtest:evals.bin/teston this branch: 4123 runs, 0 failures, 5 errors, 29 skips. The 5 errors areRubyLLMIntegrationTest(live OpenAI calls under the placeholder key in.env.test), unrelated.bin/rubocopon every changed file: no offenses.npm run docs:buildpasses.Docs
docs/framework/evaluations.md: "In your test suite" (layout, the switch and the runner, the helper, scoring, suites, overrides, the block form, MCP caveat, an RSpec subsection).docs/framework/testing.md: an "Evals" section pointing there.AGENTS.md(Testing): the module's own tests vs the convention it offers hosts, and where each piece lives.CHANGELOG.mdunder Unreleased.Open questions
Settled: ordinary_eval.rbsuffix vs ordinary test files*_test.rbfiles in the suite, gated byEVALS, so they run only on demand or on release. A skip guard rather than an exclude glob because the Rails runner readsDEFAULT_TEST_EXCLUDEbefore the app boots whenevertest:prepareis skipped (-n, an explicit file), so the gem cannot addtest/evalsto it reliably; an app that wants no skips in its summary can set that variable itself.assert_eval_passesfailing with the diagnosis is the dashboard's reading of a failure. Is that the right default in a test, or should the plain assertions be the default and scoring opt-in?Captureandscore_evalwork there now; matchers in a follow-up (Evals in the test suite: test/evals, run on demand, written like tests #625 lists it).Not in this PR
MCP-served tool calls are not captured (dispatched before
tools_function, same gap as the telemetry tool spans); generator skeletons; publishing a report from a test. All on #625.🤖 Generated with Claude Code