Requirements

Python
3.10+
License
MIT
Current version
0.5.0 — release history. Academy is pre-1.0, so minor releases may contain breaking changes.

1 Install

Academy is published on PyPI as academy-py.

Install command
pip install academy-py

2 Write a first program

An agent is a class with @action-decorated methods. A manager launches it against an exchange and returns a handle you call actions on. The whole example runs in one process and needs no external services.

A first Academy program
import asyncio
from concurrent.futures import ThreadPoolExecutor

from academy.agent import Agent, action
from academy.exchange import LocalExchangeFactory
from academy.logging.recommended import recommended_logging
from academy.manager import Manager


class ExampleAgent(Agent):
    @action
    async def square(self, value: float) -> float:
        return value * value


async def main() -> None:
    async with await Manager.from_exchange_factory(
        factory=LocalExchangeFactory(),
        executors=ThreadPoolExecutor(),
        log_config=recommended_logging(),
    ) as manager:
        agent_handle = await manager.launch(ExampleAgent())
        result = await agent_handle.square(2)
        assert result == 4
        await agent_handle.shutdown()


if __name__ == '__main__':
    asyncio.run(main())

3 Add a control loop

@loop-decorated methods run continuously alongside the agent's actions until shutdown is signaled. An agent with a loop keeps working when nothing is calling it.

Shown on its own for clarity — combine the @action and @loop methods in a single class rather than redefining it.

A control loop
import asyncio

from academy.agent import Agent, action, loop


class CountingAgent(Agent):
    def __init__(self) -> None:
        super().__init__()
        self.count = 0

    @action
    async def get_count(self) -> int:
        return self.count

    @loop
    async def counter(self, shutdown: asyncio.Event) -> None:
        while not shutdown.is_set():
            self.count += 1
            await asyncio.sleep(1)

4 Let agents call each other

Handles to other agents are passed in as constructor arguments, so a coordinator can delegate work without knowing where those agents run.

from __future__ import annotations matters here: without it the Handle[Lowerer] annotation is evaluated at class-definition time, before Lowerer exists, on Python 3.10–3.13.

Agent-to-agent calls
from __future__ import annotations

from academy.agent import Agent, action
from academy.handle import Handle


class Lowerer(Agent):
    @action
    async def lower(self, text: str) -> str:
        return text.lower()


class Reverser(Agent):
    @action
    async def reverse(self, text: str) -> str:
        return text[::-1]


class Coordinator(Agent):
    def __init__(
        self,
        lowerer: Handle[Lowerer],
        reverser: Handle[Reverser],
    ) -> None:
        super().__init__()
        self.lowerer = lowerer
        self.reverser = reverser

    @action
    async def process(self, text: str) -> str:
        text = await self.lowerer.lower(text)
        text = await self.reverser.reverse(text)
        return text

Launch them together, passing the dependencies through args. This block continues the same file as the one above — it relies on the three classes defined there.

Note the two launch styles: step 2 passed an instance (ExampleAgent()) while this passes the class and lets the manager construct it with args. Both are supported; pass the class when the agent needs constructor arguments.

Launching a multi-agent system
import asyncio
from concurrent.futures import ThreadPoolExecutor

from academy.exchange import LocalExchangeFactory
from academy.manager import Manager


async def main() -> None:
    async with await Manager.from_exchange_factory(
        factory=LocalExchangeFactory(),
        executors=ThreadPoolExecutor(),
    ) as manager:
        lowerer = await manager.launch(Lowerer)
        reverser = await manager.launch(Reverser)
        coordinator = await manager.launch(
            Coordinator,
            args=(lowerer, reverser),
        )

        result = await coordinator.process('DEADBEEF')
        assert result == 'feebdaed'


if __name__ == '__main__':
    asyncio.run(main())

5 Distribute it

The agent code above does not change. Swap the exchange for one that crosses process and node boundaries, and the executor for one that places agents where the compute is.

Distributing across nodes
from concurrent.futures import ProcessPoolExecutor

from academy.exchange import RedisExchangeFactory
from academy.manager import Manager


async def main() -> None:
    async with await Manager.from_exchange_factory(
        factory=RedisExchangeFactory('<REDIS HOST>', port=6379),
        executors=ProcessPoolExecutor(max_workers=4),
    ) as manager:
        ...

What each exchange needs from you

Swapping the exchange is a one-line change in code, but the exchanges differ in what infrastructure you have to provide. Names below are the actual factory classes exported from academy.exchange.

Academy exchange implementations and their prerequisites
Exchange What you provide Use it when
LocalExchangeFactory Nothing Development, tests, a single process
RedisExchangeFactory A Redis server you run, reachable from every node Agents spread across processes or nodes
HybridExchangeFactory A Redis server, and direct TCP between peers. Redis holds mailbox state and queues for offline agents; live traffic goes peer-to-peer. Lower-latency messaging between agents that can reach each other
HttpExchangeFactory An HTTP exchange server Agents that can reach a shared endpoint but not each other
GlobusExchangeFactory A Globus account and an authentication flow Crossing institutional and identity boundaries
ProxyStoreExchangeFactory A base exchange factory and a ProxyStore Store; install with pip install academy-py[proxystore] Wrapping any of the above to pass large objects by reference

Prerequisites here were read from the factory signatures and docstrings in Academy 0.5.0. The exchange documentation is the authority if the two ever disagree.

Where to go next