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.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 // FIXME(@andreynering): Temporary fix to get it working.
409 // We need to prefix the model with with `{region}.`
410 if largeModelCfg.Provider == bedrock.Name {
411 largeModelID = fmt.Sprintf("us.%s", largeModelID)
412 }
413 if smallModelCfg.Provider == bedrock.Name {
414 smallModelID = fmt.Sprintf("us.%s", smallModelID)
415 }
416
417 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
418 if err != nil {
419 return Model{}, Model{}, err
420 }
421 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
422 if err != nil {
423 return Model{}, Model{}, err
424 }
425
426 return Model{
427 Model: largeModel,
428 CatwalkCfg: *largeCatwalkModel,
429 ModelCfg: largeModelCfg,
430 }, Model{
431 Model: smallModel,
432 CatwalkCfg: *smallCatwalkModel,
433 ModelCfg: smallModelCfg,
434 }, nil
435}
436
437func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
438 hasBearerAuth := false
439 for key := range headers {
440 if strings.ToLower(key) == "authorization" {
441 hasBearerAuth = true
442 break
443 }
444 }
445 if hasBearerAuth {
446 apiKey = "" // clear apiKey to avoid using X-Api-Key header
447 }
448
449 var opts []anthropic.Option
450
451 if apiKey != "" {
452 // Use standard X-Api-Key header
453 opts = append(opts, anthropic.WithAPIKey(apiKey))
454 }
455
456 if len(headers) > 0 {
457 opts = append(opts, anthropic.WithHeaders(headers))
458 }
459
460 if baseURL != "" {
461 opts = append(opts, anthropic.WithBaseURL(baseURL))
462 }
463
464 if c.cfg.Options.Debug {
465 httpClient := log.NewHTTPClient()
466 opts = append(opts, anthropic.WithHTTPClient(httpClient))
467 }
468
469 return anthropic.New(opts...)
470}
471
472func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
473 opts := []openai.Option{
474 openai.WithAPIKey(apiKey),
475 openai.WithUseResponsesAPI(),
476 }
477 if c.cfg.Options.Debug {
478 httpClient := log.NewHTTPClient()
479 opts = append(opts, openai.WithHTTPClient(httpClient))
480 }
481 if len(headers) > 0 {
482 opts = append(opts, openai.WithHeaders(headers))
483 }
484 if baseURL != "" {
485 opts = append(opts, openai.WithBaseURL(baseURL))
486 }
487 return openai.New(opts...)
488}
489
490func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
491 opts := []openrouter.Option{
492 openrouter.WithAPIKey(apiKey),
493 }
494 if c.cfg.Options.Debug {
495 httpClient := log.NewHTTPClient()
496 opts = append(opts, openrouter.WithHTTPClient(httpClient))
497 }
498 if len(headers) > 0 {
499 opts = append(opts, openrouter.WithHeaders(headers))
500 }
501 return openrouter.New(opts...)
502}
503
504func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
505 opts := []openaicompat.Option{
506 openaicompat.WithBaseURL(baseURL),
507 openaicompat.WithAPIKey(apiKey),
508 }
509 if c.cfg.Options.Debug {
510 httpClient := log.NewHTTPClient()
511 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
512 }
513 if len(headers) > 0 {
514 opts = append(opts, openaicompat.WithHeaders(headers))
515 }
516
517 return openaicompat.New(opts...)
518}
519
520func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
521 opts := []azure.Option{
522 azure.WithBaseURL(baseURL),
523 azure.WithAPIKey(apiKey),
524 }
525 if c.cfg.Options.Debug {
526 httpClient := log.NewHTTPClient()
527 opts = append(opts, azure.WithHTTPClient(httpClient))
528 }
529 if options == nil {
530 options = make(map[string]string)
531 }
532 if apiVersion, ok := options["apiVersion"]; ok {
533 opts = append(opts, azure.WithAPIVersion(apiVersion))
534 }
535 if len(headers) > 0 {
536 opts = append(opts, azure.WithHeaders(headers))
537 }
538
539 return azure.New(opts...)
540}
541
542func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
543 var opts []bedrock.Option
544 if c.cfg.Options.Debug {
545 httpClient := log.NewHTTPClient()
546 opts = append(opts, bedrock.WithHTTPClient(httpClient))
547 }
548 if len(headers) > 0 {
549 opts = append(opts, bedrock.WithHeaders(headers))
550 }
551 return bedrock.New(opts...)
552}
553
554func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
555 opts := []google.Option{
556 google.WithBaseURL(baseURL),
557 google.WithGeminiAPIKey(apiKey),
558 }
559 if c.cfg.Options.Debug {
560 httpClient := log.NewHTTPClient()
561 opts = append(opts, google.WithHTTPClient(httpClient))
562 }
563 if len(headers) > 0 {
564 opts = append(opts, google.WithHeaders(headers))
565 }
566 return google.New(opts...)
567}
568
569func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
570 opts := []google.Option{}
571 if c.cfg.Options.Debug {
572 httpClient := log.NewHTTPClient()
573 opts = append(opts, google.WithHTTPClient(httpClient))
574 }
575 if len(headers) > 0 {
576 opts = append(opts, google.WithHeaders(headers))
577 }
578
579 project := options["project"]
580 location := options["location"]
581
582 opts = append(opts, google.WithVertex(project, location))
583
584 return google.New(opts...)
585}
586
587func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
588 if model.Think {
589 return true
590 }
591
592 if model.ProviderOptions == nil {
593 return false
594 }
595
596 opts, err := anthropic.ParseOptions(model.ProviderOptions)
597 if err != nil {
598 return false
599 }
600 if opts.Thinking != nil {
601 return true
602 }
603 return false
604}
605
606func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
607 headers := providerCfg.ExtraHeaders
608
609 // handle special headers for anthropic
610 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
611 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
612 }
613
614 // TODO: make sure we have
615 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
616 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
617
618 switch providerCfg.Type {
619 case openai.Name:
620 return c.buildOpenaiProvider(baseURL, apiKey, headers)
621 case anthropic.Name:
622 return c.buildAnthropicProvider(baseURL, apiKey, headers)
623 case openrouter.Name:
624 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
625 case azure.Name:
626 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
627 case bedrock.Name:
628 return c.buildBedrockProvider(headers)
629 case google.Name:
630 return c.buildGoogleProvider(baseURL, apiKey, headers)
631 case "google-vertex", "vertexai":
632 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
633 case openaicompat.Name:
634 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers)
635 default:
636 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
637 }
638}
639
640func (c *coordinator) Cancel(sessionID string) {
641 c.currentAgent.Cancel(sessionID)
642}
643
644func (c *coordinator) CancelAll() {
645 c.currentAgent.CancelAll()
646}
647
648func (c *coordinator) ClearQueue(sessionID string) {
649 c.currentAgent.ClearQueue(sessionID)
650}
651
652func (c *coordinator) IsBusy() bool {
653 return c.currentAgent.IsBusy()
654}
655
656func (c *coordinator) IsSessionBusy(sessionID string) bool {
657 return c.currentAgent.IsSessionBusy(sessionID)
658}
659
660func (c *coordinator) Model() Model {
661 return c.currentAgent.Model()
662}
663
664func (c *coordinator) UpdateModels(ctx context.Context) error {
665 // build the models again so we make sure we get the latest config
666 large, small, err := c.buildAgentModels(ctx)
667 if err != nil {
668 return err
669 }
670 c.currentAgent.SetModels(large, small)
671
672 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
673 if !ok {
674 return errors.New("coder agent not configured")
675 }
676
677 tools, err := c.buildTools(ctx, agentCfg)
678 if err != nil {
679 return err
680 }
681 c.currentAgent.SetTools(tools)
682 return nil
683}
684
685func (c *coordinator) QueuedPrompts(sessionID string) int {
686 return c.currentAgent.QueuedPrompts(sessionID)
687}
688
689func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
690 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
691 if !ok {
692 return errors.New("model provider not configured")
693 }
694 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg.Type))
695}