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}