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

> Unlock the power of Prime Agent's agent-to-agent message passing. Learn how its dual-queue system and steer/continue methods enable deterministic communication. Dive into our complete guide.

- Repository: [Prime Intellect/prime-agent](https://github.com/PrimeIntellect-ai/prime-agent)
- Tags: deep-dive
- Published: 2026-08-15

---

**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](https://github.com/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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:

```typescript
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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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.

```typescript
// 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.

```typescript
// 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.

```typescript
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.

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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:

```typescript
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`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/agent.ts), loop driver in [`packages/agent/src/agent-loop.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/agent-loop.ts), types in [`packages/agent/src/types.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/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.