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