How agentLoop Handles Tool Execution in π‑AI: A Deep Dive into the pi‑mono Agent Loop
The agentLoop function in badlogic/pi-mono orchestrates tool execution by detecting tool calls in LLM responses, validating arguments against JSON schemas, invoking tool handlers, and streaming granular lifecycle events through an EventStream, enabling real-time UI updates during multi-turn conversations.
The agentLoop and agentLoopContinue functions located in packages/agent/src/agent-loop.ts form the core execution engine of the π‑AI (pi‑ai) agent system. This loop manages the conversation flow between the user, the LLM, and registered tools, handling everything from detecting tool calls to managing interruptions when new user input arrives mid-execution.
Detecting Tool Calls in the Agent Loop
After the LLM streams an assistant message, the loop filters the message content to identify tool invocation requests. In packages/agent/src/agent-loop.ts at lines 51‑53, the code extracts blocks where type === "toolCall":
// agent-loop.ts L51-L53
const toolCalls = message.content.filter((c) => c.type === "toolCall");
hasMoreToolCalls = toolCalls.length > 0;
If the array contains entries, the loop transitions to the executeToolCalls phase. This detection mechanism ensures that only explicit tool call content blocks trigger execution logic, keeping the conversation flow deterministic.
Executing Tool Calls with Validation
Once detected, tool calls undergo a structured execution pipeline involving validation, invocation, and event streaming.
Argument Validation
Before execution, arguments are validated against the tool's JSON schema using validateToolArguments from packages/ai/src/utils/validation.ts. This ensures type safety and schema compliance before the tool handler receives the parameters.
Tool Invocation
The executeToolCalls function (referenced at agent-loop.ts‑7 and L22‑26) locates the matching tool by name and invokes its execute method:
// agent-loop.ts L5-7, L22-26
const tool = tools?.find((t) => t.name === toolCall.name);
const validatedArgs = validateToolArguments(tool, toolCall);
result = await tool.execute(toolCall.id, validatedArgs, signal, onUpdate);
The execute method receives the tool call ID, validated arguments, an abort signal for cancellation, and an onUpdate callback for streaming partial results.
Execution Lifecycle Events
During execution, the loop emits a granular event stream enabling real-time UI feedback. The events defined in packages/agent/src/types.ts include:
tool_execution_start: Emitted beforeexecuteis called, containingtoolCallId,toolName, andargs.tool_execution_update: Emitted for each partial result via theonUpdatecallback, containing the current partial result.tool_execution_end: Emitted after resolution or rejection, containing the final result or error state.
These events are pushed to the EventStream, allowing UI components to render progress indicators, partial outputs, and error states without blocking the conversation flow.
Building Tool Result Messages
Upon completion, the loop constructs a ToolResultMessage to return the execution output to the LLM. As shown at agent-loop.ts‑55:
// agent-loop.ts L49-55
const toolResultMessage: ToolResultMessage = {
role: "toolResult",
toolCallId: toolCall.id,
toolName: toolCall.name,
content: result.content,
details: result.details,
isError,
timestamp: Date.now(),
};
This message is inserted into the conversation context, and the loop emits message_start and message_end events to signal its availability. The assistant can then reference these results in subsequent turns.
Steering and Interruption Handling
The agent loop supports cooperative multitasking through steering messages. After each tool execution, the loop checks for new user input via the optional getSteeringMessages callback (lines 63‑73):
// agent-loop.ts L63-73
if (getSteeringMessages) {
const steering = await getSteeringMessages();
if (steering.length > 0) {
steeringMessages = steering;
const remainingCalls = toolCalls.slice(index + 1);
for (const skipped of remainingCalls) {
results.push(skipToolCall(skipped, stream));
}
break;
}
}
If steering messages exist (indicating the user sent new input), the remaining pending tool calls are skipped. The skipToolCall function generates synthetic tool_execution_start and tool_execution_end events with an error-like result stating "Skipped due to queued user message." This ensures the conversation state remains consistent while prioritizing user responsiveness.
Turn Termination and Event Streaming
When all tool calls for a turn are processed (or skipped), the loop emits a turn_end event containing the assistant message and accumulated tool results:
// agent-loop.ts L73
stream.push({ type: "turn_end", message, toolResults });
If no further steering or follow-up messages appear, the outer loop concludes with an agent_end event, signaling the end of the session. The entire execution flow is observable through the EventStream, making the agent loop fully transparent to downstream consumers.
Summary
- The
agentLoopinpackages/agent/src/agent-loop.tsmanages the complete lifecycle of tool execution within π‑AI. - Detection: Filters assistant message content for
type === "toolCall"blocks to identify pending invocations. - Validation: Uses
validateToolArgumentsfrompackages/ai/src/utils/validation.tsto ensure schema compliance before execution. - Execution: Invokes the tool's
executemethod with id, validated args, abort signal, and anonUpdatecallback for streaming. - Events: Emits
tool_execution_start,tool_execution_update, andtool_execution_endevents for real-time UI updates. - Results: Constructs
ToolResultMessageobjects containing output, error states, and timestamps for the LLM context. - Interruption: Supports cooperative multitasking via
getSteeringMessages, skipping pending tool calls when new user input arrives. - Termination: Signals turn and session completion via
turn_endandagent_endevents.
Frequently Asked Questions
What is the agentLoop in pi‑mono?
The agentLoop is the core orchestration function in the π‑AI (pi‑ai) agent system, located in packages/agent/src/agent-loop.ts. It manages the conversation flow between users, LLMs, and registered tools, handling message streaming, tool detection, execution, and interruption logic in a turn-based cycle until the conversation terminates or the user interrupts the flow.
How does agentLoop validate tool arguments before execution?
Before invoking a tool, the loop calls validateToolArguments (imported from packages/ai/src/utils/validation.ts) with the tool definition and the raw tool call. This helper validates the provided arguments against the tool's JSON schema, ensuring type safety, required field presence, and structural compliance before the execute method receives the sanitized parameters.
Can agentLoop handle multiple tool calls in a single assistant response?
Yes. The loop extracts all content blocks where type === "toolCall" into an array and iterates through them sequentially in executeToolCalls. After each execution, it checks for steering messages (new user input). If detected, it skips any remaining pending tool calls for that turn by calling skipToolCall, ensuring the system remains responsive while maintaining conversation state integrity.
How does the UI receive real-time updates during tool execution?
The agentLoop emits a granular event stream through the EventStream interface defined in packages/agent/src/types.ts. Specifically, it pushes tool_execution_start when execution begins, tool_execution_update for each partial result (via the onUpdate callback passed to execute), and tool_execution_end upon completion or error. UI components subscribe to this stream to render progress indicators, partial outputs, and error states without blocking the main conversation flow.
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 →