agent.go

  1package agent
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"strings"
  8	"sync"
  9	"time"
 10
 11	"github.com/opencode-ai/opencode/internal/config"
 12	"github.com/opencode-ai/opencode/internal/llm/models"
 13	"github.com/opencode-ai/opencode/internal/llm/prompt"
 14	"github.com/opencode-ai/opencode/internal/llm/provider"
 15	"github.com/opencode-ai/opencode/internal/llm/tools"
 16	"github.com/opencode-ai/opencode/internal/logging"
 17	"github.com/opencode-ai/opencode/internal/message"
 18	"github.com/opencode-ai/opencode/internal/permission"
 19	"github.com/opencode-ai/opencode/internal/pubsub"
 20	"github.com/opencode-ai/opencode/internal/session"
 21)
 22
 23// Common errors
 24var (
 25	ErrRequestCancelled = errors.New("request cancelled by user")
 26	ErrSessionBusy      = errors.New("session is currently processing another request")
 27)
 28
 29type AgentEventType string
 30
 31const (
 32	AgentEventTypeError     AgentEventType = "error"
 33	AgentEventTypeResponse  AgentEventType = "response"
 34	AgentEventTypeSummarize AgentEventType = "summarize"
 35)
 36
 37type AgentEvent struct {
 38	Type    AgentEventType
 39	Message message.Message
 40	Error   error
 41
 42	// When summarizing
 43	SessionID string
 44	Progress  string
 45	Done      bool
 46}
 47
 48type Service interface {
 49	pubsub.Suscriber[AgentEvent]
 50	Model() models.Model
 51	Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
 52	Cancel(sessionID string)
 53	IsSessionBusy(sessionID string) bool
 54	IsBusy() bool
 55	Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
 56	Summarize(ctx context.Context, sessionID string) error
 57}
 58
 59type agent struct {
 60	*pubsub.Broker[AgentEvent]
 61	sessions session.Service
 62	messages message.Service
 63
 64	tools    []tools.BaseTool
 65	provider provider.Provider
 66
 67	titleProvider     provider.Provider
 68	summarizeProvider provider.Provider
 69
 70	activeRequests sync.Map
 71}
 72
 73func NewAgent(
 74	agentName config.AgentName,
 75	sessions session.Service,
 76	messages message.Service,
 77	agentTools []tools.BaseTool,
 78) (Service, error) {
 79	agentProvider, err := createAgentProvider(agentName)
 80	if err != nil {
 81		return nil, err
 82	}
 83	var titleProvider provider.Provider
 84	// Only generate titles for the coder agent
 85	if agentName == config.AgentCoder {
 86		titleProvider, err = createAgentProvider(config.AgentTitle)
 87		if err != nil {
 88			return nil, err
 89		}
 90	}
 91	var summarizeProvider provider.Provider
 92	if agentName == config.AgentCoder {
 93		summarizeProvider, err = createAgentProvider(config.AgentSummarizer)
 94		if err != nil {
 95			return nil, err
 96		}
 97	}
 98
 99	agent := &agent{
100		Broker:            pubsub.NewBroker[AgentEvent](),
101		provider:          agentProvider,
102		messages:          messages,
103		sessions:          sessions,
104		tools:             agentTools,
105		titleProvider:     titleProvider,
106		summarizeProvider: summarizeProvider,
107		activeRequests:    sync.Map{},
108	}
109
110	return agent, nil
111}
112
113func (a *agent) Model() models.Model {
114	return a.provider.Model()
115}
116
117func (a *agent) Cancel(sessionID string) {
118	// Cancel regular requests
119	if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
120		if cancel, ok := cancelFunc.(context.CancelFunc); ok {
121			logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
122			cancel()
123		}
124	}
125
126	// Also check for summarize requests
127	if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
128		if cancel, ok := cancelFunc.(context.CancelFunc); ok {
129			logging.InfoPersist(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
130			cancel()
131		}
132	}
133}
134
135func (a *agent) IsBusy() bool {
136	busy := false
137	a.activeRequests.Range(func(key, value interface{}) bool {
138		if cancelFunc, ok := value.(context.CancelFunc); ok {
139			if cancelFunc != nil {
140				busy = true
141				return false // Stop iterating
142			}
143		}
144		return true // Continue iterating
145	})
146	return busy
147}
148
149func (a *agent) IsSessionBusy(sessionID string) bool {
150	_, busy := a.activeRequests.Load(sessionID)
151	return busy
152}
153
154func (a *agent) generateTitle(ctx context.Context, sessionID string, content string) error {
155	if content == "" {
156		return nil
157	}
158	if a.titleProvider == nil {
159		return nil
160	}
161	session, err := a.sessions.Get(ctx, sessionID)
162	if err != nil {
163		return err
164	}
165	ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
166	parts := []message.ContentPart{message.TextContent{Text: content}}
167	response, err := a.titleProvider.SendMessages(
168		ctx,
169		[]message.Message{
170			{
171				Role:  message.User,
172				Parts: parts,
173			},
174		},
175		make([]tools.BaseTool, 0),
176	)
177	if err != nil {
178		return err
179	}
180
181	title := strings.TrimSpace(strings.ReplaceAll(response.Content, "\n", " "))
182	if title == "" {
183		return nil
184	}
185
186	session.Title = title
187	_, err = a.sessions.Save(ctx, session)
188	return err
189}
190
191func (a *agent) err(err error) AgentEvent {
192	return AgentEvent{
193		Type:  AgentEventTypeError,
194		Error: err,
195	}
196}
197
198func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) {
199	if !a.provider.Model().SupportsAttachments && attachments != nil {
200		attachments = nil
201	}
202	events := make(chan AgentEvent)
203	if a.IsSessionBusy(sessionID) {
204		return nil, ErrSessionBusy
205	}
206
207	genCtx, cancel := context.WithCancel(ctx)
208
209	a.activeRequests.Store(sessionID, cancel)
210	go func() {
211		logging.Debug("Request started", "sessionID", sessionID)
212		defer logging.RecoverPanic("agent.Run", func() {
213			events <- a.err(fmt.Errorf("panic while running the agent"))
214		})
215		var attachmentParts []message.ContentPart
216		for _, attachment := range attachments {
217			attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
218		}
219		result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
220		if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
221			logging.ErrorPersist(result.Error.Error())
222		}
223		logging.Debug("Request completed", "sessionID", sessionID)
224		a.activeRequests.Delete(sessionID)
225		cancel()
226		a.Publish(pubsub.CreatedEvent, result)
227		events <- result
228		close(events)
229	}()
230	return events, nil
231}
232
233func (a *agent) processGeneration(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) AgentEvent {
234	cfg := config.Get()
235	// List existing messages; if none, start title generation asynchronously.
236	msgs, err := a.messages.List(ctx, sessionID)
237	if err != nil {
238		return a.err(fmt.Errorf("failed to list messages: %w", err))
239	}
240	if len(msgs) == 0 {
241		go func() {
242			defer logging.RecoverPanic("agent.Run", func() {
243				logging.ErrorPersist("panic while generating title")
244			})
245			titleErr := a.generateTitle(context.Background(), sessionID, content)
246			if titleErr != nil {
247				logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr))
248			}
249		}()
250	}
251	session, err := a.sessions.Get(ctx, sessionID)
252	if err != nil {
253		return a.err(fmt.Errorf("failed to get session: %w", err))
254	}
255	if session.SummaryMessageID != "" {
256		summaryMsgInex := -1
257		for i, msg := range msgs {
258			if msg.ID == session.SummaryMessageID {
259				summaryMsgInex = i
260				break
261			}
262		}
263		if summaryMsgInex != -1 {
264			msgs = msgs[summaryMsgInex:]
265			msgs[0].Role = message.User
266		}
267	}
268
269	userMsg, err := a.createUserMessage(ctx, sessionID, content, attachmentParts)
270	if err != nil {
271		return a.err(fmt.Errorf("failed to create user message: %w", err))
272	}
273	// Append the new user message to the conversation history.
274	msgHistory := append(msgs, userMsg)
275
276	for {
277		// Check for cancellation before each iteration
278		select {
279		case <-ctx.Done():
280			return a.err(ctx.Err())
281		default:
282			// Continue processing
283		}
284		agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
285		if err != nil {
286			if errors.Is(err, context.Canceled) {
287				agentMessage.AddFinish(message.FinishReasonCanceled)
288				a.messages.Update(context.Background(), agentMessage)
289				return a.err(ErrRequestCancelled)
290			}
291			return a.err(fmt.Errorf("failed to process events: %w", err))
292		}
293		if cfg.Debug {
294			seqId := (len(msgHistory) + 1) / 2
295			toolResultFilepath := logging.WriteToolResultsJson(sessionID, seqId, toolResults)
296			logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", "{}", "filepath", toolResultFilepath)
297		} else {
298			logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
299		}
300		if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
301			// We are not done, we need to respond with the tool response
302			msgHistory = append(msgHistory, agentMessage, *toolResults)
303			continue
304		}
305		return AgentEvent{
306			Type:    AgentEventTypeResponse,
307			Message: agentMessage,
308			Done:    true,
309		}
310	}
311}
312
313func (a *agent) createUserMessage(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) (message.Message, error) {
314	parts := []message.ContentPart{message.TextContent{Text: content}}
315	parts = append(parts, attachmentParts...)
316	return a.messages.Create(ctx, sessionID, message.CreateMessageParams{
317		Role:  message.User,
318		Parts: parts,
319	})
320}
321
322func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
323	ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
324	eventChan := a.provider.StreamResponse(ctx, msgHistory, a.tools)
325
326	assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
327		Role:  message.Assistant,
328		Parts: []message.ContentPart{},
329		Model: a.provider.Model().ID,
330	})
331	if err != nil {
332		return assistantMsg, nil, fmt.Errorf("failed to create assistant message: %w", err)
333	}
334
335	// Add the session and message ID into the context if needed by tools.
336	ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
337
338	// Process each event in the stream.
339	for event := range eventChan {
340		if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
341			a.finishMessage(ctx, &assistantMsg, message.FinishReasonCanceled)
342			return assistantMsg, nil, processErr
343		}
344		if ctx.Err() != nil {
345			a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
346			return assistantMsg, nil, ctx.Err()
347		}
348	}
349
350	toolResults := make([]message.ToolResult, len(assistantMsg.ToolCalls()))
351	toolCalls := assistantMsg.ToolCalls()
352	for i, toolCall := range toolCalls {
353		select {
354		case <-ctx.Done():
355			a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
356			// Make all future tool calls cancelled
357			for j := i; j < len(toolCalls); j++ {
358				toolResults[j] = message.ToolResult{
359					ToolCallID: toolCalls[j].ID,
360					Content:    "Tool execution canceled by user",
361					IsError:    true,
362				}
363			}
364			goto out
365		default:
366			// Continue processing
367			var tool tools.BaseTool
368			for _, availableTool := range a.tools {
369				if availableTool.Info().Name == toolCall.Name {
370					tool = availableTool
371					break
372				}
373				// Monkey patch for Copilot Sonnet-4 tool repetition obfuscation
374				// if strings.HasPrefix(toolCall.Name, availableTool.Info().Name) &&
375				// 	strings.HasPrefix(toolCall.Name, availableTool.Info().Name+availableTool.Info().Name) {
376				// 	tool = availableTool
377				// 	break
378				// }
379			}
380
381			// Tool not found
382			if tool == nil {
383				toolResults[i] = message.ToolResult{
384					ToolCallID: toolCall.ID,
385					Content:    fmt.Sprintf("Tool not found: %s", toolCall.Name),
386					IsError:    true,
387				}
388				continue
389			}
390			toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
391				ID:    toolCall.ID,
392				Name:  toolCall.Name,
393				Input: toolCall.Input,
394			})
395			if toolErr != nil {
396				if errors.Is(toolErr, permission.ErrorPermissionDenied) {
397					toolResults[i] = message.ToolResult{
398						ToolCallID: toolCall.ID,
399						Content:    "Permission denied",
400						IsError:    true,
401					}
402					for j := i + 1; j < len(toolCalls); j++ {
403						toolResults[j] = message.ToolResult{
404							ToolCallID: toolCalls[j].ID,
405							Content:    "Tool execution canceled by user",
406							IsError:    true,
407						}
408					}
409					a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied)
410					break
411				}
412			}
413			toolResults[i] = message.ToolResult{
414				ToolCallID: toolCall.ID,
415				Content:    toolResult.Content,
416				Metadata:   toolResult.Metadata,
417				IsError:    toolResult.IsError,
418			}
419		}
420	}
421out:
422	if len(toolResults) == 0 {
423		return assistantMsg, nil, nil
424	}
425	parts := make([]message.ContentPart, 0)
426	for _, tr := range toolResults {
427		parts = append(parts, tr)
428	}
429	msg, err := a.messages.Create(context.Background(), assistantMsg.SessionID, message.CreateMessageParams{
430		Role:  message.Tool,
431		Parts: parts,
432	})
433	if err != nil {
434		return assistantMsg, nil, fmt.Errorf("failed to create cancelled tool message: %w", err)
435	}
436
437	return assistantMsg, &msg, err
438}
439
440func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason) {
441	msg.AddFinish(finishReson)
442	_ = a.messages.Update(ctx, *msg)
443}
444
445func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg *message.Message, event provider.ProviderEvent) error {
446	select {
447	case <-ctx.Done():
448		return ctx.Err()
449	default:
450		// Continue processing.
451	}
452
453	switch event.Type {
454	case provider.EventThinkingDelta:
455		assistantMsg.AppendReasoningContent(event.Content)
456		return a.messages.Update(ctx, *assistantMsg)
457	case provider.EventContentDelta:
458		assistantMsg.AppendContent(event.Content)
459		return a.messages.Update(ctx, *assistantMsg)
460	case provider.EventToolUseStart:
461		assistantMsg.AddToolCall(*event.ToolCall)
462		return a.messages.Update(ctx, *assistantMsg)
463	// TODO: see how to handle this
464	// case provider.EventToolUseDelta:
465	// 	tm := time.Unix(assistantMsg.UpdatedAt, 0)
466	// 	assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
467	// 	if time.Since(tm) > 1000*time.Millisecond {
468	// 		err := a.messages.Update(ctx, *assistantMsg)
469	// 		assistantMsg.UpdatedAt = time.Now().Unix()
470	// 		return err
471	// 	}
472	case provider.EventToolUseStop:
473		assistantMsg.FinishToolCall(event.ToolCall.ID)
474		return a.messages.Update(ctx, *assistantMsg)
475	case provider.EventError:
476		if errors.Is(event.Error, context.Canceled) {
477			logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
478			return context.Canceled
479		}
480		logging.ErrorPersist(event.Error.Error())
481		return event.Error
482	case provider.EventComplete:
483		assistantMsg.SetToolCalls(event.Response.ToolCalls)
484		assistantMsg.AddFinish(event.Response.FinishReason)
485		if err := a.messages.Update(ctx, *assistantMsg); err != nil {
486			return fmt.Errorf("failed to update message: %w", err)
487		}
488		return a.TrackUsage(ctx, sessionID, a.provider.Model(), event.Response.Usage)
489	}
490
491	return nil
492}
493
494func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error {
495	sess, err := a.sessions.Get(ctx, sessionID)
496	if err != nil {
497		return fmt.Errorf("failed to get session: %w", err)
498	}
499
500	cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
501		model.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
502		model.CostPer1MIn/1e6*float64(usage.InputTokens) +
503		model.CostPer1MOut/1e6*float64(usage.OutputTokens)
504
505	sess.Cost += cost
506	sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens
507	sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens
508
509	_, err = a.sessions.Save(ctx, sess)
510	if err != nil {
511		return fmt.Errorf("failed to save session: %w", err)
512	}
513	return nil
514}
515
516func (a *agent) Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) {
517	if a.IsBusy() {
518		return models.Model{}, fmt.Errorf("cannot change model while processing requests")
519	}
520
521	if err := config.UpdateAgentModel(agentName, modelID); err != nil {
522		return models.Model{}, fmt.Errorf("failed to update config: %w", err)
523	}
524
525	provider, err := createAgentProvider(agentName)
526	if err != nil {
527		return models.Model{}, fmt.Errorf("failed to create provider for model %s: %w", modelID, err)
528	}
529
530	a.provider = provider
531
532	return a.provider.Model(), nil
533}
534
535func (a *agent) Summarize(ctx context.Context, sessionID string) error {
536	if a.summarizeProvider == nil {
537		return fmt.Errorf("summarize provider not available")
538	}
539
540	// Check if session is busy
541	if a.IsSessionBusy(sessionID) {
542		return ErrSessionBusy
543	}
544
545	// Create a new context with cancellation
546	summarizeCtx, cancel := context.WithCancel(ctx)
547
548	// Store the cancel function in activeRequests to allow cancellation
549	a.activeRequests.Store(sessionID+"-summarize", cancel)
550
551	go func() {
552		defer a.activeRequests.Delete(sessionID + "-summarize")
553		defer cancel()
554		event := AgentEvent{
555			Type:     AgentEventTypeSummarize,
556			Progress: "Starting summarization...",
557		}
558
559		a.Publish(pubsub.CreatedEvent, event)
560		// Get all messages from the session
561		msgs, err := a.messages.List(summarizeCtx, sessionID)
562		if err != nil {
563			event = AgentEvent{
564				Type:  AgentEventTypeError,
565				Error: fmt.Errorf("failed to list messages: %w", err),
566				Done:  true,
567			}
568			a.Publish(pubsub.CreatedEvent, event)
569			return
570		}
571		summarizeCtx = context.WithValue(summarizeCtx, tools.SessionIDContextKey, sessionID)
572
573		if len(msgs) == 0 {
574			event = AgentEvent{
575				Type:  AgentEventTypeError,
576				Error: fmt.Errorf("no messages to summarize"),
577				Done:  true,
578			}
579			a.Publish(pubsub.CreatedEvent, event)
580			return
581		}
582
583		event = AgentEvent{
584			Type:     AgentEventTypeSummarize,
585			Progress: "Analyzing conversation...",
586		}
587		a.Publish(pubsub.CreatedEvent, event)
588
589		// Add a system message to guide the summarization
590		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."
591
592		// Create a new message with the summarize prompt
593		promptMsg := message.Message{
594			Role:  message.User,
595			Parts: []message.ContentPart{message.TextContent{Text: summarizePrompt}},
596		}
597
598		// Append the prompt to the messages
599		msgsWithPrompt := append(msgs, promptMsg)
600
601		event = AgentEvent{
602			Type:     AgentEventTypeSummarize,
603			Progress: "Generating summary...",
604		}
605
606		a.Publish(pubsub.CreatedEvent, event)
607
608		// Send the messages to the summarize provider
609		response, err := a.summarizeProvider.SendMessages(
610			summarizeCtx,
611			msgsWithPrompt,
612			make([]tools.BaseTool, 0),
613		)
614		if err != nil {
615			event = AgentEvent{
616				Type:  AgentEventTypeError,
617				Error: fmt.Errorf("failed to summarize: %w", err),
618				Done:  true,
619			}
620			a.Publish(pubsub.CreatedEvent, event)
621			return
622		}
623
624		summary := strings.TrimSpace(response.Content)
625		if summary == "" {
626			event = AgentEvent{
627				Type:  AgentEventTypeError,
628				Error: fmt.Errorf("empty summary returned"),
629				Done:  true,
630			}
631			a.Publish(pubsub.CreatedEvent, event)
632			return
633		}
634		event = AgentEvent{
635			Type:     AgentEventTypeSummarize,
636			Progress: "Creating new session...",
637		}
638
639		a.Publish(pubsub.CreatedEvent, event)
640		oldSession, err := a.sessions.Get(summarizeCtx, sessionID)
641		if err != nil {
642			event = AgentEvent{
643				Type:  AgentEventTypeError,
644				Error: fmt.Errorf("failed to get session: %w", err),
645				Done:  true,
646			}
647
648			a.Publish(pubsub.CreatedEvent, event)
649			return
650		}
651		// Create a message in the new session with the summary
652		msg, err := a.messages.Create(summarizeCtx, oldSession.ID, message.CreateMessageParams{
653			Role: message.Assistant,
654			Parts: []message.ContentPart{
655				message.TextContent{Text: summary},
656				message.Finish{
657					Reason: message.FinishReasonEndTurn,
658					Time:   time.Now().Unix(),
659				},
660			},
661			Model: a.summarizeProvider.Model().ID,
662		})
663		if err != nil {
664			event = AgentEvent{
665				Type:  AgentEventTypeError,
666				Error: fmt.Errorf("failed to create summary message: %w", err),
667				Done:  true,
668			}
669
670			a.Publish(pubsub.CreatedEvent, event)
671			return
672		}
673		oldSession.SummaryMessageID = msg.ID
674		oldSession.CompletionTokens = response.Usage.OutputTokens
675		oldSession.PromptTokens = 0
676		model := a.summarizeProvider.Model()
677		usage := response.Usage
678		cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
679			model.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
680			model.CostPer1MIn/1e6*float64(usage.InputTokens) +
681			model.CostPer1MOut/1e6*float64(usage.OutputTokens)
682		oldSession.Cost += cost
683		_, err = a.sessions.Save(summarizeCtx, oldSession)
684		if err != nil {
685			event = AgentEvent{
686				Type:  AgentEventTypeError,
687				Error: fmt.Errorf("failed to save session: %w", err),
688				Done:  true,
689			}
690			a.Publish(pubsub.CreatedEvent, event)
691		}
692
693		event = AgentEvent{
694			Type:      AgentEventTypeSummarize,
695			SessionID: oldSession.ID,
696			Progress:  "Summary complete",
697			Done:      true,
698		}
699		a.Publish(pubsub.CreatedEvent, event)
700		// Send final success event with the new session ID
701	}()
702
703	return nil
704}
705
706func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
707	cfg := config.Get()
708	agentConfig, ok := cfg.Agents[agentName]
709	if !ok {
710		return nil, fmt.Errorf("agent %s not found", agentName)
711	}
712	model, ok := models.SupportedModels[agentConfig.Model]
713	if !ok {
714		return nil, fmt.Errorf("model %s not supported", agentConfig.Model)
715	}
716
717	providerCfg, ok := cfg.Providers[model.Provider]
718	if !ok {
719		return nil, fmt.Errorf("provider %s not supported", model.Provider)
720	}
721	if providerCfg.Disabled {
722		return nil, fmt.Errorf("provider %s is not enabled", model.Provider)
723	}
724	maxTokens := model.DefaultMaxTokens
725	if agentConfig.MaxTokens > 0 {
726		maxTokens = agentConfig.MaxTokens
727	}
728	opts := []provider.ProviderClientOption{
729		provider.WithAPIKey(providerCfg.APIKey),
730		provider.WithModel(model),
731		provider.WithSystemMessage(prompt.GetAgentPrompt(agentName, model.Provider)),
732		provider.WithMaxTokens(maxTokens),
733	}
734	if model.Provider == models.ProviderOpenAI || model.Provider == models.ProviderLocal && model.CanReason {
735		opts = append(
736			opts,
737			provider.WithOpenAIOptions(
738				provider.WithReasoningEffort(agentConfig.ReasoningEffort),
739			),
740		)
741	} else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder {
742		opts = append(
743			opts,
744			provider.WithAnthropicOptions(
745				provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn),
746			),
747		)
748	}
749	agentProvider, err := provider.NewProvider(
750		model.Provider,
751		opts...,
752	)
753	if err != nil {
754		return nil, fmt.Errorf("could not create provider: %v", err)
755	}
756
757	return agentProvider, nil
758}