# How the Earendil Pi Agent Runtime Handles Tool Calling: Architecture and Implementation

> Discover how the Earendil Pi agent runtime manages LLM tool calls by instantiating components, tracking pending operations, and correlating asynchronous results. Learn the architecture and implementation details.

- Repository: [Earendil Works/pi](https://github.com/earendil-works/pi)
- Tags: architecture
- Published: 2026-05-25

---

**The Earendil Pi agent runtime processes LLM tool calls by instantiating a `ToolExecutionComponent` for each request, tracking pending operations in a `pendingTools` map keyed by `toolCallId`, and correlating asynchronous results back to the UI when the tool finishes.**

The Earendil Pi agent (located in the `earendil-works/pi` repository) executes external tools through an interactive mode runtime that bridges large language model (LLM) requests with file system and shell operations. Understanding how this runtime handles tool calling is essential for developers extending the agent's capabilities or debugging execution flows. The implementation centers on the `InteractiveMode` class in [`packages/coding-agent/src/modes/interactive/interactive-mode.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/interactive/interactive-mode.ts), which orchestrates the entire lifecycle from request to render.

## The Tool Call Message Protocol

Before execution begins, the LLM emits a structured message requesting tool use. The runtime expects a specific interface defined in [`scripts/tool-stats.ts`](https://github.com/earendil-works/pi/blob/main/scripts/tool-stats.ts).

### Message Structure Definition

According to the source analysis, the message adheres to this TypeScript shape found around lines 10-14 in [`scripts/tool-stats.ts`](https://github.com/earendil-works/pi/blob/main/scripts/tool-stats.ts):

```typescript
{
  type: "toolCall";
  id: string;
  name: string;
  arguments?: Record<string, unknown>;
}

```

The `id` field serves as the unique **toolCallId** that persists throughout the request lifecycle.

## The Interactive Mode Execution Loop

When the agent operates in interactive mode, the `InteractiveMode` class manages the conversation loop. Incoming messages stream through `handleMessage`, which detects tool requests and initiates execution.

### Parsing and Registering Pending Tools

Upon detecting a block with `type === "toolCall"`, the runtime immediately creates a `ToolExecutionComponent` to represent the pending operation. This component registers in an internal `Map` named `pendingTools`, keyed by the `toolCallId`. The logic around line 2790 in [`interactive-mode.ts`](https://github.com/earendil-works/pi/blob/main/interactive-mode.ts) performs this registration:

```typescript
if (content.type === "toolCall") {
  const component = new ToolExecutionComponent({
    toolCallId: content.id,
    /* …other UI parameters… */
  });
  this.pendingTools.set(event.toolCallId, component);
}

```

The component renders a placeholder line (e.g., “⧗ read …”) while awaiting completion.

### Tool Registry and Execution

The runtime resolves tool names through a registry built by `createCodingTools`, which includes built-in implementations such as `read`, `write`, and `bash`. Each tool’s `execute` method receives the same `toolCallId` to enable result correlation. For example, in [`packages/coding-agent/src/core/tools/read.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/tools/read.ts) (around line 220), the signature appears as:

```typescript
export const createReadTool = (cwd: string): ToolDefinition => ({
  name: "read",
  execute: async (toolCallId, { path }, signal, onUpdate) => {
    // File reading logic
    return { type: "toolResult", toolName: "read", toolCallId, content: fileText };
  },
});

```

### Streaming Incremental Updates

While the tool executes, it emits progress updates via the `onUpdate` callback. The `ToolExecutionComponent` forwards these updates to the terminal UI, appending output beneath the placeholder defined in [`packages/coding-agent/src/modes/interactive/components/tool-execution.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) (lines 23-54).

## Handling Tool Results and Lifecycle Completion

Once a tool finishes, it returns a result message that the runtime must match to its pending UI component.

### Correlating Results with Pending Calls

The result message carries `role: "toolResult"` along with the `toolCallId`. The `InteractiveMode` retrieves the corresponding component from `pendingTools` and finalizes the display. The cleanup logic around line 2835 in [`interactive-mode.ts`](https://github.com/earendil-works/pi/blob/main/interactive-mode.ts) performs this matching:

```typescript
if (message.role === "toolResult" && message.toolName) {
  const component = this.pendingTools.get(message.toolCallId!);
  if (component) {
    component.renderResult(message);
    this.pendingTools.delete(message.toolCallId!);
  }
}

```

### Rendering Final Output

After correlation, `ToolExecutionComponent.renderResult` formats the final output. For HTML export scenarios, [`packages/coding-agent/src/core/export-html/tool-renderer.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/export-html/tool-renderer.ts) manages rendering state using `renderedArgs.set(toolCallId, args)` to cache argument displays.

## Tool Call Accounting and UI Status

The runtime maintains counters for diagnostic purposes. Around line 5184 in [`interactive-mode.ts`](https://github.com/earendil-works/pi/blob/main/interactive-mode.ts), the UI status line increments `stats.toolCalls` and displays the total:

```typescript
info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;

```

## Summary

- The Earendil Pi runtime defines tool calls via the `ToolCallContent` interface in [`scripts/tool-stats.ts`](https://github.com/earendil-works/pi/blob/main/scripts/tool-stats.ts), requiring a unique `toolCallId` for every request.
- `InteractiveMode` tracks active executions in a `pendingTools` Map that associates each `toolCallId` with a `ToolExecutionComponent`.
- The `ToolExecutionComponent` handles UI placeholders and streams incremental updates via the `onUpdate` callback during tool execution.
- Built-in tools in `packages/coding-agent/src/core/tools/` receive the `toolCallId` as a parameter, ensuring results can be correlated back to the correct conversation thread.
- Completion triggers a `toolResult` message; the runtime matches it via `toolCallId`, renders final output, and removes the entry from `pendingTools`.
- Execution statistics aggregate in real-time, displayed in the interactive UI status line.

## Frequently Asked Questions

### What is the shape of a tool call message in the Earendil Pi runtime?

The runtime expects a JSON object with `type: "toolCall"`, a unique string `id`, the `name` of the tool, and an optional `arguments` record. This structure is formally defined in [`scripts/tool-stats.ts`](https://github.com/earendil-works/pi/blob/main/scripts/tool-stats.ts) and parsed by `InteractiveMode.handleMessage` in [`packages/coding-agent/src/modes/interactive/interactive-mode.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/interactive/interactive-mode.ts).

### How does the runtime match tool results to the correct UI component?

It uses a `Map` called `pendingTools` keyed by `toolCallId`. When a `toolResult` message arrives, the runtime looks up the ID in [`interactive-mode.ts`](https://github.com/earendil-works/pi/blob/main/interactive-mode.ts), invokes `renderResult` on the stored `ToolExecutionComponent`, and deletes the entry to free resources.

### Where is the tool execution logic implemented for built-in tools like read and write?

Built-in tool implementations reside in `packages/coding-agent/src/core/tools/` (e.g., [`read.ts`](https://github.com/earendil-works/pi/blob/main/read.ts), [`write.ts`](https://github.com/earendil-works/pi/blob/main/write.ts)). The [`tool-definition-wrapper.ts`](https://github.com/earendil-works/pi/blob/main/tool-definition-wrapper.ts) file wraps these definitions to ensure the `toolCallId` propagates through the extension context to each `execute` method.

### How does the runtime handle streaming output from long-running tools?

Tools stream incremental data through the `onUpdate` callback passed to their `execute` function. The `ToolExecutionComponent` subscribed to these events updates the terminal UI in real-time, keeping the user informed of progress before the final result arrives.