coordinator.go

  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}