@@ -15,6 +15,7 @@ import (
1515 "github.com/opencode-ai/opencode/internal/logging"
1616 "github.com/opencode-ai/opencode/internal/message"
1717 "github.com/opencode-ai/opencode/internal/permission"
18+ "github.com/opencode-ai/opencode/internal/pubsub"
1819 "github.com/opencode-ai/opencode/internal/session"
1920)
2021
@@ -24,35 +25,46 @@ var (
2425 ErrSessionBusy = errors.New("session is currently processing another request")
2526)
2627
27−type AgentEvent struct {
28− message message.Message
29− err error
30−}
28+type AgentEventType string
3129
32−func (e *AgentEvent) Err() error {
33− return e.err
34−}
30+const (
31+ AgentEventTypeError AgentEventType = "error"
32+ AgentEventTypeResponse AgentEventType = "response"
33+ AgentEventTypeSummarize AgentEventType = "summarize"
34+)
3535
36−func (e *AgentEvent) Response() message.Message {
37− return e.message
36+type AgentEvent struct {
37+ Type AgentEventType
38+ Message message.Message
39+ Error error
40+
41+ // When summarizing
42+ SessionID string
43+ Progress string
44+ Done bool
3845}
3946
4047type Service interface {
48+ pubsub.Suscriber[AgentEvent]
49+ Model() models.Model
4150 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
4251 Cancel(sessionID string)
4352 IsSessionBusy(sessionID string) bool
4453 IsBusy() bool
4554 Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
55+ Summarize(ctx context.Context, sessionID string) error
4656}
4757
4858type agent struct {
59+ *pubsub.Broker[AgentEvent]
4960 sessions session.Service
5061 messages message.Service
5162
5263 tools []tools.BaseTool
5364 provider provider.Provider
5465
55− titleProvider provider.Provider
66+ titleProvider provider.Provider
67+ summarizeProvider provider.Provider
5668
5769 activeRequests sync.Map
5870}
@@ -75,26 +87,48 @@ func NewAgent(
7587 return nil, err
7688 }
7789 }
90+ var summarizeProvider provider.Provider
91+ if agentName == config.AgentCoder {
92+ summarizeProvider, err = createAgentProvider(config.AgentSummarizer)
93+ if err != nil {
94+ return nil, err
95+ }
96+ }
7897
7998 agent := &agent{
80− provider: agentProvider,
81− messages: messages,
82− sessions: sessions,
83− tools: agentTools,
84− titleProvider: titleProvider,
85− activeRequests: sync.Map{},
99+ Broker: pubsub.NewBroker[AgentEvent](),
100+ provider: agentProvider,
101+ messages: messages,
102+ sessions: sessions,
103+ tools: agentTools,
104+ titleProvider: titleProvider,
105+ summarizeProvider: summarizeProvider,
106+ activeRequests: sync.Map{},
86107 }
87108
88109 return agent, nil
89110}
90111
112+func (a *agent) Model() models.Model {
113+ return a.provider.Model()
114+}
115+
91116func (a *agent) Cancel(sessionID string) {
117+ // Cancel regular requests
92118 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
93119 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
94120 logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
95121 cancel()
96122 }
97123 }
124+
125+ // Also check for summarize requests
126+ if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
127+ if cancel, ok := cancelFunc.(context.CancelFunc); ok {
128+ logging.InfoPersist(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
129+ cancel()
130+ }
131+ }
98132}
99133
100134func (a *agent) IsBusy() bool {
@@ -154,7 +188,8 @@ func (a *agent) generateTitle(ctx context.Context, sessionID string, content str
154188
155189func (a *agent) err(err error) AgentEvent {
156190 return AgentEvent{
157− err: err,
191+ Type: AgentEventTypeError,
192+ Error: err,
158193 }
159194}
160195
@@ -180,12 +215,13 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
180215 attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
181216 }
182217 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
183− if result.Err() != nil && !errors.Is(result.Err(), ErrRequestCancelled) && !errors.Is(result.Err(), context.Canceled) {
184− logging.ErrorPersist(result.Err().Error())
218+ if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
219+ logging.ErrorPersist(result.Error.Error())
185220 }
186221 logging.Debug("Request completed", "sessionID", sessionID)
187222 a.activeRequests.Delete(sessionID)
188223 cancel()
224+ a.Publish(pubsub.CreatedEvent, result)
189225 events <- result
190226 close(events)
191227 }()
@@ -241,7 +277,9 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
241277 continue
242278 }
243279 return AgentEvent{
244− message: agentMessage,
280+ Type: AgentEventTypeResponse,
281+ Message: agentMessage,
282+ Done: true,
245283 }
246284 }
247285}
@@ -432,8 +470,8 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.M
432470 model.CostPer1MOut/1e6*float64(usage.OutputTokens)
433471
434472 sess.Cost += cost
435− sess.CompletionTokens += usage.OutputTokens
436− sess.PromptTokens += usage.InputTokens
473+ sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens
474+ sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens
437475
438476 _, err = a.sessions.Save(ctx, sess)
439477 if err != nil {
@@ -461,6 +499,162 @@ func (a *agent) Update(agentName config.AgentName, modelID models.ModelID) (mode
461499 return a.provider.Model(), nil
462500}
463501
502+func (a *agent) Summarize(ctx context.Context, sessionID string) error {
503+ if a.summarizeProvider == nil {
504+ return fmt.Errorf("summarize provider not available")
505+ }
506+
507+ // Check if session is busy
508+ if a.IsSessionBusy(sessionID) {
509+ return ErrSessionBusy
510+ }
511+
512+ // Create a new context with cancellation
513+ summarizeCtx, cancel := context.WithCancel(ctx)
514+
515+ // Store the cancel function in activeRequests to allow cancellation
516+ a.activeRequests.Store(sessionID+"-summarize", cancel)
517+
518+ go func() {
519+ defer a.activeRequests.Delete(sessionID + "-summarize")
520+ defer cancel()
521+ event := AgentEvent{
522+ Type: AgentEventTypeSummarize,
523+ Progress: "Starting summarization...",
524+ }
525+
526+ a.Publish(pubsub.CreatedEvent, event)
527+ // Get all messages from the session
528+ msgs, err := a.messages.List(summarizeCtx, sessionID)
529+ if err != nil {
530+ event = AgentEvent{
531+ Type: AgentEventTypeError,
532+ Error: fmt.Errorf("failed to list messages: %w", err),
533+ Done: true,
534+ }
535+ a.Publish(pubsub.CreatedEvent, event)
536+ return
537+ }
538+
539+ if len(msgs) == 0 {
540+ event = AgentEvent{
541+ Type: AgentEventTypeError,
542+ Error: fmt.Errorf("no messages to summarize"),
543+ Done: true,
544+ }
545+ a.Publish(pubsub.CreatedEvent, event)
546+ return
547+ }
548+
549+ event = AgentEvent{
550+ Type: AgentEventTypeSummarize,
551+ Progress: "Analyzing conversation...",
552+ }
553+ a.Publish(pubsub.CreatedEvent, event)
554+
555+ // Add a system message to guide the summarization
556+ summarizePrompt := "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next."
557+
558+ // Create a new message with the summarize prompt
559+ promptMsg := message.Message{
560+ Role: message.User,
561+ Parts: []message.ContentPart{message.TextContent{Text: summarizePrompt}},
562+ }
563+
564+ // Append the prompt to the messages
565+ msgsWithPrompt := append(msgs, promptMsg)
566+
567+ event = AgentEvent{
568+ Type: AgentEventTypeSummarize,
569+ Progress: "Generating summary...",
570+ }
571+
572+ a.Publish(pubsub.CreatedEvent, event)
573+
574+ // Send the messages to the summarize provider
575+ response, err := a.summarizeProvider.SendMessages(
576+ summarizeCtx,
577+ msgsWithPrompt,
578+ make([]tools.BaseTool, 0),
579+ )
580+ if err != nil {
581+ event = AgentEvent{
582+ Type: AgentEventTypeError,
583+ Error: fmt.Errorf("failed to summarize: %w", err),
584+ Done: true,
585+ }
586+ a.Publish(pubsub.CreatedEvent, event)
587+ return
588+ }
589+
590+ summary := strings.TrimSpace(response.Content)
591+ if summary == "" {
592+ event = AgentEvent{
593+ Type: AgentEventTypeError,
594+ Error: fmt.Errorf("empty summary returned"),
595+ Done: true,
596+ }
597+ a.Publish(pubsub.CreatedEvent, event)
598+ return
599+ }
600+ event = AgentEvent{
601+ Type: AgentEventTypeSummarize,
602+ Progress: "Creating new session...",
603+ }
604+
605+ a.Publish(pubsub.CreatedEvent, event)
606+ oldSession, err := a.sessions.Get(summarizeCtx, sessionID)
607+ if err != nil {
608+ event = AgentEvent{
609+ Type: AgentEventTypeError,
610+ Error: fmt.Errorf("failed to get session: %w", err),
611+ Done: true,
612+ }
613+
614+ a.Publish(pubsub.CreatedEvent, event)
615+ return
616+ }
617+ // Create a new session with the summary
618+ newSession, err := a.sessions.Create(summarizeCtx, oldSession.Title+" - Continuation")
619+ if err != nil {
620+ event = AgentEvent{
621+ Type: AgentEventTypeError,
622+ Error: fmt.Errorf("failed to create new session: %w", err),
623+ Done: true,
624+ }
625+ a.Publish(pubsub.CreatedEvent, event)
626+ return
627+ }
628+
629+ // Create a message in the new session with the summary
630+ _, err = a.messages.Create(summarizeCtx, newSession.ID, message.CreateMessageParams{
631+ Role: message.Assistant,
632+ Parts: []message.ContentPart{message.TextContent{Text: summary}},
633+ Model: a.summarizeProvider.Model().ID,
634+ })
635+ if err != nil {
636+ event = AgentEvent{
637+ Type: AgentEventTypeError,
638+ Error: fmt.Errorf("failed to create summary message: %w", err),
639+ Done: true,
640+ }
641+
642+ a.Publish(pubsub.CreatedEvent, event)
643+ return
644+ }
645+ event = AgentEvent{
646+ Type: AgentEventTypeSummarize,
647+ SessionID: newSession.ID,
648+ Progress: "Summary complete",
649+ Done: true,
650+ }
651+ a.Publish(pubsub.CreatedEvent, event)
652+ // Send final success event with the new session ID
653+ }()
654+
655+ return nil
656+}
657+
464658func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
465659 cfg := config.Get()
466660 agentConfig, ok := cfg.Agents[agentName]