agent-effects 0.2.0

Reliable side-effect execution for AI agents and autonomous applications
Documentation

Agent Effects — The Rust-native reliability layer for AI agent actions

crates.io docs.rs Version: 0.2.0 (unreleased) License: MIT OR Apache-2.0 Rust: 2024 Edition MSRV: Rust 1.90 CI Async: Tokio Stores: Memory, SQLite, PostgreSQL SQL stores: Rust 1.94

agent-effects is a Rust library for reliable AI agent tool calls and side-effect execution. It combines idempotency keys, durable execution, safe retries and crash recovery for payments, emails, cloud resources and tickets initiated by LLM agents or other autonomous applications.

The runtime records intent before acting, distinguishes definite failures from unknown outcomes, and uses remote verification or idempotent retry to resolve uncertainty. Store effects in SQLite or PostgreSQL, and track their lifecycle with tracing and OpenTelemetry metrics.

Quick start · Architecture · Features · Examples · Documentation

Why agent tool calls need idempotency

agent decides to charge a customer
  → request reaches the payment provider → customer is charged
  → connection drops before the response
  → caller sees a timeout

Retrying charges the customer twice; giving up loses a payment that happened. The honest answer is "unknown", and the runtime treats it that way:

Unknown ─┬─ verification available ─→ ask the provider what happened
         ├─ idempotent / idempotency key ─→ safe to run again
         └─ neither ─→ needs an operator

When to use agent-effects

  • AI agent and LLM tools: attach repeated tool calls to the same effect and replay committed results.
  • Payments, emails and resource provisioning: handle timeouts without blindly repeating an operation that may already have applied.
  • Rust services that need crash recovery: resume registered handlers from durable storage after a worker restarts.
  • Operations that need human oversight: require approval, retain an audit trail and compensate committed effects.

Architecture

Agent Effects architecture: host application, Rust runtime, remote systems, durable stores, and recovery

Agent Effects sits inside your Rust application, between an authorized tool call and the external system it may change. It is a reliability boundary, not an agent framework, workflow orchestrator, or authorization service.

  1. Identify and check. The application supplies an effect name, stable key, input and safety requirements. The runtime validates the identity and fingerprint, applies risk policy, and checks persisted execution contracts.
  2. Record before acting. The selected EffectStore records intent, status, attempts and an audit trail. Leases and fencing coordinate concurrent workers before a closure or registered EffectHandler executes the action.
  3. Reconcile outcomes. A confirmed success is committed and replayed for the same key. A definite non-application may fail; an ambiguous result is Unknown, never automatically assumed to be a failure. The runtime prefers verification, otherwise retries only when provably safe, or escalates to NeedsIntervention for an operator.
  4. Recover and observe. Registered handlers can resume from stored input after a restart. Closure-based effects need a caller to re-attach. Memory, SQLite and PostgreSQL stores implement the persistence contract; tracing and optional OpenTelemetry observers expose lifecycle activity.

Remote idempotency keys provide the strongest protection when the provider honors them. No local runtime can guarantee exactly-once effects across an uncooperative external system. See reliability guarantees and limits.

Quick start

Add the runtime and a SQLite store to your Rust application:

[dependencies]
agent-effects = "0.2"
agent-effects-sqlite = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use agent_effects::{EffectFailure, EffectKind, EffectOutcome, Runtime};
use agent_effects_sqlite::SqliteStore;

let runtime = Runtime::new(SqliteStore::open("effects.db").await?);

let outcome = runtime
    .effect("payment.charge", &order.id)        // one record per (name, key) while retained
    .kind(EffectKind::IrreversibleWrite)
    .input(&charge)                             // a reused key with another input is an error
    .remote_idempotency(true)                   // the provider deduplicates on our key
    .run(move |ctx| {
        let (provider, charge) = (provider.clone(), charge.clone());
        async move {
            provider
                .charge(&charge, ctx.idempotency_key()) // same key on every attempt
                .await
                .map_err(classify)                      // -> EffectFailure::transient / ambiguous / ...
        }
    })
    .await?;                                    // Err only for infrastructure problems

match outcome {
    EffectOutcome::Committed(payment) => { /* done; calling again replays it */ }
    EffectOutcome::Failed(error) => { /* definitely did not happen */ }
    EffectOutcome::Unknown { id } | EffectOutcome::NeedsIntervention { id } => {
        /* may have happened: do not retry blindly */
    }
    _ => {}
}

Idempotency and outcome verification

Pick the protection your remote system allows:

The remote… Builder After an unknown outcome
deduplicates on an idempotency key .remote_idempotency(true) and forward ctx.idempotency_key() re-sent; applied once
can be queried .verify(...) or .verify_eventually(settle, ...) checked; re-run only if verifiably not applied
neither escalated to an operator (runtime.resolve)

Features

  • Versioned payloads. Declare durable input/output schema versions and optional JSON migrations across deployments. Unreadable output preserves the committed effect.
  • Persisted safety contracts. Recovery refuses incompatible changes to idempotency, verification, handler semantics, approval, or retry ceilings. Reconcile legacy unresolved effects and stop older workers before upgrading.
  • Durable handlers. Implement EffectHandler, register it, and runtime.submit::<H>(key, input). Recovery then finishes the effect from its stored input even if the caller never comes back.
  • Metrics. .observer(OtelObserver::global()) exports the lifecycle as OpenTelemetry metrics; implement EffectObserver for anything else.
  • Redaction. Secret<T> fields are stored as "[REDACTED]", and a Redactor such as RedactKeys scrubs inputs, outputs, audit notes and error messages before anything is written.
  • Risk policy. .risk(RiskLevel::High) plus a runtime RiskPolicy can require approval, verification, or no automatic retries. Rules only ever add requirements.
  • Approval. .require_approval() waits durably for a human: an ApprovalProvider (a CLI prompt is included) or runtime.approve / runtime.deny.
  • Compensation. runtime.compensation(name, key).run(...) or runtime.compensate::<H>(key) undoes a committed effect durably, with retries and its own idempotency key.
  • HTTP. agent-effects-http turns a reqwest request into an action that sends Idempotency-Key and classifies failures. Timeouts, 429 and 503 responses are ambiguous; Retry-After delays safe retries without authorizing them.
  • Retention. .retention(RetentionPolicy::settled(age)) prunes settled records once old enough; anything unresolved is kept. A pruned key is new again.
  • Retries and timeouts. .retry(policy) sets a lifetime attempt budget and backoff with jitter, honouring retry_after; .attempt_timeout(d) bounds each action attempt.
  • Preconditions. .precondition(...) rejects a stale decision before the first attempt.
  • Recovery and operator tools. runtime.run_recovery(interval) resumes durable handlers; runtime.pending(..) lists unresolved effects and runtime.resolve(..) records an operator's decision. Use runtime.wait(id, timeout) to wait for another caller's effect.

Examples

Run the payment reliability and LLM agent tool examples locally:

cargo run --example payment
cargo run --example agent_tool
  • Payment retries and verification: a provider that charges and then drops the connection, shown under each protection, with the audit trail.
  • LLM agent refund tool: a refund_order tool for an LLM agent, handling duplicate calls, a changed amount and a stale decision.

Reliability guarantees and limits

  • Intent is persisted before the external call.
  • One logical effect (name + application key) maps to one record, however many times agents, workers or restarts ask for it.
  • An effect is only marked failed when it definitely did not apply.
  • A retry that could duplicate an effect is never made automatically.
  • Crash recovery is tested in-process and with real process death. In-flight requests can outlive a lease; remote idempotency remains the strongest protection.

agent-effects does not provide exactly-once side effects. Nothing can without cooperation from the target system. Targets that honour an idempotency key get the strongest guarantee, and the runtime derives a stable key for every effect.

It is not a workflow engine, a job queue or an agent framework, and it does not decide whether an agent is allowed to act. Application authorization still applies.

Use a workflow engine when you need durable branching processes, scheduling, signals, or orchestration across a large business workflow. Embed Agent Effects when your existing application needs a focused reliability boundary around consequential actions. It does not supply application authorization.

Rust crates and storage backends

Crate Purpose
agent-effects The runtime; the crate applications depend on. Features: testkit (FakeRemote), fault-injection (FaultInjector).
agent-effects-sqlite SQLite store; several processes may share one file.
agent-effects-http HTTP requests as effects (reqwest): failure classification, Idempotency-Key.
agent-effects-otel OpenTelemetry metrics through an EffectObserver.
agent-effects-postgres PostgreSQL store for many workers on many hosts; leases use the database's clock. Can share an application's database, in a schema of its own, through PgBouncer in transaction mode.
agent-effects-memory In-memory store for tests and development.
agent-effects-store Storage contract and state machine, for writing new backends; includes the backend conformance suite (testkit).

MSRV: Rust 1.90; agent-effects-sqlite and agent-effects-postgres need 1.94 (sqlx).

Documentation

Development

The reliability suites inject storage failures before and after real backend operations, cancel callers at execution boundaries, race operator decisions, and recover durable intents across processes and handler schema versions. They count external applications separately from retry attempts. PostgreSQL and PgBouncer tests require isolated test services and their environment flags.

cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features
cargo +1.90 test --workspace --exclude agent-effects-sqlite --exclude agent-effects-postgres --all-features   # MSRV
cargo +1.94 test -p agent-effects-sqlite -p agent-effects-postgres                                           # their MSRV (sqlx)

# PostgreSQL tests (they skip without a database), and through PgBouncer in
# transaction mode (skipped without AGENT_EFFECTS_PGBOUNCER_URL):
podman network create ae
podman run --rm -d --name ae-pg --network ae -p 55432:5432 -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=effects postgres:17
podman run --rm -d --network ae -p 56432:5432 -e DATABASE_URL=postgres://postgres:pw@ae-pg:5432/effects \
  -e AUTH_TYPE=scram-sha-256 -e POOL_MODE=transaction -e MAX_PREPARED_STATEMENTS=0 -e DEFAULT_POOL_SIZE=4 \
  -e 'IGNORE_STARTUP_PARAMETERS= ' \
  docker.io/edoburu/pgbouncer:v1.25.2-p0
AGENT_EFFECTS_POSTGRES_URL=postgres://postgres:pw@localhost:55432/effects \
AGENT_EFFECTS_PGBOUNCER_URL=postgres://postgres:pw@localhost:56432/effects \
  cargo test -p agent-effects-postgres

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.