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.
| Function | Gives |
|---|---|
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) ! ApprovalError | Answers 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)
}
}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}")
}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 neededline goes to standard error, so it is shown last here. bynames the person who answers, and goes to the audit log. Who may answer asfinanceis 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.tomlpoints at the worker's folder, withfolder = "../agents"under[agents]. - Under
kavach testand in a dry run, agents keep nothing, sowaiting()is empty andanswerfails withNotWaiting.
Each request is an ApprovalRequest, with these fields, all String:
| Field | Holds |
|---|---|
id | The request's number, RefundBot-17-2 (8.3) |
agent_name | The agent that asks, RefundBot |
call | The call waiting, with secrets hidden: Payments.refund(order_id: 17, amount: 6000, card: <secret>) |
by | The role asked, finance |
why | The rule that asked: amount > 5_000 |
asked, until | When 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)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)