# How Pi-Web Normalizes Tool Call Field Names Between Pi and UI Formats

> Explore how Pi-Web normalizes tool call field names using normalizeToolCalls. Learn to map Pi engine fields like id name arguments to UI fields toolCallId toolName input.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-26

---

**Pi-Web normalizes tool call field names through the `normalizeToolCalls` utility in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), which maps Pi engine fields (`id`, `name`, `arguments`) to UI-compatible fields (`toolCallId`, `toolName`, `input`) with intelligent fallbacks.**

The `agegr/pi-web` repository bridges the Pi AI engine with a React-based user interface, requiring careful transformation of message formats. When handling tool calls, the system must reconcile differences between the Pi engine's wire format and the UI's expected type definitions. This article examines how the codebase normalizes tool call field names to ensure type safety and consistent rendering across the application.

## Why Tool Call Field Name Normalization Is Required

The Pi engine transmits tool calls using a specific wire format that differs from the UI's internal `ToolCallContent` type. The engine sends objects with the shape `{type: "toolCall", id, name, arguments}`, while the UI expects `{toolCallId, toolName, input}`. Directly passing the raw engine objects would violate TypeScript contracts and cause runtime rendering errors.

This impedance mismatch necessitates a centralized transformation layer that converts incoming Pi messages into the canonical UI format before they reach React components.

## Core Normalization Logic in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)

The normalization implementation resides in **[`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)**, which exports two key functions: `normalizeToolCalls` (lines 21-31) and the helper `normalizeToolCallBlock` (lines 7-18).

### Guarding Against Non-Assistant Messages

The `normalizeToolCalls` function first validates the message role before attempting transformation. According to lines 22-24, if the message role is not `"assistant"`, the function returns the message unchanged. This guard clause prevents unnecessary processing of user or system messages that cannot contain tool calls.

For assistant messages, the function iterates over the `content` array (line 26) and passes each block to `normalizeToolCallBlock`.

### Field Mapping with Fallback Strategy

Inside `normalizeToolCallBlock` (lines 9-18), the helper performs defensive field mapping with fallback chains to handle both legacy and current Pi engine formats:

- **`toolCallId`**: Prefers `block.toolCallId`; falls back to `block.id` if undefined. If neither exists, defaults to an empty string (line 11).
- **`toolName`**: Prefers `block.toolName`; falls back to `block.name` with the same empty string default (line 12).
- **`input`**: Prefers `block.input`; falls back to `block.arguments` while ensuring the result is always an object. If neither field exists, it returns an empty object (lines 13-17).

If the block's `type` property is not `"toolCall"`, the helper returns `null`, causing `normalizeToolCalls` to preserve the original block unchanged (line 29).

## Integration Points in the Codebase

Normalization occurs at the boundaries where external Pi data enters the UI state layer, ensuring all downstream components receive consistently shaped objects.

### Session Loading in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)

When loading historical sessions from JSONL files, the application calls `normalizeToolCalls` at lines 307-308 before storing messages in the UI state. This ensures that persisted sessions conform to current UI type definitions regardless of when they were originally generated.

### Realtime Streaming in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)

During live SSE (Server-Sent Events) streaming, every incoming assistant message undergoes normalization before dispatch. The hook applies `normalizeToolCalls` at lines 938, 954, and 968 to transform streaming tool calls into the UI format in real-time, preventing type mismatches during active conversations.

## Practical Implementation Example

The following TypeScript example demonstrates the transformation performed by the normalization utilities:

```typescript
// Raw Pi engine format (incoming wire data)
const rawBlock = {
  type: "toolCall",
  id: "abc123",
  name: "weather",
  arguments: { city: "Paris" },
};

import { normalizeToolCalls } from "@/lib/normalize";

const assistantMsg = {
  role: "assistant",
  content: [rawBlock],
};

const uiMsg = normalizeToolCalls(assistantMsg);
// uiMsg.content[0] now conforms to UI expectations:
// {
//   type: "toolCall",
//   toolCallId: "abc123",
//   toolName: "weather",
//   input: { city: "Paris" }
// }

```

## Summary

- **Centralized transformation**: All field name conversion logic lives in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), specifically within `normalizeToolCalls` and `normalizeToolCallBlock`.
- **Defensive fallbacks**: The system uses preference-based field resolution (`toolCallId` → `id`, `toolName` → `name`, `input` → `arguments`) to maintain backward compatibility with varying Pi engine versions.
- **Boundary enforcement**: Normalization is applied at data ingestion points—specifically in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) during session loading and [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) during realtime streaming—ensuring the UI never receives malformed tool call objects.

## Frequently Asked Questions

### What fields does the Pi engine use for tool calls versus the UI format?

The Pi engine uses `id`, `name`, and `arguments` fields, while the UI expects `toolCallId`, `toolName`, and `input`. The normalization layer in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) bridges these schemas using the `normalizeToolCallBlock` helper.

### What happens if a tool call block is missing the preferred `toolCallId` field?

According to line 11 of [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), the function falls back to checking `block.id`, and if that is also missing, it defaults to an empty string. This ensures the UI always receives a string value for the identifier.

### Why normalize at the session reader and hook level rather than in UI components?

Centralizing normalization at the data layer ([`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) and [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)) prevents type errors throughout the component tree and ensures that all React components can assume the `ToolCallContent` shape without defensive checks.

### How does the system handle non-tool-call content blocks?

The `normalizeToolCallBlock` helper checks the block type at line 8 and returns `null` for non-tool-call blocks. Consequently, `normalizeToolCalls` preserves the original block unchanged (line 29), allowing text and image content to pass through unmodified.