diff --git a/internal/parser/parser.go b/internal/parser/parser.go index bae23c6..260d1fe 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -65,10 +65,10 @@ type UserMessage struct { // ToolResult represents a tool result in a user message type ToolResult struct { - Type string `json:"type"` - ToolUseID string `json:"tool_use_id"` - Content string `json:"content"` - IsError bool `json:"is_error"` + Type string `json:"type"` + ToolUseID string `json:"tool_use_id"` + Content json.RawMessage `json:"content"` + IsError bool `json:"is_error"` } // ToolInput represents the input field for various tools @@ -184,7 +184,7 @@ func parseUserMessage(raw RawMessage, timestamp time.Time) []StreamItem { AgentID: raw.AgentID, AgentName: agentName, Timestamp: timestamp, - Content: result.Content, + Content: extractToolResultContent(result.Content), ToolID: result.ToolUseID, }) } @@ -193,6 +193,38 @@ func parseUserMessage(raw RawMessage, timestamp time.Time) []StreamItem { return items } +// extractToolResultContent handles both string and array-of-blocks content. +// Built-in tools return a plain string; MCP tools return [{"type":"text","text":"..."}]. +func extractToolResultContent(raw json.RawMessage) string { + if len(raw) == 0 { + return "" + } + + // Try as plain string first (built-in tools) + var s string + if err := json.Unmarshal(raw, &s); err == nil { + return s + } + + // Try as array of content blocks (MCP tools) + var blocks []struct { + Type string `json:"type"` + Text string `json:"text"` + } + if err := json.Unmarshal(raw, &blocks); err == nil { + var parts []string + for _, b := range blocks { + if b.Text != "" { + parts = append(parts, b.Text) + } + } + return strings.Join(parts, "\n") + } + + // Fallback: return raw JSON + return string(raw) +} + func formatToolInput(toolName string, inputRaw json.RawMessage) string { var input ToolInput if err := json.Unmarshal(inputRaw, &input); err != nil { diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 0fc49f7..b6a7eae 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -178,6 +178,43 @@ func TestParseLine_UserToolResult(t *testing.T) { } } +func TestParseLine_MCPToolResult(t *testing.T) { + // MCP tools return content as an array of content blocks, not a plain string + line := `{"type":"user","timestamp":"2025-01-01T12:00:00Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_mcp1","content":[{"type":"text","text":"MCP result here"}]}]}}` + items, err := ParseLine(line) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 1 { + t.Fatalf("expected 1 item, got %d", len(items)) + } + item := items[0] + if item.Type != TypeToolOutput { + t.Errorf("type = %q, want %q", item.Type, TypeToolOutput) + } + if item.ToolID != "toolu_mcp1" { + t.Errorf("toolID = %q, want %q", item.ToolID, "toolu_mcp1") + } + if item.Content != "MCP result here" { + t.Errorf("content = %q, want %q", item.Content, "MCP result here") + } +} + +func TestParseLine_MCPToolResultMultiBlock(t *testing.T) { + // MCP tools can return multiple content blocks + line := `{"type":"user","timestamp":"2025-01-01T12:00:00Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_mcp2","content":[{"type":"text","text":"block one"},{"type":"text","text":"block two"}]}]}}` + items, err := ParseLine(line) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 1 { + t.Fatalf("expected 1 item, got %d", len(items)) + } + if items[0].Content != "block one\nblock two" { + t.Errorf("content = %q, want %q", items[0].Content, "block one\nblock two") + } +} + func TestParseLine_SubagentMessage(t *testing.T) { line := `{"type":"assistant","agentId":"abc1234567890","timestamp":"2025-01-01T12:00:00Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"subagent thinking"}]}}` items, err := ParseLine(line)