1package agent
2
3import (
4 "bytes"
5 "cmp"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "log/slog"
12 "slices"
13 "strings"
14
15 "charm.land/fantasy"
16 "github.com/charmbracelet/catwalk/pkg/catwalk"
17 "github.com/charmbracelet/crush/internal/agent/prompt"
18 "github.com/charmbracelet/crush/internal/agent/tools"
19 "github.com/charmbracelet/crush/internal/config"
20 "github.com/charmbracelet/crush/internal/csync"
21 "github.com/charmbracelet/crush/internal/history"
22 "github.com/charmbracelet/crush/internal/log"
23 "github.com/charmbracelet/crush/internal/lsp"
24 "github.com/charmbracelet/crush/internal/message"
25 "github.com/charmbracelet/crush/internal/permission"
26 "github.com/charmbracelet/crush/internal/session"
27
28 "charm.land/fantasy/providers/anthropic"
29 "charm.land/fantasy/providers/azure"
30 "charm.land/fantasy/providers/bedrock"
31 "charm.land/fantasy/providers/google"
32 "charm.land/fantasy/providers/openai"
33 "charm.land/fantasy/providers/openaicompat"
34 "charm.land/fantasy/providers/openrouter"
35 "github.com/qjebbs/go-jsons"
36)
37
38type Coordinator interface {
39 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
40 // SetMainAgent(string)
41 Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
42 Cancel(sessionID string)
43 CancelAll()
44 IsSessionBusy(sessionID string) bool
45 IsBusy() bool
46 QueuedPrompts(sessionID string) int
47 ClearQueue(sessionID string)
48 Summarize(context.Context, string) error
49 Model() Model
50 UpdateModels(ctx context.Context) error
51}
52
53type coordinator struct {
54 cfg *config.Config
55 sessions session.Service
56 messages message.Service
57 permissions permission.Service
58 history history.Service
59 lspClients *csync.Map[string, *lsp.Client]
60
61 currentAgent SessionAgent
62 agents map[string]SessionAgent
63}
64
65func NewCoordinator(
66 ctx context.Context,
67 cfg *config.Config,
68 sessions session.Service,
69 messages message.Service,
70 permissions permission.Service,
71 history history.Service,
72 lspClients *csync.Map[string, *lsp.Client],
73) (Coordinator, error) {
74 c := &coordinator{
75 cfg: cfg,
76 sessions: sessions,
77 messages: messages,
78 permissions: permissions,
79 history: history,
80 lspClients: lspClients,
81 agents: make(map[string]SessionAgent),
82 }
83
84 agentCfg, ok := cfg.Agents[config.AgentCoder]
85 if !ok {
86 return nil, errors.New("coder agent not configured")
87 }
88
89 // TODO: make this dynamic when we support multiple agents
90 prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
91 if err != nil {
92 return nil, err
93 }
94
95 agent, err := c.buildAgent(ctx, prompt, agentCfg)
96 if err != nil {
97 return nil, err
98 }
99 c.currentAgent = agent
100 c.agents[config.AgentCoder] = agent
101 return c, nil
102}
103
104// Run implements Coordinator.
105func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
106 model := c.currentAgent.Model()
107 maxTokens := model.CatwalkCfg.DefaultMaxTokens
108 if model.ModelCfg.MaxTokens != 0 {
109 maxTokens = model.ModelCfg.MaxTokens
110 }
111
112 if !model.CatwalkCfg.SupportsImages && attachments != nil {
113 attachments = nil
114 }
115
116 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
117 if !ok {
118 return nil, errors.New("model provider not configured")
119 }
120
121 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg.Type)
122
123 return c.currentAgent.Run(ctx, SessionAgentCall{
124 SessionID: sessionID,
125 Prompt: prompt,
126 Attachments: attachments,
127 MaxOutputTokens: maxTokens,
128 ProviderOptions: mergedOptions,
129 Temperature: temp,
130 TopP: topP,
131 TopK: topK,
132 FrequencyPenalty: freqPenalty,
133 PresencePenalty: presPenalty,
134 })
135}
136
137func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions {
138 options := fantasy.ProviderOptions{}
139
140 cfgOpts := []byte("{}")
141 catwalkOpts := []byte("{}")
142
143 if model.ModelCfg.ProviderOptions != nil {
144 data, err := json.Marshal(model.ModelCfg.ProviderOptions)
145 if err == nil {
146 cfgOpts = data
147 }
148 }
149
150 if model.CatwalkCfg.Options.ProviderOptions != nil {
151 data, err := json.Marshal(model.CatwalkCfg.Options.ProviderOptions)
152 if err == nil {
153 catwalkOpts = data
154 }
155 }
156
157 readers := []io.Reader{
158 bytes.NewReader(catwalkOpts),
159 bytes.NewReader(cfgOpts),
160 }
161
162 got, err := jsons.Merge(readers)
163 if err != nil {
164 slog.Error("Could not merge call config", "err", err)
165 return options
166 }
167
168 mergedOptions := make(map[string]any)
169
170 err = json.Unmarshal([]byte(got), &mergedOptions)
171 if err != nil {
172 slog.Error("Could not create config for call", "err", err)
173 return options
174 }
175
176 switch tp {
177 case openai.Name:
178 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
179 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
180 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
181 }
182 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
183 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
184 mergedOptions["reasoning_summary"] = "auto"
185 mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
186 }
187 parsed, err := openai.ParseResponsesOptions(mergedOptions)
188 if err == nil {
189 options[openai.Name] = parsed
190 }
191 } else {
192 parsed, err := openai.ParseOptions(mergedOptions)
193 if err == nil {
194 options[openai.Name] = parsed
195 }
196 }
197 case anthropic.Name:
198 _, hasThink := mergedOptions["thinking"]
199 if !hasThink && model.ModelCfg.Think {
200 mergedOptions["thinking"] = map[string]any{
201 // TODO: kujtim see if we need to make this dynamic
202 "budget_tokens": 2000,
203 }
204 }
205 parsed, err := anthropic.ParseOptions(mergedOptions)
206 if err == nil {
207 options[anthropic.Name] = parsed
208 }
209
210 case openrouter.Name:
211 _, hasReasoning := mergedOptions["reasoning"]
212 if !hasReasoning && model.ModelCfg.ReasoningEffort != "" {
213 mergedOptions["reasoning"] = map[string]any{
214 "enabled": true,
215 "effort": model.ModelCfg.ReasoningEffort,
216 }
217 }
218 parsed, err := openrouter.ParseOptions(mergedOptions)
219 if err == nil {
220 options[openrouter.Name] = parsed
221 }
222 case google.Name:
223 _, hasReasoning := mergedOptions["thinking_config"]
224 if !hasReasoning {
225 mergedOptions["thinking_config"] = map[string]any{
226 "thinking_budget": 2000,
227 "include_thoughts": true,
228 }
229 }
230 parsed, err := google.ParseOptions(mergedOptions)
231 if err == nil {
232 options[google.Name] = parsed
233 }
234 case azure.Name:
235 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
236 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
237 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
238 }
239 // azure uses the same options as openaicompat
240 parsed, err := openaicompat.ParseOptions(mergedOptions)
241 if err == nil {
242 options[azure.Name] = parsed
243 }
244 case openaicompat.Name:
245 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
246 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
247 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
248 }
249 parsed, err := openaicompat.ParseOptions(mergedOptions)
250 if err == nil {
251 options[openaicompat.Name] = parsed
252 }
253 }
254
255 return options
256}
257
258func mergeCallOptions(model Model, tp catwalk.Type) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
259 modelOptions := getProviderOptions(model, tp)
260 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
261 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
262 topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
263 freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
264 presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
265 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
266}
267
268func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent) (SessionAgent, error) {
269 large, small, err := c.buildAgentModels(ctx)
270 if err != nil {
271 return nil, err
272 }
273
274 systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
275 if err != nil {
276 return nil, err
277 }
278
279 largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
280 tools, err := c.buildTools(ctx, agent)
281 if err != nil {
282 return nil, err
283 }
284 return NewSessionAgent(SessionAgentOptions{large, small, largeProviderCfg.SystemPromptPrefix, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.permissions.SkipRequests(), c.sessions, c.messages, tools}), nil
285}
286
287func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
288 var allTools []fantasy.AgentTool
289 if slices.Contains(agent.AllowedTools, AgentToolName) {
290 agentTool, err := c.agentTool(ctx)
291 if err != nil {
292 return nil, err
293 }
294 allTools = append(allTools, agentTool)
295 }
296
297 allTools = append(allTools,
298 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
299 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
300 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
301 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
302 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
303 tools.NewGlobTool(c.cfg.WorkingDir()),
304 tools.NewGrepTool(c.cfg.WorkingDir()),
305 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
306 tools.NewSourcegraphTool(nil),
307 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
308 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
309 )
310
311 if len(c.cfg.LSP) > 0 {
312 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
313 }
314
315 var filteredTools []fantasy.AgentTool
316 for _, tool := range allTools {
317 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
318 filteredTools = append(filteredTools, tool)
319 }
320 }
321
322 mcpTools := tools.GetMCPTools(context.Background(), c.permissions, c.cfg)
323
324 for _, mcpTool := range mcpTools {
325 if agent.AllowedMCP == nil {
326 // No MCP restrictions
327 filteredTools = append(filteredTools, mcpTool)
328 } else if len(agent.AllowedMCP) == 0 {
329 // no mcps allowed
330 break
331 }
332
333 for mcp, tools := range agent.AllowedMCP {
334 if mcp == mcpTool.MCP() {
335 if len(tools) == 0 {
336 filteredTools = append(filteredTools, mcpTool)
337 }
338 for _, t := range tools {
339 if t == mcpTool.MCPToolName() {
340 filteredTools = append(filteredTools, mcpTool)
341 }
342 }
343 break
344 }
345 }
346 }
347 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
348 return strings.Compare(a.Info().Name, b.Info().Name)
349 })
350 return filteredTools, nil
351}
352
353// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
354func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
355 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
356 if !ok {
357 return Model{}, Model{}, errors.New("large model not selected")
358 }
359 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
360 if !ok {
361 return Model{}, Model{}, errors.New("small model not selected")
362 }
363
364 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
365 if !ok {
366 return Model{}, Model{}, errors.New("large model provider not configured")
367 }
368
369 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
370 if err != nil {
371 return Model{}, Model{}, err
372 }
373
374 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
375 if !ok {
376 return Model{}, Model{}, errors.New("large model provider not configured")
377 }
378
379 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
380 if err != nil {
381 return Model{}, Model{}, err
382 }
383
384 var largeCatwalkModel *catwalk.Model
385 var smallCatwalkModel *catwalk.Model
386
387 for _, m := range largeProviderCfg.Models {
388 if m.ID == largeModelCfg.Model {
389 largeCatwalkModel = &m
390 }
391 }
392 for _, m := range smallProviderCfg.Models {
393 if m.ID == smallModelCfg.Model {
394 smallCatwalkModel = &m
395 }
396 }
397
398 if largeCatwalkModel == nil {
399 return Model{}, Model{}, errors.New("large model not found in provider config")
400 }
401
402 if smallCatwalkModel == nil {
403 return Model{}, Model{}, errors.New("snall model not found in provider config")
404 }
405
406 largeModelID := largeModelCfg.Model
407 smallModelID := smallModelCfg.Model
408
409 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
410 largeModelID += ":exacto"
411 }
412
413 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
414 smallModelID += ":exacto"
415 }
416
417 // FIXME(@andreynering): Temporary fix to get it working.
418 // We need to prefix the model with with `{region}.`
419 if largeModelCfg.Provider == bedrock.Name {
420 largeModelID = fmt.Sprintf("us.%s", largeModelID)
421 }
422 if smallModelCfg.Provider == bedrock.Name {
423 smallModelID = fmt.Sprintf("us.%s", smallModelID)
424 }
425
426 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
427 if err != nil {
428 return Model{}, Model{}, err
429 }
430 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
431 if err != nil {
432 return Model{}, Model{}, err
433 }
434
435 return Model{
436 Model: largeModel,
437 CatwalkCfg: *largeCatwalkModel,
438 ModelCfg: largeModelCfg,
439 }, Model{
440 Model: smallModel,
441 CatwalkCfg: *smallCatwalkModel,
442 ModelCfg: smallModelCfg,
443 }, nil
444}
445
446func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
447 hasBearerAuth := false
448 for key := range headers {
449 if strings.ToLower(key) == "authorization" {
450 hasBearerAuth = true
451 break
452 }
453 }
454
455 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
456
457 var opts []anthropic.Option
458 if apiKey != "" && !hasBearerAuth {
459 if isBearerToken {
460 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
461 headers["Authorization"] = apiKey
462 apiKey = "" // clear apiKey to avoid using X-Api-Key header
463 }
464 }
465
466 if apiKey != "" {
467 // Use standard X-Api-Key header
468 opts = append(opts, anthropic.WithAPIKey(apiKey))
469 }
470
471 if len(headers) > 0 {
472 opts = append(opts, anthropic.WithHeaders(headers))
473 }
474
475 if baseURL != "" {
476 opts = append(opts, anthropic.WithBaseURL(baseURL))
477 }
478
479 if c.cfg.Options.Debug {
480 httpClient := log.NewHTTPClient()
481 opts = append(opts, anthropic.WithHTTPClient(httpClient))
482 }
483
484 return anthropic.New(opts...)
485}
486
487func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
488 opts := []openai.Option{
489 openai.WithAPIKey(apiKey),
490 openai.WithUseResponsesAPI(),
491 }
492 if c.cfg.Options.Debug {
493 httpClient := log.NewHTTPClient()
494 opts = append(opts, openai.WithHTTPClient(httpClient))
495 }
496 if len(headers) > 0 {
497 opts = append(opts, openai.WithHeaders(headers))
498 }
499 if baseURL != "" {
500 opts = append(opts, openai.WithBaseURL(baseURL))
501 }
502 return openai.New(opts...)
503}
504
505func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
506 opts := []openrouter.Option{
507 openrouter.WithAPIKey(apiKey),
508 }
509 if c.cfg.Options.Debug {
510 httpClient := log.NewHTTPClient()
511 opts = append(opts, openrouter.WithHTTPClient(httpClient))
512 }
513 if len(headers) > 0 {
514 opts = append(opts, openrouter.WithHeaders(headers))
515 }
516 return openrouter.New(opts...)
517}
518
519func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
520 opts := []openaicompat.Option{
521 openaicompat.WithBaseURL(baseURL),
522 openaicompat.WithAPIKey(apiKey),
523 }
524 if c.cfg.Options.Debug {
525 httpClient := log.NewHTTPClient()
526 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
527 }
528 if len(headers) > 0 {
529 opts = append(opts, openaicompat.WithHeaders(headers))
530 }
531
532 return openaicompat.New(opts...)
533}
534
535func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
536 opts := []azure.Option{
537 azure.WithBaseURL(baseURL),
538 azure.WithAPIKey(apiKey),
539 }
540 if c.cfg.Options.Debug {
541 httpClient := log.NewHTTPClient()
542 opts = append(opts, azure.WithHTTPClient(httpClient))
543 }
544 if options == nil {
545 options = make(map[string]string)
546 }
547 if apiVersion, ok := options["apiVersion"]; ok {
548 opts = append(opts, azure.WithAPIVersion(apiVersion))
549 }
550 if len(headers) > 0 {
551 opts = append(opts, azure.WithHeaders(headers))
552 }
553
554 return azure.New(opts...)
555}
556
557func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
558 var opts []bedrock.Option
559 if c.cfg.Options.Debug {
560 httpClient := log.NewHTTPClient()
561 opts = append(opts, bedrock.WithHTTPClient(httpClient))
562 }
563 if len(headers) > 0 {
564 opts = append(opts, bedrock.WithHeaders(headers))
565 }
566 return bedrock.New(opts...)
567}
568
569func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
570 opts := []google.Option{
571 google.WithBaseURL(baseURL),
572 google.WithGeminiAPIKey(apiKey),
573 }
574 if c.cfg.Options.Debug {
575 httpClient := log.NewHTTPClient()
576 opts = append(opts, google.WithHTTPClient(httpClient))
577 }
578 if len(headers) > 0 {
579 opts = append(opts, google.WithHeaders(headers))
580 }
581 return google.New(opts...)
582}
583
584func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
585 opts := []google.Option{}
586 if c.cfg.Options.Debug {
587 httpClient := log.NewHTTPClient()
588 opts = append(opts, google.WithHTTPClient(httpClient))
589 }
590 if len(headers) > 0 {
591 opts = append(opts, google.WithHeaders(headers))
592 }
593
594 project := options["project"]
595 location := options["location"]
596
597 opts = append(opts, google.WithVertex(project, location))
598
599 return google.New(opts...)
600}
601
602func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
603 if model.Think {
604 return true
605 }
606
607 if model.ProviderOptions == nil {
608 return false
609 }
610
611 opts, err := anthropic.ParseOptions(model.ProviderOptions)
612 if err != nil {
613 return false
614 }
615 if opts.Thinking != nil {
616 return true
617 }
618 return false
619}
620
621func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
622 headers := providerCfg.ExtraHeaders
623
624 // handle special headers for anthropic
625 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
626 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
627 }
628
629 // TODO: make sure we have
630 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
631 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
632
633 switch providerCfg.Type {
634 case openai.Name:
635 return c.buildOpenaiProvider(baseURL, apiKey, headers)
636 case anthropic.Name:
637 return c.buildAnthropicProvider(baseURL, apiKey, headers)
638 case openrouter.Name:
639 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
640 case azure.Name:
641 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
642 case bedrock.Name:
643 return c.buildBedrockProvider(headers)
644 case google.Name:
645 return c.buildGoogleProvider(baseURL, apiKey, headers)
646 case "google-vertex", "vertexai":
647 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
648 case openaicompat.Name:
649 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers)
650 default:
651 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
652 }
653}
654
655func isExactoSupported(modelID string) bool {
656 supportedModels := []string{
657 "moonshotai/kimi-k2-0905",
658 "deepseek/deepseek-v3.1-terminus",
659 "z-ai/glm-4.6",
660 "openai/gpt-oss-120b",
661 "qwen/qwen3-coder",
662 }
663 return slices.Contains(supportedModels, modelID)
664}
665
666func (c *coordinator) Cancel(sessionID string) {
667 c.currentAgent.Cancel(sessionID)
668}
669
670func (c *coordinator) CancelAll() {
671 c.currentAgent.CancelAll()
672}
673
674func (c *coordinator) ClearQueue(sessionID string) {
675 c.currentAgent.ClearQueue(sessionID)
676}
677
678func (c *coordinator) IsBusy() bool {
679 return c.currentAgent.IsBusy()
680}
681
682func (c *coordinator) IsSessionBusy(sessionID string) bool {
683 return c.currentAgent.IsSessionBusy(sessionID)
684}
685
686func (c *coordinator) Model() Model {
687 return c.currentAgent.Model()
688}
689
690func (c *coordinator) UpdateModels(ctx context.Context) error {
691 // build the models again so we make sure we get the latest config
692 large, small, err := c.buildAgentModels(ctx)
693 if err != nil {
694 return err
695 }
696 c.currentAgent.SetModels(large, small)
697
698 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
699 if !ok {
700 return errors.New("coder agent not configured")
701 }
702
703 tools, err := c.buildTools(ctx, agentCfg)
704 if err != nil {
705 return err
706 }
707 c.currentAgent.SetTools(tools)
708 return nil
709}
710
711func (c *coordinator) QueuedPrompts(sessionID string) int {
712 return c.currentAgent.QueuedPrompts(sessionID)
713}
714
715func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
716 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
717 if !ok {
718 return errors.New("model provider not configured")
719 }
720 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg.Type))
721}