Files
kandev/apps/backend/docs/debugging_normalization.md
T
2026-01-31 19:45:33 +00:00

13 KiB

Debugging Tool Normalization Issues

This guide explains how to debug and fix issues where tool calls from agents are not rendering correctly in the frontend (e.g., a shell command showing as generic tool_call instead of tool_execute).

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Tool Call Normalization Flow                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Agent CLI          Adapter              Lifecycle           Orchestrator   │
│  ┌────────┐        ┌──────────┐         ┌─────────┐         ┌────────────┐  │
│  │ Codex  │──JSON──│ Codex    │──Event──│ Manager │──Event──│ Event      │  │
│  │ Claude │        │ Adapter  │         │         │         │ Handlers   │  │
│  │ etc.   │        └────┬─────┘         └────┬────┘         └─────┬──────┘  │
│  └────────┘             │                    │                    │         │
│                         │                    │                    │         │
│                         ▼                    ▼                    ▼         │
│                   NormalizedPayload    AgentEvent          Message DB       │
│                   ┌─────────────┐      ┌──────────┐        ┌──────────┐    │
│                   │ kind: ...   │ ───▶ │Normalized│ ──────▶│type: ... │    │
│                   │ shell_exec  │      │Payload   │        │tool_exec │    │
│                   └─────────────┘      └──────────┘        └────┬─────┘    │
│                                                                  │          │
│                                                                  ▼          │
│                                                           WebSocket Event   │
│                                                           ┌──────────────┐  │
│                                                           │type:tool_exec│  │
│                                                           └──────┬───────┘  │
│                                                                  │          │
│                                                                  ▼          │
│                                                             Frontend        │
│                                                           ┌──────────────┐  │
│                                                           │ToolExecute   │  │
│                                                           │ Message.tsx  │  │
│                                                           └──────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘

Key Files

Layer File Purpose
Types internal/agentctl/types/streams/tool_payload.go NormalizedPayload struct and factory functions
Codex Adapter internal/agentctl/server/adapter/transport/codex/adapter.go Converts Codex events to AgentEvent
Codex Normalizer internal/agentctl/server/adapter/transport/codex/normalize.go Creates NormalizedPayload for Codex tools
StreamJSON Adapter internal/agentctl/server/adapter/transport/streamjson/adapter.go Converts Claude Code events to AgentEvent
StreamJSON Normalizer internal/agentctl/server/adapter/transport/streamjson/normalize.go Creates NormalizedPayload for Claude tools
OpenCode Normalizer internal/agentctl/server/adapter/transport/opencode/normalize.go Creates NormalizedPayload for OpenCode tools
Lifecycle Events internal/agent/lifecycle/events.go Publishes AgentStreamEvent to event bus
Orchestrator Handlers internal/orchestrator/event_handlers.go Handles events and creates DB messages
Message Creator cmd/kandev/adapters.go toolKindToMessageType() maps kind to message type
Frontend Renderer apps/web/components/task/chat/message-renderer.tsx Routes message types to components

Tool Kind to Message Type Mapping

The NormalizedPayload.Kind() determines which frontend component renders the tool:

Tool Kind Message Type Frontend Component
read_file tool_read ToolReadMessage
code_search tool_search ToolSearchMessage
modify_file tool_edit ToolEditMessage
shell_exec tool_execute ToolExecuteMessage
manage_todos todo TodoMessage
subagent_task tool_call ToolSubagentMessage
generic tool_call ToolCallMessage
(nil/unknown) tool_call ToolCallMessage

Debugging Steps

1. Check Raw Events

Raw events are logged to raw-<protocol>-<agent>.jsonl files in the backend directory:

# View raw Codex events
tail -f raw-codex-codex.jsonl | jq .

# View raw Claude events
tail -f raw-streamjson-claude.jsonl | jq .

2. Check Normalized Events

Normalized events show what the adapter produced:

# View normalized events
tail -f normalized-codex-codex.jsonl | jq .

Look for the normalized field in tool_call events:

{
  "event": {
    "type": "tool_call",
    "normalized": {
      "kind": "shell_exec",
      "shell_exec": {
        "command": "ls -la",
        "work_dir": "/path/to/dir"
      }
    }
  }
}

If normalized is missing or kind is wrong, the issue is in the adapter.

3. Check Database Message Type

Query the database to see what message type was stored:

SELECT id, type, metadata->>'normalized' as normalized
FROM task_session_messages
WHERE metadata->>'tool_call_id' = '<tool_call_id>'
ORDER BY created_at DESC LIMIT 1;

If the type is tool_call but should be tool_execute, the issue is in the message creation flow.

4. Check WebSocket Events

Use browser DevTools Network tab to inspect WebSocket messages. Look for session.message.added events and check the type field.

Common Issues

Issue 1: Missing NormalizedPayload in tool_update Events

Symptom: Tool calls render as generic tool_call instead of specialized type.

Cause: The adapter sends tool_update events (e.g., on completion) without the NormalizedPayload. When there's a race condition and the message doesn't exist yet, the fallback creation uses tool_call as default.

Example (Codex):

// BAD - tool_update without normalized payload
update := AgentEvent{
    Type:       streams.EventTypeToolUpdate,
    ToolCallID: p.Item.ID,
    ToolStatus: "complete",
}

// GOOD - tool_update with normalized payload
update := AgentEvent{
    Type:              streams.EventTypeToolUpdate,
    ToolCallID:        p.Item.ID,
    ToolStatus:        "complete",
    NormalizedPayload: a.normalizer.NormalizeToolCall("commandExecution", args),
}

Fix Location: Adapter's notification handler (e.g., NotifyItemCompleted in Codex adapter).

Issue 2: Wrong Tool Name Passed to Normalizer

Symptom: Tool renders as tool_call (generic) instead of specialized type.

Cause: The normalizer's switch statement doesn't match the tool name.

Example:

// Normalizer expects "commandExecution" but receives "command_execution"
switch toolName {
case CodexItemCommandExecution:  // "commandExecution"
    return n.normalizeCommand(args)
default:
    return n.normalizeGeneric(toolName, args)  // Falls through here
}

Fix: Check the exact tool name string in the protocol and match it in the normalizer.

Issue 3: Factory Function Not Setting Kind

Symptom: normalized.Kind() returns empty or wrong value.

Cause: Factory function doesn't set the kind field correctly.

Example:

// BAD - missing kind
func NewShellExec(...) *NormalizedPayload {
    return &NormalizedPayload{
        shellExec: &ShellExecPayload{...},
    }
}

// GOOD - kind is set
func NewShellExec(...) *NormalizedPayload {
    return &NormalizedPayload{
        kind:      ToolKindShellExec,
        shellExec: &ShellExecPayload{...},
    }
}

Fix Location: internal/agentctl/types/streams/tool_payload.go

Issue 4: toolKindToMessageType Not Handling New Kind

Symptom: New tool kind renders as generic tool_call.

Cause: The toolKindToMessageType function doesn't have a case for the new kind.

Fix Location: Two places need updating:

  1. internal/orchestrator/event_handlers.go - toolKindToMessageType()
  2. cmd/kandev/adapters.go - toolKindToMessageType()

Issue 5: Missing UnmarshalJSON for NormalizedPayload

Symptom: All tool calls render as tool_call even though adapter logs show correct kind.

Cause: The NormalizedPayload struct has unexported fields. Without a custom UnmarshalJSON method, the standard library cannot populate these fields when deserializing events from WebSocket/JSON.

Example:

// NormalizedPayload with unexported fields requires both methods:
func (p *NormalizedPayload) MarshalJSON() ([]byte, error) { ... }
func (p *NormalizedPayload) UnmarshalJSON(data []byte) error { ... }  // REQUIRED!

Fix Location: internal/agentctl/types/streams/tool_payload.go

How to verify: Run the round-trip test:

go test ./internal/agentctl/types/streams/... -v -run MarshalRoundTrip

Adding a New Tool Kind

  1. Add kind constant in internal/agentctl/types/streams/tool_payload.go:

    const (
        ToolKindMyNewTool ToolKind = "my_new_tool"
    )
    
  2. Add payload struct with fields:

    type MyNewToolPayload struct {
        SomeField string `json:"some_field"`
    }
    
  3. Add unexported field to NormalizedPayload:

    type NormalizedPayload struct {
        kind       ToolKind
        myNewTool  *MyNewToolPayload
        // ...
    }
    
  4. Add getter method:

    func (p *NormalizedPayload) MyNewTool() *MyNewToolPayload { return p.myNewTool }
    
  5. Add to MarshalJSON:

    type jsonPayload struct {
        MyNewTool *MyNewToolPayload `json:"my_new_tool,omitempty"`
        // ...
    }
    
  6. Add factory function:

    func NewMyNewTool(someField string) *NormalizedPayload {
        return &NormalizedPayload{
            kind: ToolKindMyNewTool,
            myNewTool: &MyNewToolPayload{
                SomeField: someField,
            },
        }
    }
    
  7. Update normalizers in each adapter to call the factory function.

  8. Update toolKindToMessageType in both locations:

    case streams.ToolKindMyNewTool:
        return "tool_my_new"
    
  9. Add frontend component and update message-renderer.tsx:

    {
        matches: (comment) => comment.type === 'tool_my_new',
        render: (comment) => <ToolMyNewMessage comment={comment} />,
    },
    

Testing

Unit Tests

Run normalizer tests:

go test ./internal/agentctl/server/adapter/transport/streamjson/... -v
go test ./internal/agentctl/server/adapter/transport/codex/... -v

Integration Testing

  1. Start a session with the agent
  2. Trigger the tool you're testing
  3. Check normalized events file
  4. Check database message type
  5. Verify frontend renders correct component

Checklist for Fixing Normalization Issues

  • Check raw events to confirm agent sends expected data
  • Check normalized events to confirm adapter produces correct payload
  • Verify NormalizedPayload.Kind() returns correct kind
  • Verify tool_call event includes NormalizedPayload
  • Verify tool_update event includes NormalizedPayload (for race condition handling)
  • Check toolKindToMessageType handles the kind in both files
  • Verify database stores correct message type
  • Verify frontend message-renderer.tsx has matching adapter