# How normalizeToolCalls Handles Field Name Mismatches Between Pi and the Web UI

> Learn how normalizeToolCalls resolves field name mismatches between Pi and the Web UI. This function ensures tool calls render correctly by mapping legacy names to the expected schema.

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

---

**The `normalizeToolCalls` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) bridges the gap between the Pi SDK's legacy field names (`id`, `name`, `arguments`) and the web UI's expected schema (`toolCallId`, `toolName`, `input`), ensuring tool calls render correctly across the application.**

When building AI applications with the `agegr/pi-web` repository, developers encounter a field name mismatch between the Pi SDK's tool call blocks and the TypeScript types defined for the web UI. The `normalizeToolCalls` utility serves as the single source of truth for translating these incompatible data shapes, preventing rendering errors and enabling seamless tool result processing.

## Why Field Name Translation Is Necessary

The Pi SDK emits tool call blocks using a legacy JSON structure:

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

```

However, the web UI's `ToolCallContent` type expects a different schema:

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

```

Without normalization, the UI treats these blocks as opaque objects, breaking tool result rendering and downstream logic that depends on the standardized shape.

## How normalizeToolCalls Works

The implementation in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) applies a defensive transformation strategy that preserves data integrity while reconciling the two schemas.

### Scope Filtering

The function only processes messages where `msg.role === "assistant"`. All other roles—including `user`, `toolResult`, and `bashExecution`—pass through unchanged. This narrow scope prevents accidental transformation of non-assistant payloads that might contain similarly named fields with different semantics.

### Block-Level Conversion

For each element in the assistant message's `content` array, `normalizeToolCalls` delegates to `normalizeToolCallBlock`. This modular approach allows granular control over individual block transformation while maintaining the overall message structure.

### Field Mapping Logic

Inside `normalizeToolCallBlock`, the function performs intelligent field mapping with fallbacks:

- **`toolCallId`** – Uses `block.toolCallId` when present; otherwise falls back to `block.id`.
- **`toolName`** – Uses `block.toolName` when 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, supplies an empty object `{}`.

### Graceful Fallbacks

If a block's `type` property is not `"toolCall"`, the function returns the original block untouched. This prevents data loss for text blocks, image blocks, and other content types that should not undergo transformation.

## Code Implementation Examples

The normalization layer integrates at three critical points in the application lifecycle.

### Direct Message Normalization

Import the utility directly to normalize raw assistant messages before processing:

```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" }
// }

```

### Session Reader Integration

In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), the function normalizes messages when converting session entries to UI messages at lines 306‑308:

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

```

### Real-time SSE Processing

The [`useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/useAgentSession.ts) hook normalizes incoming Server-Sent Events before dispatching to React state at lines 973, 989, and 1003:

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

```

## Summary

- **`normalizeToolCalls`** lives in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) and acts as the bridge between Pi SDK output and web UI expectations.
- It only transforms **assistant role** messages, leaving user and tool messages untouched.
- The function maps legacy fields (`id`, `name`, `arguments`) to modern fields (`toolCallId`, `toolName`, `input`) with intelligent fallbacks.
- Integration points include session reading ([`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)) and real-time SSE handling ([`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)).
- Non-toolCall blocks pass through unchanged, preventing accidental data corruption.

## Frequently Asked Questions

### What happens if the input field is not an object?

If `block.input` is not an object, the function attempts to use `block.arguments`. If neither field contains an object, it supplies an empty object `{}` as the `input` value, ensuring the UI always receives a valid `Record<string, unknown>` type and preventing runtime errors during tool execution.

### Does normalizeToolCalls modify messages from users or tools?

No. The function explicitly checks `msg.role === "assistant"` and returns all other message types unchanged. This prevents accidental transformation of user messages, tool results, or bash execution blocks that might contain fields named `id` or `name` with different semantic meanings.

### Where is the source of truth for the normalized tool call shape?

The `ToolCallContent` TypeScript type in the UI defines the canonical shape expected after normalization, requiring `toolCallId`, `toolName`, and `input` fields. The `normalizeToolCalls` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) serves as the runtime implementation that enforces this contract, making it the single source of truth for the conversion logic across the application.

### Why not update the Pi SDK instead of normalizing in the UI?

Updating the SDK would require coordinated changes across multiple services and potentially break existing consumers. By normalizing client-side in `agegr/pi-web`, the application maintains backward compatibility with existing Pi backend deployments while allowing the UI to evolve its type definitions independently. This approach follows the adapter pattern, isolating schema differences at the integration boundary.