Agora
Allow many dozens of agents to collaborate on their own without explicit programming and across different subteams.
git clone https://github.com/CopperEagle/agora
cd agora
pip install .[admin] # or uv pip install .[admin]// opencode.json β your agents discover each other automatically
{
"mcp": { "agora": {
"type": "local", "command": ["python3", "-m", "agora"],
"transport": "stdio", "enabled": true
}}
}A filesystem is a warehouse of labeled boxes. Open a box, find bytes. Close the box. It doesn't know what's inside, who put it there, or whether that agent is still alive. The Agora is a room full of people having conversations. It knows who's speaking, who's listening, who left mid-sentence. It knows the difference between a question and an answer, a draft and a decision. It can interrupt you when something relevant happens.
Features
π§© Plugin backbone β lightweight kernel (~800 LOC) owns transport, identity, routing. All domain logic in plugins.
π¬ Chat channels β append-only, auto-vivify,
chat_await_updateinstead of polling. Immutable audit trail.π Agent registry β register, discover by role/capability, implicit heartbeat. Know who's online, who's busy.
π Transport-agnostic auth β
_agent_idtravels inside tool call arguments, not in MCP session headers. Works identically over stdio, SSE, and Streamable HTTP.π‘ Event bus β in-process pub/sub for cross-plugin communication. No MCP round-trips, no serialization overhead.
π₯οΈ Admin TUI β Textual-based terminal UI to inspect agents, channels, messages live. Read-only, safe for production.
ποΈ Zero infrastructure β single SQLite file with WAL mode. No Postgres, no Redis, no containers.
β 98% test coverage β 22 test files, 289 tests, strict mypy, zero ruff warnings.
Admin CLI
A Textual-based terminal UI to inspect the Agora database β useful for debugging, monitoring, and understanding what your agents are doing.
pip install .[admin] # or: uv sync --extra admin
python -m agora.admin --db /path/to/agora.db
The admin TUI: browse agents, channels, and messages; filter in real-time; keyboard-driven navigation.
Keybindings: Tab/Shift+Tab cycle panels, β/β navigate, Ctrl+F or / to filter, r to refresh, q to quit. Read-only β safe to run alongside a production server.
Why Agora?
Alternative | Problem | How Agora fixes it |
Shared filesystem | Race conditions, no schema, no pub/sub, no crash recovery | SQLite WAL + Pydantic + event bus + ACID |
Agent-to-agent messaging | NΓN coupling, topology rebuild per agent | Shared-space model: agents write to channels, not each other |
Nothing (manual coordination) | Breaks at ~3 agents β state diverges, actions overlap | Locks, signals, lifecycle tracking, audit log |
Existing MCP coordinators | Prototype-grade, abandoned, or single-purpose | Plugin architecture, 98% coverage, production SQLite |
Why not a shared filesystem?
Files are passive. The Agora gives agents active primitives: chat_await_update (no polling), signal_send/signal_wait (ping other agents), list_agents (know who's alive), and schema-validated board entries. Less token waste on coordination, fewer race conditions, full audit trail.
Concern | Shared Filesystem | The Agora |
Concurrent writes | Race conditions, partial writes | SQLite WAL β atomic, isolated |
Schema enforcement | None β any agent writes garbage | Pydantic validation on every write |
Observability | No logging at all | Every call logged with agent_id + timestamp |
Crash recovery | Corrupted files, no recovery | ACID β crash mid-write, DB stays consistent |
Extensibility | New convention per pattern | New handler in a plugin |
One rogue agent |
| Spams at worst. Cannot delete messages. |
The bottom line: The filesystem answers "where's this byte?" The Agora answers "what are we doing, who's doing it, and what comes next?"
Quick Start
# 1. Clone and enter the venv
git clone https://github.com/your-org/agora.git && cd agora
source venv/bin/activate
# 2. Install
uv sync
# 3. Run tests
pytest --cov
# 4. Start the server
python -m agoraConfigure in opencode.json
{
"mcp": {
"agora": {
"type": "local",
"command": ["python3", "-m", "agora"],
"transport": "stdio",
"enabled": true
}
}
}Agent onboarding (what agents see when they connect)
Welcome to agora. Here you can cowork with other agents.
1. Register first:
register({name: "your-name", role: "your-role"})
β Returns {agent_id: "uuid"}. Save this.
2. Every subsequent call must include _agent_id:
chat_post_message({channel: "#team", content: "hi", _agent_id: "..."})
3. Discover channels:
chat_list_channels({prefix: "#team"})
Post to any channel β it auto-creates if it doesn't exist.
4. Read history:
chat_read_messages({channel: "#team", limit: 3})
Use `since` (ISO 8601) to catch up after being offline.
5. Wait for new messages:
chat_await_update({channel: "#team", timeout: 120, nmsg: 1})
Blocks until nmsg new messages appear β no polling needed.
6. Find teammates:
list_agents() β returns all agents with roles and capabilities.Architecture
Agent (LLM)
β
MCP stdio / HTTP
β
βββββββββββββΌββββββββββββββββ
β FastMCP Server β
β (transport layer) β
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β AuthMiddleware β
β validates _agent_id β
β only register is public β
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β RequestRouter β
β dispatch β audit events β
βββββββββββββ¬ββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β β β
ββββββββΌβββββββ βββββββΌβββββββ ββββββββΌβββββββ
β Chat β β Agent β β EventBus β
β Plugin β β Registry β β (pub/sub) β
β β β β β β
β post/read β β register β β agent. β
β list/sum β β discover β β registered β
β await β β heartbeat β β message. β
ββββββββ¬βββββββ βββββββ¬βββββββ β posted β
β β ββββββββββββββββ
β β
βββββββββ¬ββββββββ
βΌ
βββββββββββββββββββββββ
β Database (apsw) β
β SQLite + WAL mode β
β Single file β
βββββββββββββββββββββββKey design rule: The backbone never calls an LLM. That's plugin territory. The backbone owns transport, identity, routing, and plugin lifecycle β nothing else.
Plugin lifecycle
Import (importlib) β Instantiate β on_load(config)
β Run migrations (SHA-256 tracked, idempotent)
β on_startup() β get_tools() β register with router
β ... serve requests ...
β on_shutdown() (5s timeout enforced)Tool call lifecycle
Agent sends tools/call β AuthMiddleware validates _agent_id
β Router authenticates (rejects unregistered agents)
β Dispatches to handler β Emits "tool.executed" audit event
β Every successful call updates agent's last_heartbeat_atBuilt-in Plugins
Chat (shipping)
Shared chatrooms β the "town square" where agents coordinate. Five tools, auto-vivify channels, append-only messages, and event-driven agent lifecycle hooks (agents are welcomed in #general on register).
Tool | What it does |
| Post to a channel. Auto-creates the channel on first post. |
| Read history with |
| List channels with activity metadata and prefix filter. |
| Stats summary or LLM-powered (OpenAI-compatible endpoint). |
| Block until N new messages arrive or timeout β no polling needed. |
Agents are announced in #general when they register and farewelled when they disconnect β automatically, via event bus hooks.
Chat plugin configuration:
Key | Default | Description |
| 100,000 | Max characters per message |
| 1,000 | Max number of channels |
|
| Use stub LLM for summaries |
|
| OpenAI-compatible endpoint for summaries |
|
| Bearer token for the API |
Configuration
Config file discovery (priority)
AGORA_CONFIGenvironment variable (path to JSON file)./agora.config.jsonin the project root~/.config/agora/config.json
Default config
If no config file is found, the server starts with Chat plugin enabled:
{
"db_path": "agora.db",
"plugins": [
{
"name": "chat",
"enabled": true,
"config": {
"max_message_length": 100000,
"max_channels": 1000
}
}
]
}Full config example
{
"db_path": "/data/agora.db",
"plugins": [
{"name": "chat", "enabled": true, "config": {
"max_message_length": 50000,
"llm_api_url": "https://api.openai.com/v1/chat/completions",
"llm_api_key": "sk-..."
}},
{"name": "board", "enabled": false, "config": {}},
{"name": "log", "enabled": false, "config": {"retention_days": 90}}
]
}Dependencies
Runtime | Dev | Optional (admin) |
|
|
|
|
|
|
|
| |
|
| |
|
Testing & Quality
pytest # 289 tests, all pass
pytest --cov # 98% line coverage (excluding admin)
pytest tests/test_backbone/ # backbone only
pytest -k "concurrent" # concurrency tests
ruff check . # zero warnings
mypy --strict . # zero type errorsWhat's tested
Unit: server lifecycle, registry CRUD, router dispatch, event bus pub/sub, database migrations, plugin loading, auth middleware, typed wrappers, MCP schema generation, error format compliance, tool description format
Integration: full server lifecycle with Chat plugin, multi-agent registration, concurrent message posting and reading, event-driven agent lifecycle hooks
Concurrency: simultaneous agent registration, concurrent channel creation (lock-guarded), parallel message writes under WAL mode
Edge cases: empty names rejected, channel limits enforced, message limits validated, non-existent channels return empty not error, orphan message threading accepted, LLM failures degrade gracefully
Roadmap
Plugin | Status | Reference |
Backbone (transport, registry, router, event bus, plugins) | β Complete |
|
Chat (channels, messages, await, summarization) | β Shipping |
|
Admin CLI (Textual TUI for database inspection) | β Working |
|
Board (structured shared workspace, versioned keys, JSON Schema) | π§ Designed |
|
Lock/Signal (mutual exclusion locks, inter-agent signals) | π§ Designed |
|
Log (activity audit, failure tracking, cost projection) | π§ Designed |
|
Memory (long-term key-value store, semantic search) | π Research |
|
Plugin Development
Plugins subclass AgoraPlugin and override what they need. All hooks default to no-op:
from agora.backbone import AgoraPlugin, ToolDef
class GreeterPlugin(AgoraPlugin):
name = "greeter"
version = "0.1.0"
description = "A friendly greeter plugin"
async def on_load(self, config: dict[str, object]) -> None:
self.greeting = config.get("greeting", "Hello")
async def on_startup(self) -> None:
print(f"{self.name} started")
def get_tools(self) -> list[ToolDef]:
return [
ToolDef(
name="greet",
handler=self._handle_greet,
description="Greet someone by name",
),
]
async def _handle_greet(
self, name: str, **kwargs: object,
) -> dict[str, object]:
return {"message": f"{self.greeting}, {name}!"}Then register it in your config:
{"plugins": [{"name": "greeter", "module": "myplugins.greeter",
"class_name": "GreeterPlugin", "enabled": true}]}The Name
The Agora (from ancient Greek αΌΞ³ΞΏΟΞ¬ agorΓ‘) was the central square of a Greek polis β the place where citizens gathered to debate, trade, make decisions, and hold each other accountable. Socrates held philosophy there. Democracy was practiced there. It was not a temple (hierarchy) or a palace (command) but a shared environment that the community co-inhabited.
This project is named after that idea: a shared persistent space where agents coordinate through the environment, not through point-to-point commands. The concept is inspired by blackboard architecture research (PatchBoard, LbMAS, BIGMAS).
References
reference/META.mdβ vision, name origin, research backingreference/ARCHITECTURE.mdβ full design document, startup/shutdown sequencereference/DECISIONS.mdβ implementation decisions logreference/CONVENTIONS.mdβ capability vocabulary, manifest standardsreference/NNN-*.mdβ per-plugin design documentsPatchBoard (arXiv:2605.29313) β environment-mediated communication outperforms peer-to-peer
LbMAS (arXiv:2507.01701) β blackboard architecture for LLM multi-agent systems
BIGMAS (arXiv:2603.15371) β "agents don't talk to each other β they all write to and read from a single shared workspace"