How Agent-to-Agent Message Passing Works in Prime Agent: Complete Guide

Prime Agent uses a dual-queue system where each Agent instance maintains its own steeringQueue and followUpQueue, enabling deterministic inter-agent communication through steer() and continue() methods.

Agent-to-agent message passing in PrimeIntellect-ai/prime-agent is built on a fundamentally simple principle: each agent owns its state and exposes explicit queues for incoming messages. This architecture decouples agent execution from message routing, making multi-agent systems predictable and debuggable. The following sections break down the queue mechanics, message lifecycle, and practical implementation patterns found in the source code.

The Dual-Queue Architecture

Every Agent instantiated from packages/agent/src/agent.ts contains two internal PendingMessageQueue instances that separate messages by execution timing:

Queue Purpose Drain Behavior
Steering queue Interrupt the current turn with new messages Controlled by steeringMode: "all" or "one-at-a-time"
Follow-up queue Execute messages after the turn would naturally end Processes post-turn logic, tool results, or chained agent outputs

These queues are private to the Agent and accessed through callbacks getSteeringMessages and getFollowUpMessages that the agent loop (packages/agent/src/agent-loop.ts) invokes at specific lifecycle points.

Queue Modes Explained

The steeringMode parameter determines how aggressively the loop drains messages:

  • "all" – Drain the entire queue before yielding control back to the LLM stream
  • "one-at-a-time" – Process exactly one message per callback, allowing finer interleaving with streaming output

Set this mode in your Agent constructor options:

const agent = new Agent({
  steeringMode: "all" // or "one-at-a-time"
});

Message Flow Through the System

Understanding the full pipeline clarifies where agent-to-agent handoffs occur:

  1. Input normalization – Agent.prompt() or Agent.continue() calls normalizePromptInput() to convert varied inputs into AgentMessage[] format
  2. Loop initialization – runAgentLoop() receives initial messages, a state snapshot from createContextSnapshot(), and LoopConfig from createLoopConfig()
  3. Queue polling – During streaming, the loop periodically calls getSteeringMessages(); after turn completion, it calls getFollowUpMessages()
  4. Recursive execution – Returned messages feed into runPromptMessages() recursively, preserving order and enabling agent seeding from previous turns
  5. Event propagation – Each AgentEvent (defined in packages/agent/src/types.ts) updates _state via processEvents() and broadcasts to subscribers via Agent.subscribe()

The AgentEvent type includes: message_start, message_complete, tool_execution_start, tool_execution_complete, turn_start, turn_end, error, and state_change.

Core APIs for Inter-Agent Communication

steer(messages: AgentMessage[])

Injects messages into the steering queue. These messages interrupt the current turn if execution is active, or start a new processing cycle if idle.

// In packages/agent/src/agent.ts
steer(messages: PendingMessages): void {
  this.steeringQueue.enqueue(normalizeMessages(messages));
}

followUp(messages: AgentMessage[])

Injects messages into the follow-up queue. These execute only after the current turn completes naturally.

// In packages/agent/src/agent.ts
followUp(messages: PendingMessages): void {
  this.followUpQueue.enqueue(normalizeMessages(messages));
}

continue()

Resumes processing from the current state, draining either queue according to priority. Essential for agent chains.

await agent.continue(); // Processes queued messages without new user input

Practical Agent-to-Agent Patterns

Pattern 1: Sequential Agent Chain

The most common pattern: Agent A produces output, Agent B consumes it as input.

import { Agent } from "packages/agent/src/index.js";

const analyst = new Agent({
  model: "claude-3-opus-20240229",
  systemPrompt: "You are a technical analyst. Summarize complex topics."
});

const writer = new Agent({
  model: "claude-3-opus-20240229",
  systemPrompt: "You are a science writer. Expand summaries into articles."
});

// Step 1: Analyst generates summary
await analyst.prompt("Explain quantum error correction in 3 sentences.");

// Step 2: Writer receives the summary via steering queue
writer.steer(analyst.state.messages.slice(-1)); // Last assistant message
await writer.continue(); // Writer produces expanded article

console.log(writer.state.messages.slice(-1)[0].content);

The state.messages array maintains the full transcript with role and content properties, making message extraction straightforward.

Pattern 2: Parallel Message Injection

Use "all" mode to batch-process multiple steering messages:

const coordinator = new Agent({ steeringMode: "all" });

await coordinator.prompt("Divide this task among sub-agents.");

// Queue multiple follow-ups that execute in order
coordinator.followUp([
  { 
    role: "user", 
    content: [{ type: "text", text: "Sub-task 1: Research component A" }] 
  },
  { 
    role: "assistant", 
    content: [{ type: "text", text: "Component A involves..." }] 
  },
  { 
    role: "user", 
    content: [{ type: "text", text: "Sub-task 2: Research component B" }] 
  }
]);

await coordinator.continue(); // Processes entire batch sequentially

Pattern 3: Observability and Reactive Agents

External agents can subscribe to events without participating in the message flow:

const observer = new Agent(); // No active role, just listening
const worker = new Agent();

// Observer reacts to worker's progress
worker.subscribe((event) => {
  if (event.type === "tool_execution_complete") {
    console.log(`Tool used: ${event.toolName}`);
    
    // Observer can steer itself based on worker activity
    observer.steer([{
      role: "user",
      content: [{ type: "text", text: `Worker completed: ${event.toolName}` }]
    }]);
  }
});

await worker.prompt("Execute data analysis workflow.");

State Synchronization Guarantees

The Agent class ensures deterministic behavior through:

  • Immutable state snapshots – createContextSnapshot() captures state.messages, state.metadata, and state.checkpoint at loop start
  • Ordered queue draining – PendingMessageQueue preserves FIFO order within each queue, with steering queue always prioritized over follow-up
  • Explicit idle states – waitForIdle() returns a Promise that resolves only when both queues are empty and no active run exists

This design eliminates race conditions common in event-driven agent systems.

Integration with External Proxies

For distributed agent systems, packages/agent/src/proxy.ts provides streamProxy as a custom streamFn. This allows agents running in separate processes or servers to maintain the same queue-based semantics over HTTP/WebSocket transports:

import { streamProxy } from "packages/agent/src/proxy.js";

const remoteAgent = new Agent({
  streamFn: streamProxy({ endpoint: "wss://agent-cluster.internal/run" })
});

// Local and remote agents use identical steering/follow-up APIs
localAgent.steer(remoteAgent.state.messages.slice(-1));

Summary

  • Each Agent owns isolated state with steeringQueue and followUpQueue for message prioritization
  • steer() interrupts, followUp() defers, and continue() resumes processing
  • Queue modes "all" and "one-at-a-time" control throughput vs. responsiveness tradeoffs
  • Event subscriptions enable reactive patterns without direct message passing
  • Source files: Core logic in packages/agent/src/agent.ts, loop driver in packages/agent/src/agent-loop.ts, types in packages/agent/src/types.ts

Frequently Asked Questions

How do I pass a message from one agent to another without losing context?

Extract the target message from sourceAgent.state.messages (typically slice(-1) for the last assistant response) and pass it to targetAgent.steer(). The full message object including role, content, and any tool metadata transfers intact. Call await targetAgent.continue() to trigger processing.

What happens if I steer() while an agent is actively streaming?

The steering message enters steeringQueue immediately. The agent loop polls this queue during streaming via getSteeringMessages(). Depending on steeringMode, the message either interrupts the stream ("one-at-a-time") or queues for immediate post-processing ("all"). The LLM connection remains open; this is cooperative multitasking, not preemption.

Can an agent consume multiple other agents' outputs in parallel?

Yes. Collect messages from multiple source agents and steer() them in batch, or use "all" mode to process them sequentially. For true parallel execution, instantiate separate Agent instances and coordinate through Promise.all() or event subscriptions rather than shared queues.

How do I observe agent activity without modifying message flow?

Use agent.subscribe(callback) to receive all AgentEvent emissions without interacting with queues. This is ideal for logging, monitoring, or triggering side effects. The callback receives the same events that update internal state, ensuring consistency between observed and actual behavior.

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 →