events.go

  1package server
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"log/slog"
  7
  8	"github.com/charmbracelet/crush/internal/agent/notify"
  9	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 10	"github.com/charmbracelet/crush/internal/app"
 11	"github.com/charmbracelet/crush/internal/config"
 12	"github.com/charmbracelet/crush/internal/history"
 13	"github.com/charmbracelet/crush/internal/message"
 14	"github.com/charmbracelet/crush/internal/permission"
 15	"github.com/charmbracelet/crush/internal/proto"
 16	"github.com/charmbracelet/crush/internal/pubsub"
 17	"github.com/charmbracelet/crush/internal/session"
 18)
 19
 20// wrapEvent converts a raw tea.Msg (a pubsub.Event[T] from the app
 21// event fan-in) into a pubsub.Payload envelope with the correct
 22// PayloadType discriminator and a proto-typed inner payload that has
 23// proper JSON tags. Returns nil if the event type is unrecognized.
 24func wrapEvent(ev any) *pubsub.Payload {
 25	switch e := ev.(type) {
 26	case pubsub.Event[app.LSPEvent]:
 27		return envelope(pubsub.PayloadTypeLSPEvent, pubsub.Event[proto.LSPEvent]{
 28			Type: e.Type,
 29			Payload: proto.LSPEvent{
 30				Type:            proto.LSPEventType(e.Payload.Type),
 31				Name:            e.Payload.Name,
 32				State:           e.Payload.State,
 33				Error:           e.Payload.Error,
 34				DiagnosticCount: e.Payload.DiagnosticCount,
 35			},
 36		})
 37	case pubsub.Event[mcp.Event]:
 38		return envelope(pubsub.PayloadTypeMCPEvent, pubsub.Event[proto.MCPEvent]{
 39			Type: e.Type,
 40			Payload: proto.MCPEvent{
 41				Type:      mcpEventTypeToProto(e.Payload.Type),
 42				Name:      e.Payload.Name,
 43				State:     proto.MCPState(e.Payload.State),
 44				Error:     e.Payload.Error,
 45				ToolCount: e.Payload.Counts.Tools,
 46			},
 47		})
 48	case pubsub.Event[permission.PermissionRequest]:
 49		return envelope(pubsub.PayloadTypePermissionRequest, pubsub.Event[proto.PermissionRequest]{
 50			Type: e.Type,
 51			Payload: proto.PermissionRequest{
 52				ID:          e.Payload.ID,
 53				SessionID:   e.Payload.SessionID,
 54				ToolCallID:  e.Payload.ToolCallID,
 55				ToolName:    e.Payload.ToolName,
 56				Description: e.Payload.Description,
 57				Action:      e.Payload.Action,
 58				Path:        e.Payload.Path,
 59				Params:      e.Payload.Params,
 60			},
 61		})
 62	case pubsub.Event[permission.PermissionNotification]:
 63		return envelope(pubsub.PayloadTypePermissionNotification, pubsub.Event[proto.PermissionNotification]{
 64			Type: e.Type,
 65			Payload: proto.PermissionNotification{
 66				ToolCallID: e.Payload.ToolCallID,
 67				Granted:    e.Payload.Granted,
 68				Denied:     e.Payload.Denied,
 69			},
 70		})
 71	case pubsub.Event[message.Message]:
 72		return envelope(pubsub.PayloadTypeMessage, pubsub.Event[proto.Message]{
 73			Type:    e.Type,
 74			Payload: messageToProto(e.Payload),
 75		})
 76	case pubsub.Event[session.Session]:
 77		return envelope(pubsub.PayloadTypeSession, pubsub.Event[proto.Session]{
 78			Type:    e.Type,
 79			Payload: sessionToProto(e.Payload),
 80		})
 81	case pubsub.Event[history.File]:
 82		return envelope(pubsub.PayloadTypeFile, pubsub.Event[proto.File]{
 83			Type:    e.Type,
 84			Payload: fileToProto(e.Payload),
 85		})
 86	case pubsub.Event[notify.Notification]:
 87		return envelope(pubsub.PayloadTypeAgentEvent, pubsub.Event[proto.AgentEvent]{
 88			Type: e.Type,
 89			Payload: proto.AgentEvent{
 90				SessionID:    e.Payload.SessionID,
 91				SessionTitle: e.Payload.SessionTitle,
 92				Type:         proto.AgentEventType(e.Payload.Type),
 93			},
 94		})
 95	default:
 96		slog.Warn("Unrecognized event type for SSE wrapping", "type", fmt.Sprintf("%T", ev))
 97		return nil
 98	}
 99}
100
101// envelope marshals the inner event and wraps it in a pubsub.Payload.
102func envelope(payloadType pubsub.PayloadType, inner any) *pubsub.Payload {
103	raw, err := json.Marshal(inner)
104	if err != nil {
105		slog.Error("Failed to marshal event payload", "error", err)
106		return nil
107	}
108	return &pubsub.Payload{
109		Type:    payloadType,
110		Payload: raw,
111	}
112}
113
114func mcpEventTypeToProto(t mcp.EventType) proto.MCPEventType {
115	switch t {
116	case mcp.EventStateChanged:
117		return proto.MCPEventStateChanged
118	case mcp.EventToolsListChanged:
119		return proto.MCPEventToolsListChanged
120	case mcp.EventPromptsListChanged:
121		return proto.MCPEventPromptsListChanged
122	case mcp.EventResourcesListChanged:
123		return proto.MCPEventResourcesListChanged
124	default:
125		return proto.MCPEventStateChanged
126	}
127}
128
129func sessionToProto(s session.Session) proto.Session {
130	return proto.Session{
131		ID:               s.ID,
132		ParentSessionID:  s.ParentSessionID,
133		Title:            s.Title,
134		SummaryMessageID: s.SummaryMessageID,
135		MessageCount:     s.MessageCount,
136		PromptTokens:     s.PromptTokens,
137		CompletionTokens: s.CompletionTokens,
138		Cost:             s.Cost,
139		CreatedAt:        s.CreatedAt,
140		UpdatedAt:        s.UpdatedAt,
141		Models:           convertModelsToProto(s.Models),
142	}
143}
144
145func convertModelsToProto(models map[config.SelectedModelType]config.SelectedModel) map[proto.SelectedModelType]proto.SelectedModel {
146	if models == nil {
147		return nil
148	}
149	result := make(map[proto.SelectedModelType]proto.SelectedModel, len(models))
150	for k, v := range models {
151		result[proto.SelectedModelType(k)] = proto.SelectedModel{
152			Model:            v.Model,
153			Provider:         v.Provider,
154			ReasoningEffort:  v.ReasoningEffort,
155			Think:            v.Think,
156			MaxTokens:        v.MaxTokens,
157			Temperature:      v.Temperature,
158			TopP:             v.TopP,
159			TopK:             v.TopK,
160			FrequencyPenalty: v.FrequencyPenalty,
161			PresencePenalty:  v.PresencePenalty,
162			ProviderOptions:  v.ProviderOptions,
163		}
164	}
165	return result
166}
167
168func fileToProto(f history.File) proto.File {
169	return proto.File{
170		ID:        f.ID,
171		SessionID: f.SessionID,
172		Path:      f.Path,
173		Content:   f.Content,
174		Version:   f.Version,
175		CreatedAt: f.CreatedAt,
176		UpdatedAt: f.UpdatedAt,
177	}
178}
179
180func messageToProto(m message.Message) proto.Message {
181	msg := proto.Message{
182		ID:        m.ID,
183		SessionID: m.SessionID,
184		Role:      proto.MessageRole(m.Role),
185		Model:     m.Model,
186		Provider:  m.Provider,
187		CreatedAt: m.CreatedAt,
188		UpdatedAt: m.UpdatedAt,
189	}
190
191	for _, p := range m.Parts {
192		switch v := p.(type) {
193		case message.TextContent:
194			msg.Parts = append(msg.Parts, proto.TextContent{Text: v.Text})
195		case message.ReasoningContent:
196			msg.Parts = append(msg.Parts, proto.ReasoningContent{
197				Thinking:   v.Thinking,
198				Signature:  v.Signature,
199				StartedAt:  v.StartedAt,
200				FinishedAt: v.FinishedAt,
201			})
202		case message.ToolCall:
203			msg.Parts = append(msg.Parts, proto.ToolCall{
204				ID:       v.ID,
205				Name:     v.Name,
206				Input:    v.Input,
207				Finished: v.Finished,
208			})
209		case message.ToolResult:
210			msg.Parts = append(msg.Parts, proto.ToolResult{
211				ToolCallID: v.ToolCallID,
212				Name:       v.Name,
213				Content:    v.Content,
214				IsError:    v.IsError,
215			})
216		case message.Finish:
217			msg.Parts = append(msg.Parts, proto.Finish{
218				Reason:  proto.FinishReason(v.Reason),
219				Time:    v.Time,
220				Message: v.Message,
221				Details: v.Details,
222			})
223		case message.ImageURLContent:
224			msg.Parts = append(msg.Parts, proto.ImageURLContent{URL: v.URL, Detail: v.Detail})
225		case message.BinaryContent:
226			msg.Parts = append(msg.Parts, proto.BinaryContent{Path: v.Path, MIMEType: v.MIMEType, Data: v.Data})
227		}
228	}
229
230	return msg
231}
232
233func messagesToProto(msgs []message.Message) []proto.Message {
234	out := make([]proto.Message, len(msgs))
235	for i, m := range msgs {
236		out[i] = messageToProto(m)
237	}
238	return out
239}