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 session, err := a.sessions.Get(summarizeCtx, sessionID)
567 if err != nil {
568 event = AgentEvent{
569 Type: AgentEventTypeError,
570 Error: fmt.Errorf("failed to get session: %w", err),
571 Done: true,
572 }
573 a.Publish(pubsub.CreatedEvent, event)
574 return
575 }
576 if session.SummaryMessageID != "" {
577 summaryMsgInex := -1
578 for i, msg := range msgs {
579 if msg.ID == session.SummaryMessageID {
580 summaryMsgInex = i
581 break
582 }
583 }
584 if summaryMsgInex != -1 {
585 msgs = msgs[summaryMsgInex:]
586 msgs[0].Role = message.User
587 }
588 }
589
590 event = AgentEvent{
591 Type: AgentEventTypeSummarize,
592 Progress: "Analyzing conversation...",
593 }
594 a.Publish(pubsub.CreatedEvent, event)
595
596 // Add a system message to guide the summarization
597 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."
598
599 // Create a new message with the summarize prompt
600 promptMsg := message.Message{
601 Role: message.User,
602 Parts: []message.ContentPart{message.TextContent{Text: summarizePrompt}},
603 }
604
605 // Append the prompt to the messages
606 msgsWithPrompt := append(msgs, promptMsg)
607
608 event = AgentEvent{
609 Type: AgentEventTypeSummarize,
610 Progress: "Generating summary...",
611 }
612
613 a.Publish(pubsub.CreatedEvent, event)
614
615 // Send the messages to the summarize provider
616 response, err := a.summarizeProvider.SendMessages(
617 summarizeCtx,
618 msgsWithPrompt,
619 make([]tools.BaseTool, 0),
620 )
621 if err != nil {
622 event = AgentEvent{
623 Type: AgentEventTypeError,
624 Error: fmt.Errorf("failed to summarize: %w", err),
625 Done: true,
626 }
627 a.Publish(pubsub.CreatedEvent, event)
628 return
629 }
630
631 summary := strings.TrimSpace(response.Content)
632 if summary == "" {
633 event = AgentEvent{
634 Type: AgentEventTypeError,
635 Error: fmt.Errorf("empty summary returned"),
636 Done: true,
637 }
638 a.Publish(pubsub.CreatedEvent, event)
639 return
640 }
641 event = AgentEvent{
642 Type: AgentEventTypeSummarize,
643 Progress: "Creating new session...",
644 }
645
646 a.Publish(pubsub.CreatedEvent, event)
647 oldSession, err := a.sessions.Get(summarizeCtx, sessionID)
648 if err != nil {
649 event = AgentEvent{
650 Type: AgentEventTypeError,
651 Error: fmt.Errorf("failed to get session: %w", err),
652 Done: true,
653 }
654
655 a.Publish(pubsub.CreatedEvent, event)
656 return
657 }
658 // Create a message in the new session with the summary
659 msg, err := a.messages.Create(summarizeCtx, oldSession.ID, message.CreateMessageParams{
660 Role: message.Assistant,
661 Parts: []message.ContentPart{
662 message.TextContent{Text: summary},
663 message.Finish{
664 Reason: message.FinishReasonEndTurn,
665 Time: time.Now().Unix(),
666 },
667 },
668 Model: a.summarizeProvider.Model().ID,
669 })
670 if err != nil {
671 event = AgentEvent{
672 Type: AgentEventTypeError,
673 Error: fmt.Errorf("failed to create summary message: %w", err),
674 Done: true,
675 }
676
677 a.Publish(pubsub.CreatedEvent, event)
678 return
679 }
680 oldSession.SummaryMessageID = msg.ID
681 oldSession.CompletionTokens = response.Usage.OutputTokens
682 oldSession.PromptTokens = 0
683 model := a.summarizeProvider.Model()
684 usage := response.Usage
685 cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
686 model.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
687 model.CostPer1MIn/1e6*float64(usage.InputTokens) +
688 model.CostPer1MOut/1e6*float64(usage.OutputTokens)
689 oldSession.Cost += cost
690 _, err = a.sessions.Save(summarizeCtx, oldSession)
691 if err != nil {
692 event = AgentEvent{
693 Type: AgentEventTypeError,
694 Error: fmt.Errorf("failed to save session: %w", err),
695 Done: true,
696 }
697 a.Publish(pubsub.CreatedEvent, event)
698 }
699
700 event = AgentEvent{
701 Type: AgentEventTypeSummarize,
702 SessionID: oldSession.ID,
703 Progress: "Summary complete",
704 Done: true,
705 }
706 a.Publish(pubsub.CreatedEvent, event)
707 // Send final success event with the new session ID
708 }()
709
710 return nil
711}
712
713func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
714 cfg := config.Get()
715 agentConfig, ok := cfg.Agents[agentName]
716 if !ok {
717 return nil, fmt.Errorf("agent %s not found", agentName)
718 }
719 model, ok := models.SupportedModels[agentConfig.Model]
720 if !ok {
721 return nil, fmt.Errorf("model %s not supported", agentConfig.Model)
722 }
723
724 providerCfg, ok := cfg.Providers[model.Provider]
725 if !ok {
726 return nil, fmt.Errorf("provider %s not supported", model.Provider)
727 }
728 if providerCfg.Disabled {
729 return nil, fmt.Errorf("provider %s is not enabled", model.Provider)
730 }
731 maxTokens := model.DefaultMaxTokens
732 if agentConfig.MaxTokens > 0 {
733 maxTokens = agentConfig.MaxTokens
734 }
735 opts := []provider.ProviderClientOption{
736 provider.WithAPIKey(providerCfg.APIKey),
737 provider.WithModel(model),
738 provider.WithSystemMessage(prompt.GetAgentPrompt(agentName, model.Provider)),
739 provider.WithMaxTokens(maxTokens),
740 }
741 if model.Provider == models.ProviderOpenAI && model.CanReason {
742 opts = append(
743 opts,
744 provider.WithOpenAIOptions(
745 provider.WithReasoningEffort(agentConfig.ReasoningEffort),
746 ),
747 )
748 } else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder {
749 opts = append(
750 opts,
751 provider.WithAnthropicOptions(
752 provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn),
753 ),
754 )
755 }
756 agentProvider, err := provider.NewProvider(
757 model.Provider,
758 opts...,
759 )
760 if err != nil {
761 return nil, fmt.Errorf("could not create provider: %v", err)
762 }
763
764 return agentProvider, nil
765}