# How the PrimeAgent Agent Execution Flow Works: A Deep Dive into the Core Loop

> Explore the PrimeAgent agent execution flow. Discover how agentLoop and agentLoopContinue manage LLM responses, tool calls, and events in a deterministic state machine.

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

---

**PrimeAgent's execution flow is a deterministic state machine built around [`packages/agent/src/agent-loop.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/agent-loop.ts), where `agentLoop()` and `agentLoopContinue()` drive a streaming event loop that processes LLM responses, executes tool calls, and emits lifecycle events until termination conditions are met.**

This article breaks down the **agent execution flow** in PrimeAgent, an open-source AI agent framework developed by Prime Intellect. Understanding this flow is essential for customizing behavior, debugging streaming issues, or extending the framework with custom providers.

## Entry Points: Starting and Resuming Agent Execution

PrimeAgent exposes two public functions to initiate the execution loop, defined at lines 15–22 and 81–87 of [`packages/agent/src/agent-loop.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/agent-loop.ts):

- **`agentLoop(prompts, context, config, signal?, streamFn?)`** — Begins a fresh conversation by injecting new prompt messages into the context.
- **`agentLoopContinue(context, config, signal?, streamFn?)`** — Resumes an existing conversation without adding new prompts, useful for recovery after tool failures or user interruptions.

Both functions create an `EventStream` via `createAgentStream()` and delegate to internal runners (`runAgentLoop` / `runAgentLoopContinue`). The stream terminates when an `agent_end` event fires, returning the complete `AgentMessage[]` history.

```typescript
import { agentLoop, agentLoopContinue } from "prime-agent/packages/agent";

// Start a new conversation
const stream = agentLoop(
  [{ role: "user", content: [{ type: "text", text: "What is the weather?" }] }],
  initialContext,
  loopConfig,
);

// Resume after a tool failure without new prompts
const continueStream = agentLoopContinue(currentContext, loopConfig);

```

## Event Stream Architecture

The `createAgentStream()` function (lines 97–101) constructs an `EventStream` that collects messages until it receives an `agent_end` event. This streaming architecture enables real-time UI updates and fine-grained control over the conversation lifecycle.

Throughout execution, the loop emits eight standard events:

1. `agent_start` — Loop initialization
2. `turn_start` — Beginning of a new turn
3. `message_start` — LLM response begins
4. `message_update` — Streaming delta received
5. `message_end` — LLM response complete
6. `turn_end` — Turn processing finished
7. `agent_end` — Full conversation terminated
8. `tool_call_*` events — Tool execution status

## The Core Run Loop: Turn-Based Processing

The primary orchestration happens in `runLoop` (starting at line 304). Each iteration represents one **turn**, maintaining three mutable collections:

| Collection | Purpose |
|------------|---------|
| `newMessages` | Messages produced during the current run |
| `currentContext` | Evolving conversation state across turns |
| `pendingMessages` | Messages from steering, follow-up, or continuation callbacks |

### Turn Initialization: Steering and Follow-Up Hooks

Before each LLM request, the loop polls three configuration hooks:

- `config.getSteeringMessages` — Dynamic prompt injection
- `config.getFollowUpMessages` — Automatic follow-up generation
- `config.getContinuationMessages` — Recovery or retry messages

If any callback returns messages, they become `pendingMessages` and the turn repeats without contacting the LLM. This enables advanced behaviors like automatic retries, dynamic prompting, or human-in-the-loop interventions.

### LLM Streaming and Partial Updates

The `streamAssistantResponse` function converts internal `AgentMessage[]` to provider-specific formats via `config.convertToLlm`, applies optional transforms, and streams responses through either a custom `streamFn` or the default `streamSimple` (lines 51–56).

As the LLM emits streaming events (`start`, `text_delta`, `toolcall_begin`, `toolcall_delta`, `toolcall_end`), the loop updates in-memory messages and emits `message_update` events (lines 17–42). This preserves partial state and enables real-time UI rendering even if the connection drops.

### Tool Call Execution

When the LLM produces `toolCall` components, `executeToolCalls` runs them according to `config.toolExecutionMode` — either **parallel** or **sequential**. The resulting `ToolResultMessage` objects are appended to `currentContext` and immediately become available for the next turn (lines 52–57).

```typescript
// Example: Tool results flow back into context automatically
stream.on("message_end", ({ message }) => {
  if (message.toolCalls) {
    // Tool execution happens internally; results appear in next turn
    console.log("Tools executed:", message.toolCalls.length);
  }
});

```

## Abort Handling and Graceful Termination

Every async operation in the loop is guarded by `throwIfAborted` and `raceWithAbort`. If the optional `AbortSignal` fires, the loop generates an `AbortError` (lines 38–45), emits an `assistantMessage` with `stopReason: "aborted"`, and terminates gracefully without losing conversation state.

This design ensures that long-running agent operations can be cancelled by users or timeout handlers without corrupting the message history.

## Termination Conditions

After each turn completes, the loop evaluates three stop conditions:

1. **`config.shouldStopAfterTurn`** — Post-turn callback for custom exit logic
2. **`config.shouldStopBeforeTurn`** — Pre-turn callback checked before steering hooks
3. **Exhaustion of pending messages** — No steering, follow-up, or continuation messages remain

When any condition satisfies, the loop emits `agent_end` and resolves the `EventStream` with the full `AgentMessage[]`.

## Key Implementation Files

| File | Responsibility |
|------|---------------|
| [`packages/agent/src/agent-loop.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/agent-loop.ts) | Core loop, streaming, tool execution, abort handling |
| [`packages/agent/src/types.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/types.ts) | `AgentMessage`, `AgentContext`, `AgentLoopConfig` definitions |
| [`packages/ai/src/index.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/ai/src/index.ts) | `streamSimple` and LLM streaming utilities |
| [`packages/ai/src/bedrock-provider.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/ai/src/bedrock-provider.ts) | Reference provider implementation |
| [`packages/agent/test/agent-loop.test.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/test/agent-loop.test.ts) | Full loop behavior test suite |

## Summary

- **PrimeAgent's execution flow** centers on [`packages/agent/src/agent-loop.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/agent/src/agent-loop.ts), implementing a deterministic state machine around `agentLoop()` and `agentLoopContinue()` entry points.
- **Turn-based processing** with steering, follow-up, and continuation hooks enables dynamic conversation control without LLM round-trips.
- **Streaming architecture** preserves partial LLM state through `message_update` events while supporting custom `streamFn` implementations.
- **Tool calls** execute in configurable parallel or sequential mode, with results automatically reintegrated into conversation context.
- **Abort-aware design** ensures graceful termination via `AbortSignal` without state corruption.

## Frequently Asked Questions

### What is the difference between `agentLoop` and `agentLoopContinue`?

**`agentLoop`** starts fresh conversations by adding new prompt messages to context, while **`agentLoopContinue`** resumes existing conversations without injecting new prompts. Use `agentLoopContinue` when recovering from tool failures, handling user interruptions, or implementing retry logic where the context already contains the necessary information.

### How does PrimeAgent handle streaming LLM responses?

The loop calls `streamAssistantResponse`, which converts messages via `config.convertToLlm` and streams through either a custom function or `streamSimple`. As chunks arrive, the loop updates in-memory state and emits `message_update` events for real-time consumption, preserving partial progress even if the stream disconnects.

### Can I customize when the agent stops executing?

Yes. Provide **`shouldStopBeforeTurn`** or **`shouldStopAfterTurn`** callbacks in your `AgentLoopConfig`. These execute before steering hooks or after turn completion respectively. Additionally, returning messages from `getSteeringMessages`, `getFollowUpMessages`, or `getContinuationMessages` extends execution, while their exhaustion contributes to normal termination.

### What happens if I abort an agent loop mid-execution?

The `AbortSignal` triggers `throwIfAborted` or `raceWithAbort`, generating an `AbortError` that gracefully terminates the loop. An `assistantMessage` with `stopReason: "aborted"` is emitted, and the `EventStream` resolves with conversation state intact up to that point—no messages are lost or corrupted.