The honest, ungameable harness metric the agentic-coding field omits.
It reports a SWE-bench-style pass % (gameable) and $/task-attempted
(token-priced, flat). Neither expresses an energy-optimised harness's
goal: the lowest energy AND dollar cost to land a fix that holds up.
This crate computes (joules, dollars) per VERIFIED fix over any Runtime
cascade:
- Verified, not self-graded: "fixed" is a deterministic Verifier verdict
over the answer (receipt-sealable). Only verified tasks count toward
the denominator, so a confident wrong answer can't buy the score.
- Energy-priced local, call-priced remote: PriceModel charges local tiers
(cache/skill) for energy only (~$0) and remote model tiers per call;
remote dwarfs local by ~1e9x. The TX cost thesis in one assertion.
- The repeat curve bends to zero: once a task class recurs past a
threshold it compiles into one deterministic skill (SkillTier, ~20 nJ)
and later rounds resolve at ~0 -- deterministically correct, model
untouched. without_compilation() gives the flat baseline for contrast.
- Verification-preserving compilation: a skill is registered ONLY if it
reproduces its verified answer byte-exactly; else dropped. Caught a
real bug -- PatternInducer's {hole} Format collides with literal { } in
Rust bodies -- which would freeze mangled code and crater the verified
count. Locked by test.
swebench module: loads the standardized SWE-bench / SWE-bench-Pro
instance schema (robust to the list-or-stringified-list encoding of
FAIL_TO_PASS/PASS_TO_PASS) into econ Tasks. Verification is a
PatchValidator trait: GoldPatchOracle is an offline proxy (documented as
NOT the real test verdict); a container-backed validator is the seam for
a genuine score. SWE-bench is one-shot, so the contribution there is the
joules axis + verified denominator the field omits.
Reuses jouleclaw-cascade + jouleclaw-skill + jouleclaw-energy only (no
private deps). Reusable FixedModelTier stand-in. Two examples (econ_demo,
swebench_econ). 9 tests, clippy clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
149 lines
6 KiB
Rust
149 lines
6 KiB
Rust
//! Harness economics on a recurring code-generation class.
|
||
//!
|
||
//! Scenario: an agent is repeatedly asked to declare a struct field — the kind
|
||
//! of boilerplate that dominates real coding sessions. We run the same suite twice
|
||
//! through a JouleClaw cascade (`SkillTier` over a frontier model stand-in) and
|
||
//! print the joules-and-dollars **per verified fix**:
|
||
//!
|
||
//! 1. **Baseline** (compilation off): every getter re-pays the frontier
|
||
//! model — the flat, token-priced curve everyone ships today.
|
||
//! 2. **JouleClaw** (compilation on): once the field-declaration *class*
|
||
//! recurs past a threshold it compiles into ONE deterministic skill, and
|
||
//! every later field — including names the model is never asked about
|
||
//! again — resolves at ~20 nJ / ~$0, deterministically correct.
|
||
//!
|
||
//! Run: `cargo run -p jouleclaw-harness-econ --example econ_demo`
|
||
|
||
use jouleclaw_cascade::{
|
||
Cascade, ContextRef, JouleBudget, QualityFloor, Query, QueryInput, Runtime,
|
||
};
|
||
use jouleclaw_harness_econ::{AnswerOutput, EconHarness, FixedModelTier, Task};
|
||
use jouleclaw_skill::{PatternInducer, SkillTier};
|
||
use std::sync::atomic::{AtomicU32, Ordering};
|
||
use std::sync::Arc;
|
||
|
||
/// The query the agent receives for field `name`.
|
||
fn prompt(name: &str) -> String {
|
||
format!("declare public string field {name}")
|
||
}
|
||
|
||
/// The correct declaration for field `name`. (The model's answer AND the
|
||
/// verifier's oracle — `name` appears verbatim, which is what lets a pattern
|
||
/// skill compile, and the artifact is brace-free so the skill round-trips.)
|
||
fn getter(name: &str) -> String {
|
||
format!("pub {name}: String,")
|
||
}
|
||
|
||
fn text_query(s: &str) -> Query {
|
||
Query {
|
||
input: QueryInput::Text(s.to_string()),
|
||
budget: JouleBudget::expensive(),
|
||
quality: QualityFloor::chat(),
|
||
context: ContextRef::fresh(),
|
||
deadline: None,
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
// The fields an agent gets asked for across a working session.
|
||
let fields = ["user_id", "email", "created_at", "is_active", "balance_cents", "tenant_id"];
|
||
|
||
// The frontier model "knows" how to write any getter — at a frontier price.
|
||
let table: Vec<(String, String)> =
|
||
fields.iter().map(|f| (prompt(f), getter(f))).collect();
|
||
|
||
// One task per field; each verifies byte-exactly against the correct getter.
|
||
let build_tasks = || -> Vec<Task> {
|
||
fields
|
||
.iter()
|
||
.map(|f| {
|
||
let want = getter(f);
|
||
Task::text(
|
||
format!("getter-{f}"),
|
||
"getter", // one recurrence class — what compiles into a skill
|
||
text_query(&prompt(f)),
|
||
move |o| matches!(o, AnswerOutput::Text(t) if *t == want),
|
||
)
|
||
})
|
||
.collect()
|
||
};
|
||
|
||
let rounds = 5;
|
||
let frontier_joules = 0.5; // 500 mJ per frontier dispatch
|
||
|
||
// A pattern inducer that recognises the getter class and generalises it:
|
||
// any "generate getter for field {name}" compiles to one Format skill.
|
||
let make_inducer =
|
||
|| Box::new(PatternInducer::from_patterns(["declare public string field {name}"]));
|
||
|
||
// ---- 1. Baseline: skill-compilation OFF -------------------------------
|
||
let base_calls = Arc::new(AtomicU32::new(0));
|
||
let bc = base_calls.clone();
|
||
let bt = table.clone();
|
||
let baseline = EconHarness::new(make_inducer(), 3)
|
||
.without_compilation()
|
||
.run(
|
||
move |store| {
|
||
let mut casc = Cascade::new();
|
||
casc.register(Box::new(SkillTier::new(store)));
|
||
casc.register(Box::new(
|
||
FixedModelTier::new(bt.clone(), frontier_joules)
|
||
.with_call_counter(bc.clone()),
|
||
));
|
||
Runtime::new_without_l0(casc)
|
||
},
|
||
&build_tasks(),
|
||
rounds,
|
||
);
|
||
|
||
// ---- 2. JouleClaw: skill-compilation ON -------------------------------
|
||
let jc_calls = Arc::new(AtomicU32::new(0));
|
||
let jc = jc_calls.clone();
|
||
let jt = table.clone();
|
||
let jouleclaw = EconHarness::new(make_inducer(), 3).run(
|
||
move |store| {
|
||
let mut casc = Cascade::new();
|
||
casc.register(Box::new(SkillTier::new(store)));
|
||
casc.register(Box::new(
|
||
FixedModelTier::new(jt.clone(), frontier_joules).with_call_counter(jc.clone()),
|
||
));
|
||
Runtime::new_without_l0(casc)
|
||
},
|
||
&build_tasks(),
|
||
rounds,
|
||
);
|
||
|
||
let total_dispatches = fields.len() * rounds;
|
||
|
||
println!("\n=== Recurring field-declaration codegen — {total_dispatches} dispatches ({} fields × {rounds} rounds) ===\n", fields.len());
|
||
|
||
println!("--- BASELINE (token-priced, no skill-compilation) ---");
|
||
print!("{}", baseline.to_table());
|
||
println!("model calls paid: {}/{total_dispatches}\n", base_calls.load(Ordering::SeqCst));
|
||
|
||
println!("--- JOULECLAW (skill-compilation on) ---");
|
||
print!("{}", jouleclaw.to_table());
|
||
println!("model calls paid: {}/{total_dispatches}\n", jc_calls.load(Ordering::SeqCst));
|
||
|
||
// ---- Headline ---------------------------------------------------------
|
||
let usd_drop = baseline.usd_per_verified_fix / jouleclaw.usd_per_verified_fix;
|
||
let j_drop = baseline.joules_per_verified_fix / jouleclaw.joules_per_verified_fix;
|
||
println!("=== Headline (both verified {}/{}) ===", jouleclaw.verified, total_dispatches);
|
||
println!(
|
||
" $/verified fix : ${:.6} -> ${:.9} ({:.0}× cheaper)",
|
||
baseline.usd_per_verified_fix, jouleclaw.usd_per_verified_fix, usd_drop
|
||
);
|
||
println!(
|
||
" J/verified fix : {:.3e} -> {:.3e} ({:.0}× cheaper)",
|
||
baseline.joules_per_verified_fix, jouleclaw.joules_per_verified_fix, j_drop
|
||
);
|
||
println!(
|
||
" model calls : {} -> {} (the recurring class stopped invoking the model)",
|
||
base_calls.load(Ordering::SeqCst),
|
||
jc_calls.load(Ordering::SeqCst)
|
||
);
|
||
println!(
|
||
"\nEvery verified fix is byte-identical across both runs — JouleClaw just stopped\n\
|
||
paying the model for work it had already proven how to do."
|
||
);
|
||
}
|