Skip to content
All projects
Capture d’écran d’Agora : la liste des conversations et le panneau de modification de l’agent Signature
  • 2026
  • Agents IA
  • Messenger
  • Self-hosted
  • TypeScript

Agora is a self-hosted team messenger for Hermes Agent, Nous Research’s open-source agent runtime: each agent is a Hermes profile, and the team talks to it in direct messages or in groups. I built it in TypeScript, from the interface to deployment, and the code is public under the AGPL-3.0 license.

Agora is a messaging app for working as a team with AI agents, installed on your own server. It relies on Hermes Agent, Nous Research’s open-source runtime: each agent in Agora is a Hermes profile, with its own model, personality, tools, skills and MCP connectors. I built it so that an organization can use Hermes without going through the command line, and to handle what’s missing as soon as several people share the same agents: access, shared memory, administration and updates.

The problem

Hermes is organized around profiles: one folder per agent, with its configuration, memory and sessions, plus an HTTP API, an internal dashboard and a CLI. To give the same agents to a whole team, a layer on top is missing: deciding who has access to which agent, how an agent knows who it’s talking to, where what the team has learned lives, and how to update Hermes without breaking the application that depends on it.

A concrete example: Hermes’s built-in memory about the user (USER.md) is unique per profile. If several people talk to the same agent, everything ends up in the same place. So on every turn, Agora sends the identity of the person speaking and asks the agent to prefix whatever it notes about someone with that person’s name.

What Agora does

It’s a messenger: direct and group conversations, between humans and with agents, streamed responses with tool activity shown live, attachments, “/” commands to call a skill or a connector. When an agent wants to run a sensitive action, Hermes pauses the turn and the approval request appears in the conversation. In a group, an agent replies when it’s mentioned or replied to, and can mention another agent to hand over; these handoffs are capped at twelve per exchange so that two agents don’t keep prompting each other forever.

An admin creates the agents and sets their model, their personality (the profile’s SOUL.md file), their tools, skills and MCP servers. A new bot can also configure itself through conversation: it asks its questions, then writes its own SOUL.md. Access is by invitation, with per-person permissions on each agent and mandatory two-factor authentication since v0.4.0. On first launch, a wizard sets up the organization, the admin account, the AI provider (key tested live) and the first agent; creating the admin account is protected by a code shown in the server logs.

Architecture

Agora is a TypeScript monorepo managed with pnpm and Turborepo:

apps/api
the server: Bun, Hono, Drizzle ORM on Postgres, Better Auth.
apps/web
the client: React 19, TanStack Router and Query, Tailwind v4, shadcn/ui.
apps/updater
the update service for the app and for Hermes.
packages/core
shared code: event types, translations.
apps/mobile
an iOS app in React Native with Expo, on the same core package.

To produce responses, the API goes through Hermes, in multi-profile mode. Each profile is reachable under /p/<profil> with its own key, derived via HMAC from a shared secret, which avoids storing one key per agent. The repository also contains three Hermes plugins in Python: agora_wiki for shared memory, agora_files for read-only access to attachments, agora_screen for watching the browser an agent is driving, live. The one exception to Hermes: the owner of a Claude or ChatGPT subscription signed in on the machine can have their agents answer through Claude Code or Codex, on the turns they start themselves.

From message to streamed response

Each message creates one turn per agent involved, placed in a queue specific to that conversation–agent pair: an agent never handles two turns at once in the same Hermes session, but different agents reply in parallel. Turns are stored in the database: when the API restarts, pending ones are relaunched and in-progress ones are flagged as interrupted rather than resent, because Hermes may already have run tools.

The API reads the SSE stream of the Hermes run (text, tools, approval requests) and republishes each event on an in-memory bus, to a single SSE stream per tab (GET /api/events). Messages are sent through regular POST requests: the design spec ruled out WebSocket. Agora is also the one that names Hermes sessions. Since Hermes caches each session’s system prompt, a personality change increments a revision that forces a new session, and /new or /compact move on to a new generation:

apps/api/src/company.ts
export const hermesSessionId = (conversationId: string, revision: number, groupAgentId?: string, generation = 0) => {
  const base = groupAgentId ? `agora-${conversationId}-${groupAgentId}` : `agora-${conversationId}`;
  return `${base}${revision > 0 ? `-r${revision}` : ""}${generation > 0 ? `-g${generation}` : ""}`;
};

A memory shared by all agents

Shared memory has two levels: a short organization memory (20,000 characters at most, edited by an admin, sent on every turn) and a markdown wiki shared by all profiles, modeled on Andrej Karpathy’s “LLM Wiki”, readable in the app or in Obsidian. The agora_wiki plugin hooks in as Hermes’s memory provider: it gives agents wiki_* tools, injects a hot.md hot-context file into every session and logs each turn in raw/, under a file lock since several profiles write in parallel. A curator on the API side then compiles this raw material into durable pages, checks that every claimed source is actually cited, and leaves the pages that fail three times to human review.

Technical choices and challenges

A good part of the work went into what agents can do on the server: anyone who writes to an agent drives its tools, including through hidden text in a web page it reads. And some Hermes tools (terminal, code execution, files, local browser, computer control) give control over the machine: one agent could rewrite another profile’s configuration. So every profile starts without them, and an admin can re-enable them agent by agent after a warning. For the same reason, external images in responses only load on click.

Agora also depends on fast-moving software: since Hermes v2026.9.14, the /v1/skills route is broken and Agora reads skills through the dashboard; the 9.x versions no longer enable multi-profile mode on their own, so Agora now declares it at startup. So I wrote a contract: a script that checks the routes, formats and SSE streams the API depends on, all the way to a real agent response.

On the client side, fast responses updated the thread on every token, and Streamdown’s code highlighter triggered a setState in the same commit: past fifty nested updates, React stopped with its error #185. The fix, in v0.8.1, shows the first token right away, then batches the rest into at most one update every 50 ms.

Deployment and updates

Agora is installed with Docker Compose on a Linux server: Caddy (automatic HTTPS and the web app), the API, Hermes, Postgres and the update service. Hermes only listens on 127.0.0.1, Postgres sits on an internal network, and only Caddy exposes ports. An agora script bundles the day-to-day operations, including encrypted off-site backups with restic.

cd agora/infra
cp .env.example .env   # DOMAIN et secrets (openssl rand -hex 32)
./agora build
./agora up             # HTTPS via Caddy
./agora setup-code     # code demandé pour créer le compte admin

Every push to main that passes CI becomes a versioned release based on conventional commits, with images published to ghcr.io. The updater checks every hour and applies updates at night, between 3 and 5 a.m. by default. For the app, patches and minor versions install on their own, major versions with one click: backup, switchover, migrations when the API starts, contract check, and otherwise a rollback with a database restore.

For Hermes, it’s more cautious. The updater runs the contract against production to get a baseline, makes a backup, then starts a “canary” Hermes on a copy of the data, with no scheduled tasks and no messaging tokens so it doesn’t trigger anything twice. Only a regression against the baseline blocks the update. If everything passes, production switches over and the contract is run again; otherwise, it goes back to the previous version and restores the data, and the rejected version is no longer retried automatically.

Where the project stands

Agora is at v0.10.0, under the AGPL-3.0 license. The public history starts with a commit that already contains most of the application; later versions hardened security (internal network for Postgres, Caddy logs for CrowdSec, SSH via Tailscale), fixed streaming and agents’ access to MCP connectors, then added charts in responses, multiple Claude Code accounts, a Codex engine and connector forms tailored to each field.

Some known limitations remain. The event bus lives in memory, so the API runs as a single instance; the code notes that running several would require Postgres LISTEN/NOTIFY. After a network outage, the client reloads its data, but the server doesn’t replay missed events.