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.
Concierge.start(Email: Outbox())names each implementation by its capability.- Built-in capabilities, like
AiorConsole, 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: …`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`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)
}
}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.