Actor Agents
Actor-based agents run inside a runtime and communicate via typed messages and protocol events. Use them when you need:
- Event streaming for UI updates (turn started/completed, tool calls, streaming chunks)
- Pub/Sub between agents or external actors
- Long-running orchestrations rather than a single direct call
Core Building Blocks
RuntimeandEnvironment: Manage event routing and lifecycle of actor systems.Topic<M>: Typed pub/sub channels for messages of typeM.ActorAgentandActorAgentHandle: Wrap your agent with an actor reference so it can receiveTasks via pub/sub/direct messaging.Event: Streamed protocol events (task start/complete, tool calls, streaming chunks) published by agents during execution.
Typical Wiring Pattern
- Create a runtime and register it in an
Environment. - Spawn your agent as
ActorAgentand subscribe it to one or moreTopic<Task>. - Take the environment's event receiver and forward events to your UI/log sink.
- Publish
Taskmessages to the relevant topic to trigger work. - Call
Environment::run, thenwaitorshutdownto manage runtime lifecycle.
Environment Lifecycle
After wiring agents and publishing work, start the registered runtimes:
run()— spawns a background task that runs all registered runtimes. ReturnsErr(EnvironmentError::AlreadyRunning)if a run task is already in progress. Does not block until work completes.wait().await— joins the background task started byrun(). Clears the stored handle when the task finishes, so later calls return immediately withOk(Ok(())). Use this in short-lived programs once messages/tasks have been published.shutdown().await— requests shutdown on all runtimes and joins the run handle. ReturnsResult<(), EnvironmentError>so runtime or join failures are visible. Use for graceful exit (for example onCtrl+C).run_background().await— starts runtimes without storing a join handle on the environment. Useful when you manage lifecycle elsewhere. Cannot be combined withrun()on the sameEnvironmentuntilshutdown().awaitis called.is_running()— returns whether a runtime launch is active (run()join handle not finished, orrun_background()untilshutdown()).
If a managed run task finishes without calling wait() or shutdown(), a subsequent run() joins the finished task first and returns any runtime or join error before spawning a new run.
Calling wait() inside tokio::select! is safe: if another branch wins, the join handle stays on the environment so shutdown() can still drain the run task.
SingleThreadedRuntime is single-cycle: after its event loop exits, register a new runtime instance to run again on the same Environment.
Migration from earlier releases
Environment::run() no longer returns a JoinHandle. Use the stored lifecycle instead:
// Before
let handle = environment.run();
handle.await??;
// After
environment.run()?;
let run_result = environment.wait().await?;
run_result?;
For long-running programs that previously dropped the join handle, call environment.shutdown().await? on exit.
Common patterns:
// Fire-and-wait: publish work, start runtimes, block until they finish
environment.run()?;
let run_result = environment.wait().await?;
if let Err(runtime_err) = run_result {
eprintln!("runtime error: {runtime_err}");
}
// Interactive or long-running: start runtimes, handle Ctrl+C gracefully
environment.run()?;
// `wait()` is select-safe — if Ctrl+C wins, the join handle is restored
// so shutdown can still drain runtimes. See design_patterns::environment_lifecycle.
tokio::select! {
result = environment.wait() => {
if let Ok(Err(e)) = result {
eprintln!("runtime error: {e}");
} else if let Err(e) = result {
eprintln!("run task join error: {e}");
}
}
_ = tokio::signal::ctrl_c() => {
if let Err(err) = environment.shutdown().await {
eprintln!("shutdown failed: {err}");
}
}
}
Call run() after registering runtimes and before or after publishing tasks depending on your program. Actor agents process messages only while runtimes are running.
Minimal Example
use autoagents::core::{
agent::prebuilt::executor::ReActAgent,
agent::{AgentBuilder, ActorAgent},
agent::task::Task,
environment::Environment,
runtime::{SingleThreadedRuntime, TypedRuntime},
actor::Topic,
};
use std::sync::Arc;
// 1) Create runtime and environment
let runtime = SingleThreadedRuntime::new(None);
let mut env = Environment::new(None);
env.register_runtime(runtime.clone()).await?;
// 2) Build actor agent and subscribe to a topic
let chat_topic = Topic::<Task>::new("chat");
let handle = AgentBuilder::<_, ActorAgent>::new(ReActAgent::new(MyAgent {}))
.llm(my_llm)
.runtime(runtime.clone())
.subscribe(chat_topic.clone())
.build()
.await?;
// 3) Consume events (UI updates, tool calls, streaming)
let receiver = env.take_event_receiver(None).await?;
tokio::spawn(async move { /* forward events to UI */ });
// 4) Publish tasks
runtime.publish(&chat_topic, Task::new("Hello!"))
.await?;
// 5) Start runtimes and wait for the background run task to finish
env.run()?;
let _ = env.wait().await?;
Event Handling Patterns
- Pub/Sub: Publish
TasktoTopic<Task>; all subscribed agents receive the message. - Direct send: Use
TypedRuntime::send_messageto deliver a message directly to a specific actor. - Protocol events:
Event::TaskStarted,Event::TurnStarted,Event::ToolCallRequested,Event::StreamChunk, etc. are emitted by agents while running.
Protocol Events Reference
These map to autoagents::core::protocol::Event variants emitted by actor agents and the runtime:
TaskStarted { sub_id, actor_id, actor_name, task_description }- Emitted when an agent begins processing a task.
TaskComplete { sub_id, actor_id, actor_name, result }- Final result for a task.
resultis a pretty JSON string; parse into your agent output type when needed.
- Final result for a task.
TaskError { sub_id, actor_id, error }- Any executor/provider error surfaced during execution.
TurnStarted { sub_id, actor_id, turn_number, max_turns }- Multi-turn executors (e.g., ReAct) mark each turn start.
TurnCompleted { sub_id, actor_id, turn_number, final_turn }- Marks turn completion;
final_turnis true when the loop ends.
- Marks turn completion;
ToolCallRequested { sub_id, actor_id, id, tool_name, arguments }- The LLM requested a tool call with JSON arguments (as string).
ToolCallCompleted { sub_id, actor_id, id, tool_name, result }- Tool finished successfully;
resultis JSON.
- Tool finished successfully;
ToolCallFailed { sub_id, actor_id, id, tool_name, error }- Tool failed; error string is included.
StreamChunk { sub_id, chunk }- Streaming delta content;
chunkmatches provider’s streaming shape.
- Streaming delta content;
StreamToolCall { sub_id, tool_call }- Streaming tool call delta (when provider emits incremental tool-call info).
StreamComplete { sub_id }- Streaming finished for the current task.
Internally, the runtime also routes PublishMessage for typed pub/sub (Topic<M>), but that variant is skipped in serde and used only inside the runtime.
Actor streaming APIs
Actor agents expose two streaming entry points with different event contracts:
| API | Terminal events (TaskComplete / TaskError) | Hooks | Typical use |
|---|---|---|---|
run_stream() | No — failures are Err items on the returned stream only | Skipped | Incremental output; poll the stream directly |
run_stream_to_completion() | Yes — full task lifecycle on the event channel | Run | Runtime pub/sub dispatch, event subscribers, select! on TaskError |
Footgun: If you subscribe to runtime or agent events and call run_stream() directly, terminal failures will not emit TaskError on the event channel even though mid-run events (StreamChunk, tool calls) may still arrive. Listeners waiting only for TaskComplete / TaskError can hang. Use run_stream_to_completion() instead, or handle errors on the returned output stream.
Non-streaming run() always emits TaskComplete or TaskError. This matches run_stream_to_completion(), not run_stream().
Pub/sub dispatch (AgentActor::handle) calls run() / run_stream_to_completion() and does not propagate task failures to ractor. A failed task emits TaskError on the event channel but the actor keeps running so it can process subsequent messages.
For the direct-agent variant of this contract, see Agents — Direct agent event contract.
When To Use Actor Agents vs Direct Agents
- Use Direct agents for one-shot calls (no runtime, minimal wiring).
- Use Actor agents when you need: real-time events, multiple agents, pub/sub routing, or running agents as durable tasks.