Purpose of normalize.ts in Nodeterm: Unifying AI Agent Hook Events
The src/shared/agents/normalize.ts file in the Nodeterm repository serves as the central architecture layer that converts disparate raw hook payloads from Claude, Codex, Gemini, Copilot, Opencode, and Grok into a unified NormalizedAgentEvent schema, enabling consistent state management across all supported AI agents.
Managing multiple AI agents within a single terminal application requires handling incompatible webhook formats and event naming conventions. The normalize.ts module eliminates this fragmentation by exposing a single entry point—normalizeFor—that translates proprietary agent payloads into a standardized internal representation consumed by the UI and status store.
The Problem: Disparate Agent Hook Formats
Each AI agent integrated into Nodeterm emits hooks with unique field names, event types, and payload structures. Claude might emit UserPromptSubmit events, while Grok uses notification with different casing and metadata. Without normalization, every consumer in the codebase would need agent-specific parsing logic.
The normalize.ts file solves this by defining a unified event model through the NormalizedAgentEvent interface (lines 7-83) and the RawHookEnvelope helper (lines 85-91). This abstraction allows downstream components to reason about agent activity using consistent fields like sessionId, state, and task, regardless of which AI backend generated the event.
Per-Agent Normalization Logic
The module contains dedicated normalization functions for each supported agent, mapping proprietary events to high-level states (working, waiting, blocked, done).
Claude Normalization
The normalizeClaude function (lines 42-88) handles events like UserPromptSubmit, Stop, and PermissionRequest. It extracts the session_id from the raw payload and maps interaction states to the standardized schema while calculating durationMs for performance tracking.
// Example: Normalizing a Claude hook envelope
import { normalizeFor } from '@/shared/agents/normalize';
const rawClaude = {
nodeId: 'nt-123',
agentId: 'claude',
payload: {
hook_event_name: 'UserPromptSubmit',
session_id: 's1',
prompt: 'Explain the code',
},
};
const event = normalizeFor('claude', rawClaude);
// event => {
// nodeId: 'nt-123',
// agentId: 'claude',
// sessionId: 's1',
// kind: 'state',
// state: 'working',
// task: 'Explain the code',
// newTurn: true,
// }
Codex, Gemini, and Copilot
Similarly, normalizeCodex (lines 101-136), normalizeGemini (lines 188-229), and normalizeCopilot (lines 254-302) each implement agent-specific parsing logic. These functions handle tool use confirmations, permission requests, and completion events while normalizing metadata fields such as pendingId and toolUseId into the shared format.
Opencode and Grok Edge Cases
The normalizeOpencode function (lines 322-371) manages async sub-agent launches through the isAsyncSubagentLaunch helper (lines 138-140). Meanwhile, normalizeGrok (lines 384-439) utilizes grokRawFields (lines 664-679) to extract common fields from Grok's notification-heavy payload structure.
// Example: Handling a Grok notification
import { normalizeFor } from '@/shared/agents/normalize';
const rawGrok = {
nodeId: 'nt-456',
agentId: 'grok',
payload: {
hookEventName: 'notification',
notification_type: 'permission_prompt',
message: 'Tool permission requested',
level: 'info',
},
};
const event = normalizeFor('grok', rawGrok);
// event => null (notification suppressed per Grok rules)
The Dispatcher: normalizeFor Entry Point
Rather than calling individual normalizers directly, the rest of the codebase uses the normalizeFor function (lines 711-718) as a dispatcher. This function accepts an agentId and RawHookEnvelope, then routes to the appropriate agent-specific normalizer.
// Example: Using the dispatcher for any agent
function handleHook(env: RawHookEnvelope) {
const normalized = normalizeFor(env.agentId, env);
if (!normalized) return;
// Store the normalized event in the agent‑status store
// storeAgentEvent(normalized);
}
This design pattern ensures that components like agent-hook-listener.ts remain agnostic about the specific AI backend, consuming only the standardized NormalizedAgentEvent interface.
Related Files and Testing
The normalization layer relies on several supporting files:
src/shared/agents/config.ts– Defines theAgentIdtype and capability lists used by the normalizers to determine which features each agent supports.src/shared/agents/normalize.test.ts– Comprehensive unit tests verifying that each agent's normalizer correctly maps raw payloads to the expectedNormalizedAgentEventstructure.src/shared/agents/normalize.grok.test.ts– Specific test coverage for Grok's unique notification filtering logic.src/shared/agents/agent-hook-listener.ts– Consumes normalized events and updates the UI state accordingly.
Summary
src/shared/agents/normalize.tsacts as the translation layer between proprietary AI agent hooks and Nodeterm's internal event system.- Six dedicated normalizers—
normalizeClaude,normalizeCodex,normalizeGemini,normalizeCopilot,normalizeOpencode, andnormalizeGrok—handle agent-specific parsing logic. normalizeForprovides a single entry point that routes raw hooks to the correct normalizer based onagentId.- The
NormalizedAgentEventinterface unifies disparate payload formats into consistent fields includingsessionId,state,task, anddurationMs. - Helper functions like
isAsyncSubagentLaunchandgrokRawFieldsmanage edge cases for specific agent behaviors.
Frequently Asked Questions
What is the difference between RawHookEnvelope and NormalizedAgentEvent?
The RawHookEnvelope interface (lines 85-91) represents the incoming webhook structure containing nodeId, agentId, and an opaque payload specific to each AI provider. NormalizedAgentEvent (lines 7-83) is the standardized output format containing unified fields like sessionId, state, and metadata that the rest of the application consumes.
How does normalize.ts handle unsupported agent IDs?
The normalizeFor dispatcher (lines 711-718) selects normalizers based on the agentId field using a switch statement or mapping object. If an unknown agent ID is passed, the function returns null or undefined, preventing the application from crashing and allowing the hook listener to silently drop unsupported events.
Why does the Grok normalizer return null for certain notifications?
According to the source code in normalizeGrok (lines 384-439), certain Grok notification types—such as informational messages or permission prompts—are intentionally suppressed and return null. This filtering prevents noise in the agent status store, ensuring only actionable state changes (working, blocked, etc.) propagate through the system.
Which AI agents are supported by the normalize.ts module?
As implemented in eneskirca/nodeterm, the module supports six AI agents: Claude, Codex, Gemini, Copilot, Opencode, and Grok. Each has a dedicated normalization function that handles its specific hook event naming conventions and payload structures.
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 →