# How Pi Web Normalizes Tool‑Call Fields Between Its Session Format and Internal Types

> Discover how Pi Web normalizes toolCall fields, mapping legacy SDK names to its internal ToolCallContent type. Learn about backward compatibility and type safety in this technical guide.

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

---

**Pi Web uses a centralized `normalizeToolCalls` function to map legacy Pi SDK field names (`id`, `name`, `arguments`) to its stricter internal `ToolCallContent` type (`toolCallId`, `toolName`, `input`), ensuring backward compatibility with older session files while maintaining type safety throughout the application.**

The Pi Web repository (agegr/pi-web) handles AI assistant conversations that include tool invocations—function calls the model makes to retrieve data or perform actions. These tool calls arrive from the Pi SDK in various formats depending on when the session was created. The codebase solves this schema drift through a dedicated normalization layer that bridges external and internal representations.

## The Field Mapping Challenge

Tool‑call data in Pi Web sessions can contain inconsistent property names across different file versions:

| External Field | Possible Names | Internal Field |
|:-------------:|:-------------|:--------------|
| Identifier | `toolCallId` or legacy `id` | `toolCallId` |
| Tool name | `toolName` or legacy `name` | `toolName` |
| Arguments | `input` or legacy `arguments` | `input` |

The internal `ToolCallContent` type enforces stricter naming. Rather than scattering conditional checks throughout the UI components, Pi Web centralizes this conversion in a single utility that runs at data ingestion points.

## Core Normalization Logic in lib/normalize.ts

The `normalizeToolCalls` function implements a three‑step pipeline for **assistant messages only**:

### Step 1: Role Guard

```typescript
if (msg.role !== "assistant") return msg;

```

Non‑assistant messages pass through unchanged. This guard prevents unnecessary processing and ensures type narrowing for the content array.

### Step 2: Block‑Level Transformation

Each content block flows through `normalizeToolCallBlock`:

```typescript
const normalized = content.map(block => normalizeToolCallBlock(block) ?? block);

```

The `?? block` fallback preserves non‑tool‑call content blocks (text, images, etc.).

### Step 3: Field‑Level Mapping

The actual field normalization performs explicit type checks with cascading fallbacks:

```typescript
toolCallId: typeof block.toolCallId === "string"
    ? block.toolCallId
    : (typeof block.id === "string" ? block.id : ""),
toolName: typeof block.toolName === "string"
    ? block.toolName
    : (typeof block.name === "string" ? block.name : ""),
input: typeof block.input === "object" && block.input !== null && !Array.isArray(block.input)
    ? block.input
    : (typeof block.arguments === "object" && block.arguments !== null && !Array.isArray(block.arguments)
        ? block.arguments
        : {}),

```

*Source:* [lib/normalize.ts#L7-L18](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts#L7-L18)

This pattern prioritizes modern field names while safely degrading to legacy equivalents. Missing values resolve to empty strings or empty objects rather than `undefined`, preventing downstream runtime errors.

The complete function packages the normalized blocks into a new message object:

*Source:* [lib/normalize.ts#L21-L32](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts#L21-L32)

## Where Normalization Runs in Pi Web

Pi Web applies `normalizeToolCalls` at three critical data boundaries:

### Session File Loading

The session reader ingests `.jsonl` session files and normalizes each assistant message before it reaches the UI layer:

*Source:* [lib/session-reader.ts#L303-L308](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts#L303-L308)

### Streaming Response Handling

Real‑time assistant messages from the backend stream undergo normalization before processing:

*Source:* [lib/streaming-message.ts#L100-L106](https://github.com/agegr/pi-web/blob/main/lib/streaming-message.ts#L100-L106)

### React State Synchronization

The `useAgentSession` hook normalizes completed messages before updating application state, ensuring components like `MessageView` receive consistently‑shaped data:

*Source:* [hooks/useAgentSession.ts#L1130-L1155](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts#L1130-L1155)

## Practical Code Examples

### Normalizing a Legacy Session Message

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

const rawLegacyMessage = {
  role: "assistant",
  content: [
    {
      type: "toolCall",
      id: "call_abc123",           // legacy identifier field
      name: "web_search",          // legacy name field
      arguments: { query: "pi web normalization" }  // legacy arguments field
    }
  ],
  model: "gpt-4",
  provider: "openai",
};

const normalized = normalizeToolCalls(rawLegacyMessage);

// Result: content[0] now contains { toolCallId, toolName, input }
console.log(normalized.content[0]);
// {
//   type: "toolCall",
//   toolCallId: "call_abc123",
//   toolName: "web_search",
//   input: { query: "pi web normalization" }
// }

```

### Streaming Handler Integration

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

socket.onmessage = (event) => {
  const payload = JSON.parse(event.data);
  
  if (payload.role === "assistant") {
    const safeMessage = normalizeToolCalls(payload);
    dispatch({ type: "ADD_MESSAGE", payload: safeMessage });
  }
};

```

## Key Architectural Benefits

- **Backward compatibility**: Older Pi session files with legacy field names load without migration scripts
- **Type safety downstream**: Components can trust that `toolCallId`, `toolName`, and `input` exist with correct types
- **Single maintenance point**: Field mapping logic lives in one file rather than scattered across the codebase
- **Runtime durability**: Explicit `typeof` checks and fallback defaults prevent crashes on malformed data

## Summary

- **Pi Web normalize tool‑call fields** through the `normalizeToolCalls` utility in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)
- The function maps `id`→`toolCallId`, `name`→`toolName`, and `arguments`→`input` with runtime type guards
- Normalization applies exclusively to assistant messages at three ingestion points: session reading, streaming, and React state updates
- Empty defaults (`""`, `{}`) replace missing values to maintain structural guarantees
- This architecture supports both current Pi SDK schemas and legacy session formats without code duplication

## Frequently Asked Questions

### What happens if a tool‑call block has neither modern nor legacy field names?

The normalization returns empty defaults: `toolCallId: ""`, `toolName: ""`, and `input: {}`. This prevents `undefined` errors in downstream components while preserving the message structure. The UI can then render a placeholder or warning for malformed tool calls.

### Does normalization modify the original message object?

No. `normalizeToolCalls` returns a new object with a shallow copy of the message properties and a replaced `content` array. The original message remains unmutated, supporting React's immutable update patterns and enabling time‑travel debugging.

### Why is normalization restricted to assistant messages?

User messages, system messages, and tool results follow different schemas that do not contain tool‑call blocks. The role guard (`if (msg.role !== "assistant")`) avoids unnecessary computation and prevents accidental transformation of unrelated content structures.