package agents import ( "context" "encoding/json" "log/slog" "strings" "g.fc.run/misc/byre/internal/codex" ) var codexToolItems = map[string]bool{ codex.ThreadItemTypeCommandExecution: true, codex.ThreadItemTypeFileChange: true, codex.ThreadItemTypeDynamicToolCall: true, codex.ThreadItemTypeMcpToolCall: true, codex.ThreadItemTypeWebSearch: true, } func (m *Manager) handleCodex(ctx context.Context, l *live, s *codex.Session, ev codex.Event) error { switch ev.Method { case codex.MethodItemAgentMessageDelta: n := codex.AgentMessageDeltaNotification{} err := json.Unmarshal(ev.Params, &n) if err != nil { return err } m.store.Broadcast(m.event(l, agentActor, "agent.delta", map[string]any{"text": n.Delta})) return nil case codex.MethodItemStarted: n := codex.ItemStartedNotification{} err := json.Unmarshal(ev.Params, &n) if err != nil { return err } if !codexToolItems[n.Item.Type] { return nil } name, input, err := codexToolCall(n.Item) if err != nil { return err } return m.emit(ctx, l, agentActor, "agent.tool_call", map[string]any{"id": n.Item.ID, "name": name, "input": input}) case codex.MethodItemCompleted: n := codex.ItemCompletedNotification{} err := json.Unmarshal(ev.Params, &n) if err != nil { return err } return m.codexItemCompleted(ctx, l, s, n.Item) case codex.MethodThreadTokenUsageUpdated: n := codex.ThreadTokenUsageUpdatedNotification{} err := json.Unmarshal(ev.Params, &n) if err != nil { return err } l.contextModel = l.model return m.updateContext(ctx, l, int(n.TokenUsage.Last.TotalTokens), int(n.TokenUsage.ModelContextWindow)) case codex.MethodAccountRateLimitsUpdated: n := codex.AccountRateLimitsUpdatedNotification{} err := json.Unmarshal(ev.Params, &n) if err != nil { return err } m.mu.Lock() m.rateLimits[l.vendor] = codexRateLimit(n.RateLimits) m.mu.Unlock() return nil case codex.MethodTurnCompleted: n := codex.TurnCompletedNotification{} err := json.Unmarshal(ev.Params, &n) if err != nil { return err } return m.codexTurnCompleted(ctx, l, s, n.Turn) case codex.MethodError, codex.MethodWarning: params := map[string]any{} err := json.Unmarshal(ev.Params, ¶ms) if err != nil { return err } return m.emit(ctx, l, agentActor, "agent.vendor", map[string]any{"method": ev.Method, "params": params}) } return nil } func codexToolCall(item codex.ThreadItem) (string, any, error) { switch item.Type { case codex.ThreadItemTypeCommandExecution: return "Bash", map[string]any{"command": item.Command}, nil case codex.ThreadItemTypeFileChange: return "apply_patch", map[string]any{"changes": item.Changes}, nil case codex.ThreadItemTypeWebSearch: return "WebSearch", map[string]any{"query": item.Query}, nil case codex.ThreadItemTypeMcpToolCall: tool := "" err := json.Unmarshal(item.Tool, &tool) if err != nil { return "", nil, err } args, err := decode(item.Arguments) return "mcp__" + item.Server + "__" + tool, args, err } tool := "" err := json.Unmarshal(item.Tool, &tool) if err != nil { return "", nil, err } args, err := decode(item.Arguments) return tool, args, err } func codexStatus(item codex.ThreadItem) string { status := "" json.Unmarshal(item.Status, &status) return status } func (m *Manager) codexItemCompleted(ctx context.Context, l *live, s *codex.Session, item codex.ThreadItem) error { switch item.Type { case codex.ThreadItemTypeUserMessage: return nil case codex.ThreadItemTypeAgentMessage: m.mu.Lock() l.lastText = item.Text m.mu.Unlock() return m.emit(ctx, l, agentActor, "agent.message", map[string]any{"text": item.Text}) case codex.ThreadItemTypeReasoning: text := strings.TrimSpace(strings.Join(item.Summary, "\n\n")) if text == "" { return nil } return m.emit(ctx, l, agentActor, "agent.thinking", map[string]any{"text": text}) } if !codexToolItems[item.Type] { raw, err := json.Marshal(item) if err != nil { return err } payload := map[string]any{} err = json.Unmarshal(raw, &payload) if err != nil { return err } return m.emit(ctx, l, agentActor, "agent.vendor", payload) } content, isError := codexToolResult(item) err := m.emit(ctx, l, agentActor, "agent.tool_result", map[string]any{"tool_use_id": item.ID, "is_error": isError, "content": content}) if err != nil { return err } if !l.implementer() { return nil } text := m.deliverMidTurn(ctx, l) if text == "" { return nil } err = s.Steer(ctx, text) if err != nil { slog.Error("steer mid-turn delivery", "session", l.id, "err", err) } return nil } func codexToolResult(item codex.ThreadItem) (string, bool) { status := codexStatus(item) switch item.Type { case codex.ThreadItemTypeCommandExecution: return item.AggregatedOutput, status != "completed" || item.ExitCode != 0 case codex.ThreadItemTypeDynamicToolCall: texts := []string{} for _, c := range item.ContentItems { texts = append(texts, c.Text) } return strings.Join(texts, "\n"), !item.Success case codex.ThreadItemTypeMcpToolCall: if item.Error != nil { return item.Error.Message, true } return string(item.Result), false } return status, status != "completed" } func codexRateLimit(r codex.RateLimitSnapshot) map[string]any { status := "allowed" if r.RateLimitReachedType != "" { status = "rejected" } windows := map[string]any{} for name, w := range map[string]*codex.RateLimitWindow{"primary": r.Primary, "secondary": r.Secondary} { if w == nil { continue } window := map[string]any{"utilization": float64(w.UsedPercent) / 100} if w.ResetsAt != 0 { window["resetsAt"] = w.ResetsAt } windows[name] = window } info := map[string]any{"status": status, "unifiedWindows": windows, "planType": r.PlanType} if r.RateLimitReachedType != "" { info["rateLimitType"] = r.RateLimitReachedType } return info } func (m *Manager) codexTurnCompleted(ctx context.Context, l *live, s *codex.Session, turn codex.Turn) error { m.mu.Lock() final := l.lastText m.mu.Unlock() if turn.Status == codex.TurnStatusCompleted && l.implementer() && m.takeBounce(l, final) { err := m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true}) if err != nil { return err } m.mu.Lock() l.lastText = "" m.mu.Unlock() _, err = s.Send(ctx, proseQuestionNudge) return err } res := endResult{ subtype: string(turn.Status), isError: turn.Status == codex.TurnStatusFailed, result: final, } if turn.Error != nil { res.result = turn.Error.Message info := "" json.Unmarshal(turn.Error.CodexErrorInfo, &info) if info == "usageLimitExceeded" || info == "rateLimitExceeded" { m.mu.Lock() l.rateLimited = map[string]any{"status": "rejected", "rateLimitType": info} m.mu.Unlock() } } res.payload = map[string]any{ "subtype": res.subtype, "is_error": res.isError, "result": res.result, "vendor_uuid": turn.ID, } return m.turnEnded(ctx, l, res) }