#[non_exhaustive]pub struct Action {Show 18 fields
pub id: String,
pub action_type: ActionType,
pub tool: Option<String>,
pub parameters: HashMap<String, Value>,
pub preconditions: Vec<Precondition>,
pub expected_effects: HashMap<String, Value>,
pub state_dependencies: Vec<String>,
pub read_set: Vec<String>,
pub write_set: Vec<String>,
pub assumptions: Vec<StateAssumption>,
pub invocation_mode: ToolInvocationMode,
pub reversibility: Reversibility,
pub compensation: Option<Compensation>,
pub idempotent: bool,
pub max_retries: u32,
pub failure_behavior: FailureBehavior,
pub timeout_ms: Option<u64>,
pub metadata: HashMap<String, Value>,
}Expand description
A single unit of agent intent compiled into IR.
This is the core primitive. Models produce these (directly or via compilation), and the runtime validates and executes them.
§Construction
#[non_exhaustive], so from outside car-ir this must be built with
Action::new (or Action::tool_call / Action::state_write /
Action::state_read) and then mutated — struct-literal syntax, including
functional update (..other), is rejected in other crates.
That is the point. car-ir is published on crates.io and re-exported by
car-runtime, so while the wire format tolerates new fields (they carry
#[serde(default)], and a proposal authored before a field existed still
deserializes), the Rust API previously did not: every added field was a
source-breaking error[E0063] for every out-of-tree consumer building an
Action literal, and cost ~20 files of churn in-tree the last time it
happened. Adding a field is now backwards-compatible in both directions.
Parslee-ai/car#855.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.id: String§action_type: ActionType§tool: Option<String>§parameters: HashMap<String, Value>§preconditions: Vec<Precondition>§expected_effects: HashMap<String, Value>§state_dependencies: Vec<String>§read_set: Vec<String>Explicit transactional read set — the state keys this action
reads (survey §5.2.4: “each action should declare its read set,
write set, assumptions, version dependencies”). When empty, the
effective read set falls back to Action::effective_read_set
(derived from state_dependencies + assumption keys), so existing
proposals keep working. Used to detect read-write conflicts and
stale reads across concurrent actions/agents.
write_set: Vec<String>Explicit transactional write set — the state keys this action
writes. When empty, falls back to Action::effective_write_set
(derived from expected_effects + a StateWrite’s key param).
assumptions: Vec<StateAssumption>Assumptions this action makes about shared state it did not itself produce — the basis for belief-divergence / stale-read detection in a multi-agent transaction (§5.2.4). An assumption can pin an expected value, a version the action read at, or both.
invocation_mode: ToolInvocationModeHow a ToolCall runs: one_shot (default — dispatch awaits the
result inline), or a detached mode (streaming / long_running)
where dispatch starts the tool, returns a crate::ToolHandle
as the action’s output, and the DAG proceeds without blocking on
completion. Chunks/status are consumed via the handle (C2).
Ignored for non-ToolCall actions.
reversibility: ReversibilityThe rollback contract for this action’s effects — can this be
undone? Orthogonal to car_policy::PermissionTier, which answers the
separate question of who may authorize this; the two were conflated
in a single ladder until this field existed. See Reversibility for
the axis, and for why the #[serde(default)] is the conservative
Reversibility::Irreversible rather than the quiet-failing
Reversibility::Reversible.
Nothing in the runtime gates on this yet — it is typed and audited, not
enforced (see the reversibility module docs).
compensation: Option<Compensation>How to undo this action once it has already run. Meaningful only when
reversibility is Reversibility::Compensable: a reversible action
is undone by restoring its scope, and an irreversible one cannot be
undone at all.
The pairing is not enforced by the type system — Compensable is a
bare variant, not Compensable { compensation }, so
Compensable + None is representable. That is a deliberate trade,
argued in Compensation’s docs: making the state unrepresentable
costs Reversibility its plain C-like shape, which every FFI surface
mirrors as a string enum. Action::missing_required_compensation is
the check that stands in for the type.
idempotent: bool§max_retries: u32§failure_behavior: FailureBehavior§timeout_ms: Option<u64>§metadata: HashMap<String, Value>Implementations§
Source§impl Action
impl Action
Sourcepub fn new(action_type: ActionType) -> Action
pub fn new(action_type: ActionType) -> Action
A new action of action_type, with every optional field at its
default and a freshly generated Action::id.
This is the only way to build an Action from outside car-ir.
The struct is #[non_exhaustive] (see its docs), so struct-literal
syntax — including functional update, ..other — is rejected in other
crates. Set the fields you need afterwards; they are all public:
let mut a = Action::new(ActionType::ToolCall);
a.tool = Some("deploy".into());
a.idempotent = true;There is deliberately no Default for Action. An action’s type
decides what the rest of it means, and defaulting it would let
Action::default() produce a ToolCall naming no tool — a value that
is invalid the moment it exists, and that the validator would have to
reject. Requiring the type at construction makes that unrepresentable.
Sourcepub fn state_write(key: impl Into<String>, value: Value) -> Action
pub fn state_write(key: impl Into<String>, value: Value) -> Action
An ActionType::StateWrite of value to key, carrying both the
key/value parameters the executor reads and the expected_effects
entry the static verifier reads.
Sourcepub fn state_read(key: impl Into<String>) -> Action
pub fn state_read(key: impl Into<String>) -> Action
An ActionType::StateRead of key.
Sourcepub fn with_id(self, id: impl Into<String>) -> Action
pub fn with_id(self, id: impl Into<String>) -> Action
Replace the generated Action::id. Chainable, so a caller that cares
about the id can still write one expression.
Sourcepub fn with_param(self, key: impl Into<String>, value: Value) -> Action
pub fn with_param(self, key: impl Into<String>, value: Value) -> Action
Set one parameter. Chainable.
Sourcepub fn effective_write_set(&self) -> Vec<String>
pub fn effective_write_set(&self) -> Vec<String>
The keys this action writes — the union of the explicit
write_set with the keys derived from expected_effects and a
StateWrite action’s key parameter. Union, not replacement: a
partial write_set must never narrow the real write footprint, or
a conflict on an unlisted side-effect key would go undetected (a
false negative in the soundness-critical path). Proposals authored
before the transactional fields existed still participate via the
derived keys.
Sourcepub fn effective_read_set(&self) -> Vec<String>
pub fn effective_read_set(&self) -> Vec<String>
The keys this action reads — the union of the explicit read_set
with state_dependencies, assumption keys, and a StateRead
action’s key parameter. Union for the same soundness reason as
Action::effective_write_set.
Sourcepub fn missing_required_compensation(&self) -> bool
pub fn missing_required_compensation(&self) -> bool
Whether this action claims Reversibility::Compensable but declares
no Action::compensation — the one incoherent combination the enum
could not exclude by construction (see Compensation for why the
compensation is a sibling field rather than a variant payload).
It is a check, not a guarantee: it catches the missing declaration,
not a declaration that names a tool which cannot actually reverse the
effect. Nothing here can establish that a compensating call is a true
inverse — that remains the author’s claim, exactly as
expected_effects is.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Action
impl<'de> Deserialize<'de> for Action
Source§fn deserialize<__D>(
__deserializer: __D,
) -> Result<Action, <__D as Deserializer<'de>>::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(
__deserializer: __D,
) -> Result<Action, <__D as Deserializer<'de>>::Error>where
__D: Deserializer<'de>,
Source§impl Serialize for Action
impl Serialize for Action
Source§fn serialize<__S>(
&self,
__serializer: __S,
) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>where
__S: Serializer,
fn serialize<__S>(
&self,
__serializer: __S,
) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>where
__S: Serializer,
impl StructuralPartialEq for Action
Auto Trait Implementations§
impl Freeze for Action
impl RefUnwindSafe for Action
impl Send for Action
impl Sync for Action
impl Unpin for Action
impl UnsafeUnpin for Action
impl UnwindSafe for Action
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more