Skip to main content

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.

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

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

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:
SurfaceHow
SlackPost the tool + args to a channel; wait for an emoji or button click.
Web UIPersist the pending call to your DB; poll for the human’s decision.
Mobile pushPush notification with approve / reject quick actions.
Auto-approve under thresholdApprove 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 for the time-travel surface.

See also

Human-in-the-loop

Approver, Decision, graph interrupts.

Tools

Define the tools being gated.

Checkpointing

Pause across processes.