car-ir
Agent intermediate representation types for the Common Agent Runtime.
What it does
Defines the typed IR that sits between model intent and runtime execution. Encodes richer semantics than raw function-call JSON: preconditions, expected effects, idempotency keys, failure behavior, state dependencies, and DAG construction. This is the shared vocabulary that all other CAR crates depend on.
Usage
Action is #[non_exhaustive], so from another crate it is built with a
constructor rather than a struct literal — Action::new(action_type), or the
typed shortcuts Action::tool_call(tool) / Action::state_write(key, value) /
Action::state_read(key). Every field is public, so set what you need
afterwards; with_id and with_param chain:
use Action;
let mut action = tool_call;
action.id = "a1".into;
action.idempotent = true;
That is what makes adding an IR field a non-breaking change for consumers of
the published crate — a struct literal would have to name every field, so each
new one was an error[E0063] downstream. There is deliberately no Default
impl: an action's type decides what the rest of it means, and defaulting it
would make Action::default() a tool_call naming no tool.
Every field except type carries a #[serde(default)], so deserializing the
JSON form is the other short way to build one — and it is how actions really
arrive (a model emits JSON; the runtime parses it into this type):
use ;
let action: Action = from_str
.unwrap;
let levels = build_dag;
Reversibility: "can this be undone?"
Action::reversibility is a Reversibility — Reversible, Compensable, or
Irreversible — and it is a different axis from car_policy::PermissionTier
(ReadOnly < SandboxEdit < FullAccess), which answers who may authorize this.
The two were fused for a long time: PermissionTier documented FullAccess as
"externally-consequential or irreversible", and that or put a git push
(undo by force-pushing the prior ref), a production INSERT (undo by deleting
the row), and a charged card (no undo at all) on one rung. With one lever for
both questions the runtime has two options and no third — gate every
FullAccess action identically, which is approval fatigue, or relax the tier
and lose the permanent cases along with the recoverable ones.
The axes genuinely disagree in both directions. read_secret is FullAccess
and Reversible — a read leaves nothing to undo, which is exactly why this
must never be used as a stand-in for the tier. db_insert is SandboxEdit
and Compensable.
use ;
let mut action: Action =
from_str.unwrap;
// Parsed from JSON that never mentioned the axis, so it defaulted to the
// conservative end — an unclassified action is treated as permanent.
assert_eq!;
action.reversibility = Compensable;
action.compensation = Some;
assert!;
In the JSON form the same pair is "reversibility": "compensable" plus
"compensation": {"type": "tool", "tool": "db.delete", "parameters": {"id": 7}}
— the shape every binding surface and the daemon see on the wire.
Both fields are #[serde(default)], so a proposal authored before the axis
existed still deserializes; compensation is omitted from the wire form when
absent. Three things are worth knowing before you rely on it:
- The default is
Irreversible, deliberately. A default here decides what the runtime believes about an unclassified action, and the directions fail asymmetrically — guessingReversiblewrongly means silently believing a sent email can be unsent, and nothing surfaces that; guessingIrreversiblewrongly means over-asking on something recoverable, which is visible and locally fixable. Compensableis a bare variant, notCompensable { compensation }. Carrying a payload would cost the enum its plain string-enum shape, which every FFI surface mirrors, soCompensablewith no compensation is representable.Action::missing_required_compensation()is the check that stands in for the type, andActionProposal::rollback_contract()reports a batch's contract as the worst of its actions — a plan is only as recoverable as its least recoverable step.- Nothing in the runtime enforces on it yet. The property is typed,
classified (
car_policy::classify_reversibility), and audited onPermissionDecisionevents; it is not a gate. Deferring the materialization of an irreversible effect needs a checkpoint coupled to the filesystem, which CAR does not have — rollback restores the state map and leaves whatever a tool wrote to disk where it is.Reversibleis not a promise anything will be undone for you.
Background and the ranked follow-on work:
docs/proposals/shepherd-substrate-adoption.md.
Part of CAR -- see the main repo for full documentation.