Documentation
Full API reference, every exchange implementation, and the logging configuration options.
From installing Academy to running agents across distributed resources, in five steps.
Academy is published on PyPI as academy-py.
pip install academy-py
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.
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())
@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.
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)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.
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.
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())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.
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:
...
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.
| 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.
Full API reference, every exchange implementation, and the logging configuration options.
Hands-on material from the IPDPS and ISC tutorials, with conference-specific branches.
Tracing a single agent interaction end to end across processes using
JSON logs and jq.
Questions in Slack, bugs and feature requests in the issue tracker, and common problems in the FAQ.
If Academy supports your work, the BibTeX entry and the papers behind it are on the publications page.