open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-harness-econ/examples/econ_demo.rs
Transaction Science eb137fd96a Add jouleclaw-harness-econ: joules+$ per verified fix
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]>
2026-06-22 04:57:37 -04:00

149 lines
6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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."
);
}