How normalizeToolCalls Handles Field Name Mismatches Between Pi and the Web UI
The normalizeToolCalls function in 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:
{
"type": "toolCall",
"id": "<uuid>",
"name": "<tool>",
"arguments": { "key": "value" }
}
However, the web UI's ToolCallContent type expects a different schema:
{
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 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– Usesblock.toolCallIdwhen present; otherwise falls back toblock.id.toolName– Usesblock.toolNamewhen present; otherwise falls back toblock.name.input– Usesblock.inputwhen it is an object; otherwise falls back toblock.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:
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, the function normalizes messages when converting session entries to UI messages at lines 306‑308:
// lib/session-reader.ts
const message = options.deferToolResultImages
? omitToolResultBase64Images(normalizeToolCalls(entry.message))
: normalizeToolCalls(entry.message);
Real-time SSE Processing
The useAgentSession.ts hook normalizes incoming Server-Sent Events before dispatching to React state at lines 973, 989, and 1003:
// hooks/useAgentSession.ts
dispatch({ type: "update", message: normalizeToolCalls(msg as AgentMessage) });
Summary
normalizeToolCallslives inlib/normalize.tsand 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) and real-time SSE handling (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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →