Skip to content
Kavachकवच

Agent Use Cases

Also known as: examples use cases recipes shopping assistant deploy bot support triage customer service refund budget spending limit

● Intermediate8 minBefore this: Agents, ask<T>Spec 8.3

Four agents, each with a real risk and the line of code that removes it: a spending limit, a person who approves, a model that can only sort, a folder that can't be left.

Each program here runs as shown, and its output is checked on every build of this site. Copy one and change it: the compiler tells you when a change would let the agent do more than it says.

A shopping assistant with a budget

An assistant buys what the user asks for. A confused model, or a tricky product page, must never empty the account.

What holds it: A limit on each purchase, on the day's total, and on purchases per hour.

type StoreError = OutOfStock | Denied(reason: String)

capability Store {
    fn buy(item: String, price: Int) ! StoreError
}

type TestStore: Store {
    fn buy(self, item: String, price: Int) ! StoreError {
        print("bought {item} for {price}")
    }
}

agent Shopper {
    can Store.buy

    limit Store.buy {
        price <= 2_000
        sum(price) <= 3_000 per day
        count <= 3 per hour
    }

    on order(item: String, price: Int) ! StoreError {
        try Store.buy(item, price)
    }
}

fn main() {
    const bot = Shopper.start(Store: TestStore())
    for r in [(item: "rice", price: 900), (item: "laptop", price: 45_000), (item: "pan", price: 1_500), (item: "tea", price: 800)] {
        bot.order(r.item, r.price) catch e => print("not bought {r.item}: {e}")
    }
}

The program ran and printed:

kavach run uc-shopping.kav

bought rice for 900not bought laptop: Denied(reason: "limit broken: price <= 2_000")bought pan for 1500not bought tea: Denied(reason: "limit broken: sum(price) <= 3_000 per day")
uc-shopping.kav: Kavach code, then what running it prints

The laptop is over the per-item limit; the tea would take the day past 3,000. Both refusals are errors the program handles, and both are in the audit log.

A deploy bot that asks before production

Releasing to a test server should be quick; releasing to production needs a person on call to agree.

What holds it: approve Deploy.release when env == "production" by "oncall".

type DeployError = BuildFailed | Denied(reason: String)

capability Deploy {
    fn release(version: String, env: String) ! DeployError
}

type Pipeline: Deploy {
    fn release(self, version: String, env: String) ! DeployError {
        print("released {version} to {env}")
    }
}

agent Releaser {
    can Deploy.release

    approve Deploy.release when env == "production" by "oncall"

    on ship(version: String, env: String) ! DeployError {
        try Deploy.release(version, env)
    }
}

fn main() {
    const bot = Releaser.start(Deploy: Pipeline())
    bot.ship("2.4.0", "staging") catch e => print("not released: {e}")
    bot.ship("2.4.0", "production") catch e => print("not released: {e}")
}

The program ran and printed:

kavach run uc-deploy.kav

released 2.4.0 to stagingnot released: Denied(reason: "not approved by oncall (env == \"production\")")
uc-deploy.kav: Kavach code, then what running it prints

Staging goes out at once. Production waits for the on-call person; in this run no one answered, so nothing was released.

A support inbox sorted by AI

Each message goes to the right team, without the model being able to do anything but sort.

What holds it: ask<Triage> can only answer with one of the three teams, and the agent can only assign tickets, at most 200 an hour, with at most 1,000 model calls a day.

type Team = Billing | Engineering | HelpDesk

type Triage {
    /// the team that should answer
    team: Team
    /// one line for that team
    summary: String
}

type DeskError = Closed | Denied(reason: String)

capability Desk {
    fn assign(team: Team, summary: String) ! DeskError
}

type TestDesk: Desk {
    fn assign(self, team: Team, summary: String) ! DeskError {
        print("assigned to {team}")
    }
}

agent Support {
    can Ai, Desk.assign

    limit Desk.assign {
        count <= 200 per hour
    }

    limit Ai {
        count <= 1_000 per day
    }

    on message(text: String) ! Error {
        const t = try ask<Triage>("Which team should answer this message? {text}")
        try Desk.assign(t.team, t.summary)
    }
}

fn main() {
    const bot = Support.start(Desk: TestDesk())
    bot.message("I was charged twice this month") catch e => print("failed: {e}")
}

The program ran and printed:

kavach run uc-support.kav

[dry run] ask("Which team should answer this message? I was charged twice t") -> Triage(team: HelpDesk, summary: " fry ey")[dry run] Desk.assign(team: HelpDesk, summary: " fry ey")
uc-support.kav: Kavach code, then what running it prints

Run with --dry-run: the model and the help desk are never called, and the lines show what would have been. Try a new agent this way before it touches anything real.

A file agent that stays in its folder

An agent reads documents from an inbox folder. A file name it is given, by a person or a model, must not reach anything else.

What holds it: The implementation says uses Fs.read("./inbox"): it can only read, and only there.

use std.fs

type InboxError = Missing(name: String) | Denied(reason: String)

capability Inbox {
    fn read(name: String) -> String ! InboxError
}

type InboxFolder: Inbox {
    fn read(self, name: String) -> String ! InboxError uses Fs.read("./inbox") {
        fs.read_text("./inbox/{name}") catch e => match e {
            FsError.Denied(path) => fail InboxError.Denied(reason: "outside ./inbox: {path}"),
            _ => fail InboxError.Missing(name: name),
        }
    }
}

agent Summarizer {
    can Inbox.read, Console

    on summarize(name: String) ! InboxError {
        const text = try Inbox.read(name)
        print("{name}: {text.len()} characters")
    }
}

fn main() ! Error {
    try fs.create_dir("inbox")
    try fs.write_text("inbox/note.txt", "Call the bank on Monday.")
    try fs.write_text("passwords.txt", "do not share")
    const bot = Summarizer.start(Inbox: InboxFolder())
    bot.summarize("note.txt") catch e => print("failed: {e}")
    bot.summarize("../passwords.txt") catch e => print("failed: {e}")
}

The program ran and printed:

kavach run uc-files.kav

note.txt: 24 charactersfailed: Denied(reason: "outside ./inbox: ./inbox/../passwords.txt")
uc-files.kav: Kavach code, then what running it prints

../passwords.txt climbs out of the folder, so the read is refused while the program runs, and the agent reports it.

A refund agent, start to finish

The repository's refund bot puts it all together: ask decides, limits cap each refund and the day's total, finance approves large ones, every call is audited, and a crash half way never pays twice.

More ideas

  • An expense checker: ask reads each receipt into a typed record, and payouts above a limit go to a manager.
  • A newsletter sender: count <= 5_000 per day on Email.send, started in observe mode for its first week.
  • A database clean-up agent: it may archive rows, never delete them, because only Db.archive is in its can.

Every one of these keeps its promise for the same reason: the agent touches the world only through capabilities, and those are checked by the compiler and the runtime, not by the model. See What You Can Build for projects of every level.