atrium — Docs
docs/agents/messaging.md

Agent-to-agent messaging across panes and locations

Send framed messages between local and remote agents, discover them with agent list, suppress replies, and scope which panes each agent can reach.

Agents running in atrium panes can send messages to each other through a single CLI command: atrium agent message. This is the only sanctioned inter-agent channel. Humans can also send a message to any agent from the UI — the Activity sidebar has an inline Message field on every agent card (Cmd+Enter / Ctrl+Enter sends), and any card expands into an Omni Chat dock for a fuller back-and-forth. Both use the same framing protocol under the hood.

The command

"$ATRIUM_CLI_PATH" agent message <target> "message body"
  • <target> — the recipient's pane ID or short unambiguous prefix. For an agent on another box, use <id>@<location> as shown by atrium agent list --all-locations.
  • The message body is a single string; newlines are fine.
  • --no-frame skips the sender envelope and delivers a plain user-typed prompt. Use it when the message is a human talking, not an agent relaying — the recipient sees a normal user turn with no [Message from … via atrium] header.
  • --no-reply removes the reply instruction from the frame. Use it for a one-way update that should not start another conversational turn.

Framing

atrium does not just forward the raw text. It wraps the message in a frame so the recipient knows:

  • Who sent it.
  • Which adapter they are.
  • How to reply.
A received message arrives in the recipient's stdin as something like:
[Message from "Quinn (QA Engineer)" (claude-code) via atrium]

Please implement the auth middleware.

[Reply with the command: "$ATRIUM_CLI_PATH" agent message <sender-short-id> "your reply"]

The recipient agent sees this as a fresh user turn and responds normally. When it replies using the suggested command, the sender receives the reply as its own fresh user turn. Messages are fire-and-forget from the sender's perspective — do not poll pane read for a response.

Discovering agents

# Just the panes running an adapter (excludes plain terminals)
"$ATRIUM_CLI_PATH" agent list

# Output as JSON for scripting
"$ATRIUM_CLI_PATH" agent list --json

# Include registered SSH locations; remote IDs are qualified with @location
"$ATRIUM_CLI_PATH" agent list --all-locations

"$ATRIUM_CLI_PATH" agent message <short-id>@build-box \
    "Report your current status" --no-reply

Each entry has:

  • pane_id (UUID) and a short prefix.
  • adapter (claude-code, codex, grok, …).
  • workspace and room.
  • location when the pane is not local.
  • status (idle, working, waiting-for-input, …).

Access scope

Each pane has an MCP access scope that controls which other panes it can see when it runs atrium pane list / atrium agent list:

  • same-tab (default) — only panes in the same room.
  • same-workspace — all panes in the project.
  • all — every permitted pane across projects on the current location (the broadest local scope).
Change a pane's scope from its header menu or by setting mcpAccessScope on the pane. The scope is persisted in the pane snapshot.

Additionally, each pane has mcpVisible — if false, the pane is hidden from other panes' discovery calls entirely.

Use scopes to keep agents from messaging rooms they should not see (for example, a parent project's agents should not reach into a worktree's sandbox by default). --all-locations is a separate, explicit fleet discovery option; cross-location delivery also requires the target location to be reachable. atrium never silently redirects a failed remote message to a local pane.

When not to use messaging

  • Do not use pane write to poke a message into another agent's stdin directly. That bypasses framing, so the recipient cannot tell who is talking.
  • Do not use pane read as a reply mechanism. Polling the recipient's scrollback is brittle and loses framing.
If you need structured data exchange (for example, one agent handing a JSON payload to another), send the payload in the framed message body. Both agents are LLMs and can parse whatever they receive.