diff --git a/codeagent-wrapper/parser.go b/codeagent-wrapper/parser.go index 79388f9..5f186ca 100644 --- a/codeagent-wrapper/parser.go +++ b/codeagent-wrapper/parser.go @@ -67,6 +67,34 @@ type codexHeader struct { } `json:"item,omitempty"` } +// UnifiedEvent combines all backend event formats into a single structure +// to avoid multiple JSON unmarshal operations per event +type UnifiedEvent struct { + // Common fields + Type string `json:"type"` + + // Codex-specific fields + ThreadID string `json:"thread_id,omitempty"` + Item json.RawMessage `json:"item,omitempty"` // Lazy parse + + // Claude-specific fields + Subtype string `json:"subtype,omitempty"` + SessionID string `json:"session_id,omitempty"` + Result string `json:"result,omitempty"` + + // Gemini-specific fields + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + Delta bool `json:"delta,omitempty"` + Status string `json:"status,omitempty"` +} + +// ItemContent represents the parsed item.text field for Codex events +type ItemContent struct { + Type string `json:"type"` + Text interface{} `json:"text"` +} + func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(string), onMessage func()) (message, threadID string) { reader := bufio.NewReaderSize(r, jsonLineReaderSize) @@ -112,71 +140,71 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin continue } - var codex codexHeader - if err := json.Unmarshal(line, &codex); err == nil { - isCodex := codex.ThreadID != "" || (codex.Item != nil && codex.Item.Type != "") - if isCodex { - var details []string - if codex.ThreadID != "" { - details = append(details, fmt.Sprintf("thread_id=%s", codex.ThreadID)) - } - if codex.Item != nil && codex.Item.Type != "" { - details = append(details, fmt.Sprintf("item_type=%s", codex.Item.Type)) - } - if len(details) > 0 { - infoFn(fmt.Sprintf("Parsed event #%d type=%s (%s)", totalEvents, codex.Type, strings.Join(details, ", "))) - } else { - infoFn(fmt.Sprintf("Parsed event #%d type=%s", totalEvents, codex.Type)) - } + // Single unmarshal for all backend types + var event UnifiedEvent + if err := json.Unmarshal(line, &event); err != nil { + warnFn(fmt.Sprintf("Failed to parse event: %s", truncateBytes(line, 100))) + continue + } - switch codex.Type { - case "thread.started": - threadID = codex.ThreadID - infoFn(fmt.Sprintf("thread.started event thread_id=%s", threadID)) - case "item.completed": - itemType := "" - if codex.Item != nil { - itemType = codex.Item.Type - } + // Detect backend type by field presence + isCodex := event.ThreadID != "" || len(event.Item) > 0 + isClaude := event.Subtype != "" || event.Result != "" + isGemini := event.Role != "" || event.Delta - if itemType == "agent_message" { - var event JSONEvent - if err := json.Unmarshal(line, &event); err != nil { - warnFn(fmt.Sprintf("Failed to parse Codex event: %s", truncateBytes(line, 100))) - continue - } + // Handle Codex events + if isCodex { + var details []string + if event.ThreadID != "" { + details = append(details, fmt.Sprintf("thread_id=%s", event.ThreadID)) + } - normalized := "" - if event.Item != nil { - normalized = normalizeText(event.Item.Text) - } + // Parse item type if present + var itemType string + if len(event.Item) > 0 { + var itemHeader struct { + Type string `json:"type"` + } + if err := json.Unmarshal(event.Item, &itemHeader); err == nil { + itemType = itemHeader.Type + details = append(details, fmt.Sprintf("item_type=%s", itemType)) + } + } + + if len(details) > 0 { + infoFn(fmt.Sprintf("Parsed event #%d type=%s (%s)", totalEvents, event.Type, strings.Join(details, ", "))) + } else { + infoFn(fmt.Sprintf("Parsed event #%d type=%s", totalEvents, event.Type)) + } + + switch event.Type { + case "thread.started": + threadID = event.ThreadID + infoFn(fmt.Sprintf("thread.started event thread_id=%s", threadID)) + + case "item.completed": + if itemType == "agent_message" && len(event.Item) > 0 { + // Lazy parse: only parse item content when needed + var item ItemContent + if err := json.Unmarshal(event.Item, &item); err == nil { + normalized := normalizeText(item.Text) infoFn(fmt.Sprintf("item.completed event item_type=%s message_len=%d", itemType, len(normalized))) if normalized != "" { codexMessage = normalized notifyMessage() } } else { - infoFn(fmt.Sprintf("item.completed event item_type=%s", itemType)) + warnFn(fmt.Sprintf("Failed to parse item content: %s", err.Error())) } + } else { + infoFn(fmt.Sprintf("item.completed event item_type=%s", itemType)) } - continue } - } - - var raw map[string]json.RawMessage - if err := json.Unmarshal(line, &raw); err != nil { - warnFn(fmt.Sprintf("Failed to parse line: %s", truncateBytes(line, 100))) continue } - switch { - case hasKey(raw, "subtype") || hasKey(raw, "result"): - var event ClaudeEvent - if err := json.Unmarshal(line, &event); err != nil { - warnFn(fmt.Sprintf("Failed to parse Claude event: %s", truncateBytes(line, 100))) - continue - } - + // Handle Claude events + if isClaude { if event.SessionID != "" && threadID == "" { threadID = event.SessionID } @@ -187,14 +215,11 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin claudeMessage = event.Result notifyMessage() } + continue + } - case hasKey(raw, "role") || hasKey(raw, "delta"): - var event GeminiEvent - if err := json.Unmarshal(line, &event); err != nil { - warnFn(fmt.Sprintf("Failed to parse Gemini event: %s", truncateBytes(line, 100))) - continue - } - + // Handle Gemini events + if isGemini { if event.SessionID != "" && threadID == "" { threadID = event.SessionID } @@ -205,10 +230,11 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin } infoFn(fmt.Sprintf("Parsed Gemini event #%d type=%s role=%s delta=%t status=%s content_len=%d", totalEvents, event.Type, event.Role, event.Delta, event.Status, len(event.Content))) - - default: - warnFn(fmt.Sprintf("Unknown event format: %s", truncateBytes(line, 100))) + continue } + + // Unknown event format + warnFn(fmt.Sprintf("Unknown event format: %s", truncateBytes(line, 100))) } switch {