# How ToolCall Field Normalization Handles Schema Mismatches in pi-web

> Discover how pi-web's ToolCall field normalization resolves schema mismatches. Learn how it maps legacy fields to modern equivalents using a prioritized fallback strategy for seamless integration.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: technical-explanation
- Published: 2026-07-24

---

**The `normalizeToolCalls` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) bridges legacy and current schemas by mapping old field names (`id`, `name`, `arguments`) to their modern equivalents (`toolCallId`, `toolName`, `input`) using a prioritized fallback strategy.**

The `agegr/pi-web` repository maintains backward compatibility with historic session data while enforcing a strict TypeScript interface for tool-call interactions. When older session files contain tool-call blocks using deprecated field names, the normalization layer ensures these records conform to the current `ToolCallContent` model without requiring database migrations or manual intervention.

## Understanding the Schema Evolution

The pi-web codebase has transitioned between two JSON schemas for representing tool invocations. Legacy session files store tool metadata using the fields **`id`**, **`name`**, and **`arguments`**, whereas the current TypeScript definitions in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) expect **`toolCallId`**, **`toolName`**, and **`input`**. This divergence creates a compatibility challenge when loading historical conversations or processing streaming events from mixed sources.

Rather than maintaining separate parsing logic throughout the application, pi-web centralizes schema reconciliation within a single normalization utility. This approach isolates legacy complexity and provides a uniform API for downstream UI components.

## The Normalization Implementation

The core logic resides in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), which exports the `normalizeToolCalls` function. This utility processes message objects recursively, applying field remapping only where necessary while preserving data integrity through defensive fallbacks.

### Role-Based Filtering

Normalization applies exclusively to assistant messages. The function returns the original message unchanged if `msg.role !== "assistant"`, preventing unnecessary computation on user messages, tool results, or system notifications. This early exit pattern ensures that only content arrays potentially containing tool-call blocks undergo transformation.

### Field Mapping with Fallback Logic

For each content block where `block.type === "toolCall"`, the helper `normalizeToolCallBlock` executes a prioritized lookup sequence. The function checks for the modern field name first, then falls back to legacy alternatives, and finally applies safe defaults:

- **`toolCallId`** ← `block.toolCallId` ? : `block.id` ? : `""`
- **`toolName`** ← `block.toolName` ? : `block.name` ? : `""`
- **`input`** ← `block.input` ? : `block.arguments` ? : `{}`

This three-tier resolution guarantees that every `ToolCallContent` object emerges fully populated regardless of the source schema version. Non-tool-call blocks pass through untouched via the `result ?? block` pattern, maintaining the integrity of text, image, or other content types.

```typescript
// Example: Transforming legacy session data
const legacyBlock = {
  type: "toolCall",
  id: "call_123",           // deprecated field
  name: "web_search",       // deprecated field
  arguments: { query: "typescript" }  // deprecated field
};

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

const message = {
  role: "assistant",
  content: [legacyBlock]
};

const normalized = normalizeToolCalls(message);
// normalized.content[0] now conforms to current schema:
// {
//   type: "toolCall",
//   toolCallId: "call_123",
//   toolName: "web_search",
//   input: { query: "typescript" }
// }

```

## Integration Points

The normalization layer operates at two critical boundaries between storage and presentation, ensuring schema consistency regardless of data provenance.

### Session File Reading

In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), the `entryToUiMessage` function invokes `normalizeToolCalls` when converting stored session entries into UI-ready message objects. This invocation occurs immediately after retrieval and before any image optimization or rendering logic:

```typescript
import { normalizeToolCalls } from "./normalize";

function entryToUiMessage(entry: SessionEntry, opts) {
  // ...
  case "message": {
    const message = opts.deferToolResultImages
      ? omitToolResultBase64Images(normalizeToolCalls(entry.message))
      : normalizeToolCalls(entry.message);
    return message;
  }
}

```

By normalizing at the data access layer, the application ensures that all downstream components receive uniformly shaped objects even when reading files created by earlier versions of the software.

### Real-Time Event Handling

The same normalization utility processes live agent events within `ChatWindow.handleAgentEvent`. When streaming responses arrive from the agent runtime, they potentially contain tool-call blocks that must integrate seamlessly with historic conversation context. Applying `normalizeToolCalls` during event ingestion prevents schema collisions between newly generated tool calls and legacy session data displayed in the same thread.

## Summary

- **Schema bridging**: The `normalizeToolCalls` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) maps legacy fields (`id`, `name`, `arguments`) to current fields (`toolCallId`, `toolName`, `input`) using prioritized fallbacks.
- **Assistant-only processing**: Non-assistant messages bypass transformation entirely, optimizing performance for irrelevant message types.
- **Defensive defaults**: Empty strings and empty objects serve as ultimate fallbacks, preventing undefined values from propagating through the UI.
- **Dual integration**: Normalization occurs both when loading historic sessions ([`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)) and processing real-time events (`ChatWindow.handleAgentEvent`).
- **Type safety**: The implementation guarantees valid `ToolCallContent` objects per the definitions in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) regardless of input schema vintage.

## Frequently Asked Questions

### What happens if a tool-call block contains both old and new field names?

The normalization logic prioritizes modern field names. If both `toolCallId` and `id` exist in the same block, the function selects `toolCallId` and ignores the legacy `id` field. This precedence ensures that intentionally updated records retain their current values while legacy records receive appropriate migration.

### Does the normalization function modify the original message object?

The implementation returns a new message object with a replaced `content` array rather than mutating the input. This immutability pattern prevents side effects when the same session data is referenced by multiple components or cached for state comparison.

### Why is normalization restricted to assistant messages?

Tool-call blocks only appear within assistant-generated content according to the pi-web architecture. By checking `msg.role !== "assistant"` at the function entry point, the system avoids iterating through user messages or tool results that cannot contain the target block types, reducing computational overhead during session loading.

### How does pi-web handle tool-call blocks that are neither old nor new format?

The `normalizeToolCallBlock` helper returns the original block unchanged when it fails to match expected patterns, using the expression `result ?? block`. This pass-through behavior ensures that future schema extensions or custom block types do not trigger errors, maintaining forward compatibility with potential API evolution.