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

# Tool calling with human approval

> An agent that pauses before sensitive actions and waits for a human to approve, edit, or reject.

Some tools you don't want the model running unsupervised — sending email, executing code, charging cards. This pattern wires an `Approver` so the agent loop pauses before specified tools run, surfaces the pending call to a human, and only proceeds when they say yes.

## What you'll build

An agent with two tools — one safe (`get_balance`), one sensitive (`transfer_funds`). The safe tool runs without intervention; the sensitive one prompts for approval at a CLI before dispatching.

## How it works

* **`with_approver(...)`** plus the `HumanInTheLoop` middleware tap into the agent loop's tool-dispatch step.
* **Your `Approver` decides per call.** Approve, reject (with a reason the model sees), or edit (rewrite the args before running).
* **Rejection becomes a tool message.** The agent sees "the tool was not approved" and can apologize, ask for clarification, or try a different approach.

## The approver

```rust theme={null}
use async_trait::async_trait;
use cognis::tools::{Approver, Decision};
use cognis::Result;
use serde_json::Value;

struct CliApprover {
    sensitive: Vec<&'static str>,
}

#[async_trait]
impl Approver for CliApprover {
    async fn approve(&self, tool_name: &str, args: &Value) -> Result<Decision> {
        if !self.sensitive.contains(&tool_name) {
            return Ok(Decision::Approve);
        }

        println!("\n[approval needed]");
        println!("  tool: {tool_name}");
        println!("  args: {}", serde_json::to_string_pretty(args)?);
        print!("  approve? (y / n / e=edit): ");
        std::io::Write::flush(&mut std::io::stdout())?;

        let mut line = String::new();
        std::io::stdin().read_line(&mut line)?;
        match line.trim() {
            "y" | "yes" => Ok(Decision::Approve),
            "e" | "edit" => {
                print!("  new JSON args: ");
                std::io::Write::flush(&mut std::io::stdout())?;
                let mut edit = String::new();
                std::io::stdin().read_line(&mut edit)?;
                let new_args: Value = serde_json::from_str(edit.trim())?;
                Ok(Decision::Edit { args: new_args })
            }
            _ => Ok(Decision::reject("rejected by operator")),
        }
    }
}
```

## The agent

```rust theme={null}
use std::sync::Arc;
use cognis::prelude::*;
use cognis::AgentBuilder;
use cognis::Client;

#[tokio::main]
async fn main() -> Result<()> {
    let approver = Arc::new(CliApprover {
        sensitive: vec!["transfer_funds"],
    });

    let mut agent = AgentBuilder::new()
        .with_llm(Client::from_env()?)
        .with_tool(Arc::new(GetBalanceTool))     // (your impls)
        .with_tool(Arc::new(TransferFundsTool))
        .with_approver(approver)                 // wraps every registered tool
        .with_system_prompt(
            "You are a banking assistant. Use tools to inspect accounts and \
             move money. Confirm intent in plain English before any transfer.",
        )
        .with_max_iterations(6)
        .build()?;

    let resp = agent.run(Message::human(
        "Move $50 from checking to savings, then tell me the new balances."
    )).await?;
    println!("\n{}", resp.content);
    Ok(())
}
```

## How it works

* **The approver runs *before* dispatch.** `with_approver(...)` wraps every registered tool in an `ApprovalGatedTool`. When the agent's dispatcher tries to run the tool, the wrapper consults the approver first.
* **A `Reject` decision short-circuits the tool**, returning the rejection reason as the tool's output — the agent sees "rejected by operator" and can apologize, retry, or take a different path.
* **`Edit` lets the human fix the model's args** without rejecting. Useful when the model picked the right tool but the wrong amount or recipient.
* **`Arc<dyn Approver>` is the shape passed in** — the builder takes ownership and uses the same instance for every gated tool, so the approver can hold state (a counter, a queue handle, an audit log) without divergence.

## Production approver

A CLI approver is a starting point. In production you'd swap in:

| Surface                      | How                                                                   |
| ---------------------------- | --------------------------------------------------------------------- |
| Slack                        | Post the tool + args to a channel; wait for an emoji or button click. |
| Web UI                       | Persist the pending call to your DB; poll for the human's decision.   |
| Mobile push                  | Push notification with approve / reject quick actions.                |
| Auto-approve under threshold | Approve transfers under \$X, prompt above.                            |

The trait is `async`, so you can wait on a long-lived signal. Just don't block the agent's hot path — if approval might take minutes, push to a queue and resume from a checkpoint.

## Pair with checkpoints

For long-running approvals (the human is asleep), persist the agent's state and resume later. Use a `Checkpointer` and rebuild the agent from state on the resume side. See [Graph workflows → Checkpointing](/graph-workflows/checkpointing) for the time-travel surface.

## See also

<CardGroup cols={2}>
  <Card title="Human-in-the-loop" icon="user-check" href="/building-agents/hitl">
    Approver, Decision, graph interrupts.
  </Card>

  <Card title="Tools" icon="screwdriver-wrench" href="/building-agents/tools">
    Define the tools being gated.
  </Card>

  <Card title="Checkpointing" icon="floppy-disk" href="/graph-workflows/checkpointing">
    Pause across processes.
  </Card>
</CardGroup>
