Messaging
Agents can receive typed messages from other agents using the actor model pattern.
The receives Clause
An agent declares what type of messages it accepts using the receives clause:
enum WorkerMsg {
Task,
Ping,
Shutdown,
}
agent Worker receives WorkerMsg {
id: Int
on start {
// This agent can now receive WorkerMsg messages
yield(0);
}
}
Agents without a receives clause are pure summon/await agents and cannot receive messages.
The receive() Expression
Inside an agent with a receives clause, use receive() to wait for a message:
agent Worker receives WorkerMsg {
id: Int
on start {
let msg: WorkerMsg = receive();
match msg {
Task => print("Got a task"),
Ping => print("Pinged"),
Shutdown => print("Shutting down"),
}
yield(0);
}
}
receive() blocks until a message arrives in the agent’s mailbox.
The send() Function
Send a message to a running agent using its handle. send is fallible (the agent might have terminated), so use try:
agent Main {
on start {
let w = summon Worker { id: 1 };
try send(w, Task);
try send(w, Shutdown);
try await w;
yield(0);
}
on error(e) {
yield(1);
}
}
run Main;
send queues the message and returns immediately.
Long-Running Agents with loop
Combine receive() with loop for agents that process multiple messages:
agent Worker receives WorkerMsg {
id: Int
on start {
loop {
let msg: WorkerMsg = receive();
match msg {
Task => {
let result = try divine("Process a task");
print("Worker {self.id}: {result}");
}
Ping => {
print("Worker {self.id} is alive");
}
Shutdown => {
break;
}
}
}
yield(0);
}
on error(e) {
print("Worker {self.id} failed: " ++ e);
yield(1);
}
}
Complete Example: Worker Pool
enum WorkerMsg {
Task,
Shutdown,
}
agent Worker receives WorkerMsg {
id: Int
on start {
loop {
let msg: WorkerMsg = receive();
match msg {
Task => {
let result = try divine("Summarise something interesting");
print("Worker {self.id}: {result}");
}
Shutdown => {
break;
}
}
}
yield(0);
}
on error(e) {
print("Worker {self.id} failed");
yield(1);
}
}
agent Coordinator {
on start {
let w1 = summon Worker { id: 1 };
let w2 = summon Worker { id: 2 };
// Distribute tasks
try send(w1, Task);
try send(w2, Task);
try send(w1, Task);
try send(w2, Task);
// Shut down workers
try send(w1, Shutdown);
try send(w2, Shutdown);
// Wait for completion
try await w1;
try await w2;
yield(0);
}
on error(e) {
print("Coordination failed");
yield(1);
}
}
run Coordinator;
Type Safety
The compiler ensures type safety:
agent Worker receives WorkerMsg {
on start {
let msg: WorkerMsg = receive();
yield(0);
}
}
agent Main {
on start {
let w = summon Worker {};
try send(w, Task); // OK - Task is a WorkerMsg variant
try send(w, "hello"); // Error: expected WorkerMsg, got String
yield(0);
}
on error(e) {
yield(1);
}
}
Messaging vs Awaiting
await | send / receive | |
|---|---|---|
| Direction | Get final result from agent | Ongoing communication |
| Blocking | Yes, waits for agent to complete | send returns immediately, receive blocks until message arrives |
| Use case | One-shot tasks | Long-running workers, event loops |
Mailbox Semantics
- Each agent has a bounded mailbox (128 messages by default)
- When the mailbox is full,
sendblocks until space opens (backpressure) - Messages from a single sender arrive in order
- Messages from multiple senders are interleaved (no global ordering)
Current Limitations
- No
receive_timeoutin the language yet (available in runtime) - No broadcast channels (one-to-many messaging)
- Error handling for closed channels needs its own RFC