← Blog

Agent-to-agent messaging: from terminal scraping to structured channels

AI agents working together shouldn't have to read each other's terminal output. TUICommander provides two direct communication paths: peer messaging for structured coordination, and MCP channels for real-time event push. Here's how they work and when to use each.

The evolution of agent communication

In our previous post, we showed how one agent can drive another through TUICommander's ai_terminal_* tools. That approach works well when the orchestrator needs to literally type commands and read terminal output. But it has a fundamental limitation: communication is mediated by the terminal.

When Agent A wants to tell Agent B "the tests pass, move on to the next file," it has to:

  1. Send the text as a terminal command to B's PTY
  2. Wait for B to process it
  3. Read B's terminal output to confirm receipt

This is like communicating by writing notes on a whiteboard and waiting for the other person to read them. It works, but there's a faster way: just talk directly.

Approach 1: Peer messaging

TUICommander maintains a peer registry. Any agent connected via MCP can register itself, discover other agents, and send messages directly — without touching their terminal.

The protocol

// Step 1: Register yourself (once, at session start)
agent({ action: "register" })
// TUIC uses your MCP session to identify you — no manual UUID needed

// Step 2: Discover peers
agent({ action: "list_peers" })
→ [{ tuic_session: "abc-123", name: "claude-worker-1", project: "/code/myapp" },
   { tuic_session: "def-456", name: "claude-worker-2", project: "/code/myapp" }]

// Step 3: Send a message (max 64 KB)
agent({ action: "send", to: "abc-123", message: "Tests pass. Proceed to auth module." })
→ { ok: true, message_id: "msg-789", delivered_via_channel: true }

// Step 4: Check your inbox
agent({ action: "inbox", limit: 10, since: 1747400000000 })
→ [{ from_name: "orchestrator", content: "Review complete. Ship it.", timestamp: ... }]

Delivery: push first, queue always

When you send a message, TUICommander does two things:

  1. Channel push — if the recipient has an active SSE stream (which Claude Code always does), the message is delivered instantly as a notifications/claude/channel event. It appears in the recipient's context immediately, as if the system injected a note mid-conversation.
  2. Inbox queue — the message is always stored in a FIFO queue (100 messages per agent, oldest evicted first). This handles cases where the push fails or the recipient reconnects later.

The delivered_via_channel: true response tells you the message was pushed in real-time. If it's false, the recipient will see it on their next inbox poll — not a failure, just deferred delivery.

Approach 2: MCP channels (event push)

MCP channels are a lower-level mechanism. Instead of agent-to-agent messaging, they let TUICommander push arbitrary events into an agent's context. The agent sees them as <channel> tags — structured metadata injected by the system.

TUICommander declares the claude/channel experimental capability when any Claude Code client connects:

// In the MCP initialize response:
{
  "capabilities": {
    "experimental": { "claude/channel": {} },
    "tools": { ... }
  }
}

When a peer message is sent, the recipient sees it as:

<channel source="tuicommander" from_tuic_session="abc-123"
         from_name="orchestrator" message_id="msg-789">
Message from orchestrator: Tests pass. Proceed to auth module.
</channel>

This is powerful because it's interrupt-driven. The recipient doesn't need to poll an inbox — the message appears in their active conversation context as if a human typed it. Claude Code processes it in the current turn, not on the next tool call.

Example: Opus orchestrating three Sonnet workers

A practical multi-agent setup: one Claude Opus session acts as an orchestrator, dispatching work to three Claude Sonnet sessions running in parallel.

Setup

// Orchestrator (Opus) spawns three workers
agent({ action: "spawn", agent_type: "claude", model: "sonnet",
        prompt: "You are worker-1. Register with TUIC, then wait for instructions.",
        cwd: "/code/myapp" })
agent({ action: "spawn", agent_type: "claude", model: "sonnet",
        prompt: "You are worker-2. Register with TUIC, then wait for instructions.",
        cwd: "/code/myapp" })
agent({ action: "spawn", agent_type: "claude", model: "sonnet",
        prompt: "You are worker-3. Register with TUIC, then wait for instructions.",
        cwd: "/code/myapp" })

// Orchestrator registers itself
agent({ action: "register" })

// Wait a moment, then discover workers
agent({ action: "list_peers", project: "/code/myapp" })
→ [worker-1, worker-2, worker-3]

Dispatch

// Send parallel tasks
agent({ action: "send", to: "worker-1-uuid",
        message: "Refactor src/auth/ to use the new token format. Run tests when done." })
agent({ action: "send", to: "worker-2-uuid",
        message: "Add pagination to the /users endpoint. Include integration tests." })
agent({ action: "send", to: "worker-3-uuid",
        message: "Fix the 3 failing tests in src/billing/. Don't change the implementation." })

Coordination

// Workers send results back when done
// (from worker-1's perspective, after finishing its task)
agent({ action: "send", to: "orchestrator-uuid",
        message: "Auth refactoring complete. All 12 tests pass. Changed 4 files." })

// Orchestrator checks inbox periodically
agent({ action: "inbox", since: last_check_timestamp })
→ [{ from_name: "worker-1", content: "Auth refactoring complete..." },
   { from_name: "worker-3", content: "Fixed 3 tests. Root cause was stale mock data." }]
// worker-2 still working — no message yet

The orchestrator can monitor progress without reading terminal output. If it needs details, it can still use ai_terminal_read_screen for a specific worker — but for coordination, peer messaging is faster and cheaper.

When to use which approach

ScenarioBest approachWhy
Type a command into another agent's terminal ai_terminal_drive_agent You need PTY I/O — there's no alternative
Read what an agent produced ai_terminal_read_screen Terminal output is the source of truth
Tell an agent "move to the next task" Peer messaging (send) Coordination signal, not a terminal command
Broadcast "deploy succeeded" to all workers Peer messaging (loop over list_peers) Event notification to multiple recipients
React to another agent finishing Channel push (automatic via send) Interrupt-driven, zero polling
Monitor agent state without interacting ai_terminal_get_state Structured JSON about shell state, no messages needed

The key distinction: PTY tools are for when you need to interact with the terminal (type, read, wait). Peer messaging is for when you need to coordinate between agents (dispatch, report, synchronize). They're complementary — most real orchestration uses both.

Token cost comparison

Coordinating three workers across 10 task cycles. Each cycle: dispatch a task, wait for completion, collect results.

PTY drive (read terminal)Peer messaging
Dispatch~200 tokens (drive_agent call)~100 tokens (send call)
Wait for completion~150 tokens/poll × N polls0 tokens (channel push arrives)
Collect results~500 tokens (read_screen)~150 tokens (inbox message)
Per-cycle total~1000–2000 tokens~250 tokens
10 cycles × 3 workers~30,000–60,000 tokens~7,500 tokens

The savings come from two places: no terminal output parsing (messages are structured text, not screen dumps), and no polling (channel push delivers results the instant they're sent).

Implementation details

Registration

When an agent calls agent({ action: "register" }), TUICommander maps their MCP session ID to a PeerAgent record: display name, project path, and registration timestamp. The TUIC_SESSION environment variable (a stable UUID set when the terminal tab was created) serves as the peer's primary identifier.

Message delivery

Messages are stored in a per-agent FIFO queue (100 messages max, oldest evicted on overflow). On send, TUIC checks whether the recipient has an active SSE stream. If yes, it pushes the message as a JSON-RPC notification over that stream — Claude Code's MCP client receives it as a claude/channel event and injects it into the conversation context.

Lifecycle

When a terminal session closes, its peer registration is automatically removed. Pending messages in its inbox are preserved briefly (for reconnection scenarios) then garbage-collected. Other peers see the session disappear from list_peers.

The tuic CLI: orchestration from the command line

Everything above uses MCP tool calls — the natural interface for AI agents. But you can do the same from a shell script or a human terminal using the tuic CLI:

# Spawn a Claude Sonnet worker
tuic agent spawn --type claude --model sonnet --prompt "Wait for instructions." --cwd /code/myapp

# List active agent peers
tuic agent list-peers --project /code/myapp

# Send a message to a specific peer
tuic agent send --to abc-123 --message "Run the integration tests and report back."

# Read your inbox
tuic agent inbox --limit 5

# Or combine with session tools for hybrid workflows:
tuic session list                          # See all terminals
tuic session output --id abc-123           # Read clean terminal output
tuic session input --id abc-123 "npm test" # Type a command

This is the same protocol — the CLI is a thin wrapper over the MCP HTTP API. You can script an entire multi-agent pipeline in bash, use it from CI, or mix human commands with agent coordination. Install it from Settings > General > Command Line Interface.

Limitations and future work

We're exploring structured message types (task assignment, completion report, error escalation) with schemas that agents can parse without free-text interpretation. The goal: agent coordination with zero ambiguity and minimal tokens.