open-standards/eoc/eoc-rs
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 1352aeea07
All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1m39s
standards workspaces build + test / aac-rs (pull_request) Successful in 35s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 35s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 32s
standards workspaces build + test / map-rs (pull_request) Successful in 29s
standards workspaces build + test / mesh-rs (pull_request) Successful in 25s
standards workspaces build + test / openpay (pull_request) Successful in 1m23s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m50s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m18s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m15s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m8s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m33s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m19s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 2m49s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m51s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 2m48s
standards workspaces build + test / sense-rs (pull_request) Successful in 2m50s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 2m49s
eoc-agent: every span says whether its joule figure was measured or estimated
A trace `Span` carried a micro-joule figure and nothing about where it came
from. A provider's own figure, a tool step's duration estimate, a zero for an
observation, and a delta a live counter read all looked the same, so nothing
reading a trace could tell a measurement from an estimate. Every other eoc
surface that reports energy says which it is (`JouleCost`, eoc-eval's
`joule_source`); an agent trace did not.

`Span` gains `joule_source: JouleSource`, eoc-core's tag, under eoc-core's
rule: `Measured` only for what a live counter read over the step — both reads
succeeded and the counter advanced — and `Estimated` for anything else.

- The field is additive on the wire. It is `#[serde(default)]`, as eoc-eval's
  report field is: a trace written before it reads back with every span
  `Estimated`, since nothing in it says it was measured. Nothing in the
  monorepo outside eoc-agent uses eoc-agent, so no struct literal of `Span`
  breaks.
- `Span::new` is `Estimated` (0 µJ that no counter read). `with_joule_cost`
  attaches a figure and its tag together, and `joule_cost()` reads them back.
  `with_microjoules` attaches a bare figure, which is an estimate: it no
  longer leaves an earlier `Measured` tag on a figure the counter did not read.
- `AgentLoop::measured_with` and `measured_tool` return a `JouleCost` instead
  of a bare `u64`. The figure is unchanged — the reading, or the estimate when
  larger, or the estimate alone — and it is `Measured` exactly when it is the
  reading: a reading smaller than the estimate is not what is charged, so the
  estimate charged in its place is `Estimated`. `measured` still returns the
  bare figure, as on main. Both changed functions are new on this branch.
- Model steps: `charge_llm_step` tags the provider's own figure `Estimated`;
  `LlmProvider`'s contract calls it an estimate and no counter here read it.
  This covers every loop's model steps (ReAct, CodeAct, Reflexion, ToT, LATS).
- Tool steps: ReAct and CodeAct put `measured_tool`'s tag on the act span, and
  Plan-and-Execute on its plan and execute spans.
- Other steps: ReAct's observation of an unknown tool is `Span::new`'s
  estimated 0, as nothing ran.

Tests:
- a new span and a bare figure are estimates; a measured cost survives
  `with_joule_cost`, and a bare figure attached after it is an estimate;
- a trace round-trips through JSON with a `Measured` and an `Estimated` span,
  and a trace in the old format, without the field, reads back all
  `Estimated`;
- `measured_tool`: the stub, a failed first or last read, a counter that held
  or went down, and one that advanced 10 µJ over a 20 ms step are all
  `Estimated` and charged at least the 300,000 µJ estimate; a 50 J reading is
  `Measured`. `measured_with` at a fixed estimate: a 450 µJ reading is
  `Measured` at estimates of 100 and 450, and an estimate of 1,000 is charged
  as `Estimated`;
- `charge_llm_step` records the reply's figure as `Estimated`;
- ReAct, with a counter that advances 50 J over the tool step: think spans
  `Estimated` at 1,000 µJ, the unknown tool's observation `Estimated` at 0,
  the act span `Measured` at 50 J, and the budget 3,000 µJ plus 50 J; with
  the stub, every span `Estimated`;
- CodeAct, the same over code that succeeds and code that fails;
- Plan-and-Execute: with the stub every span `Estimated`; with the scripted
  meter the plan step (40 J) and worker `b` (50 J) `Measured`, and worker `a`,
  over which the counter held, `Estimated`.

Falsified, each mutant applied to this commit, run, and restored with
`git checkout`:
- every span pushed to a trace tagged `Measured`: 8 tests fail;
- `Span::new` and `with_joule_cost` always `Measured`: 9 fail;
- every span pushed to a trace tagged `Estimated`: 4 fail;
- a span without the field reading back `Measured`: 1 fails;
- `measured_with` tagging any reading `Measured`, including when the
  estimate is what is charged: 1 fails;
- `charge_llm_step` tagging the provider's figure `Measured`: 5 fail;
- `with_microjoules` keeping an earlier tag: 1 fails;
- ReAct, CodeAct, and Plan-and-Execute each dropping the tag
  (`with_microjoules(cost.microjoules)`): 1 fails in each.

`cargo test -p eoc-agent`: 38 unit and 9 integration tests pass.
The eoc workspace (`cargo test --workspace`): 695 passed, 0 failed,
3 ignored, on an M5 Max (macOS 27.0.1). clippy for x86_64-unknown-linux-gnu
on eoc-agent: the same two pre-existing warnings and no others.
On macOS too clippy adds no warning (the two collapsible-if warnings predate
this), and rustdoc reports nothing in the lines changed here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:38:48 -04:00
..
crates eoc-agent: every span says whether its joule figure was measured or estimated 2026-09-30 22:38:48 -04:00
Cargo.lock eoc-meter: CPU-rail liveness pairs consecutive samples, and every step of it is tested 2026-09-30 08:04:39 -04:00
Cargo.toml msrv: raise the rust-version floor to 1.97 across the workspaces 2026-07-23 00:17:08 -04:00
README.md Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc 2026-05-23 13:24:05 -04:00
rust-toolchain.toml chore(deps): cargo update across the workspace; eoc toolchain → 1.98.0 2026-08-26 07:39:26 -04:00

eoc-rs — EOC reference implementation in Rust

Apache-2.0 reference implementation of the Energy-Optimized Compute specification (CC-BY-4.0 spec lives in ../spec/).

EOC is a four-stage memoizing cascade. Every query is tried at each stage in turn and the first that can answer wins. The four stages, cheapest to most expensive, are:

  1. Cache (eoc-cache) — content-addressed LRU. Cache hits are nearly free.
  2. Key-value (eoc-kv) — exact key match plus cosine-similarity embedding match.
  3. Graph (eoc-graph) — triple-store retrieval. Reference impl is a small in-memory matcher; production deployments swap in a real graph backend driven by DCY (EOC-4).
  4. Neural (eoc-neural) — last-resort inference. Reference impl ships an EchoBackend; production deployments wire in llama.cpp / Ollama / Anthropic API / etc.

Joule cost is the unit of accounting. The eoc-meter crate reads cumulative micro-joules from whatever hardware counter is available (Linux RAPL, macOS powermetrics, NVIDIA NVML behind the cuda feature) and falls back to a StubCounter everywhere else — including WASM.

Workspace layout

Crate Purpose
eoc-core Types: Query, Response, Stage, JouleCost, Receipt.
eoc-cache Stage 1 — LRU cache + the Stage trait.
eoc-kv Stage 2 — KV + embedding-similarity lookup.
eoc-graph Stage 3 — in-memory triple store.
eoc-neural Stage 4 — neural inference trait + EchoBackend.
eoc-meter Hardware energy counters with stub fallback.
eoc-cascade Glue: walks the four stages, attributes joule cost.
eoc Reference CLI.
eoc-wasm wasm-bindgen wrapper — browser-runnable cascade.

Running

cargo test --workspace
cargo clippy --workspace --all-targets
cargo run --bin eoc -- query "what is 2+2"
cargo run --bin eoc -- meter

Example output:

$ cargo run --bin eoc -- query "what is 2+2"
stage  : kv
payload: 4
cost   : 50 µJ (estimated)
receipt: <64-char hex>

CUDA / NVML

NVIDIA NVML support is feature-gated and off by default so the workspace builds without libnvidia-ml. To enable:

cargo build -p eoc-meter --features cuda

WASM

The cascade compiles to wasm32-unknown-unknown. The meter does not — RAPL, NVML, and powermetrics are all absent in the browser, so WASM builds use StubCounter and report estimated joule cost.

rustup target add wasm32-unknown-unknown
cargo build -p eoc-wasm --target wasm32-unknown-unknown --release
wasm-bindgen target/wasm32-unknown-unknown/release/eoc_wasm.wasm \
    --out-dir ./pkg --target web

Then from JavaScript:

import init, { WasmCascade } from "./pkg/eoc_wasm.js";
await init();
const cascade = new WasmCascade();
const r = await cascade.resolve("ping");
console.log(r); // { stage: "kv", payload: "pong", microjoules: 50, ... }

License

Apache-2.0 for the Rust source in this workspace. The EOC specification text in ../spec/ is CC-BY-4.0. See ../LICENSE.