← Blog May 16, 2026

# 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

Scenario | Best approach | Why
--- | --- | ---
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 polls | 0 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

- No broadcast primitive — to message all peers, the orchestrator loops over `list_peers` and sends individually. A native broadcast action is planned.
- Channel push requires SSE — if Claude Code disconnects its SSE stream (rare, but possible during reconnection), messages fall back to inbox-only delivery. The agent sees them on next `inbox` call.
- 64 KB message limit — large payloads (full file contents, large diffs) should go through the filesystem or `ai_terminal_read_screen`. Peer messages are for coordination signals, not data transfer.
- No message acknowledgment — the sender knows if the channel push succeeded, but not whether the recipient acted on it. For critical workflows, agents should send explicit reply messages.

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.