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_callhere, 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.