Skip to main content

eval_core/
baseline.rs

1//! A shipped, ready-to-run **baseline capability suite** — basic checks any agent can be measured
2//! against in one call.
3//!
4//! ```ignore
5//! let report = eval_core::run_suite(&my_agent, &eval_core::baseline());
6//! println!("{report}");
7//! ```
8//!
9//! The suite covers three capability areas, authored as RON files grouped by capability and EMBEDDED
10//! into this crate (so they ship with the published crate — nothing is read from disk at runtime):
11//!
12//! - `arithmetic.ron` — basic math (number assertions, plus a clearly-labelled opt-in tool-use subset),
13//! - `language.ron` — instruction following over the agent's final text (fully portable),
14//! - `tool_use.ron` — tool-calling with parameters (assumes a DOCUMENTED tool-name convention — adapt it).
15//!
16//! ## Portable vs. adapt-me
17//!
18//! The number/text assertions are PORTABLE: they inspect only the agent's final reply, so they hold
19//! whether the agent uses tools or reasons inline. The tool-use cases (and a small subset of the
20//! arithmetic file) additionally assert specific tool names/args, which a user must adapt to their own
21//! agent — every such case is clearly commented at the top of its file.
22//!
23//! ## Use it, or fork it
24//!
25//! Run it directly via [`baseline`], OR dump the raw embedded RON via [`baseline_files`] as a starting
26//! template you copy into your own suite directory and edit. The same files back both paths, so the
27//! template you copy is exactly what [`baseline`] runs.
28
29use include_dir::{Dir, include_dir};
30
31use crate::EvalCase;
32use crate::case::parse_cases_from_str;
33use crate::expect::Expectation;
34
35/// The baseline RON files, embedded at compile time so they ship inside the crate (not read from disk).
36static BASELINE_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/baseline");
37
38/// The ready-made baseline capability suite: every embedded case, parsed and concatenated.
39///
40/// Returns `EvalCase<(), Expectation>` cases (no `setup`; the easy [`Agent`](crate::Agent) path), ready
41/// to hand straight to [`run_suite`](crate::run_suite):
42///
43/// ```ignore
44/// let report = eval_core::run_suite(&my_agent, &eval_core::baseline());
45/// ```
46///
47/// ## Why this is infallible at the call site
48///
49/// The embedded RON is authored and tested IN THIS CRATE, so a parse failure here would be OUR bug, not
50/// the user's — and the `baseline_parses_and_is_well_formed` test parses exactly these files on every
51/// `cargo test`, catching any malformed edit before release. Hence this returns the `Vec` directly and
52/// `expect`s on parse, rather than burdening every caller with a `Result` they can't meaningfully handle.
53/// Files load in sorted (filename) order, cases in authored order within each file.
54///
55/// To instead get the raw files as a copy-and-edit template, see [`baseline_files`].
56pub fn baseline() -> Vec<EvalCase<(), Expectation>> {
57    let mut cases = Vec::new();
58    for (name, contents) in baseline_files() {
59        // `expect`, not `Result`: the data is ours and a test parses it — a panic here is a build-time
60        // contract violation a developer fixes, never something a downstream user hits at runtime.
61        let parsed = parse_cases_from_str::<(), Expectation>(name, contents)
62            .unwrap_or_else(|e| panic!("embedded baseline file `{name}` is malformed: {e}"));
63        cases.extend(parsed);
64    }
65    cases
66}
67
68/// The raw embedded baseline files as `(file_name, ron_contents)` pairs, in sorted (filename) order.
69///
70/// Use this to DUMP the suite as a starting template — write each pair to your own cases directory, then
71/// edit the instructions, values, and (in `tool_use.ron`) the tool names to match your agent:
72///
73/// ```ignore
74/// for (name, contents) in eval_core::baseline_files() {
75///     std::fs::write(my_suite_dir.join(name), contents)?;
76/// }
77/// ```
78///
79/// The same files back [`baseline`], so what you copy here is exactly what [`baseline`] runs.
80pub fn baseline_files() -> &'static [(&'static str, &'static str)] {
81    // Build the sorted slice ONCE into a process-lifetime `OnceLock`. `include_dir` iteration order is
82    // unspecified, so we sort by file name to match `load_cases`' deterministic ordering. The pairs
83    // borrow the compile-time-embedded `Dir<'static>`, so the stored data is `'static` with no leak.
84    use std::sync::OnceLock;
85    static FILES: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
86    FILES.get_or_init(|| {
87        let mut files: Vec<(&'static str, &'static str)> = BASELINE_DIR
88            .files()
89            .filter(|f| {
90                f.path()
91                    .extension()
92                    .is_some_and(|e| e.eq_ignore_ascii_case("ron"))
93            })
94            .filter_map(|f| {
95                // File name (e.g. "arithmetic.ron") + its UTF-8 contents. Embedded RON is UTF-8.
96                let name = f.path().file_name()?.to_str()?;
97                let contents = f.contents_utf8()?;
98                Some((name, contents))
99            })
100            .collect();
101        files.sort_by_key(|(name, _)| *name);
102        files
103    })
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::collections::HashSet;
110
111    /// The embedded baseline parses, has the expected number of cases, and every case is well-formed
112    /// (non-empty name + instruction, at least one expectation) with a unique name. This is the test
113    /// that justifies [`baseline`]'s internal `expect`: a malformed embedded edit fails CI here.
114    #[test]
115    fn baseline_parses_and_is_well_formed() {
116        let cases = baseline();
117
118        // 6 arithmetic + 5 language + 7 tool-use = 18.
119        assert_eq!(
120            cases.len(),
121            18,
122            "expected 18 baseline cases, got {}",
123            cases.len()
124        );
125
126        let mut names = HashSet::new();
127        for case in &cases {
128            assert!(
129                !case.name.trim().is_empty(),
130                "a baseline case has an empty name"
131            );
132            assert!(
133                !case.instruction.trim().is_empty(),
134                "baseline case `{}` has an empty instruction",
135                case.name
136            );
137            assert!(
138                !case.expect.is_empty(),
139                "baseline case `{}` has no expectations",
140                case.name
141            );
142            assert!(
143                names.insert(case.name.clone()),
144                "duplicate baseline case name `{}`",
145                case.name
146            );
147        }
148    }
149
150    /// The three expected capability files are present in the embedded set, in sorted order.
151    #[test]
152    fn baseline_files_are_the_three_capability_files() {
153        let names: Vec<&str> = baseline_files().iter().map(|(n, _)| *n).collect();
154        assert_eq!(
155            names,
156            vec!["arithmetic.ron", "language.ron", "tool_use.ron"]
157        );
158    }
159}