Skip to content
Kavachकवच

Running an Agent

Also known as: start instantiate wire up dependency injection provide implementation event handler call send fail panic in handler crash keep serving

● Intermediate5 minBefore this: AgentsSpec 8.8

Start an agent with Name.start(…), giving one implementation for each capability it can use. Each on handler is then a method you call, and that call can always fail.

type EmailError = NotSent | Denied(reason: String)

capability Email {
    fn send(to: String, text: String) ! EmailError
}

type Outbox: Email {
    fn send(self, to: String, text: String) ! EmailError {
        print("to {to}: {text}")
    }
}

agent Concierge {
    can Email.send

    on welcome(name: String, email: String) ! EmailError {
        try Email.send(email, "Welcome, {name}!")
    }

    on shipped(order: Int, email: String) ! EmailError {
        try Email.send(email, "Order {order} is on its way.")
    }
}

fn main() ! Error {
    const bot = Concierge.start(Email: Outbox())
    try bot.welcome("Asha", "asha@example.com")
    try bot.shipped(1042, "asha@example.com")
}

The program ran and printed:

kavach run agent-events.kav

to asha@example.com: Welcome, Asha!to asha@example.com: Order 1042 is on its way.
agent-events.kav: Kavach code, then what running it prints
  • Concierge.start(Email: Outbox()) names each implementation by its capability.
  • Built-in capabilities, like Ai or Console, need none.
  • While a handler runs, its capability calls, and those of every function it calls, go to these implementations, checked against the agent's limits.

A missing implementation, an extra one, or a value of the wrong type is a compile error:

type EmailError = NotSent | Denied(reason: String)

capability Email {
    fn send(to: String) ! EmailError
}

agent Greeter {
    can Email.send

    on welcome(to: String) ! EmailError {
        try Email.send(to)
    }
}

fn main() {
    const bot = Greeter.start()
}

The compiler refused the program:

kavach run run-missing.kav

error: `Greeter.start` needs an implementation of `Email`: `Email: …`
run-missing.kav: Kavach code, then what running it prints

Every send can fail

Calling a handler sends the agent an event. The send can fail with an Error, whatever the handler declares: the handler's own error, a refused call, or a panic in the handler. So every send needs try, catch or match, even for a handler with no !:

agent Counter {
    on pick(items: [Int], at: Int) -> Int {
        items[at]
    }
}

fn main() {
    const bot = Counter.start()
    const n: Int = bot.pick([1, 2], 1)
    print(n)
}

The compiler refused the program:

kavach run send-plain.kav

error: `pick` can fail: handle the error with `try`, `catch` or `match`
send-plain.kav: Kavach code, then what running it prints

A handler that panics stops there, and the agent goes on serving other events:

use std.fs

agent Picker {
    settings AgentSettings(audit: Audit())

    on pick(items: [Int], at: Int) -> Int {
        items[at]
    }
}

fn main() ! Error {
    const bot = Picker.start()
    const first = bot.pick([1, 2], 5) catch e => {
        print("{e}")
        0
    }
    print("first: {first}")
    const next = try bot.pick([1, 2], 1)
    print("next: {next}")
    for line in try fs.lines("agents/Picker/audit.log") {
        print(line.split("Z ").last() ?? line)
    }
}
src/main.kav: Kavach code
terminal
kavach run src/main.kav
panicked: index 5 is out of range: the list has 2 items at src/main.kav:7
first: 0
next: 2
Picker pick panicked: index 5 is out of range: the list has 2 items at src/main.kav:7
panic: index 5 is out of range: the list has 2 items
  at src/main.kav:7
  • The send fails with panicked: <message> at <file>:<line>, as .wait() does for a task that panicked.
  • The panic is printed to standard error when it happens, and written to the audit log.
  • In a durable agent the event stays unfinished in its journal, so it runs again at the next start.
  • The code that sent the event goes on with all its own values as they were, however many sends fail.

For now an agent is started from your own code, as here. How events reach it in production (web requests, queues, schedules) is still being designed.