Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
}
Expand All @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading