Skip to main content
Autonomy is designed on principles from the actor model, a powerful pattern for building concurrent and distributed systems. Before we dive into agents, it’s helpful to understand this foundation of actors. Think of actors as independent members of a team. Each has their own workspace (state), and they communicate only by passing notes (messages) to each other. No one shares their workspace, this prevents contention and makes applications naturally parallelizable and scalable.
Actors are lightweight, stateful objects that communicate using messages. Each actor has:
  • A unique address that other actors use to send it messages.
  • A mailbox where incoming messages queue up and wait to be processed.
  • Internal state that only this actor can access or modify.
  • Behavior, logic that defines how this actor reacts to each message it receives.
When an actor receives a message, it can:
  1. Send messages to other actors (asynchronously, without waiting for responses).
  2. Create new actors and delegate work to them.
  3. Update its internal state which may affect how future messages are handled.

No shared state Each actor’s state is completely private. Other actors cannot directly access or modify it. This eliminates entire classes of concurrency bugs like race conditions and deadlocks. Actors can only influence each other by sending messages. Message Processing While many actors run in parallel across the system, each individual actor processes only one message at a time. Messages wait in the actor’s mailbox and are handled sequentially. This means you never need to worry about two threads modifying an actor’s state simultaneously; it simply can’t happen. Asynchronous message passing When Actor A sends a message to Actor B, it doesn’t have to wait for B to process it or respond. The message is queued in B’s mailbox, and A can continue with its work immediately. This non-blocking communication allows actors to work concurrently without waiting on each other. Location Transparency Sending or receiving a message from another actor looks the same whether that other actor is running on the same machine or across the network. This makes it natural to distribute work and scale horizontally.
In Autonomy:
  • Simple actors are called workers.
  • Agents are intelligent autonomous actors that use large language models.
  • Both follow the actor model.
When an actor is idle and there are no messages in its mailbox, it consumes no CPU. This design makes it easy to run thousands of concurrent stateful actors that make the most optimal use of available CPU cores. Agents, for example, spend a majority of their lifespan waiting for calls to language models or tools to finish. While one agent is waiting, the actor runtime automatically gives the CPU core to a different actor that has a new message to process. This enables highly efficient and horizontally scalable applications.

Workers

Workers are Autonomy’s implementation of actors. Autonomy’s actor runtime is implemented in Rust and exposed to Python code using the Node class. All nodes in a zone can create encrypted, mutually authenticated, secure communication channels with other nodes.
Here’s a simple worker that echoes messages back:
What’s happening:
  1. Define a worker class with handle_message().
  2. When Node.start(main) is called, it turns the main function itself into a worker that can send and receive messages like other workers. This is why main is able to communicate with greeter.
  3. Start the Greeter worker with a unique name (greeter).
  4. Send a message from main to the greeter worker.
  5. The greeter worker processes the message and replies.
  6. The main worker receives the reply.
Worker Lifecycle Workers can be started and stopped dynamically:

Messages

Message Types Messages must be strings. This is because the messaging layer transmits messages across the network, and strings are simple, universal, and work everywhere.
Structured Data with JSON For structured data, use JSON serialization:
Message Flow Messages flow asynchronously between workers:
  1. Sender sends message to worker by name
  2. Message is queued if worker is busy
  3. Worker processes messages one at a time
  4. Worker can optionally reply
  5. Sender receives reply (if waiting)

State

Stateful Workers Workers can maintain state across messages. This is safe because workers process one message at a time:
Use cases for stateful workers:
  • Session management
  • Connection pooling
  • Rate limiting
  • Caching
  • Accumulating results
State Isolation No shared state between workers - each worker is completely isolated:
Workers are single-threaded - state is safe within a worker:
Initialization Use __init__() for one-time setup:

Communication

Message Patterns

Fire and Forget Send a message without waiting for a reply:
Request-Reply Send a message and wait for a response:
Important: Always specify a timeout (in seconds) to prevent hanging forever if the worker doesn’t respond. Timeout Handling Handle timeouts gracefully:

Error Handling

Timeout Errors Symptoms: RuntimeError with “timeout” in the message Possible causes:
  • Worker doing heavy computation (increase timeout)
  • Worker crashed (check logs)
  • Network issues (for distributed workers)
  • Worker stuck on previous message
Recovery Strategies Implement fallback strategies for resilience:

Distribution

Architecture

Nodes and Pods One of the most powerful features: workers can run on different machines and communicate seamlessly.
Clones Configuration Use clones to create multiple machines running the same container:
autonomy.yaml

Implementation

Node Discovery Discover and connect to remote nodes:
Remote Worker Management Start and manage workers on remote nodes:
images/main/main.py
What’s happening:
  1. Zone.nodes() discovers all nodes matching a filter
  2. Start workers on each remote node
  3. Send messages to workers by name—they run on different machines!
  4. Messages are automatically routed across the network

Complete Multi-Node Example

Here’s a complete working example that demonstrates communication between nodes running on different pods. Architecture:
Configuration:
autonomy.yaml
Runner Node:
images/runner/main.py
Client Node:
images/client/main.py
Dockerfiles:
images/runner/Dockerfile
images/client/Dockerfile
Deploy and Test:
Expected Output: Client logs show:
Runner logs show:
Key Points:
  • Use Zone.nodes(node, filter="runner") to discover nodes by pod name prefix
  • Implement a retry loop to wait for nodes to become available
  • Messages are automatically routed across pods and machines
  • Workers on remote nodes are accessed the same way as local workers
  • The filter parameter matches against pod names (e.g., “runner” matches “runner-pod”)

Patterns

Echo Worker - Simple message echo:
Stateful Counter - Maintains count across messages:
JSON Message Handler - Structured message processing:
In-Memory Store - Key-value storage pattern:
Distributed Processing - Parallel processing across multiple machines:
Fire-and-Forget Logger - No-reply logging pattern:
Error Handling with Cleanup - Always clean up resources:
Worker Monitoring - Get a real-time view of all workers:
Resource Management - Manage limited resources across workers:

Operations

Monitoring

Listing Workers - Monitor active workers across your system:
Tracking Messages - Add logging to track message flow:
System Health - Monitor system health and performance:

Troubleshooting

Worker Not Responding Symptoms: send_and_receive() times out Solutions:
  1. Check worker was started successfully: await node.list_workers()
  2. Increase timeout for complex operations
  3. Check logs for errors in worker’s handle_message()
  4. Verify message format (must be a string)
Timeout Issues - Debug timeout problems:
State Problems - Debug state issues:
Serialization Errors Symptom: TypeError when sending messages Cause: Trying to send non-string messages Solution: