Skip to main content

Asyncio-native Actor framework for Python agent systems

Project description

everything-is-an-actor

Asyncio-native Actor framework for Python agent systems — supervision trees, event streaming, and pluggable mailbox.

Inspired by Erlang/Akka. Built for AI agent orchestration.

Documentation · Getting Started · Agent Layer · API Reference

Install

pip install everything-is-an-actor

# With Redis mailbox support
pip install everything-is-an-actor[redis]

Agent Quick Start

import asyncio
from actor_for_agents.agents import AgentSystem, AgentActor, Task

class SummaryAgent(AgentActor[str, str]):
    async def execute(self, input: str) -> str:
        await self.emit_progress("summarizing...")
        return f"Summary: {input[:80]}..."

async def main():
    system = AgentSystem("app")

    # Stream every event from the entire agent tree
    async for event in system.run(SummaryAgent, "Long document..."):
        print(event.type, event.agent_path, event.data)

asyncio.run(main())

Agent Layer

AgentActor — implement execute(), not on_receive()

class ResearchAgent(AgentActor[str, list]):
    async def on_started(self):
        self.client = aiohttp.ClientSession()

    async def execute(self, query: str) -> list:
        await self.emit_progress("searching...")
        results = await self.client.get(f"/search?q={query}")
        return results

    async def on_stopped(self):
        await self.client.close()

Streaming output via yield

Each yield inside execute() emits a task_chunk event immediately — ideal for LLM tokens or file chunks.

class LLMAgent(AgentActor[str, list]):
    async def execute(self, prompt: str):
        async for token in openai.stream(prompt):
            yield token   # → task_chunk event

Orchestration

Six concurrency primitives for composing child agents — all ephemeral actors are cleaned up automatically.

class OrchestratorAgent(AgentActor[str, Any]):
    async def execute(self, query: str):

        # ask — single child, one result
        r = await self.context.ask(SearchAgent, Task(input=query))
        result = r.output

        # sequence — fan-out, results in order, fail-fast
        a, b = await self.context.sequence([
            (SearchAgent, Task(input=query)),
            (FactCheckAgent, Task(input=query)),
        ])
        combined = {"search": a.output, "facts": b.output}

        # traverse — map a list through one agent
        summaries = await self.context.traverse(["doc1", "doc2", "doc3"], SummaryAgent)
        texts = [r.output for r in summaries]

        # race — first wins, cancel the rest
        fastest = await self.context.race([
            (FastAgent, Task(input=query)),
            (SlowAgent, Task(input=query)),
        ])
        winner = fastest.output

        # zip — two tasks, typed pair
        search, facts = await self.context.zip(
            (SearchAgent, Task(input=query)),
            (FactCheckAgent, Task(input=query)),
        )

        # stream — forward child chunks upstream
        async for item in self.context.stream(LLMAgent, Task(input=query)):
            match item:
                case StreamEvent(event=e) if e.type == "task_chunk":
                    yield e.data
                case StreamResult():
                    pass

Event streaming

system = AgentSystem("app")

# Stream all events from a fresh agent run
async for event in system.run(OrchestratorAgent, user_query):
    if event.type == "task_progress":
        print(event.data)

# Or stream from an existing ref
ref = await system.spawn(SummaryAgent, "summarizer")
async for item in ref.ask_stream(Task(input="document...")):
    match item:
        case StreamEvent(event=e):
            print(e.type, e.data)
        case StreamResult(result=r):
            print(r.output)

Span linking

Every TaskEvent carries parent_task_id and parent_agent_path — reconstruct the full call tree from a flat event stream (OpenTelemetry-style spans).


Core Actor API

Class Description
Actor Base class. Override on_receive, on_started, on_stopped, on_restart
ActorRef Lightweight handle. tell(msg) / ask(msg) / ask_stream(task)
ActorSystem Container. spawn(cls, name) / shutdown()
AgentSystem Drop-in replacement with event streaming. run(cls, input) / abort(run_id)
Mailbox Interface. MemoryMailbox (default) or RedisMailbox
Middleware Interceptor chain for all lifecycle events
OneForOneStrategy Restart only the failing child
AllForOneStrategy Restart all siblings when one fails

Supervision

class ParentActor(Actor):
    def supervisor_strategy(self):
        return OneForOneStrategy(max_restarts=3, within_seconds=60)

    async def on_started(self):
        self.child = await self.context.spawn(WorkerActor, "worker")

Directives: resume | restart | stop | escalate

Middleware

class LogMiddleware(Middleware):
    async def on_receive(self, ctx, message, next_fn):
        print(f"[{ctx.recipient.path}] ← {message}")
        result = await next_fn(ctx, message)
        print(f"[{ctx.recipient.path}] → {result}")
        return result

ref = await system.spawn(MyActor, "worker", middlewares=[LogMiddleware()])

Redis Mailbox

from actor_for_agents.plugins.redis import RedisMailbox

ref = await system.spawn(
    MyActor, "worker",
    mailbox=RedisMailbox(pool, "actor:inbox:worker", maxlen=1000),
)

Benchmarks

Apple M-series, Python 3.12, asyncio:

Actor core

Metric Value
tell throughput 945K msg/s
ask throughput 29K msg/s
ask latency p50 32 µs
ask latency p99 46 µs
100-hop actor chain 2.0 ms, 20 µs/hop
1000 actors × 100 msgs 879K msg/s, 0 loss
Middleware overhead +0% (~1 middleware)
Spawn 5000 actors 27 ms

Agent layer

Metric Value
AgentActor ask throughput 27K tasks/s
AgentActor ask latency p50 36 µs
AgentActor ask latency p99 50 µs
sequence(50) fan-out 32K child tasks/s
traverse(100) map 28K items/s
ask_stream chunk throughput 227K chunks/s
AgentSystem.run() latency p50 0.2 ms (spawn+run+stream)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

everything_is_an_actor-0.1.0.tar.gz (61.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

everything_is_an_actor-0.1.0-py3-none-any.whl (33.8 kB view details)

Uploaded Python 3

File details

Details for the file everything_is_an_actor-0.1.0.tar.gz.

File metadata

  • Download URL: everything_is_an_actor-0.1.0.tar.gz
  • Upload date:
  • Size: 61.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for everything_is_an_actor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2277f97dd87a64ba00c7d4acccf118d1aca7a200ff1fd9fc1176c80da9f154af
MD5 433a951080c6bc1be78270cc37655593
BLAKE2b-256 43b3e89ffe95046accd7126ec605aa6938d09188ef3305f87ef9a64d31e30426

See more details on using hashes here.

File details

Details for the file everything_is_an_actor-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for everything_is_an_actor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dce065d8e4fee04ae103458a0059f4bbab36fbf352fcb7fc90451319cdc8df2a
MD5 22bf5980eb9c11cea6f357ad0da12306
BLAKE2b-256 d356dd9f962d8d3d0fb297d09d1d53f177f6f704bde18eb43a114608a7d1c983

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page