# Working with agents

## What you want to do

The shop now works with coding agents, which change its steps and, sometimes, its workflow. You want an agent to change a business rule without changing how an order travels through the shop, and to know, without reading every line it wrote, whether the flow changed and how.

## The code

The program is [chapter 30](https://itinera-dev.org/book/rust/30-testing-a-whole-journey.md)'s, with its approved paths, so it has none of what chapters 31 to 35 added to the shop. An agent was asked to cap discounts, and changed the pricing step alone:

Rust, from the program [`ch43_working_with_agents`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents.rs)

```rust
/// Works out what an order costs: its price, less its discount if it has one, given back as the
/// amount to charge. A discount takes at most half the price off.
#[derive(Step)]
struct Price {
    #[input]
    price: i64,
    #[input]
    discount: Option<i64>,
}

#[step]
impl Price {
    fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
        let discount = self.discount.unwrap_or(0).min(self.price / 2);
        contributor.contribute("amount", self.price - discount);
        Self::succeed()
    }
}
```

Rust, the same in its asynchronous form, from the program [`ch43_working_with_agents_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents_async.rs)

```rust
/// Works out what an order costs: its price, less its discount if it has one, given back as the
/// amount to charge. A discount takes at most half the price off.
#[derive(Step)]
struct Price {
    #[input]
    price: i64,
    #[input]
    discount: Option<i64>,
}

#[step]
impl Price {
    async fn run(self, #[contributor] mut contributor: Contributor<'_>) -> Result<Outcome, Error> {
        let discount = self.discount.unwrap_or(0).min(self.price / 2);
        contributor.contribute("amount", self.price - discount);
        Self::succeed()
    }
}
```

Another agent proposes a change to the flow: a parcel the carrier won't collect should be given up at once, rather than tried again with the backup carrier. Before it replaces the workflow, the shop declares the proposal in its tests, beside the approved workflow of [chapter 23](https://itinera-dev.org/book/rust/23-undoing-a-charge-when-the-shipping-fails.md), with `ship` and no retry budget, and only the main carrier:

Rust, from the program [`ch43_working_with_agents`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents.rs)

```rust
/// The shop's workflow as an agent proposes to change it: a parcel the carrier won't collect
/// is given up at once, without trying the backup carrier.
#[workflow(name = "orders", reporters = [Log, Desk], policies = [Notify])]
#[with_step(Lookup, name = "lookup", policies = [AlreadyCompleted])]
#[with_step(Price, name = "price")]
#[with_step(Charge, name = "charge", rescue = RenewCard, policies = [Audit, Outage])]
#[with_step(Ship, name = "ship", policies = [FailedShipping])]
struct Proposed;

#[role]
impl Refunds for Proposed {
    fn refund(&self, receipt: &str, amount: i64) {
        println!("the bank gives {amount} back on receipt {receipt}");
    }
}

#[role]
impl Messages for Proposed {
    fn tell(&self, customer: &str, message: &str) {
        println!("email to {customer}: {message}");
    }
}

#[workflow]
impl Proposed {
    /// Names each journey after its order.
    #[id_generator]
    fn order_number(&self, data: &DataBag) -> Result<String, Error> {
        Ok(data.required("order")?)
    }

    /// Gives the shipping the weight of the order's parcel.
    #[input_adapter(steps = ["ship"])]
    fn parcels(
        &self,
        #[key] key: &str,
        #[from_workflow] parcel: Parcel,
    ) -> Result<Option<AnyValue>, Error> {
        if key != "weight" {
            return Self::from_data_bag();
        }
        Self::supply(parcel.weight)
    }

    /// The order book, which has the orders completed before today.
    #[provider(key = "order_book")]
    fn ledger(&self) -> Result<Provided<dyn OrderBook>, Error> {
        Ok(provide!(Ledger {
            completed: vec!["A-13"],
        }))
    }

    /// The bank, as the payment client of the journey.
    #[provider(key = "payments")]
    fn bank(&self) -> Result<Provided<dyn Payments>, Error> {
        Ok(provide!(Bank { this_year: 2026 }))
    }

    /// The main carrier.
    #[provider(key = "carrier", per_attempt)]
    fn carriers(&self) -> Result<Provided<dyn Carrier>, Error> {
        Ok(provide!(MainCarrier))
    }
}
```

Rust, the same in its asynchronous form, from the program [`ch43_working_with_agents_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents_async.rs)

```rust
/// The shop's workflow as an agent proposes to change it: a parcel the carrier won't collect
/// is given up at once, without trying the backup carrier.
#[workflow(name = "orders", reporters = [Log, Desk], policies = [Notify])]
#[with_step(Lookup, name = "lookup", policies = [AlreadyCompleted])]
#[with_step(Price, name = "price")]
#[with_step(Charge, name = "charge", rescue = RenewCard, policies = [Audit, Outage])]
#[with_step(Ship, name = "ship", policies = [FailedShipping])]
struct Proposed;

#[role]
#[async_trait]
impl Refunds for Proposed {
    async fn refund(&self, receipt: &str, amount: i64) {
        network().await;
        println!("the bank gives {amount} back on receipt {receipt}");
    }
}

#[role]
#[async_trait]
impl Messages for Proposed {
    async fn tell(&self, customer: &str, message: &str) {
        network().await;
        println!("email to {customer}: {message}");
    }
}

#[workflow]
impl Proposed {
    /// Names each journey after its order.
    #[id_generator]
    fn order_number(&self, data: &DataBag) -> Result<String, Error> {
        Ok(data.required("order")?)
    }

    /// Gives the shipping the weight of the order's parcel.
    #[input_adapter(steps = ["ship"])]
    fn parcels(
        &self,
        #[key] key: &str,
        #[from_workflow] parcel: Parcel,
    ) -> Result<Option<AnyValue>, Error> {
        if key != "weight" {
            return Self::from_data_bag();
        }
        Self::supply(parcel.weight)
    }

    /// The order book, which has the orders completed before today.
    #[provider(key = "order_book")]
    fn ledger(&self) -> Result<Provided<dyn OrderBook>, Error> {
        Ok(provide!(Ledger {
            completed: vec!["A-13"],
        }))
    }

    /// The bank, as the payment client of the journey.
    #[provider(key = "payments")]
    fn bank(&self) -> Result<Provided<dyn Payments>, Error> {
        Ok(provide!(Bank { this_year: 2026 }))
    }

    /// The main carrier.
    #[provider(key = "carrier", per_attempt)]
    fn carriers(&self) -> Result<Provided<dyn Carrier>, Error> {
        Ok(provide!(MainCarrier))
    }
}
```

What it names is as earlier chapters show it: the lookup, `already_completed`, `OrderBook` and the log in [chapter 22](https://itinera-dev.org/book/rust/22-finishing-a-journey-early.md), `Payments` in [chapter 14](https://itinera-dev.org/book/rust/14-repairing-then-trying-once-more.md), `Carrier` in [chapter 12](https://itinera-dev.org/book/rust/12-trying-again.md), the charge and the audit in [chapter 20](https://itinera-dev.org/book/rust/20-an-optional-input.md), `RenewCard` in [chapter 14](https://itinera-dev.org/book/rust/14-repairing-then-trying-once-more.md), the shipping and `outage` in [chapter 17](https://itinera-dev.org/book/rust/17-giving-up-a-step-early.md), `failed_shipping` and `Refunds` in [chapter 23](https://itinera-dev.org/book/rust/23-undoing-a-charge-when-the-shipping-fails.md), `notify` and `Messages` in [chapter 19](https://itinera-dev.org/book/rust/19-letting-policies-act-on-the-outside-world.md) and the desk in [chapter 11](https://itinera-dev.org/book/rust/11-naming-a-journey.md).

[Chapter 30](https://itinera-dev.org/book/rust/30-testing-a-whole-journey.md)'s `path_of` now makes its order with `order_of`, and `proposed_path_of` runs the same order through the proposal. `NewOrder` is as [chapter 25](https://itinera-dev.org/book/rust/25-reading-the-result.md) shows it, and `Parcel` as [chapter 21](https://itinera-dev.org/book/rust/21-deciding-where-a-steps-data-comes-from.md) does:

Rust, from the program [`ch43_working_with_agents`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents.rs)

```rust
/// What the shop knows about an order of 30 with a parcel of this weight.
fn order_of(order: &str, weight: i64) -> NewOrder {
    NewOrder {
        order: order.to_owned(),
        customer: "Ann".to_owned(),
        price: 30,
        discount: None,
        parcel: Parcel {
            weight,
            size: [20, 15, 5],
        },
        card: Some("ann-2028".to_owned()),
        gift_card: false,
    }
}

/// Runs an order of 30 with a parcel of this weight, through the shop's real steps and
/// policies, with the test's bank and carriers in place of the shop's, and gives back its
/// journey's path.
fn path_of(
    order: &str,
    weight: i64,
    bank: TestBank,
    carrier: TestCarrier,
) -> Result<Vec<String>, Error> {
    let recorder = RecordingReporter::default();
    let instance = Orders::builder()
        .channel("email")
        .initial_data(order_of(order, weight))
        .reporter(recorder.clone())
        .fake_provider::<dyn Payments>("bank", move || Ok(provide!(bank)))
        .fake_provider::<dyn Carrier>("carriers", move || Ok(provide!(carrier)))
        .create()?;
    local_executor_for!(Orders).run(instance)?;
    Ok(recorder.path())
}

/// Runs the same order as `path_of`, through the proposed workflow.
fn proposed_path_of(
    order: &str,
    weight: i64,
    bank: TestBank,
    carrier: TestCarrier,
) -> Result<Vec<String>, Error> {
    let recorder = RecordingReporter::default();
    let instance = Proposed::builder()
        .initial_data(order_of(order, weight))
        .reporter(recorder.clone())
        .fake_provider::<dyn Payments>("bank", move || Ok(provide!(bank)))
        .fake_provider::<dyn Carrier>("carriers", move || Ok(provide!(carrier)))
        .create()?;
    local_executor_for!(Proposed).run(instance)?;
    Ok(recorder.path())
}
```

Rust, the same in its asynchronous form, from the program [`ch43_working_with_agents_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents_async.rs)

```rust
/// What the shop knows about an order of 30 with a parcel of this weight.
fn order_of(order: &str, weight: i64) -> NewOrder {
    NewOrder {
        order: order.to_owned(),
        customer: "Ann".to_owned(),
        price: 30,
        discount: None,
        parcel: Parcel {
            weight,
            size: [20, 15, 5],
        },
        card: Some("ann-2028".to_owned()),
        gift_card: false,
    }
}

/// Runs an order of 30 with a parcel of this weight, through the shop's real steps and
/// policies, with the test's bank and carriers in place of the shop's, and gives back its
/// journey's path.
async fn path_of(
    order: &str,
    weight: i64,
    bank: TestBank,
    carrier: TestCarrier,
) -> Result<Vec<String>, Error> {
    let recorder = RecordingReporter::default();
    let instance = Orders::builder()
        .channel("email")
        .initial_data(order_of(order, weight))
        .reporter(recorder.clone())
        .fake_provider::<dyn Payments>("bank", move || Ok(provide!(bank)))
        .fake_provider::<dyn Carrier>("carriers", move || Ok(provide!(carrier)))
        .create()?;
    local_executor_for!(Orders).run(instance).await?;
    Ok(recorder.path())
}

/// Runs the same order as `path_of`, through the proposed workflow.
async fn proposed_path_of(
    order: &str,
    weight: i64,
    bank: TestBank,
    carrier: TestCarrier,
) -> Result<Vec<String>, Error> {
    let recorder = RecordingReporter::default();
    let instance = Proposed::builder()
        .initial_data(order_of(order, weight))
        .reporter(recorder.clone())
        .fake_provider::<dyn Payments>("bank", move || Ok(provide!(bank)))
        .fake_provider::<dyn Carrier>("carriers", move || Ok(provide!(carrier)))
        .create()?;
    local_executor_for!(Proposed).run(instance).await?;
    Ok(recorder.path())
}
```

A test runs each approved order through both, and says which paths the proposal keeps, and what it makes of the others:

Rust, from the program [`ch43_working_with_agents`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents.rs)

```rust
#[test]
fn the_proposed_flow_keeps_two_approved_paths_and_changes_two() -> Result<(), Error> {
    let kept = [
        ("A-1", 2, TestBank::Charges, TestCarrier::CollectsAll),
        ("A-3", 2, TestBank::IsDown, TestCarrier::CollectsAll),
    ];
    for (order, weight, bank, carrier) in kept {
        assert_eq!(
            proposed_path_of(order, weight, bank, carrier)?,
            path_of(order, weight, bank, carrier)?
        );
    }
    assert_eq!(
        proposed_path_of("A-2", 90, TestBank::Charges, TestCarrier::CollectsAll)?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: over-parcel-limit",
            "ship given up: failure",
            "journey failed at ship: over-parcel-limit",
        ]
    );
    assert_eq!(
        proposed_path_of("A-4", 60, TestBank::Charges, TestCarrier::CollectsNone)?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: too-heavy",
            "ship given up: failure",
            "journey failed at ship: too-heavy",
        ]
    );
    Ok(())
}
```

Rust, the same in its asynchronous form, from the program [`ch43_working_with_agents_async`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents_async.rs)

```rust
#[tokio::test]
async fn the_proposed_flow_keeps_two_approved_paths_and_changes_two() -> Result<(), Error> {
    let kept = [
        ("A-1", 2, TestBank::Charges, TestCarrier::CollectsAll),
        ("A-3", 2, TestBank::IsDown, TestCarrier::CollectsAll),
    ];
    for (order, weight, bank, carrier) in kept {
        assert_eq!(
            proposed_path_of(order, weight, bank, carrier).await?,
            path_of(order, weight, bank, carrier).await?
        );
    }
    assert_eq!(
        proposed_path_of("A-2", 90, TestBank::Charges, TestCarrier::CollectsAll).await?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: over-parcel-limit",
            "ship given up: failure",
            "journey failed at ship: over-parcel-limit",
        ]
    );
    assert_eq!(
        proposed_path_of("A-4", 60, TestBank::Charges, TestCarrier::CollectsNone).await?,
        [
            "lookup attempt 1 succeeded",
            "price attempt 1 succeeded",
            "charge attempt 1 succeeded",
            "ship attempt 1 failed: too-heavy",
            "ship given up: failure",
            "journey failed at ship: too-heavy",
        ]
    );
    Ok(())
}
```

In the asynchronous form, the pricing's `run` is an `async fn`, the proposal's roles take `#[async_trait]`, as in [chapter 26](https://itinera-dev.org/book/rust/26-running-async-journeys-while-others-wait.md), `path_of` and `proposed_path_of` are `async fn` that await the journey, and the test is an `async fn` marked `#[tokio::test]`, which awaits each path. The tests print:

What running the tests of `ch43_working_with_agents` prints, recorded in [`ch43_working_with_agents.txt`](https://github.com/itinera-dev/book/blob/main/rust/tested/ch43_working_with_agents.txt)

```text
running 5 tests
test tests::a_charge_whose_bank_is_down_is_given_up_and_never_rescued ... ok
test tests::a_parcel_no_carrier_collects_is_tried_once_more_then_given_up ... ok
test tests::a_parcel_over_the_limit_is_given_up_after_its_first_attempt ... ok
test tests::an_order_the_bank_charges_and_the_carrier_takes_is_shipped ... ok
test tests::the_proposed_flow_keeps_two_approved_paths_and_changes_two ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```

## How it works

1. **A business rule changes in a step, and the paths hold.** The pricing now caps the discount at half the price, and all four approved paths still pass: the step decides an amount, and the flow around it is the same. To change it, the agent read one step, which names its inputs by key and decides nothing about what comes next.
2. **A flow change shows in the paths it changes.** The proposal changes the path of the parcel no carrier collects, as intended: `ship given up: failure` after one attempt. It also changes the path of the parcel over the limit, which nobody asked for. That order was given up by `failed_shipping`, in the hook it runs before a retry; with no retry budget there is no retry, so the hook never runs, and the journey now fails with `over-parcel-limit` instead of `no-carrier-can-take-it`. The test says so before any order takes that path.
3. **A path is the flow a person agreed.** An approved path changes only when a person agrees to the new one. A failing path test is not a test to fix: it is a flow change waiting for that decision, here two of them.
4. **One rule for every change to a step: it decides nothing about the flow.** A step that tries its work again, calls another service when the first fails, or tells anyone anything has taken a decision that belongs elsewhere, as [chapter 40](https://itinera-dev.org/book/rust/40-recognising-a-decision-in-the-wrong-place.md) showed. A reviewer, a person or an agent, checks every change to a step for that, and every change to a workflow, a policy or a provider against the paths.

The shop gives its agents this paragraph in its `AGENTS.md`:

> Business rules live in steps; the flow lives in the workflows, their policies and their providers. A change to a step never decides anything about the flow: it does not try its work again, fall back to another service or tell anyone, and it reports what happened as an outcome. A change to a workflow, a policy or a provider is a flow change: say so in the pull request, show which approved paths it changes, and never edit an approved path to make a test pass. A person approves every new path.

This is where the book ends, for now. Its workflows run one after the other, from start to end, in one program. Later tiers of Itinera will add moving a journey to another workflow, running a workflow from inside another, and journeys that survive a crash and resume, each with a part of this book of its own.

## Outcomes

We have shown how an agent changes a business rule without touching the flow, and how a proposed change to the flow shows in the paths it changes.

| The usual way                                                                                 | With Itinera                                                                            |
| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Whether a change altered the flow is often found by reading the whole diff, or in production. | A business rule changes in a step, and the approved paths say whether the flow changed. |
| A side effect of a flow change, such as a check that no longer runs, often goes unnoticed.    | The paths it changes are listed by a test, for a person to approve.                     |
