car-ir 0.53.0

Agent IR types for Common Agent Runtime
Documentation

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 car_ir::Action;

let mut action = Action::tool_call("search");
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 car_ir::{build_dag, Action};

let action: Action = serde_json::from_str(
    r#"{
        "id": "a1",
        "type": "tool_call",
        "tool": "search",
        "failure_behavior": "abort"
    }"#,
)
.unwrap();

let levels = build_dag(&[action]);

Reversibility: "can this be undone?"

Action::reversibility is a ReversibilityReversible, 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 car_ir::{Action, Compensation, Reversibility};

let mut action: Action =
    serde_json::from_str(r#"{"id": "a1", "type": "tool_call", "tool": "db.insert"}"#).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, Reversibility::Irreversible);

action.reversibility = Reversibility::Compensable;
action.compensation = Some(Compensation::Tool {
    tool: "db.delete".into(),
    parameters: [("id".to_string(), serde_json::json!(7))].into(),
});

assert!(!action.missing_required_compensation());

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 — guessing Reversible wrongly means silently believing a sent email can be unsent, and nothing surfaces that; guessing Irreversible wrongly means over-asking on something recoverable, which is visible and locally fixable.
  • Compensable is a bare variant, not Compensable { compensation }. Carrying a payload would cost the enum its plain string-enum shape, which every FFI surface mirrors, so Compensable with no compensation is representable. Action::missing_required_compensation() is the check that stands in for the type, and ActionProposal::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 on PermissionDecision events; 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. Reversible is 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.