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 tools, err := c.buildTools(ctx, agent)
280 if err != nil {
281 return nil, err
282 }
283 return NewSessionAgent(SessionAgentOptions{large, small, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.permissions.SkipRequests(), c.sessions, c.messages, tools}), nil
284}
285
286func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
287 var allTools []fantasy.AgentTool
288 if slices.Contains(agent.AllowedTools, AgentToolName) {
289 agentTool, err := c.agentTool(ctx)
290 if err != nil {
291 return nil, err
292 }
293 allTools = append(allTools, agentTool)
294 }
295
296 allTools = append(allTools,
297 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
298 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
299 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
300 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
301 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
302 tools.NewGlobTool(c.cfg.WorkingDir()),
303 tools.NewGrepTool(c.cfg.WorkingDir()),
304 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
305 tools.NewSourcegraphTool(nil),
306 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
307 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
308 )
309
310 if len(c.cfg.LSP) > 0 {
311 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
312 }
313
314 var filteredTools []fantasy.AgentTool
315 for _, tool := range allTools {
316 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
317 filteredTools = append(filteredTools, tool)
318 }
319 }
320
321 mcpTools := tools.GetMCPTools(context.Background(), c.permissions, c.cfg)
322
323 for _, mcpTool := range mcpTools {
324 if agent.AllowedMCP == nil {
325 // No MCP restrictions
326 filteredTools = append(filteredTools, mcpTool)
327 } else if len(agent.AllowedMCP) == 0 {
328 // no mcps allowed
329 break
330 }
331
332 for mcp, tools := range agent.AllowedMCP {
333 if mcp == mcpTool.MCP() {
334 if len(tools) == 0 {
335 filteredTools = append(filteredTools, mcpTool)
336 }
337 for _, t := range tools {
338 if t == mcpTool.MCPToolName() {
339 filteredTools = append(filteredTools, mcpTool)
340 }
341 }
342 break
343 }
344 }
345 }
346 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
347 return strings.Compare(a.Info().Name, b.Info().Name)
348 })
349 return filteredTools, nil
350}
351
352// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
353func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
354 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
355 if !ok {
356 return Model{}, Model{}, errors.New("large model not selected")
357 }
358 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
359 if !ok {
360 return Model{}, Model{}, errors.New("small model not selected")
361 }
362
363 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
364 if !ok {
365 return Model{}, Model{}, errors.New("large model provider not configured")
366 }
367
368 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
369 if err != nil {
370 return Model{}, Model{}, err
371 }
372
373 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
374 if !ok {
375 return Model{}, Model{}, errors.New("large model provider not configured")
376 }
377
378 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
379 if err != nil {
380 return Model{}, Model{}, err
381 }
382
383 var largeCatwalkModel *catwalk.Model
384 var smallCatwalkModel *catwalk.Model
385
386 for _, m := range largeProviderCfg.Models {
387 if m.ID == largeModelCfg.Model {
388 largeCatwalkModel = &m
389 }
390 }
391 for _, m := range smallProviderCfg.Models {
392 if m.ID == smallModelCfg.Model {
393 smallCatwalkModel = &m
394 }
395 }
396
397 if largeCatwalkModel == nil {
398 return Model{}, Model{}, errors.New("large model not found in provider config")
399 }
400
401 if smallCatwalkModel == nil {
402 return Model{}, Model{}, errors.New("snall model not found in provider config")
403 }
404
405 largeModelID := largeModelCfg.Model
406 smallModelID := smallModelCfg.Model
407
408 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
409 largeModelID += ":exacto"
410 }
411
412 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
413 smallModelID += ":exacto"
414 }
415
416 // FIXME(@andreynering): Temporary fix to get it working.
417 // We need to prefix the model with with `{region}.`
418 if largeModelCfg.Provider == bedrock.Name {
419 largeModelID = fmt.Sprintf("us.%s", largeModelID)
420 }
421 if smallModelCfg.Provider == bedrock.Name {
422 smallModelID = fmt.Sprintf("us.%s", smallModelID)
423 }
424
425 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
426 if err != nil {
427 return Model{}, Model{}, err
428 }
429 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
430 if err != nil {
431 return Model{}, Model{}, err
432 }
433
434 return Model{
435 Model: largeModel,
436 CatwalkCfg: *largeCatwalkModel,
437 ModelCfg: largeModelCfg,
438 }, Model{
439 Model: smallModel,
440 CatwalkCfg: *smallCatwalkModel,
441 ModelCfg: smallModelCfg,
442 }, nil
443}
444
445func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
446 hasBearerAuth := false
447 for key := range headers {
448 if strings.ToLower(key) == "authorization" {
449 hasBearerAuth = true
450 break
451 }
452 }
453 if hasBearerAuth {
454 apiKey = "" // clear apiKey to avoid using X-Api-Key header
455 }
456
457 var opts []anthropic.Option
458
459 if apiKey != "" {
460 // Use standard X-Api-Key header
461 opts = append(opts, anthropic.WithAPIKey(apiKey))
462 }
463
464 if len(headers) > 0 {
465 opts = append(opts, anthropic.WithHeaders(headers))
466 }
467
468 if baseURL != "" {
469 opts = append(opts, anthropic.WithBaseURL(baseURL))
470 }
471
472 if c.cfg.Options.Debug {
473 httpClient := log.NewHTTPClient()
474 opts = append(opts, anthropic.WithHTTPClient(httpClient))
475 }
476
477 return anthropic.New(opts...)
478}
479
480func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
481 opts := []openai.Option{
482 openai.WithAPIKey(apiKey),
483 openai.WithUseResponsesAPI(),
484 }
485 if c.cfg.Options.Debug {
486 httpClient := log.NewHTTPClient()
487 opts = append(opts, openai.WithHTTPClient(httpClient))
488 }
489 if len(headers) > 0 {
490 opts = append(opts, openai.WithHeaders(headers))
491 }
492 if baseURL != "" {
493 opts = append(opts, openai.WithBaseURL(baseURL))
494 }
495 return openai.New(opts...)
496}
497
498func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
499 opts := []openrouter.Option{
500 openrouter.WithAPIKey(apiKey),
501 }
502 if c.cfg.Options.Debug {
503 httpClient := log.NewHTTPClient()
504 opts = append(opts, openrouter.WithHTTPClient(httpClient))
505 }
506 if len(headers) > 0 {
507 opts = append(opts, openrouter.WithHeaders(headers))
508 }
509 return openrouter.New(opts...)
510}
511
512func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
513 opts := []openaicompat.Option{
514 openaicompat.WithBaseURL(baseURL),
515 openaicompat.WithAPIKey(apiKey),
516 }
517 if c.cfg.Options.Debug {
518 httpClient := log.NewHTTPClient()
519 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
520 }
521 if len(headers) > 0 {
522 opts = append(opts, openaicompat.WithHeaders(headers))
523 }
524
525 return openaicompat.New(opts...)
526}
527
528func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
529 opts := []azure.Option{
530 azure.WithBaseURL(baseURL),
531 azure.WithAPIKey(apiKey),
532 }
533 if c.cfg.Options.Debug {
534 httpClient := log.NewHTTPClient()
535 opts = append(opts, azure.WithHTTPClient(httpClient))
536 }
537 if options == nil {
538 options = make(map[string]string)
539 }
540 if apiVersion, ok := options["apiVersion"]; ok {
541 opts = append(opts, azure.WithAPIVersion(apiVersion))
542 }
543 if len(headers) > 0 {
544 opts = append(opts, azure.WithHeaders(headers))
545 }
546
547 return azure.New(opts...)
548}
549
550func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
551 var opts []bedrock.Option
552 if c.cfg.Options.Debug {
553 httpClient := log.NewHTTPClient()
554 opts = append(opts, bedrock.WithHTTPClient(httpClient))
555 }
556 if len(headers) > 0 {
557 opts = append(opts, bedrock.WithHeaders(headers))
558 }
559 return bedrock.New(opts...)
560}
561
562func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
563 opts := []google.Option{
564 google.WithBaseURL(baseURL),
565 google.WithGeminiAPIKey(apiKey),
566 }
567 if c.cfg.Options.Debug {
568 httpClient := log.NewHTTPClient()
569 opts = append(opts, google.WithHTTPClient(httpClient))
570 }
571 if len(headers) > 0 {
572 opts = append(opts, google.WithHeaders(headers))
573 }
574 return google.New(opts...)
575}
576
577func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
578 opts := []google.Option{}
579 if c.cfg.Options.Debug {
580 httpClient := log.NewHTTPClient()
581 opts = append(opts, google.WithHTTPClient(httpClient))
582 }
583 if len(headers) > 0 {
584 opts = append(opts, google.WithHeaders(headers))
585 }
586
587 project := options["project"]
588 location := options["location"]
589
590 opts = append(opts, google.WithVertex(project, location))
591
592 return google.New(opts...)
593}
594
595func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
596 if model.Think {
597 return true
598 }
599
600 if model.ProviderOptions == nil {
601 return false
602 }
603
604 opts, err := anthropic.ParseOptions(model.ProviderOptions)
605 if err != nil {
606 return false
607 }
608 if opts.Thinking != nil {
609 return true
610 }
611 return false
612}
613
614func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
615 headers := providerCfg.ExtraHeaders
616
617 // handle special headers for anthropic
618 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
619 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
620 }
621
622 // TODO: make sure we have
623 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
624 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
625
626 switch providerCfg.Type {
627 case openai.Name:
628 return c.buildOpenaiProvider(baseURL, apiKey, headers)
629 case anthropic.Name:
630 return c.buildAnthropicProvider(baseURL, apiKey, headers)
631 case openrouter.Name:
632 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
633 case azure.Name:
634 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
635 case bedrock.Name:
636 return c.buildBedrockProvider(headers)
637 case google.Name:
638 return c.buildGoogleProvider(baseURL, apiKey, headers)
639 case "google-vertex", "vertexai":
640 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
641 case openaicompat.Name:
642 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers)
643 default:
644 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
645 }
646}
647
648func isExactoSupported(modelID string) bool {
649 supportedModels := []string{
650 "moonshotai/kimi-k2-0905",
651 "deepseek/deepseek-v3.1-terminus",
652 "z-ai/glm-4.6",
653 "openai/gpt-oss-120b",
654 "qwen/qwen3-coder",
655 }
656 return slices.Contains(supportedModels, modelID)
657}
658
659func (c *coordinator) Cancel(sessionID string) {
660 c.currentAgent.Cancel(sessionID)
661}
662
663func (c *coordinator) CancelAll() {
664 c.currentAgent.CancelAll()
665}
666
667func (c *coordinator) ClearQueue(sessionID string) {
668 c.currentAgent.ClearQueue(sessionID)
669}
670
671func (c *coordinator) IsBusy() bool {
672 return c.currentAgent.IsBusy()
673}
674
675func (c *coordinator) IsSessionBusy(sessionID string) bool {
676 return c.currentAgent.IsSessionBusy(sessionID)
677}
678
679func (c *coordinator) Model() Model {
680 return c.currentAgent.Model()
681}
682
683func (c *coordinator) UpdateModels(ctx context.Context) error {
684 // build the models again so we make sure we get the latest config
685 large, small, err := c.buildAgentModels(ctx)
686 if err != nil {
687 return err
688 }
689 c.currentAgent.SetModels(large, small)
690
691 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
692 if !ok {
693 return errors.New("coder agent not configured")
694 }
695
696 tools, err := c.buildTools(ctx, agentCfg)
697 if err != nil {
698 return err
699 }
700 c.currentAgent.SetTools(tools)
701 return nil
702}
703
704func (c *coordinator) QueuedPrompts(sessionID string) int {
705 return c.currentAgent.QueuedPrompts(sessionID)
706}
707
708func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
709 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
710 if !ok {
711 return errors.New("model provider not configured")
712 }
713 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg.Type))
714}