# How Tool‑Call Field Normalization Works Between Pi‑Web Session Files and `ToolCallContent` Types

> Discover how Pi-Web normalizes tool-call fields from session files to ToolCallContent types. Learn about the mapping of id to toolCallId, name to toolName, and JSON argument parsing.

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

---

**Tool‑call field normalization in Pi‑Web converts raw Pi SDK session data into the application's internal `ToolCallContent` type via the `normalizeToolCalls` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), mapping `id` to `toolCallId`, `name` to `toolName`, and parsing the JSON `arguments` string into `input`.**

The **Pi‑Web** repository (agegr/pi-web) provides a web interface for reviewing Pi SDK agent sessions. Session files persist tool invocations in Pi's native format, but the React‑based UI expects a normalized TypeScript structure. The **`normalizeToolCalls`** function bridges this gap, ensuring consistent data handling across file reads and streaming events.

---

## What Pi‑Web Normalizes: Two Different Schemas

Pi SDK session files (`.jsonl`) store tool calls with Pi's original field names and string‑encoded arguments:

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

```

The Pi‑Web application, however, works with the **`ToolCallContent`** type defined in **[`lib/pi-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/pi-types.ts)**:

```ts
export type ToolCallContent = {
  toolCallId: string;
  toolName: string;
  input: unknown;
};

```

**Field differences requiring normalization:**

- `id` → `toolCallId` (renamed for clarity)
- `name` → `toolName` (prefixed with "tool" for disambiguation)
- `arguments` (JSON string) → `input` (parsed JSON value)

---

## The `normalizeToolCalls` Function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)

The core transformation logic resides in **[`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)** at **line 57**. The function is deliberately small, pure, and safe to call repeatedly.

### How `normalizeToolCalls` works

1. **Filter to relevant messages** — Only processes messages where `role === "assistant"` and `toolCalls` exists. All other messages pass through unchanged.

2. **Map and transform each tool call** — Iterates `msg.toolCalls`, building new `ToolCallContent` objects with renamed fields and parsed JSON.

3. **Assign transformed array** — Replaces `msg.toolCalls` with the normalized `ToolCallContent[]` array.

4. **Return updated message** — Returns the mutated message for downstream consumption.

**Implementation excerpt from [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts):**

```typescript
export function normalizeToolCalls(msg: AgentMessage): AgentMessage {
  if (msg.role !== "assistant" || !msg.toolCalls) {
    return msg;
  }

  const normalized: ToolCallContent[] = msg.toolCalls.map(tc => ({
    toolCallId: tc.id,
    toolName: tc.name,
    input: JSON.parse(tc.arguments),
  }));

  msg.toolCalls = normalized;
  return msg;
}

```

---

## Where Normalization Is Applied

Pi‑Web calls `normalizeToolCalls` at critical boundaries between external data and internal state.

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

When loading historical sessions from disk, **[`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)** (lines 332‑333) applies normalization immediately after parsing each entry. This ensures archived sessions conform to current UI expectations regardless of when they were created.

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

Real‑time tool calls arrive via Server‑Sent Events (SSE). **[`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)** normalizes incoming payloads at **lines 1158 and 1172** before updating React state. This prevents malformed or outdated schemas from reaching UI components.

### UI component consumption

Components like [`MessageView.tsx`](https://github.com/agegr/pi-web/blob/main/MessageView.tsx) receive pre‑normalized messages. They reference `toolCallId`, `toolName`, and `input` directly without defensive parsing or field mapping.

---

## Why Field Normalization Matters

**Decoupling from Pi SDK evolution** — The Pi SDK may change its storage format, but Pi‑Web's transformation layer insulates the UI from upstream changes.

**TypeScript type safety** — The `ToolCallContent` type in [`lib/pi-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/pi-types.ts) provides compile‑time guarantees. Normalization ensures these guarantees hold for all data sources.

**Single JSON parse per payload** — Converting `arguments` → `input` once at the boundary eliminates repeated `JSON.parse` calls in rendering logic, reducing error surfaces and improving performance.

---

## Working with the Normalizer

Use `normalizeToolCalls` directly when importing raw session data from external sources or testing:

```typescript
import { normalizeToolCalls } from '@/lib/normalize';
import type { AgentMessage } from '@/lib/pi-types';

async function loadSessionEntry(entryPath: string): Promise<AgentMessage> {
  const raw = await fetch(entryPath).then(r => r.json());
  return normalizeToolCalls(raw as AgentMessage);
}

// Example: accessing normalized tool call data
const msg = await loadSessionEntry('/path/to/session/entry.jsonl');
console.log(msg.toolCalls?.[0].toolName);   // "weather"
console.log(msg.toolCalls?.[0].input);      // { location: "Berlin" }

```

---

## Summary

- **Raw Pi format** uses `id`, `name`, and string `arguments`; **Pi‑Web format** uses `toolCallId`, `toolName`, and parsed `input`.
- **`normalizeToolCalls`** in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) performs a pure, idempotent transformation between these schemas.
- Normalization occurs at **data boundaries**: [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) for files, [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) for streaming events.
- The pattern provides **type safety**, **schema isolation**, and **performance** by parsing JSON once at ingestion.

---

## Frequently Asked Questions

### What triggers tool‑call normalization in Pi‑Web?

Normalization runs automatically when session data enters the application—either from `.jsonl` files via [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) or from SSE streams via [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts). No manual invocation is required during normal operation.

### Why does Pi‑Web rename `id` to `toolCallId` and `name` to `toolName`?

The prefixes clarify purpose and prevent naming collisions. Messages contain multiple identifiers; `toolCallId` explicitly scopes the ID to the tool invocation, while `toolName` distinguishes the tool identifier from other names in the message context.

### What happens if `arguments` contains invalid JSON?

`normalizeToolCalls` calls `JSON.parse(tc.arguments)` directly. Invalid JSON will throw a `SyntaxError` at normalization time, causing the message to fail processing. This fail‑fast behavior prevents malformed data from propagating to UI components.

### Can `normalizeToolCalls` be called multiple times on the same message?

Yes. The function is idempotent for already‑normalized messages because it checks `msg.role !== "assistant" || !msg.toolCalls` first. Once normalized, `toolCalls` contains `ToolCallContent` objects without an `arguments` property, so subsequent calls return the message unchanged.