How Pi-Web Normalizes Tool Call Field Names Between Pi and UI Formats

Pi-Web normalizes tool call field names through the normalizeToolCalls utility in lib/normalize.ts, which maps Pi engine fields (id, name, arguments) to UI-compatible fields (toolCallId, toolName, input) with intelligent fallbacks.

The agegr/pi-web repository bridges the Pi AI engine with a React-based user interface, requiring careful transformation of message formats. When handling tool calls, the system must reconcile differences between the Pi engine's wire format and the UI's expected type definitions. This article examines how the codebase normalizes tool call field names to ensure type safety and consistent rendering across the application.

Why Tool Call Field Name Normalization Is Required

The Pi engine transmits tool calls using a specific wire format that differs from the UI's internal ToolCallContent type. The engine sends objects with the shape {type: "toolCall", id, name, arguments}, while the UI expects {toolCallId, toolName, input}. Directly passing the raw engine objects would violate TypeScript contracts and cause runtime rendering errors.

This impedance mismatch necessitates a centralized transformation layer that converts incoming Pi messages into the canonical UI format before they reach React components.

Core Normalization Logic in lib/normalize.ts

The normalization implementation resides in lib/normalize.ts, which exports two key functions: normalizeToolCalls (lines 21-31) and the helper normalizeToolCallBlock (lines 7-18).

Guarding Against Non-Assistant Messages

The normalizeToolCalls function first validates the message role before attempting transformation. According to lines 22-24, if the message role is not "assistant", the function returns the message unchanged. This guard clause prevents unnecessary processing of user or system messages that cannot contain tool calls.

For assistant messages, the function iterates over the content array (line 26) and passes each block to normalizeToolCallBlock.

Field Mapping with Fallback Strategy

Inside normalizeToolCallBlock (lines 9-18), the helper performs defensive field mapping with fallback chains to handle both legacy and current Pi engine formats:

  • toolCallId: Prefers block.toolCallId; falls back to block.id if undefined. If neither exists, defaults to an empty string (line 11).
  • toolName: Prefers block.toolName; falls back to block.name with the same empty string default (line 12).
  • input: Prefers block.input; falls back to block.arguments while ensuring the result is always an object. If neither field exists, it returns an empty object (lines 13-17).

If the block's type property is not "toolCall", the helper returns null, causing normalizeToolCalls to preserve the original block unchanged (line 29).

Integration Points in the Codebase

Normalization occurs at the boundaries where external Pi data enters the UI state layer, ensuring all downstream components receive consistently shaped objects.

Session Loading in lib/session-reader.ts

When loading historical sessions from JSONL files, the application calls normalizeToolCalls at lines 307-308 before storing messages in the UI state. This ensures that persisted sessions conform to current UI type definitions regardless of when they were originally generated.

Realtime Streaming in hooks/useAgentSession.ts

During live SSE (Server-Sent Events) streaming, every incoming assistant message undergoes normalization before dispatch. The hook applies normalizeToolCalls at lines 938, 954, and 968 to transform streaming tool calls into the UI format in real-time, preventing type mismatches during active conversations.

Practical Implementation Example

The following TypeScript example demonstrates the transformation performed by the normalization utilities:

// Raw Pi engine format (incoming wire data)
const rawBlock = {
  type: "toolCall",
  id: "abc123",
  name: "weather",
  arguments: { city: "Paris" },
};

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

const assistantMsg = {
  role: "assistant",
  content: [rawBlock],
};

const uiMsg = normalizeToolCalls(assistantMsg);
// uiMsg.content[0] now conforms to UI expectations:
// {
//   type: "toolCall",
//   toolCallId: "abc123",
//   toolName: "weather",
//   input: { city: "Paris" }
// }

Summary

  • Centralized transformation: All field name conversion logic lives in lib/normalize.ts, specifically within normalizeToolCalls and normalizeToolCallBlock.
  • Defensive fallbacks: The system uses preference-based field resolution (toolCallId → id, toolName → name, input → arguments) to maintain backward compatibility with varying Pi engine versions.
  • Boundary enforcement: Normalization is applied at data ingestion points—specifically in lib/session-reader.ts during session loading and hooks/useAgentSession.ts during realtime streaming—ensuring the UI never receives malformed tool call objects.

Frequently Asked Questions

What fields does the Pi engine use for tool calls versus the UI format?

The Pi engine uses id, name, and arguments fields, while the UI expects toolCallId, toolName, and input. The normalization layer in lib/normalize.ts bridges these schemas using the normalizeToolCallBlock helper.

What happens if a tool call block is missing the preferred toolCallId field?

According to line 11 of lib/normalize.ts, the function falls back to checking block.id, and if that is also missing, it defaults to an empty string. This ensures the UI always receives a string value for the identifier.

Why normalize at the session reader and hook level rather than in UI components?

Centralizing normalization at the data layer (lib/session-reader.ts and hooks/useAgentSession.ts) prevents type errors throughout the component tree and ensures that all React components can assume the ToolCallContent shape without defensive checks.

How does the system handle non-tool-call content blocks?

The normalizeToolCallBlock helper checks the block type at line 8 and returns null for non-tool-call blocks. Consequently, normalizeToolCalls preserves the original block unchanged (line 29), allowing text and image content to pass through unmodified.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →