Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Level 2: Budgets and costs

You'll learn how budgets bound what an agent can spend, and how to charge real costs.

Run cargo run --example mastery_02_budgets

The idea

Every agent gets a lifetime budget of credits that never refills. What a credit means is up to you: model tokens, API calls, dollars. Each tool has a minimum cost. A request may declare a higher cost, like the tokens a model call actually used, but never a lower one. The agent that spends its last credit is terminated.

The code

//! Lineage Mastery, level 2: budgets and costs.
//!
//! Every agent has a lifetime budget that never refills. Each tool has a minimum cost; a
//! request may declare a higher cost (tokens used, dollars moved) but never a lower one.
//! Spending the last credit terminates the agent.
//!
//! Run: cargo run --example mastery_02_budgets

use lineage::audit::AuditKey;
use lineage::guard::{Decision, Guard, Outcome, Policy, ToolRule};
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dir = std::path::Path::new("mastery-data/02");
    let _ = std::fs::remove_dir_all(dir);

    // 100 credits for life. A model call costs at least 1; we charge real tokens / 1000.
    let policy = Policy::new(100)
        .allow("llm_call", ToolRule::cost(1))
        .allow("web_search", ToolRule::cost(5));
    let mut guard = Guard::create(dir.join("agent.jsonl"), AuditKey::load_or_create(dir.join("audit.key"))?, "researcher", policy)?;

    let plan: [(&str, Option<u64>); 6] = [
        ("llm_call", Some(12)),   // 12,000 tokens: declared cost 12
        ("web_search", None),     // fixed cost 5
        ("llm_call", Some(0)),    // declaring 0 does not work: the minimum (1) is charged
        ("llm_call", Some(90)),   // more than what is left: denied, but no scar
        ("llm_call", Some(40)),
        ("llm_call", Some(42)),   // exactly the rest: allowed, and the agent is terminated
    ];

    for (tool, declared) in plan {
        let decision = guard.request(tool, json!({}), declared)?;
        let status = guard.status();
        match decision {
            Decision::Allowed { action_id, cost, .. } => {
                guard.report(&action_id, Outcome::success(""))?;
                println!("{tool:<10} declared {:<4} charged {cost:<3} -> {:>3} left", fmt(declared), status.remaining);
            }
            Decision::Denied { reason, .. } => println!("{tool:<10} declared {:<4} DENIED: {reason}", fmt(declared)),
            Decision::PendingApproval { .. } => unreachable!(),
        }
    }

    let status = guard.status();
    println!("\nspent {} of {}; alive: {}; reason: {}", status.spent, status.budget, status.alive,
             status.termination_reason.as_deref().unwrap_or("-"));
    println!("Any further request is denied: {:?}", guard.request("llm_call", json!({}), None)?);
    Ok(())
}

fn fmt(cost: Option<u64>) -> String {
    cost.map(|c| c.to_string()).unwrap_or_else(|| "-".into())
}

Run it

llm_call   declared 12   charged 12  ->  88 left
web_search declared -    charged 5   ->  83 left
llm_call   declared 0    charged 1   ->  82 left
llm_call   declared 90   DENIED: cost 90 exceeds remaining budget 82
llm_call   declared 40   charged 40  ->  42 left
llm_call   declared 42   charged 42  ->   0 left

spent 100 of 100; alive: false; reason: budget exhausted
Any further request is denied: Denied { action_id: "act-7", reason: Terminated { reason: "budget exhausted" } }

What happened

  • Declared costs raised the charge (12, 40, 42). Declaring 0 couldn’t lower it below the tool’s minimum of 1.
  • Asking for more than was left was denied, without a scar: running low isn’t misbehavior.
  • Spending exactly the remainder was allowed, and then the agent was terminated with budget exhausted.

In practice

  • Charge model calls by tokens used, as in llm_call here, so a loop that talks too much runs out.
  • Charge payments by their amount, so the budget is the spending authority. The DeepSeek payments agent does this with dollars.
  • Compute the cost in your code, from the request, never from what the model says it costs.

Try this

Change the budget to 50 and predict which request terminates the agent before you run it.

Next: Scars and termination →