How the PrimeAgent Agent Execution Flow Works: A Deep Dive into the Core Loop
PrimeAgent's execution flow is a deterministic state machine built around 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:
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.
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:
agent_start— Loop initializationturn_start— Beginning of a new turnmessage_start— LLM response beginsmessage_update— Streaming delta receivedmessage_end— LLM response completeturn_end— Turn processing finishedagent_end— Full conversation terminatedtool_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 injectionconfig.getFollowUpMessages— Automatic follow-up generationconfig.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).
// 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:
config.shouldStopAfterTurn— Post-turn callback for custom exit logicconfig.shouldStopBeforeTurn— Pre-turn callback checked before steering hooks- 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 |
Core loop, streaming, tool execution, abort handling |
packages/agent/src/types.ts |
AgentMessage, AgentContext, AgentLoopConfig definitions |
packages/ai/src/index.ts |
streamSimple and LLM streaming utilities |
packages/ai/src/bedrock-provider.ts |
Reference provider implementation |
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, implementing a deterministic state machine aroundagentLoop()andagentLoopContinue()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_updateevents while supporting customstreamFnimplementations. - Tool calls execute in configurable parallel or sequential mode, with results automatically reintegrated into conversation context.
- Abort-aware design ensures graceful termination via
AbortSignalwithout 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →