Actor Concurrency
This chapter opens Phase 3 of TAPL -- the stage where Auto reveals its full power as an AI Operating System. The actor model is fundamental to Auto. It is not an add-on library or an optional feature. It is baked into the language from the ground up.
In this chapter, we will learn how to write concurrent programs using Auto's built-in actor system: spawning tasks, sending messages, receiving with on, managing isolated state, and handling errors across actor boundaries.
Concurrency in Auto
Writing programs that do multiple things at once is one of the hardest problems in software development. Traditional languages use threads -- lightweight copies of the running process that share memory. Threads are powerful but dangerous: two threads accessing the same data at the same time can cause data races, deadlocks, and subtle bugs that appear only under heavy load.
Auto takes a fundamentally different approach: the Actor model. In the actor model, concurrency is expressed through independent actors that communicate exclusively by sending messages to each other. No actor can directly access another actor's state. This eliminates entire classes of concurrency bugs:
- No data races -- actors do not share memory
- No deadlocks -- there are no locks to deadlock on
- No stale references -- communication is via message passing, not shared pointers
This model maps naturally to how an AI Operating System works. In Auto, every task, every service, every background job is an actor. They run independently, communicate through messages, and fail without bringing down the whole system.
Auto's ownership system (which we learned about in Chapter 12) still plays a crucial role here: it ensures that messages sent between actors transfer ownership cleanly, preventing the sender from accidentally using data that has already been sent away.
Note: Auto also supports traditional thread-based concurrency for interoperability with Rust libraries. This chapter focuses on the Actor model, which is the idiomatic Auto approach.
Spawning Tasks
The most basic unit of concurrency in Auto is a task -- a function that runs independently. You create a task with the spawn keyword, which starts the function running in the background.
This prints:
Greeting: Hello from main!
Greeting: Auto is awesome!Let us break down what is happening:
fn greeter()-- Defines a function that will run as an actor. It enters an infinite loop (for true) waiting for messages.on msg String-- This is Auto's message receive syntax. It waits for a message of typeStringand binds it to the variablemsg. When a message arrives, the body of theonblock runs.spawn greeter-- Starts thegreeterfunction running as an independent actor. The main program continues without waiting.send greeter "Hello from main!"-- Sends a string message to thegreeteractor. Thesendkeyword is fire-and-forget: it delivers the message and returns immediately.
The contrast with other languages is stark. In Rust, you need to create a channel with mpsc::channel(), pass the receiver into the spawned thread, and call .unwrap() on every send. In Python, you need threading.Thread and queue.Queue. In C, there is no standard message-passing primitive at all. In Auto, it is two lines: spawn and send.
Sending Messages
In the previous example, the greeter actor only understood String messages. Actors can receive any type of message. A common pattern is to use an enum to define the set of commands an actor accepts.
The key ideas here:
enum Command-- Defines the protocol for what messages this actor accepts. Enums are perfect for this because they exhaustively list every possible command, and the compiler ensures you handle all of them.on cmd Command-- Receives aCommandmessage. The body uses theispattern matching syntax (from [Chapter 7][ch7]) to dispatch on each variant.send caller count-- The special identifiercallerrefers to whoever sent the current message. This is how actors send results back.send counter Command.Increment-- Messages are fully typed. The compiler ensures you can only sendCommandvalues to thecounteractor.
In Rust, this requires an enum definition, a match expression, channel creation, and thread spawning. In Python, you need a separate Enum class and explicit if/elif chains. Auto collapses all of this into the on expression with pattern matching.
Receiving with on
The on keyword is how actors receive messages. It blocks until a message arrives, then binds the message to a variable and runs the handler body. The handler can use any pattern matching syntax we have already learned.
This prints:
Fetched: https://auto.devThe Request type is a custom type (from [Chapter 6][ch6]) with two fields: url and reply_to. The fetcher actor receives Request messages and processes them. In main, we construct a Request with type literal syntax and send it to the fetcher.
The on req Request syntax does two things at once: it waits for a message of type Request, and it binds the received value to the variable req. Inside the handler, you access the fields with dot notation: req.url, req.reply_to.
This pattern -- defining a message type and receiving it with on -- is the foundation of all actor communication in Auto.
Actor State
One of the most powerful aspects of the actor model is that each actor can maintain its own private state. Because no other actor can access this state, there is no need for locks, mutexes, or any synchronization primitive. The actor processes one message at a time, so state changes are always sequential.
This prints:
counter: 1
counter: 2
Unknown command: helloThe stateful_actor function declares a var state before the message loop. This variable persists across messages. When the actor receives "inc", it increments state.count. When it receives an unknown command, it prints an error. The state is completely isolated -- only this actor can see or modify it.
In Rust, this pattern requires Arc<Mutex<State>> to share state across the thread boundary. In Python, you rely on the GIL or explicit locks. In Auto, the isolation is guaranteed by the language itself. No locks, no Arc, no Mutex -- just a var that lives inside the actor function.
This is what we mean when we say the actor model is "baked into the language." Auto does not need a separate library for concurrency because every function can become an actor with spawn, and every var inside that function is automatically actor-local.
main as Primary Actor
In Auto, main is itself an actor. It is the primary actor -- the entry point of the program and the default coordinator. This means main can also receive messages using on, just like any other actor.
This prints:
Total so far: 20This example shows a complete request-response cycle:
mainspawns aworkerand sends it the number10.workerreceives the number inon n int, computesn * 2, and sends the result back tomainusingsend main.mainreceives the result inon result intand updatestotal.
The send main syntax in the worker function refers to the primary actor. This is how workers report back to the coordinator. In Rust, this requires creating two channels (one for each direction) and passing the sender into the spawned thread. In Auto, it is a single expression.
This pattern -- main as the orchestrator, spawning workers and collecting results -- is one of the most common concurrency patterns in Auto. It is the foundation of fan-out, map-reduce, and pipeline architectures.
Multiple Actors
Real programs rarely have just one background task. The actor model truly shines when you have many actors working in parallel, each handling a different piece of the problem.
This prints:
Worker 1 computing 42
Worker 2 computing 100
Worker 3 computing 7Key observations:
fn worker(id int)-- The worker function takes a parameter. When youspawn worker(1), Auto creates a new actor withidset to1.spawn worker(1),spawn worker(2),spawn worker(3)-- Three independent actors, each with its ownidand its own message queue.send worker(1) Job.Compute(42)-- Messages are addressed to specific actor instances.worker(1)andworker(2)are different actors, even though they run the same function.is job { Compute(n) => ..., Done => ... }-- Theisexpression (pattern matching from [Chapter 7][ch7]) works insideonhandlers to destructure enum variants.Compute(n)extracts the inner value.
This is the fan-out pattern: distributing work across multiple actors. Each worker runs independently, processes its own messages, and maintains its own state. If one worker crashes, the others continue unaffected.
In Rust, this requires three separate channel pairs. In Python, three separate Queue objects. In Auto, each spawn automatically creates the actor's mailbox -- you never manage channels yourself.
Error Handling Across Actors
When actors communicate, errors need to cross actor boundaries. The actor model provides a clean way to do this: send error information as a message, just like any other data. The !T error handling from [Chapter 9][ch9] works naturally with actor message types.
This prints:
Processed: hello world
Error: Empty dataThe processor actor validates its input. If the string is non-empty, it sends a WorkResult.Success back to main. If the string is empty, it sends a WorkResult.Error. The main actor receives the result and uses is pattern matching to handle both cases.
This pattern -- wrapping results in a success/error enum and sending them as messages -- is the actor-model equivalent of the !T (Result) type from [Chapter 9][ch9]. Errors are just data. They flow through the same message system as everything else. There are no exceptions, no panic/unwind, no try/catch. If an actor encounters a problem, it sends an error message. The caller decides what to do with it.
Summary
This chapter introduced the Actor model -- Auto's built-in approach to concurrency. Here is what we learned:
| Concept | Syntax | Purpose |
|---|---|---|
| Spawn an actor | spawn name | Start a function as an independent task |
| Send a message | send actor value | Deliver data to an actor (fire-and-forget) |
| Receive a message | on var Type { } | Block until a message arrives |
| Reply to sender | send caller value | Send a result back to whoever sent the message |
| Send to main | send main value | Reply to the primary actor |
| Parameterized spawn | spawn fn(args) | Create multiple actor instances from one function |
| Address instances | send fn(id) value | Target a specific actor instance |
Key takeaways:
Actors are functions -- Any function can become an actor with
spawn. The actor runs the function body in a loop, processing messages one at a time.Communication is via messages -- Actors never share memory. They communicate exclusively through
sendandon.State is isolated -- Variables declared inside an actor function are private to that actor. No locks are needed because only one message is processed at a time.
mainis an actor -- The primary actor can receive messages withon, making it a natural coordinator for worker actors.Errors are messages -- Error propagation across actors uses the same message system as normal data. No exceptions needed.
The model eliminates bug classes -- No data races (no shared memory), no deadlocks (no locks), no stale references (ownership transfer).
The actor model is why Auto can serve as an AI Operating System. Every background task, every service, every AI agent is an actor. They communicate through messages, fail independently, and scale naturally across cores. In the next chapter, we will build on this foundation to explore more advanced concurrency patterns.