Skip to content
Kavachकवच

Agents

Also known as: idempotency key idempotent call key retry repeat duplicate twice exactly once at least once crash restart stripe approval request answer approve in code chat bot slack web page review queue waiting ApprovalRequest ApprovalError

● Intermediate4 minBefore this: Durable AgentsSpec 14.16

std.agents gives a capability's implementation the key of the call it is running, so a service can drop a repeated call, and lets a program answer agents' approval requests.

FunctionGives
agents.call_key() -> String?Inside a capability's implementation, called from a durable agent's handler (8.5): the key of the running call, the same in the run after a crash, such as RefundBot-7-2 (the agent, the event's number, the call's place in it). none anywhere else
agents.waiting() -> [ApprovalRequest]The approval requests (8.3) that wait for an answer in the project's agents folder (8.5), oldest first. Needs Approvals
agents.answer(id, approved: Bool, by: String, reason: String? = none) ! ApprovalErrorAnswers request id, as kavach approve and kavach deny do (10.5). by names the person who answers; it goes to the audit log. Needs Approvals

A call that a crash cut off runs again after the restart, so it may run twice. A service that is told the key can drop the repeat, as Stripe and other payment APIs do with an Idempotency-Key header:

use std.agents

type Bank: Payments {
    fn refund(self, order_id: Int) -> Receipt ! PaymentError {
        // The bank keeps the keys it has seen and drops a second refund.
        const key = agents.call_key() ?? "order-{order_id}"
        try self.api.refund(order_id, key)
    }
}
an implementation, from spec 14.16: Kavach code

Like reading the clock, call_key needs no capability. A whole run, crash and all, is on Durable Agents.

Answering approvals in code

waiting and answer let a program take a person's answer from a web page, a chat or an email, and pass it on, as kavach approve does. Here the program asks for a refund in one task, then answers the request for asha:

use std.agents
use std.time

type PayError = Declined | Denied(reason: String)

capability Payments {
    fn refund(order: Int, amount: Int) ! PayError
}

type Bank: Payments {
    fn refund(self, order: Int, amount: Int) ! PayError {
        print("refunded {amount} on order {order}")
    }
}

agent Refunder {
    can Payments.refund

    approve Payments.refund when amount > 100 by "finance" within 2.days

    on refund(order: Int, amount: Int) ! PayError {
        try Payments.refund(order, amount)
    }
}

fn review(person: String) ! ApprovalError uses Approvals, Console {
    for r in agents.waiting() {
        print("{r.id}: {r.call}, by {r.by} when {r.why}")
        try agents.answer(r.id, approved: true, by: person)
    }
}

fn main() ! Error uses Approvals, Clock, Console {
    const bot = Refunder.start(Payments: Bank())
    const waits = spawn {
        bot.refund(1, 900) catch e => print("refused: {e}")
    }
    time.sleep(1.seconds)
    try review("asha")
    try waits.wait()
    agents.answer("Refunder-1-1", approved: true, by: "asha") catch e => print("again: {e}")
}
src/main.kav: Kavach code
terminal
kavach run src/main.kav
Refunder-1-1: Payments.refund(order: 1, amount: 900), by finance when amount > 100
refunded 900 on order 1
again: NotWaiting(id: "Refunder-1-1")
approval needed by finance: Payments.refund(order: 1, amount: 900) (request Refunder-1-1): kavach approve Refunder-1-1 --as <name>, or kavach deny
  • agents.waiting() gives the requests that wait in the project's agents folder, oldest first.
  • The approval needed line goes to standard error, so it is shown last here.
  • by names the person who answers, and goes to the audit log. Who may answer as finance is the program's to check, with its own sign-in.
  • Answering again fails with NotWaiting: the request was answered. A request never made, or out of time, fails the same way. Failed(id, reason) is an answer that can't be written.
  • The program that answers is usually another project, such as a chat bot. Its kavach.toml points at the worker's folder, with folder = "../agents" under [agents].
  • Under kavach test and in a dry run, agents keep nothing, so waiting() is empty and answer fails with NotWaiting.

Each request is an ApprovalRequest, with these fields, all String:

FieldHolds
idThe request's number, RefundBot-17-2 (8.3)
agent_nameThe agent that asks, RefundBot
callThe call waiting, with secrets hidden: Payments.refund(order_id: 17, amount: 6000, card: <secret>)
byThe role asked, finance
whyThe rule that asked: amount > 5_000
asked, untilWhen it was asked and when it runs out, in UTC: 2026-11-06T09:30:00Z

Both functions need the capability Approvals. An agent may never list it in its can, so an agent can't approve its own requests:

type PayError = Declined | Denied(reason: String)

capability Payments {
    fn refund(order: Int, amount: Int) ! PayError
}

type Bank: Payments {
    fn refund(self, order: Int, amount: Int) ! PayError {
        print("refunded {amount} on order {order}")
    }
}

agent Refunder {
    can Payments.refund, Approvals

    approve Payments.refund when amount > 100 by "finance" within 2.days

    on refund(order: Int, amount: Int) ! PayError {
        try Payments.refund(order, amount)
    }
}

The compiler refused the program:

kavach run approvals-in-can.kav

error: an agent can't answer approvals: `Approvals` can't be in `can` (8.3)
approvals-in-can.kav: Kavach code, then what running it prints

A function that lists its capabilities must list Approvals too:

use std.agents

fn count() -> Int uses Console {
    print("counting")
    agents.waiting().len()
}

The compiler refused the program:

kavach run approvals-uses.kav

error: `count` uses Approvals (through `waiting`), but its `uses` clause doesn't list it: add `Approvals` to `uses` (spec 8.1)
approvals-uses.kav: Kavach code, then what running it prints