# How `normalizeToolCalls` Handles Field Name Mismatches in Pi Web: A Deep Dive into the Pi-to-UI Translation Layer

> Learn how normalizeToolCalls in Pi Web automatically maps legacy SDK field names like id and name to Pi-Web UI fields like toolCallId and toolName for correct tool-call rendering.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: deep-dive
- Published: 2026-08-14

---

**`normalizeToolCalls` is a utility function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) that automatically maps legacy Pi SDK field names (`id`, `name`, `arguments`) to the Pi-Web UI's expected fields (`toolCallId`, `toolName`, `input`), ensuring tool-call payloads render correctly regardless of their source format.**

The Pi backend and the Pi-Web UI don't always agree on field names. When the Pi SDK emits a tool-call block, it uses one vocabulary; the TypeScript types in the web interface expect another. This field name mismatch would break tool rendering and downstream logic if left unhandled. The `normalizeToolCalls` function bridges this gap, acting as a single source of truth for converting raw Pi messages into UI-ready shapes.

## Why Field Name Translation Is Necessary

The Pi SDK serializes tool calls using this structure:

```json
{
  "type": "toolCall",
  "id": "<uuid>",
  "name": "<tool>",
  "arguments": { ... }
}

```

However, the Pi-Web UI's `ToolCallContent` type expects:

```ts
{
  type: "toolCall";
  toolCallId: string;
  toolName: string;
  input: Record<string, unknown>;
}

```

Without normalization, the UI treats incoming tool-call blocks as opaque objects. Tool results fail to render, and any component depending on the normalized shape—such as execution tracers or input inspectors—receives incomplete data.

## How `normalizeToolCalls` Resolves Field Name Mismatches

Located in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), the function applies a scoped, block-level transformation that preserves non-tool content while intelligently mapping legacy fields.

### Scope Filtering by Message Role

The function only processes messages where `msg.role === "assistant"`. All other roles—including `user`, `toolResult`, and `bashExecution`—pass through unchanged. This prevents accidental mutation of payloads that never contain tool-call blocks.

### Block-Level Conversion via `normalizeToolCallBlock`

For each element in an assistant message's `content` array, `normalizeToolCalls` delegates to `normalizeToolCallBlock`. This inner function performs the actual field mapping:

- **`toolCallId`** – Uses `block.toolCallId` if present; otherwise falls back to `block.id`
- **`toolName`** – Uses `block.toolName` if present; otherwise falls back to `block.name`
- **`input`** – Uses `block.input` when it is an object; otherwise falls back to `block.arguments`. If neither is an object, returns `{}`

### Graceful Handling of Non-Tool Blocks

If `block.type !== "toolCall"`, the original block is returned untouched. This ensures text blocks, image blocks, and future block types survive normalization without data loss.

### Output: A New AgentMessage

The function returns a shallow copy of the input message with a transformed `content` array. Original tool-call blocks are replaced with normalized versions; all other elements maintain their position and identity.

## Where `normalizeToolCalls` Is Used in Pi Web

Three critical locations depend on this utility, guaranteeing consistent behavior across data ingestion paths.

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

When converting persisted session entries to UI messages, the normalizer runs first. Lines 306–308 apply it unconditionally, with an optional wrapper to strip base64 images:

```ts
// lib/session-reader.ts (excerpt)
const message = options.deferToolResultImages
  ? omitToolResultBase64Images(normalizeToolCalls(entry.message))
  : normalizeToolCalls(entry.message);

```

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

Incoming server-sent events are normalized before React state updates. The hook invokes `normalizeToolCalls` at lines 973, 989, and 1003:

```ts
// hooks/useAgentSession.ts (excerpt)
dispatch({ type: "update", message: normalizeToolCalls(msg as AgentMessage) });

```

### Direct Utility Import

Components and utilities can import and call the function directly for ad-hoc normalization needs.

## Code Examples

### Normalizing a Raw Assistant Message

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

const rawMsg: AssistantMessage = {
  role: "assistant",
  content: [
    { type: "toolCall", id: "42", name: "search", arguments: { query: "foo" } },
    { type: "text", text: "Here are the results." }
  ]
};

const normalized = normalizeToolCalls(rawMsg);
// normalized.content[0] now contains:
// {
//   type: "toolCall",
//   toolCallId: "42",
//   toolName: "search",
//   input: { query: "foo" }
// }

```

### Key Source Files

| File | Purpose |
|------|---------|
| [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) | Implements `normalizeToolCalls` and `normalizeToolCallBlock` |
| [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) | Applies normalization when reading session entries |
| [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) | Normalizes SSE payloads before state dispatch |

## Summary

- **`normalizeToolCalls` lives in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)** and serves as the canonical translator between Pi SDK and Pi-Web UI field names
- **Only assistant messages are transformed**, with role-based guarding preventing side effects on other message types
- **Triple fallback strategy** for `toolCallId`, `toolName`, and `input` ensures compatibility with both legacy and modern payload shapes
- **Three integration points**—session reader, React hook, and direct import—share one implementation, eliminating drift
- **Non-tool blocks pass through unchanged**, preserving forward compatibility with new content types

## Frequently Asked Questions

### What happens if a tool-call block has both `id` and `toolCallId`?

The `normalizeToolCallBlock` function prioritizes `toolCallId` when present, falling back to `id` only when the modern field is absent. This allows gradual migration: new code can emit `toolCallId` immediately, while legacy paths continue working with `id`.

### Does `normalizeToolCalls` modify the original message object?

No. The function returns a new `AgentMessage` with a new `content` array. The original message and its blocks remain unmodified, supporting immutable update patterns in React and other frameworks.

### Why not update the Pi SDK to match the UI types directly?

The normalization layer decouples deployment schedules. The Pi backend and Pi-Web UI can ship independently; `normalizeToolCalls` absorbs the impedance mismatch without requiring synchronized releases across services.

### What fields are considered for the `input` mapping?

`normalizeToolCallBlock` checks `block.input` first. If that field exists and is an object, it is used. Otherwise, it falls back to `block.arguments`. If neither is an object, it returns `{}`, ensuring `input` always satisfies the `Record<string, unknown>` type constraint.