car-ir 0.55.0

Agent IR types for Common Agent Runtime
Documentation
# car-ir

Agent intermediate representation types for the [Common Agent Runtime](https://github.com/Parslee-ai/car).

## 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:

```rust
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):

```rust
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 `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`.

```rust
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`](../../../docs/proposals/shepherd-substrate-adoption.md).

Part of [CAR](https://github.com/Parslee-ai/car) -- see the main repo for full documentation.