@@ -12,6 +12,7 @@ import (
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
15+ "github.com/charmbracelet/crush/internal/event"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
17 "github.com/charmbracelet/crush/internal/llm/prompt"
17 "github.com/charmbracelet/crush/internal/llm/provider"
18 "github.com/charmbracelet/crush/internal/llm/provider"
@@ -25,12 +26,6 @@ import (
25 "github.com/charmbracelet/crush/internal/shell"
26 "github.com/charmbracelet/crush/internal/shell"
30− ErrRequestCancelled = errors.New("request canceled by user")
31− ErrSessionBusy = errors.New("session is currently processing another request")
34type AgentEventType string
29type AgentEventType string
@@ -66,10 +61,11 @@ type Service interface {
67type agent struct {
62type agent struct {
68 *pubsub.Broker[AgentEvent]
63 *pubsub.Broker[AgentEvent]
69− agentCfg config.Agent
64+ agentCfg config.Agent
70− sessions session.Service
65+ sessions session.Service
71− messages message.Service
66+ messages message.Service
67+ permissions permission.Service
72− mcpTools []McpTool
68+ mcpTools []McpTool
74 tools *csync.LazySlice[tools.BaseTool]
70 tools *csync.LazySlice[tools.BaseTool]
75 // We need this to be able to update it when model changes
71 // We need this to be able to update it when model changes
@@ -237,6 +233,7 @@ func NewAgent(
237 activeRequests: csync.NewMap[string, context.CancelFunc](),
233 activeRequests: csync.NewMap[string, context.CancelFunc](),
238 tools: csync.NewLazySlice(toolFn),
234 tools: csync.NewLazySlice(toolFn),
239 promptQueue: csync.NewMap[string, []string](),
235 promptQueue: csync.NewMap[string, []string](),
236+ permissions: permissions,
@@ -365,8 +362,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
367 genCtx, cancel := context.WithCancel(ctx)
364 genCtx, cancel := context.WithCancel(ctx)
369 a.activeRequests.Set(sessionID, cancel)
365 a.activeRequests.Set(sessionID, cancel)
366+ startTime := time.Now()
370 go func() {
368 go func() {
371 slog.Debug("Request started", "sessionID", sessionID)
369 slog.Debug("Request started", "sessionID", sessionID)
372 defer log.RecoverPanic("agent.Run", func() {
370 defer log.RecoverPanic("agent.Run", func() {
@@ -377,16 +375,24 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
377 attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
375 attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
379 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
377 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
380− if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
378+ if result.Error != nil {
379+ if isCancelledErr(result.Error) {
380+ slog.Error("Request canceled", "sessionID", sessionID)
381− slog.Error(result.Error.Error())
382+ slog.Error("Request errored", "sessionID", sessionID, "error", result.Error.Error())
383+ event.Error(result.Error)
386+ slog.Debug("Request completed", "sessionID", sessionID)
383− slog.Debug("Request completed", "sessionID", sessionID)
388+ a.eventPromptResponded(sessionID, time.Since(startTime).Truncate(time.Second))
384 a.activeRequests.Del(sessionID)
389 a.activeRequests.Del(sessionID)
386 a.Publish(pubsub.CreatedEvent, result)
391 a.Publish(pubsub.CreatedEvent, result)
387 events <- result
392 events <- result
388 close(events)
393 close(events)
395+ a.eventPromptSent(sessionID)
390 return events, nil
396 return events, nil
@@ -726,13 +732,13 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
726 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
732 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
727 return fmt.Errorf("failed to update message: %w", err)
733 return fmt.Errorf("failed to update message: %w", err)
729− return a.TrackUsage(ctx, sessionID, a.Model(), event.Response.Usage)
735+ return a.trackUsage(ctx, sessionID, a.Model(), event.Response.Usage)
732 return nil
738 return nil
735−func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk.Model, usage provider.TokenUsage) error {
741+func (a *agent) trackUsage(ctx context.Context, sessionID string, model catwalk.Model, usage provider.TokenUsage) error {
736 sess, err := a.sessions.Get(ctx, sessionID)
742 sess, err := a.sessions.Get(ctx, sessionID)
737 if err != nil {
743 if err != nil {
738 return fmt.Errorf("failed to get session: %w", err)
744 return fmt.Errorf("failed to get session: %w", err)
@@ -743,6 +749,8 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk.
743 model.CostPer1MIn/1e6*float64(usage.InputTokens) +
749 model.CostPer1MIn/1e6*float64(usage.InputTokens) +
744 model.CostPer1MOut/1e6*float64(usage.OutputTokens)
750 model.CostPer1MOut/1e6*float64(usage.OutputTokens)
752+ a.eventTokensUsed(sessionID, usage, cost)
746 sess.Cost += cost
754 sess.Cost += cost
747 sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens
755 sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens
748 sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens
756 sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens