Effects and Escalation
Declare a request, handle it in-program, then remove the handler and watch the run park as a question a human answers.
Your bot can compute, but it cannot yet ask for anything — and a bot is mostly asking: ask a model for a reply, ask an operator for approval, ask an API for data. In Katari every ask is a request, and this chapter shows both things a request can become: a function call, when a handler is in scope; an escalation — an open question a human answers — when none is.
Declare a request
A request is an effect declaration: a capability's name and signature, with no implementation attached.
@"Ask the human operator a question; the run waits until it is answered."
request ask(question: string) -> stringAn agent that performs ask does not know — and cannot know — who answers. That is
decided by whoever calls it, which is the whole trick of this chapter and the engine of
the final bot.
A handler makes it a call
Replace src/bot.ktr with:
@"Ask the human operator a question; the run waits until it is answered."
request ask(question: string) -> string
@"Greet someone by name."
agent greet(name: string) -> string {
f"Hello, ${name}!"
}
@"The call with a handler in scope: `ask` is answered in-program, nothing escalates."
agent scripted() -> string {
use handler {
request ask(question: string) { next "Ada" }
}
greet(name = ask(question = "Who should I greet?"))
}use handler { ... } installs an implementation of ask for the rest of the block.
Inside the clause, next "Ada" answers the request: whoever performed ask resumes with
"Ada". With a handler in scope, a request is an ordinary function call:
katari apply
katari run bot.scripted"Hello, Ada!"No handler: the run parks
Now the same call with no handler. Add main to the file — the complete
src/bot.ktr is:
@"Ask the human operator a question; the run waits until it is answered."
request ask(question: string) -> string
@"Greet someone by name."
agent greet(name: string) -> string {
f"Hello, ${name}!"
}
@"Greet whoever the operator names. Nothing here handles `ask`, so the question escalates."
agent main() -> string with ask {
greet(name = ask(question = "Who should I greet?"))
}
@"The same call with a handler in scope: `ask` is answered in-program, nothing escalates."
agent scripted() -> string {
use handler {
request ask(question: string) { next "Ada" }
}
greet(name = ask(question = "Who should I greet?"))
}main performs ask and no handler is anywhere in scope, so the request escalates:
it leaves the run entirely and becomes an open question addressed to you, the operator.
katari apply
katari run bot.mainThe run starts, reaches ask, and parks. katari run shows the question and prompts you
for the answer right in the terminal — type a name and the result comes back as if
nothing unusual happened.
The more honest version of that flow is the detached one, because nothing about an escalation requires you to be watching:
katari run bot.main --detach
katari ls escalations # the open question, with its id and schema
katari answer <id> '"Grace"' # the answer is JSON, checked against the request's schema
katari status <run-id> # done — result "Hello, Grace!"While the question is open, the run is not burning a connection or a process — it is parked, durably. Restart the runtime; the question is still there. Answer it in a minute or in a week — from the CLI or from the console's escalations inbox — and the run resumes exactly where it stopped. That is the runtime's core promise, and the final bot leans on it hard: see Durable Execution.
The signature tells the truth
Look at main's signature: agent main() -> string with ask. The with clause is the
agent's effect row — the requests it may perform. The compiler checks it: declare a
row that omits a performed request and katari check rejects the agent, naming exactly
what leaked (Left effect performs a request not present in the right effect: bot.ask).
Leave the row off entirely and the compiler infers it. Spell it out on the agents you
mean others to call — it is the half of the contract that schemas cannot carry: not just
what goes in and out, but what this agent may ask for along the way.
Handlers can hold state
One more step, and it is the exact shape the finished Discord bot is built on. A handler
can carry a var — state that lives as long as the handler does and threads through
every request it answers. The complete src/bot.ktr:
@"Ask the human operator a question; the run waits until it is answered."
request ask(question: string) -> string
@"Greet someone by name."
agent greet(name: string) -> string {
f"Hello, ${name}!"
}
@"Greet whoever the operator names. Nothing here handles `ask`, so the question escalates."
agent main() -> string with ask {
greet(name = ask(question = "Who should I greet?"))
}
@"The same call with a handler in scope: `ask` is answered in-program, nothing escalates."
agent scripted() -> string {
use handler {
request ask(question: string) { next "Ada" }
}
greet(name = ask(question = "Who should I greet?"))
}
@"A stateful handler: a `var` rides the handler, and every `ask` advances it."
agent roll_call() -> string {
use handler (var count: integer = 1) {
request ask(question: string) {
next f"guest #${string.to_string(value = count)}" with { count = count + 1 }
}
}
let first = greet(name = ask(question = "Who is next?"))
let second = greet(name = ask(question = "Who is next?"))
f"${first} ${second}"
}next value with { count = count + 1 } does two things at once: answers the current
request with value, and carries the updated state forward to the next one.
katari apply
katari run bot.roll_call"Hello, guest #1! Hello, guest #2!"Squint at roll_call and you can see the bot this tutorial ends with: replace "every
ask advances a counter" with "every incoming Discord message extends a conversation
history", and you have chapter 5's message loop. The state is just a value in your
program — no session store, no cache to invalidate.
Where you are
Requests, handlers, escalation, and handler state — the full effect vocabulary. Next
chapter, the payoff for learning it: a model provider is nothing but a package-shipped
handler you use, and wiring one in takes a single line —
Talking to a Model.
Deeper treatments: Effects and Handlers and Escalation.