How the Agent-to-Agent Messaging System Works in Prime Agent

Prime Agent enables agent-to-agent messaging through a dual-queue architecture where messages are queued via steer() or followUp() APIs and injected into the LLM loop before or after the current conversation turn.

The Prime Agent framework from PrimeIntellect-ai provides a lightweight, queue-based mechanism for inter-agent communication that allows one Agent instance to send messages to another without dedicated inter-process protocols. By leveraging the PendingMessageQueue system and the canonical AgentMessage type, agents can coordinate multi-turn conversations while maintaining isolated transcripts and state.

Core Architecture of the Messaging System

The messaging infrastructure revolves around standardized message objects and two distinct queues that control when messages enter the conversation flow.

AgentMessage Schema

All communication uses the AgentMessage type defined in packages/agent/src/types.ts. This canonical shape includes properties for role, content, timestamp, and other metadata. Messages can assume four distinct roles:

  • user – Input from the human operator or another agent
  • assistant – Responses generated by the LLM
  • toolResult – Output from tool executions
  • custom – Application-specific message types

This unified schema ensures that any agent can consume messages produced by another agent without translation layers.

The Dual-Queue Design

Each Agent instance maintains two PendingMessageQueue instances created in the constructor at packages/agent/src/agent.ts (lines 20-35):

  1. Steering Queue – Holds messages to be processed immediately after the current assistant turn finishes. Created with new PendingMessageQueue(options.steeringMode ?? "one-at-a-time"), this queue ensures high-priority messages interrupt the flow before the next LLM generation.

  2. Follow-up Queue – Holds messages that execute only after the agent would otherwise stop. This queue handles background tasks or secondary conversations that should not block the primary interaction loop.

Message Queuing APIs

Prime Agent exposes public methods to populate these queues, allowing external code (including other agents) to inject messages programmatically.

The steer() Method

The steer() method (lines 84-92 in packages/agent/src/agent.ts) pushes one or more messages onto the steering queue. When called, the method immediately places the AgentMessage objects into the queue, making them available for the next iteration of the agent loop.

// Push a high-priority message to be processed next
agentA.steer({
  role: "user",
  content: "Please analyze this data from Agent B",
  timestamp: Date.now()
});

The followUp() Method

The followUp() method operates identically to steer() but targets the follow-up queue instead. Messages added here wait until the current conversation naturally concludes before executing.

Queue Draining and Lifecycle

The agent loop processes queued messages at specific points in the execution lifecycle, ensuring deterministic ordering of inter-agent communication.

How continue() Processes Queued Messages

When continue() is invoked, the agent drains queues in strict priority order (lines 69-81 in packages/agent/src/agent.ts):

// Pseudocode representing the actual implementation
async continue() {
  // 1. Process steering messages first
  const steeringMessages = await this.steeringQueue.drain();
  for (const msg of steeringMessages) {
    this._state.messages.push(msg);
  }
  
  // 2. Run the LLM loop with injected messages
  
  // 3. Only drain follow-up if the agent would stop
  if (shouldStop) {
    const followUpMessages = await this.followUpQueue.drain();
    // Process follow-ups...
  }
}

This guarantees that steering messages are injected before the next assistant response, while follow-up messages run only after the current conversational thread ends.

Message Flow in the Agent Loop

The low-level loop functions runAgentLoop and runAgentLoopContinue (referenced in lines 65-78 of packages/agent/src/agent.ts) handle the actual consumption:

  1. The loop checks for queued messages via this.steeringQueue.drain()
  2. Messages are appended to the internal transcript: this._state.messages.push(event.message)
  3. Lifecycle events (agent_end, turn_end) emit to subscribers, allowing other agents to react

Implementing Agent-to-Agent Communication

To establish communication between two agents, one agent queues messages that the other consumes through its standard prompt/continue flow. Because each agent owns its own transcript and queue instances, the agents remain isolated yet can exchange AgentMessage objects through the shared runtime.

import { Agent } from "@earendil-works/pi-agent";

// Create two agents with separate models or the same one
const agentA = new Agent({ model: myModelA });
const agentB = new Agent({ model: myModelB });

// Agent A sends a steering message that Agent B will later consume
await agentA.prompt("Hello, Agent B!");

// Forward the assistant reply from A to B
agentA.subscribe(async (event) => {
  if (event.type === "message_end" && event.message.role === "assistant") {
    // Queue the reply as a user message for B
    agentB.steer(event.message);
  }
});

// Start B so it will process the queued steering message
await agentB.continue();   // B will see A's reply as its next input

This pattern works both in-process and across network boundaries when combined with the streamProxy function from packages/agent/src/proxy.ts, which routes LLM calls through remote services.

Key Source Files

The agent-to-agent messaging system spans four critical files in the Prime Agent repository:

  • packages/agent/src/agent.ts – Core Agent class implementation containing queue initialization, the steer() and followUp() public APIs, and the continue() method that drains queues
  • packages/agent/src/types.ts – TypeScript definitions for AgentMessage, AgentState, and the message schema that enables cross-agent compatibility
  • packages/agent/src/agent-loop.ts – Low-level loop implementation that consumes queued messages, drives the LLM stream, and emits lifecycle events
  • packages/agent/src/proxy.ts – streamProxy helper for distributed setups where agents communicate across network boundaries

Summary

  • Prime Agent uses a dual-queue architecture (steering and follow-up) to manage when messages enter the conversation flow
  • The AgentMessage type standardizes communication across all agents with support for user, assistant, toolResult, and custom roles
  • steer() injects messages before the next LLM turn, while followUp() defers messages until after the conversation ends
  • Queue draining occurs in continue() at lines 69-81 of packages/agent/src/agent.ts, ensuring deterministic message ordering
  • Agents communicate by calling queue methods on each other's instances, with no dedicated inter-process protocol required

Frequently Asked Questions

What is the difference between steer() and followUp() in Prime Agent?

The steer() method pushes messages onto the steering queue, which drains immediately after the current assistant turn finishes but before the next LLM generation begins. The followUp() method pushes messages onto the follow-up queue, which only drains after the agent determines it would otherwise stop, making it suitable for background tasks or secondary conversation threads.

How does AgentMessage structure support different message types?

The AgentMessage interface defined in packages/agent/src/types.ts uses a role discriminator field that accepts "user", "assistant", "toolResult", or "custom" values. This schema allows agents to exchange not only conversational text but also structured tool outputs and application-specific metadata through the same queuing system.

Can agents communicate across network boundaries?

Yes. While the core messaging system operates through in-memory queue objects, the streamProxy function in packages/agent/src/proxy.ts enables distributed deployments where one agent routes LLM calls through a remote service. Agents can still exchange AgentMessage objects across these boundaries because the protocol relies on the serializable message schema rather than shared memory.

Where is the message queue drained in the agent lifecycle?

The queues drain inside the continue() method at packages/agent/src/agent.ts (lines 69-81). The implementation first calls this.steeringQueue.drain() to process high-priority messages, runs the LLM loop, then conditionally calls this.followUpQueue.drain() only if the agent reaches a stopping state. This sequencing ensures steering messages always preempt follow-up messages.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →