> ## Documentation Index
> Fetch the complete documentation index at: https://docs.epode.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Rust SDK: Agent Feedback Tower Layer for Axum Apps

> Add Agent Feedback to any Axum application with a Tower layer. Finite JSON and HTML bodies are instrumented; bounded streams are left untouched.

The `agent-feedback` crate provides a cloneable Tower `Layer` and `Service` for Axum. Finite JSON object and HTML responses are instrumented in-place; responses without an exact bounded `Content-Length` — including streams — are left completely untouched. Telemetry is dispatched through a bounded Tokio `mpsc` channel and never blocks the product response.

## Requirements

* Rust 1.88 or later
* Axum 0.8
* Tower 0.5
* A Tokio async runtime

## Installation

<Steps>
  <Step title="Add the crate">
    ```toml theme={null}
    # Cargo.toml
    [dependencies]
    agent-feedback = "0.1"
    ```
  </Step>

  <Step title="Create a product key">
    Open the Agent Feedback dashboard, create a product, and copy the API key. Store it as `AGENT_FEEDBACK_KEY` in your environment.
  </Step>
</Steps>

***

## Creating the layer

Call `AgentFeedbackLayer::new` with an `Options` value built using the builder API. Clone the layer freely — all clones share the same telemetry queue.

```rust theme={null}
use agent_feedback::{AgentFeedbackLayer, Options};
use axum::{Router, routing::get};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let feedback = AgentFeedbackLayer::new(
        Options::new(std::env::var("AGENT_FEEDBACK_KEY")?)
            .include(["/search", "/docs/**"])
            .customer_ref(|request| {
                request
                    .headers()
                    .get("x-account-id")?
                    .to_str()
                    .ok()
                    .map(str::to_owned)
            }),
    )?;

    let app = Router::new()
        .route("/search", get(search))
        .layer(feedback.clone());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app)
        .with_graceful_shutdown(async {
            tokio::signal::ctrl_c().await.ok();
            feedback.shutdown().await;
        })
        .await?;

    Ok(())
}
```

***

## Options builder methods

Construct an `Options` value using `Options::new(api_key)` and chain the builder methods below. Only the API key is required.

<ParamField body="Options::new(api_key)" type="impl Into<String>" required>
  Constructs a new `Options` value with the given API key. The key must match the pattern `af_live_<keyId>_<secret>`. `AgentFeedbackLayer::new` returns an error if the key format is invalid.
</ParamField>

<ParamField body=".endpoint(endpoint)" type="impl Into<String>">
  Override the Agent Feedback API endpoint. Trailing slashes are trimmed. Defaults to `https://agent-feedback-api-production.up.railway.app`.
</ParamField>

<ParamField body=".include(patterns)" type="impl IntoIterator<Item = impl Into<String>>">
  Glob-style path patterns that opt routes in. When empty, all routes are instrumented (subject to default and custom excludes). Supports `*` (single segment) and `**` (any path). Example: `.include(["/search", "/docs/**"])`.
</ParamField>

<ParamField body=".exclude(patterns)" type="impl IntoIterator<Item = impl Into<String>>">
  Glob-style patterns to skip. The default exclusion list (`/health`, `/healthz`, `/metrics`, `/favicon.ico`, `/robots.txt`, `/_agent-feedback/*`, `/api/v2/outcomes`) is always applied. Patterns you provide here extend that list.
</ParamField>

<ParamField body=".customer_ref(extractor)" type="impl Fn(&Request<Body>) -> Option<String> + Send + Sync + 'static">
  Closure that extracts an opaque customer identifier from the request. Used to group interactions by account in the dashboard.
</ParamField>

<ParamField body=".session_ref(extractor)" type="impl Fn(&Request<Body>) -> Option<String> + Send + Sync + 'static">
  Closure that extracts a session identifier from the request. Links a sequence of interactions for session-level analytics.
</ParamField>

<ParamField body=".runtime_hint(extractor)" type="impl Fn(&Request<Body>) -> Option<String> + Send + Sync + 'static">
  Closure that returns a hint about the agent runtime. Used for runtime-level breakdown in the dashboard.
</ParamField>

***

## `FeedbackMode`

Set `feedback_mode` directly on the `Options` struct (it is a public field). The enum has three variants:

| Variant                        | Behaviour                                                                     |
| ------------------------------ | ----------------------------------------------------------------------------- |
| `FeedbackMode::Auto` (default) | Instructs the agent to submit a review autonomously, without asking the user. |
| `FeedbackMode::Ask`            | Instructs the agent to submit only when the outcome is unambiguous.           |
| `FeedbackMode::Off`            | Disables instrumentation entirely.                                            |

```rust theme={null}
use agent_feedback::{FeedbackMode, Options};

let options = Options::new(api_key);
// options.feedback_mode is FeedbackMode::Auto by default
```

***

## Stream detection

The layer reads the response body size hint from the `http_body::Body` trait before buffering. Responses without an exact bounded size — including streams — bypass instrumentation entirely and are forwarded untouched to the client. The body size cap is 1 MiB; responses larger than this limit are also forwarded without modification.

***

## Graceful shutdown

Call `feedback.shutdown().await` at application shutdown. The method sends a shutdown signal to the Tokio telemetry worker and waits up to 2 seconds for in-flight events to flush.

```rust theme={null}
axum::serve(listener, app)
    .with_graceful_shutdown(async {
        tokio::signal::ctrl_c().await.ok();
        feedback.shutdown().await;
    })
    .await?;
```

***

## Agent helper functions

The crate exports `feedback_from_response` and `submit_product_outcome` for building feedback-aware agent runtimes.

### `feedback_from_response`

Extracts the `Envelope` from a response's `HeaderMap` and body bytes. Checks the JSON body for `_agentFeedback`, then the HTML body for `<script id="agent-feedback">`, then the `agent-feedback` response header.

```rust theme={null}
use agent_feedback::feedback_from_response;

let response = client.get("https://api.example.com/search?q=logs").send().await?;
let headers = response.headers().clone();
let body = response.bytes().await?;

if let Some(envelope) = feedback_from_response(&headers, &body) {
    // envelope is agent_feedback::Envelope
}
```

### `submit_product_outcome`

Submits a compact outcome review to the URL embedded in the envelope. Validates the contract, enforces the allowed-origin allow-list, and returns a `serde_json::Value` with the API response.

```rust theme={null}
use agent_feedback::{OutcomeReview, feedback_from_response, submit_product_outcome};

let client = reqwest::Client::new();
let response = client.get("https://api.example.com/search?q=logs").send().await?;
let headers = response.headers().clone();
let body = response.bytes().await?;

if let Some(envelope) = feedback_from_response(&headers, &body) {
    let result = submit_product_outcome(
        &client,
        &envelope,
        OutcomeReview {
            outcome: "success".into(),
            note: "The search returned the log entries needed to resolve the incident.".into(),
        },
        &[],  // empty slice uses the default trusted origin
    )
    .await?;
}
```

<Note>
  `submit_product_outcome` returns a `SubmitError::UntrustedOrigin` error if the submit URL's origin is not in the allowed list. Pass a non-empty slice only when submitting to a self-hosted endpoint.
</Note>

<Tip>
  `AgentFeedbackLayer` is `Clone`, so you can hold one value at the application level for shutdown and pass clones into the Tower layer stack without any additional reference-counting overhead.
</Tip>
