@@ -12,6 +12,7 @@ import (
1212 "github.com/charmbracelet/catwalk/pkg/catwalk"
1313 "github.com/charmbracelet/crush/internal/config"
1414 "github.com/charmbracelet/crush/internal/csync"
15+ "github.com/charmbracelet/crush/internal/event"
1516 "github.com/charmbracelet/crush/internal/history"
1617 "github.com/charmbracelet/crush/internal/llm/prompt"
1718 "github.com/charmbracelet/crush/internal/llm/provider"
@@ -25,12 +26,6 @@ import (
2526 "github.com/charmbracelet/crush/internal/shell"
2627)
2728
28−// Common errors
29−var (
30− ErrRequestCancelled = errors.New("request canceled by user")
31− ErrSessionBusy = errors.New("session is currently processing another request")
32−)
33−
3429type AgentEventType string
3530
3631const (
@@ -66,10 +61,11 @@ type Service interface {
6661
6762type agent struct {
6863 *pubsub.Broker[AgentEvent]
69− agentCfg config.Agent
70− sessions session.Service
71− messages message.Service
72− mcpTools []McpTool
64+ agentCfg config.Agent
65+ sessions session.Service
66+ messages message.Service
67+ permissions permission.Service
68+ mcpTools []McpTool
7369
7470 tools *csync.LazySlice[tools.BaseTool]
7571 // We need this to be able to update it when model changes
@@ -237,6 +233,7 @@ func NewAgent(
237233 activeRequests: csync.NewMap[string, context.CancelFunc](),
238234 tools: csync.NewLazySlice(toolFn),
239235 promptQueue: csync.NewMap[string, []string](),
236+ permissions: permissions,
240237 }, nil
241238}
242239
@@ -365,8 +362,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
365362 }
366363
367364 genCtx, cancel := context.WithCancel(ctx)
368−
369365 a.activeRequests.Set(sessionID, cancel)
366+ startTime := time.Now()
367+
370368 go func() {
371369 slog.Debug("Request started", "sessionID", sessionID)
372370 defer log.RecoverPanic("agent.Run", func() {
@@ -377,16 +375,24 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
377375 attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
378376 }
379377 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
380− if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
381− slog.Error(result.Error.Error())
378+ if result.Error != nil {
379+ if isCancelledErr(result.Error) {
380+ slog.Error("Request canceled", "sessionID", sessionID)
381+ } else {
382+ slog.Error("Request errored", "sessionID", sessionID, "error", result.Error.Error())
383+ event.Error(result.Error)
384+ }
385+ } else {
386+ slog.Debug("Request completed", "sessionID", sessionID)
382387 }
383− slog.Debug("Request completed", "sessionID", sessionID)
388+ a.eventPromptResponded(sessionID, time.Since(startTime).Truncate(time.Second))
384389 a.activeRequests.Del(sessionID)
385390 cancel()
386391 a.Publish(pubsub.CreatedEvent, result)
387392 events <- result
388393 close(events)
389394 }()
395+ a.eventPromptSent(sessionID)
390396 return events, nil
391397}
392398
@@ -726,13 +732,13 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
726732 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
727733 return fmt.Errorf("failed to update message: %w", err)
728734 }
729− return a.TrackUsage(ctx, sessionID, a.Model(), event.Response.Usage)
735+ return a.trackUsage(ctx, sessionID, a.Model(), event.Response.Usage)
730736 }
731737
732738 return nil
733739}
734740
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 {
736742 sess, err := a.sessions.Get(ctx, sessionID)
737743 if err != nil {
738744 return fmt.Errorf("failed to get session: %w", err)
@@ -743,6 +749,8 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk.
743749 model.CostPer1MIn/1e6*float64(usage.InputTokens) +
744750 model.CostPer1MOut/1e6*float64(usage.OutputTokens)
745751
752+ a.eventTokensUsed(sessionID, usage, cost)
753+
746754 sess.Cost += cost
747755 sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens
748756 sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens