# How agentLoop Handles Tool Execution in π‑AI: A Deep Dive into the pi‑mono Agent Loop

> Discover how agentLoop in pi-mono handles tool execution by validating arguments, invoking handlers, and streaming events for real-time UI updates. Learn more about the pi-mono agent loop.

- Repository: [Mario Zechner/pi-mono](https://github.com/badlogic/pi-mono)
- Tags: deep-dive
- Published: 2026-02-16

---

**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`](https://github.com/badlogic/pi-mono/blob/main/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`](https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent-loop.ts) at lines 51‑53, the code extracts blocks where `type === "toolCall"`:

```typescript
// 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`](https://github.com/badlogic/pi-mono/blob/main/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`](https://github.com/badlogic/pi-mono/blob/main/agent-loop.ts#L5)‑7 and L22‑26) locates the matching tool by name and invokes its `execute` method:

```typescript
// 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`](https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/types.ts) include:

- **`tool_execution_start`**: Emitted before `execute` is called, containing `toolCallId`, `toolName`, and `args`.
- **`tool_execution_update`**: Emitted for each partial result via the `onUpdate` callback, 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`](https://github.com/badlogic/pi-mono/blob/main/agent-loop.ts#L49)‑55:

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

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

```typescript
// 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 `agentLoop` in [`packages/agent/src/agent-loop.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent-loop.ts) manages the complete lifecycle of tool execution within π‑AI.
- **Detection**: Filters assistant message content for `type === "toolCall"` blocks to identify pending invocations.
- **Validation**: Uses `validateToolArguments` from [`packages/ai/src/utils/validation.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/validation.ts) to ensure schema compliance before execution.
- **Execution**: Invokes the tool's `execute` method with id, validated args, abort signal, and an `onUpdate` callback for streaming.
- **Events**: Emits `tool_execution_start`, `tool_execution_update`, and `tool_execution_end` events for real-time UI updates.
- **Results**: Constructs `ToolResultMessage` objects 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_end` and `agent_end` events.

## 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`](https://github.com/badlogic/pi-mono/blob/main/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`](https://github.com/badlogic/pi-mono/blob/main/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`](https://github.com/badlogic/pi-mono/blob/main/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.