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}