@@ -7,7 +7,6 @@ import (
77 "strings"
88 "sync"
99
10− "github.com/kujtimiihoxha/termai/internal/app"
1110 "github.com/kujtimiihoxha/termai/internal/config"
1211 "github.com/kujtimiihoxha/termai/internal/llm/models"
1312 "github.com/kujtimiihoxha/termai/internal/llm/prompt"
@@ -15,22 +14,118 @@ import (
1514 "github.com/kujtimiihoxha/termai/internal/llm/tools"
1615 "github.com/kujtimiihoxha/termai/internal/logging"
1716 "github.com/kujtimiihoxha/termai/internal/message"
17+ "github.com/kujtimiihoxha/termai/internal/session"
1818)
1919
20−type Agent interface {
20+// Common errors
21+var (
22+ ErrProviderNotEnabled = errors.New("provider is not enabled")
23+ ErrRequestCancelled = errors.New("request cancelled by user")
24+ ErrSessionBusy = errors.New("session is currently processing another request")
25+)
26+
27+// Service defines the interface for generating responses
28+type Service interface {
2129 Generate(ctx context.Context, sessionID string, content string) error
30+ Cancel(sessionID string) error
2231}
2332
2433type agent struct {
25− *app.App
34+ sessions session.Service
35+ messages message.Service
2636 model models.Model
2737 tools []tools.BaseTool
2838 agent provider.Provider
2939 titleGenerator provider.Provider
40+ activeRequests sync.Map // map[sessionID]context.CancelFunc
41+}
42+
43+// NewAgent creates a new agent instance with the given model and tools
44+func NewAgent(ctx context.Context, sessions session.Service, messages message.Service, model models.Model, tools []tools.BaseTool) (Service, error) {
45+ agentProvider, titleGenerator, err := getAgentProviders(ctx, model)
46+ if err != nil {
47+ return nil, fmt.Errorf("failed to initialize providers: %w", err)
48+ }
49+
50+ return &agent{
51+ model: model,
52+ tools: tools,
53+ sessions: sessions,
54+ messages: messages,
55+ agent: agentProvider,
56+ titleGenerator: titleGenerator,
57+ activeRequests: sync.Map{},
58+ }, nil
59+}
60+
61+// Cancel cancels an active request by session ID
62+func (a *agent) Cancel(sessionID string) error {
63+ if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
64+ if cancel, ok := cancelFunc.(context.CancelFunc); ok {
65+ logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
66+ cancel()
67+ return nil
68+ }
69+ }
70+ return errors.New("no active request found for this session")
3071}
3172
32−func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content string) {
33− response, err := c.titleGenerator.SendMessages(
73+// Generate starts the generation process
74+func (a *agent) Generate(ctx context.Context, sessionID string, content string) error {
75+ // Check if this session already has an active request
76+ if _, busy := a.activeRequests.Load(sessionID); busy {
77+ return ErrSessionBusy
78+ }
79+
80+ // Create a cancellable context
81+ genCtx, cancel := context.WithCancel(ctx)
82+
83+ // Store cancel function to allow user cancellation
84+ a.activeRequests.Store(sessionID, cancel)
85+
86+ // Launch the generation in a goroutine
87+ go func() {
88+ defer func() {
89+ if r := recover(); r != nil {
90+ logging.ErrorPersist(fmt.Sprintf("Panic in Generate: %v", r))
91+ }
92+ }()
93+ defer a.activeRequests.Delete(sessionID)
94+ defer cancel()
95+
96+ if err := a.generate(genCtx, sessionID, content); err != nil {
97+ if !errors.Is(err, ErrRequestCancelled) && !errors.Is(err, context.Canceled) {
98+ // Log the error (avoid logging cancellations as they're expected)
99+ logging.ErrorPersist(fmt.Sprintf("Generation error for session %s: %v", sessionID, err))
100+
101+ // You may want to create an error message in the chat
102+ bgCtx := context.Background()
103+ errorMsg := fmt.Sprintf("Sorry, an error occurred: %v", err)
104+ _, createErr := a.messages.Create(bgCtx, sessionID, message.CreateMessageParams{
105+ Role: message.System,
106+ Parts: []message.ContentPart{
107+ message.TextContent{
108+ Text: errorMsg,
109+ },
110+ },
111+ })
112+ if createErr != nil {
113+ logging.ErrorPersist(fmt.Sprintf("Failed to create error message: %v", createErr))
114+ }
115+ }
116+ }
117+ }()
118+
119+ return nil
120+}
121+
122+// IsSessionBusy checks if a session currently has an active request
123+func (a *agent) IsSessionBusy(sessionID string) bool {
124+ _, busy := a.activeRequests.Load(sessionID)
125+ return busy
126+} // handleTitleGeneration asynchronously generates a title for new sessions
127+func (a *agent) handleTitleGeneration(ctx context.Context, sessionID, content string) {
128+ response, err := a.titleGenerator.SendMessages(
34129 ctx,
35130 []message.Message{
36131 {
@@ -45,25 +140,30 @@ func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content st
45140 nil,
46141 )
47142 if err != nil {
143+ logging.ErrorPersist(fmt.Sprintf("Failed to generate title: %v", err))
48144 return
49145 }
50146
51− session, err := c.Sessions.Get(ctx, sessionID)
147+ session, err := a.sessions.Get(ctx, sessionID)
52148 if err != nil {
149+ logging.ErrorPersist(fmt.Sprintf("Failed to get session: %v", err))
53150 return
54151 }
152+
55153 if response.Content != "" {
56− session.Title = response.Content
57− session.Title = strings.TrimSpace(session.Title)
154+ session.Title = strings.TrimSpace(response.Content)
58155 session.Title = strings.ReplaceAll(session.Title, "\n", " ")
59− c.Sessions.Save(ctx, session)
156+ if _, err := a.sessions.Save(ctx, session); err != nil {
157+ logging.ErrorPersist(fmt.Sprintf("Failed to save session title: %v", err))
158+ }
60159 }
61160}
62161
63−func (c *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error {
64− session, err := c.Sessions.Get(ctx, sessionID)
162+// TrackUsage updates token usage statistics for the session
163+func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error {
164+ session, err := a.sessions.Get(ctx, sessionID)
65165 if err != nil {
66− return err
166+ return fmt.Errorf("failed to get session: %w", err)
67167 }
68168
69169 cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
@@ -75,189 +175,241 @@ func (c *agent) TrackUsage(ctx context.Context, sessionID string, model models.M
75175 session.CompletionTokens += usage.OutputTokens
76176 session.PromptTokens += usage.InputTokens
77177
78− _, err = c.Sessions.Save(ctx, session)
79− return err
178+ _, err = a.sessions.Save(ctx, session)
179+ if err != nil {
180+ return fmt.Errorf("failed to save session: %w", err)
181+ }
182+ return nil
80183}
81184
82−func (c *agent) processEvent(
185+// processEvent handles different types of events during generation
186+func (a *agent) processEvent(
83187 ctx context.Context,
84188 sessionID string,
85189 assistantMsg *message.Message,
86190 event provider.ProviderEvent,
87191) error {
192+ select {
193+ case <-ctx.Done():
194+ return ctx.Err()
195+ default:
196+ // Continue processing
197+ }
198+
88199 switch event.Type {
89200 case provider.EventThinkingDelta:
90201 assistantMsg.AppendReasoningContent(event.Content)
91− return c.Messages.Update(ctx, *assistantMsg)
202+ return a.messages.Update(ctx, *assistantMsg)
92203 case provider.EventContentDelta:
93204 assistantMsg.AppendContent(event.Content)
94− return c.Messages.Update(ctx, *assistantMsg)
205+ return a.messages.Update(ctx, *assistantMsg)
95206 case provider.EventError:
96207 if errors.Is(event.Error, context.Canceled) {
97− return nil
208+ logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
209+ return context.Canceled
98210 }
99211 logging.ErrorPersist(event.Error.Error())
100212 return event.Error
101213 case provider.EventWarning:
102214 logging.WarnPersist(event.Info)
103− return nil
104215 case provider.EventInfo:
105216 logging.InfoPersist(event.Info)
106217 case provider.EventComplete:
107218 assistantMsg.SetToolCalls(event.Response.ToolCalls)
108219 assistantMsg.AddFinish(event.Response.FinishReason)
109− err := c.Messages.Update(ctx, *assistantMsg)
110− if err != nil {
111− return err
220+ if err := a.messages.Update(ctx, *assistantMsg); err != nil {
221+ return fmt.Errorf("failed to update message: %w", err)
112222 }
113− return c.TrackUsage(ctx, sessionID, c.model, event.Response.Usage)
223+ return a.TrackUsage(ctx, sessionID, a.model, event.Response.Usage)
114224 }
115225
116226 return nil
117227}
118228
119−func (c *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall, tls []tools.BaseTool) ([]message.ToolResult, error) {
120− var wg sync.WaitGroup
229+// ExecuteTools runs all tool calls sequentially and returns the results
230+func (a *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall, tls []tools.BaseTool) ([]message.ToolResult, error) {
121231 toolResults := make([]message.ToolResult, len(toolCalls))
122− mutex := &sync.Mutex{}
123− errChan := make(chan error, 1)
124232
125233 // Create a child context that can be canceled
126234 ctx, cancel := context.WithCancel(ctx)
127235 defer cancel()
128236
129− for i, tc := range toolCalls {
130− wg.Add(1)
131− go func(index int, toolCall message.ToolCall) {
132− defer wg.Done()
237+ // Check if already canceled before starting any execution
238+ if ctx.Err() != nil {
239+ // Mark all tools as canceled
240+ for i, toolCall := range toolCalls {
241+ toolResults[i] = message.ToolResult{
242+ ToolCallID: toolCall.ID,
243+ Content: "Tool execution canceled by user",
244+ IsError: true,
245+ }
246+ }
247+ return toolResults, ctx.Err()
248+ }
133249
134− // Check if context is already canceled
135− select {
136− case <-ctx.Done():
137− mutex.Lock()
138− toolResults[index] = message.ToolResult{
139− ToolCallID: toolCall.ID,
140− Content: "Tool execution canceled",
250+ for i, toolCall := range toolCalls {
251+ // Check for cancellation before executing each tool
252+ select {
253+ case <-ctx.Done():
254+ // Mark this and all remaining tools as canceled
255+ for j := i; j < len(toolCalls); j++ {
256+ toolResults[j] = message.ToolResult{
257+ ToolCallID: toolCalls[j].ID,
258+ Content: "Tool execution canceled by user",
141259 IsError: true,
142260 }
143− mutex.Unlock()
144−
145− // Send cancellation error to error channel if it's empty
146− select {
147− case errChan <- ctx.Err():
148− default:
149− }
150− return
151− default:
152261 }
262+ return toolResults, ctx.Err()
263+ default:
264+ // Continue processing
265+ }
153266
154− response := ""
155− isError := false
156− found := false
157−
158− for _, tool := range tls {
159− if tool.Info().Name == toolCall.Name {
160− found = true
161− toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
162− ID: toolCall.ID,
163− Name: toolCall.Name,
164− Input: toolCall.Input,
165− })
166−
167− if toolErr != nil {
168− if errors.Is(toolErr, context.Canceled) {
169− response = "Tool execution canceled"
170−
171− // Send cancellation error to error channel if it's empty
172− select {
173− case errChan <- ctx.Err():
174− default:
175− }
176− } else {
177− response = fmt.Sprintf("error running tool: %s", toolErr)
178− }
179− isError = true
267+ response := ""
268+ isError := false
269+ found := false
270+
271+ // Find and execute the appropriate tool
272+ for _, tool := range tls {
273+ if tool.Info().Name == toolCall.Name {
274+ found = true
275+ toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
276+ ID: toolCall.ID,
277+ Name: toolCall.Name,
278+ Input: toolCall.Input,
279+ })
280+
281+ if toolErr != nil {
282+ if errors.Is(toolErr, context.Canceled) {
283+ response = "Tool execution canceled by user"
180284 } else {
181− response = toolResult.Content
182− isError = toolResult.IsError
285+ response = fmt.Sprintf("Error running tool: %s", toolErr)
183286 }
184− break
287+ isError = true
288+ } else {
289+ response = toolResult.Content
290+ isError = toolResult.IsError
185291 }
292+ break
186293 }
294+ }
187295
188− if !found {
189− response = fmt.Sprintf("tool not found: %s", toolCall.Name)
190− isError = true
191− }
192−
193− mutex.Lock()
194− defer mutex.Unlock()
195−
196− toolResults[index] = message.ToolResult{
197− ToolCallID: toolCall.ID,
198− Content: response,
199− IsError: isError,
200− }
201− }(i, tc)
202− }
203−
204− // Wait for all goroutines to finish or context to be canceled
205− done := make(chan struct{})
206− go func() {
207− wg.Wait()
208− close(done)
209− }()
296+ if !found {
297+ response = fmt.Sprintf("Tool not found: %s", toolCall.Name)
298+ isError = true
299+ }
210300
211− select {
212− case <-done:
213− // All tools completed successfully
214− case err := <-errChan:
215− // One of the tools encountered a cancellation
216− return toolResults, err
217− case <-ctx.Done():
218− // Context was canceled externally
219− return toolResults, ctx.Err()
301+ toolResults[i] = message.ToolResult{
302+ ToolCallID: toolCall.ID,
303+ Content: response,
304+ IsError: isError,
305+ }
220306 }
221307
222308 return toolResults, nil
223309}
224310
225−func (c *agent) handleToolExecution(
311+// handleToolExecution processes tool calls and creates tool result messages
312+func (a *agent) handleToolExecution(
226313 ctx context.Context,
227314 assistantMsg message.Message,
228315) (*message.Message, error) {
316+ select {
317+ case <-ctx.Done():
318+ // If cancelled, create tool results that indicate cancellation
319+ if len(assistantMsg.ToolCalls()) > 0 {
320+ toolResults := make([]message.ToolResult, 0, len(assistantMsg.ToolCalls()))
321+ for _, tc := range assistantMsg.ToolCalls() {
322+ toolResults = append(toolResults, message.ToolResult{
323+ ToolCallID: tc.ID,
324+ Content: "Tool execution canceled by user",
325+ IsError: true,
326+ })
327+ }
328+
329+ // Use background context to ensure the message is created even if original context is cancelled
330+ bgCtx := context.Background()
331+ parts := make([]message.ContentPart, 0)
332+ for _, toolResult := range toolResults {
333+ parts = append(parts, toolResult)
334+ }
335+ msg, err := a.messages.Create(bgCtx, assistantMsg.SessionID, message.CreateMessageParams{
336+ Role: message.Tool,
337+ Parts: parts,
338+ })
339+ if err != nil {
340+ return nil, fmt.Errorf("failed to create cancelled tool message: %w", err)
341+ }
342+ return &msg, ctx.Err()
343+ }
344+ return nil, ctx.Err()
345+ default:
346+ // Continue processing
347+ }
348+
229349 if len(assistantMsg.ToolCalls()) == 0 {
230350 return nil, nil
231351 }
232352
233− toolResults, err := c.ExecuteTools(ctx, assistantMsg.ToolCalls(), c.tools)
353+ toolResults, err := a.ExecuteTools(ctx, assistantMsg.ToolCalls(), a.tools)
234354 if err != nil {
355+ // If error is from cancellation, still return the partial results we have
356+ if errors.Is(err, context.Canceled) {
357+ // Use background context to ensure the message is created even if original context is cancelled
358+ bgCtx := context.Background()
359+ parts := make([]message.ContentPart, 0)
360+ for _, toolResult := range toolResults {
361+ parts = append(parts, toolResult)
362+ }
363+
364+ msg, createErr := a.messages.Create(bgCtx, assistantMsg.SessionID, message.CreateMessageParams{
365+ Role: message.Tool,
366+ Parts: parts,
367+ })
368+ if createErr != nil {
369+ logging.ErrorPersist(fmt.Sprintf("Failed to create tool message after cancellation: %v", createErr))
370+ return nil, err
371+ }
372+ return &msg, err
373+ }
235374 return nil, err
236375 }
237− parts := make([]message.ContentPart, 0)
376+
377+ parts := make([]message.ContentPart, 0, len(toolResults))
238378 for _, toolResult := range toolResults {
239379 parts = append(parts, toolResult)
240380 }
241− msg, err := c.Messages.Create(ctx, assistantMsg.SessionID, message.CreateMessageParams{
381+
382+ msg, err := a.messages.Create(ctx, assistantMsg.SessionID, message.CreateMessageParams{
242383 Role: message.Tool,
243384 Parts: parts,
244385 })
386+ if err != nil {
387+ return nil, fmt.Errorf("failed to create tool message: %w", err)
388+ }
245389
246− return &msg, err
390+ return &msg, nil
247391}
248392
249−func (c *agent) generate(ctx context.Context, sessionID string, content string) error {
393+// generate handles the main generation workflow
394+func (a *agent) generate(ctx context.Context, sessionID string, content string) error {
250395 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
251− messages, err := c.Messages.List(ctx, sessionID)
396+
397+ // Handle context cancellation at any point
398+ if err := ctx.Err(); err != nil {
399+ return ErrRequestCancelled
400+ }
401+
402+ messages, err := a.messages.List(ctx, sessionID)
252403 if err != nil {
253− return err
404+ return fmt.Errorf("failed to list messages: %w", err)
254405 }
255406
256407 if len(messages) == 0 {
257− go c.handleTitleGeneration(ctx, sessionID, content)
408+ titleCtx := context.Background()
409+ go a.handleTitleGeneration(titleCtx, sessionID, content)
258410 }
259411
260− userMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
412+ userMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
261413 Role: message.User,
262414 Parts: []message.ContentPart{
263415 message.TextContent{
@@ -266,133 +418,125 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string)
266418 },
267419 })
268420 if err != nil {
269− return err
421+ return fmt.Errorf("failed to create user message: %w", err)
270422 }
271423
272424 messages = append(messages, userMsg)
425+
273426 for {
427+ // Check for cancellation before each iteration
274428 select {
275429 case <-ctx.Done():
276− assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
277− Role: message.Assistant,
278− Parts: []message.ContentPart{},
279− })
280− if err != nil {
281− return err
282− }
283− assistantMsg.AddFinish("canceled")
284− c.Messages.Update(ctx, assistantMsg)
285− return context.Canceled
430+ return ErrRequestCancelled
286431 default:
287432 // Continue processing
288433 }
289434
290− eventChan, err := c.agent.StreamResponse(ctx, messages, c.tools)
435+ eventChan, err := a.agent.StreamResponse(ctx, messages, a.tools)
291436 if err != nil {
292437 if errors.Is(err, context.Canceled) {
293− assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
294− Role: message.Assistant,
295− Parts: []message.ContentPart{},
296− })
297− if err != nil {
298− return err
299− }
300− assistantMsg.AddFinish("canceled")
301− c.Messages.Update(ctx, assistantMsg)
302− return context.Canceled
438+ return ErrRequestCancelled
303439 }
304− return err
440+ return fmt.Errorf("failed to stream response: %w", err)
305441 }
306442
307− assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
443+ assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
308444 Role: message.Assistant,
309445 Parts: []message.ContentPart{},
310− Model: c.model.ID,
446+ Model: a.model.ID,
311447 })
312448 if err != nil {
313− return err
449+ return fmt.Errorf("failed to create assistant message: %w", err)
314450 }
315451
316452 ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
453+
454+ // Process events from the LLM provider
317455 for event := range eventChan {
318− err = c.processEvent(ctx, sessionID, &assistantMsg, event)
319− if err != nil {
456+ if err := a.processEvent(ctx, sessionID, &assistantMsg, event); err != nil {
320457 if errors.Is(err, context.Canceled) {
458+ // Mark as canceled but don't create separate message
321459 assistantMsg.AddFinish("canceled")
322− c.Messages.Update(ctx, assistantMsg)
323− return context.Canceled
460+ _ = a.messages.Update(context.Background(), assistantMsg)
461+ return ErrRequestCancelled
324462 }
325463 assistantMsg.AddFinish("error:" + err.Error())
326− c.Messages.Update(ctx, assistantMsg)
327− return err
464+ _ = a.messages.Update(ctx, assistantMsg)
465+ return fmt.Errorf("event processing error: %w", err)
328466 }
329467
468+ // Check for cancellation during event processing
330469 select {
331470 case <-ctx.Done():
471+ // Mark as canceled
332472 assistantMsg.AddFinish("canceled")
333− c.Messages.Update(ctx, assistantMsg)
334− return context.Canceled
473+ _ = a.messages.Update(context.Background(), assistantMsg)
474+ return ErrRequestCancelled
335475 default:
336476 }
337477 }
338478
339− // Check for context cancellation before tool execution
479+ // Check for cancellation before tool execution
340480 select {
341481 case <-ctx.Done():
342− assistantMsg.AddFinish("canceled")
343− c.Messages.Update(ctx, assistantMsg)
344− return context.Canceled
482+ assistantMsg.AddFinish("canceled_by_user")
483+ _ = a.messages.Update(context.Background(), assistantMsg)
484+ return ErrRequestCancelled
345485 default:
346− // Continue processing
347486 }
348487
349− msg, err := c.handleToolExecution(ctx, assistantMsg)
488+ // Execute any tool calls
489+ toolMsg, err := a.handleToolExecution(ctx, assistantMsg)
350490 if err != nil {
351491 if errors.Is(err, context.Canceled) {
352− assistantMsg.AddFinish("canceled")
353− c.Messages.Update(ctx, assistantMsg)
354− return context.Canceled
492+ assistantMsg.AddFinish("canceled_by_user")
493+ _ = a.messages.Update(context.Background(), assistantMsg)
494+ return ErrRequestCancelled
355495 }
356− return err
496+ return fmt.Errorf("tool execution error: %w", err)
357497 }
358498
359− c.Messages.Update(ctx, assistantMsg)
499+ if err := a.messages.Update(ctx, assistantMsg); err != nil {
500+ return fmt.Errorf("failed to update assistant message: %w", err)
501+ }
360502
503+ // If no tool calls, we're done
361504 if len(assistantMsg.ToolCalls()) == 0 {
362505 break
363506 }
364507
508+ // Add messages for next iteration
365509 messages = append(messages, assistantMsg)
366− if msg != nil {
367− messages = append(messages, *msg)
510+ if toolMsg != nil {
511+ messages = append(messages, *toolMsg)
368512 }
369513
370− // Check for context cancellation after tool execution
514+ // Check for cancellation after tool execution
371515 select {
372516 case <-ctx.Done():
373− assistantMsg.AddFinish("canceled")
374− c.Messages.Update(ctx, assistantMsg)
375− return context.Canceled
517+ return ErrRequestCancelled
376518 default:
377− // Continue processing
378519 }
379520 }
521+
380522 return nil
381523}
382524
525+// getAgentProviders initializes the LLM providers based on the chosen model
383526func getAgentProviders(ctx context.Context, model models.Model) (provider.Provider, provider.Provider, error) {
384527 maxTokens := config.Get().Model.CoderMaxTokens
385528
386529 providerConfig, ok := config.Get().Providers[model.Provider]
387530 if !ok || providerConfig.Disabled {
388− return nil, nil, errors.New("provider is not enabled")
531+ return nil, nil, ErrProviderNotEnabled
389532 }
533+
390534 var agentProvider provider.Provider
391535 var titleGenerator provider.Provider
536+ var err error
392537
393538 switch model.Provider {
394539 case models.ProviderOpenAI:
395− var err error
396540 agentProvider, err = provider.NewOpenAIProvider(
397541 provider.WithOpenAISystemMessage(
398542 prompt.CoderOpenAISystemPrompt(),
@@ -402,8 +546,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
402546 provider.WithOpenAIKey(providerConfig.APIKey),
403547 )
404548 if err != nil {
405− return nil, nil, err
549+ return nil, nil, fmt.Errorf("failed to create OpenAI agent provider: %w", err)
406550 }
551+
407552 titleGenerator, err = provider.NewOpenAIProvider(
408553 provider.WithOpenAISystemMessage(
409554 prompt.TitlePrompt(),
@@ -413,10 +558,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
413558 provider.WithOpenAIKey(providerConfig.APIKey),
414559 )
415560 if err != nil {
416− return nil, nil, err
561+ return nil, nil, fmt.Errorf("failed to create OpenAI title generator: %w", err)
417562 }
563+
418564 case models.ProviderAnthropic:
419− var err error
420565 agentProvider, err = provider.NewAnthropicProvider(
421566 provider.WithAnthropicSystemMessage(
422567 prompt.CoderAnthropicSystemPrompt(),
@@ -426,8 +571,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
426571 provider.WithAnthropicModel(model),
427572 )
428573 if err != nil {
429− return nil, nil, err
574+ return nil, nil, fmt.Errorf("failed to create Anthropic agent provider: %w", err)
430575 }
576+
431577 titleGenerator, err = provider.NewAnthropicProvider(
432578 provider.WithAnthropicSystemMessage(
433579 prompt.TitlePrompt(),
@@ -437,11 +583,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
437583 provider.WithAnthropicModel(model),
438584 )
439585 if err != nil {
440− return nil, nil, err
586+ return nil, nil, fmt.Errorf("failed to create Anthropic title generator: %w", err)
441587 }
442588
443589 case models.ProviderGemini:
444− var err error
445590 agentProvider, err = provider.NewGeminiProvider(
446591 ctx,
447592 provider.WithGeminiSystemMessage(
@@ -452,8 +597,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
452597 provider.WithGeminiModel(model),
453598 )
454599 if err != nil {
455− return nil, nil, err
600+ return nil, nil, fmt.Errorf("failed to create Gemini agent provider: %w", err)
456601 }
602+
457603 titleGenerator, err = provider.NewGeminiProvider(
458604 ctx,
459605 provider.WithGeminiSystemMessage(
@@ -464,10 +610,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
464610 provider.WithGeminiModel(model),
465611 )
466612 if err != nil {
467− return nil, nil, err
613+ return nil, nil, fmt.Errorf("failed to create Gemini title generator: %w", err)
468614 }
615+
469616 case models.ProviderGROQ:
470− var err error
471617 agentProvider, err = provider.NewOpenAIProvider(
472618 provider.WithOpenAISystemMessage(
473619 prompt.CoderAnthropicSystemPrompt(),
@@ -478,8 +624,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
478624 provider.WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
479625 )
480626 if err != nil {
481− return nil, nil, err
627+ return nil, nil, fmt.Errorf("failed to create GROQ agent provider: %w", err)
482628 }
629+
483630 titleGenerator, err = provider.NewOpenAIProvider(
484631 provider.WithOpenAISystemMessage(
485632 prompt.TitlePrompt(),
@@ -490,11 +637,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
490637 provider.WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
491638 )
492639 if err != nil {
493− return nil, nil, err
640+ return nil, nil, fmt.Errorf("failed to create GROQ title generator: %w", err)
494641 }
495642
496643 case models.ProviderBedrock:
497− var err error
498644 agentProvider, err = provider.NewBedrockProvider(
499645 provider.WithBedrockSystemMessage(
500646 prompt.CoderAnthropicSystemPrompt(),
@@ -503,19 +649,21 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
503649 provider.WithBedrockModel(model),
504650 )
505651 if err != nil {
506− return nil, nil, err
652+ return nil, nil, fmt.Errorf("failed to create Bedrock agent provider: %w", err)
507653 }
654+
508655 titleGenerator, err = provider.NewBedrockProvider(
509656 provider.WithBedrockSystemMessage(
510657 prompt.TitlePrompt(),
511658 ),
512− provider.WithBedrockMaxTokens(maxTokens),
659+ provider.WithBedrockMaxTokens(80),
513660 provider.WithBedrockModel(model),
514661 )
515662 if err != nil {
516− return nil, nil, err
663+ return nil, nil, fmt.Errorf("failed to create Bedrock title generator: %w", err)
517664 }
518−
665+ default:
666+ return nil, nil, fmt.Errorf("unsupported provider: %s", model.Provider)
519667 }
520668
521669 return agentProvider, titleGenerator, nil