Browse documentation

Agents and delegation

Agents are Katari's functions — typed, schema-carrying, first-class — and every call is a delegation the runtime tracks.

An agent is Katari's unit of execution: what a function is in other languages. It takes a labelled record in, returns one value out, and declares the requests it may perform. The compiler derives a JSON schema for both sides, so anything can call an agent — another agent, katari run, an HTTP client, or an AI model that discovered it as a tool.

Labelled records in, one value out

@"The area of one rectangular room."
agent area(width: number, height: number) -> number {
  width * height
}
 
@"Total floor area. Every call names its arguments; every call is a delegation."
agent floor_area(rooms: array[{ width: number, height: number }]) -> number {
  for (let room in rooms, var total: number = 0.0) {
    next with { total = total + area(width = room.width, height = room.height) }
  } then (_rooms) { total }
}

There are no positional arguments: a call names every parameter, and the parameter list is the input record's shape. The @"..." annotation is not a comment — it is the agent's description, carried into the derived schema that an AI or the admin console reads (see Types and schemas).

A parameter can carry a default with ?=; a call that omits it gets the default:

@"`suffix` is optional: a call may omit it and the default fills in."
agent decorate(body: string, suffix: string ?= "!") -> string {
  f"${body}${suffix}"
}
 
@"One call supplies the suffix, one leaves it to the default."
agent both_forms() -> string {
  f"${decorate(body = "hi", suffix = "?")} ${decorate(body = "hi")}"
}

Every call is a delegation

When floor_area calls area, the runtime spawns a child instance for the callee and suspends the caller until the child's result arrives. That tree of instances is the delegation tree, and it is the thing you see on a run's page in the admin console: which agent called which, which one is blocked, and on what. A direct, compiled call needs no runtime validation — the type system already guarantees the argument fits. Dynamic calls (an AI picking a tool, an inbound webhook) are validated against the callee's input schema at the boundary instead.

Delegation is real, not inlined: a recursive agent parks one durable frame per level. For an unbounded loop, use forever, which repeats in place — see Durable execution.

Agents are values

@"Apply a transform twice. An agent is a value, so it can be a parameter."
agent twice(transform: agent (value: number) -> number, start: number) -> number {
  transform(value = transform(value = start))
}
 
@"Double a number."
agent double(value: number) -> number { value * 2.0 }
 
@"`double` is passed as a value, not called: no parentheses."
agent twelve() -> number {
  twice(transform = double, start = 3.0)
}

An agent's type is written agent (label: T, ...) -> R with E — parameters, result, and the effect row all part of the type. Because agents are ordinary values you can put them in arrays, return them, hand them to time.watch as a delivery target, or expose them to an AI as tools (reflection reads their metadata back at runtime).

Partial application

@"Scale a value by a factor."
agent scale(factor: number, value: number) -> number { factor * value }
 
@"`_` holes a parameter: the call fixes `factor` now and yields a residual
`agent (value: number) -> number`; each later call supplies only the hole."
agent doubles(values: array[number]) -> array[number] {
  let double = scale(factor = 2.0, value = _)
  for (let value in values) { next double(value = value) }
}

A call with _ in some argument positions does not run the agent — it fixes the supplied arguments and yields a residual agent whose parameters are exactly the holes. A defaulted parameter that is neither supplied nor holed stays defaulted: the callee's default fills it on each residual call.

Private agents

@"A private agent: its handle belongs to the private world."
private agent signing_secret() -> string {
  "k-2f9a-secret"
}
 
@"Another private agent's body is a private world, so the secret is ordinary here."
private agent signature(payload: string) -> string {
  f"${payload}.${signing_secret()}"
}

private agent marks the agent's handle private. Its result is a private value: inside a private world — the body of another private agent — it is ordinary, but in a public world anything derived from the call stays private and cannot be laundered back to public (the compiler rejects the leak with K3001). The runtime treats the handle the same way: a private agent cannot be started as a run's entry point. Use it for helpers that mint or combine secrets, and keep the public surface to agents that absorb those secrets into a submission sink — see the of private semantics in Types and schemas.

Where to go next