@@ -7,7 +7,6 @@ import (
10− "github.com/kujtimiihoxha/termai/internal/app"
11 "github.com/kujtimiihoxha/termai/internal/config"
10 "github.com/kujtimiihoxha/termai/internal/config"
12 "github.com/kujtimiihoxha/termai/internal/llm/models"
11 "github.com/kujtimiihoxha/termai/internal/llm/models"
13 "github.com/kujtimiihoxha/termai/internal/llm/prompt"
12 "github.com/kujtimiihoxha/termai/internal/llm/prompt"
@@ -15,22 +14,118 @@ import (
15 "github.com/kujtimiihoxha/termai/internal/llm/tools"
14 "github.com/kujtimiihoxha/termai/internal/llm/tools"
16 "github.com/kujtimiihoxha/termai/internal/logging"
15 "github.com/kujtimiihoxha/termai/internal/logging"
17 "github.com/kujtimiihoxha/termai/internal/message"
16 "github.com/kujtimiihoxha/termai/internal/message"
17+ "github.com/kujtimiihoxha/termai/internal/session"
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")
27+// Service defines the interface for generating responses
20−type Agent interface {
28+type Service interface {
21 Generate(ctx context.Context, sessionID string, content string) error
29 Generate(ctx context.Context, sessionID string, content string) error
30+ Cancel(sessionID string) error
24type agent struct {
33type agent struct {
25− *app.App
34+ sessions session.Service
35+ messages message.Service
26 model models.Model
36 model models.Model
27 tools []tools.BaseTool
37 tools []tools.BaseTool
28 agent provider.Provider
38 agent provider.Provider
29 titleGenerator provider.Provider
39 titleGenerator provider.Provider
40+ activeRequests sync.Map // map[sessionID]context.CancelFunc
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)
47+ return nil, fmt.Errorf("failed to initialize providers: %w", err)
55+ agent: agentProvider,
56+ titleGenerator: titleGenerator,
57+ activeRequests: sync.Map{},
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))
70+ return errors.New("no active request found for this session")
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
80+ // Create a cancellable context
81+ genCtx, cancel := context.WithCancel(ctx)
83+ // Store cancel function to allow user cancellation
84+ a.activeRequests.Store(sessionID, cancel)
86+ // Launch the generation in a goroutine
89+ if r := recover(); r != nil {
90+ logging.ErrorPersist(fmt.Sprintf("Panic in Generate: %v", r))
93+ defer a.activeRequests.Delete(sessionID)
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))
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{
112+ if createErr != nil {
113+ logging.ErrorPersist(fmt.Sprintf("Failed to create error message: %v", createErr))
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)
126+} // handleTitleGeneration asynchronously generates a title for new sessions
32−func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content string) {
127+func (a *agent) handleTitleGeneration(ctx context.Context, sessionID, content string) {
33− response, err := c.titleGenerator.SendMessages(
128+ response, err := a.titleGenerator.SendMessages(
35 []message.Message{
130 []message.Message{
@@ -45,25 +140,30 @@ func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content st
47 if err != nil {
142 if err != nil {
143+ logging.ErrorPersist(fmt.Sprintf("Failed to generate title: %v", err))
51− session, err := c.Sessions.Get(ctx, sessionID)
147+ session, err := a.sessions.Get(ctx, sessionID)
52 if err != nil {
148 if err != nil {
149+ logging.ErrorPersist(fmt.Sprintf("Failed to get session: %v", err))
55 if response.Content != "" {
153 if response.Content != "" {
56− session.Title = response.Content
154+ session.Title = strings.TrimSpace(response.Content)
57− session.Title = strings.TrimSpace(session.Title)
58 session.Title = strings.ReplaceAll(session.Title, "\n", " ")
155 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))
162+// TrackUsage updates token usage statistics for the session
63−func (c *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error {
163+func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error {
64− session, err := c.Sessions.Get(ctx, sessionID)
164+ session, err := a.sessions.Get(ctx, sessionID)
65 if err != nil {
165 if err != nil {
66− return err
166+ return fmt.Errorf("failed to get session: %w", err)
69 cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
169 cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
@@ -75,189 +175,241 @@ func (c *agent) TrackUsage(ctx context.Context, sessionID string, model models.M
75 session.CompletionTokens += usage.OutputTokens
175 session.CompletionTokens += usage.OutputTokens
76 session.PromptTokens += usage.InputTokens
176 session.PromptTokens += usage.InputTokens
78− _, err = c.Sessions.Save(ctx, session)
178+ _, err = a.sessions.Save(ctx, session)
79− return err
179+ if err != nil {
180+ return fmt.Errorf("failed to save session: %w", err)
185+// processEvent handles different types of events during generation
82−func (c *agent) processEvent(
186+func (a *agent) processEvent(
83 ctx context.Context,
187 ctx context.Context,
84 sessionID string,
188 sessionID string,
85 assistantMsg *message.Message,
189 assistantMsg *message.Message,
86 event provider.ProviderEvent,
190 event provider.ProviderEvent,
196+ // Continue processing
88 switch event.Type {
199 switch event.Type {
89 case provider.EventThinkingDelta:
200 case provider.EventThinkingDelta:
90 assistantMsg.AppendReasoningContent(event.Content)
201 assistantMsg.AppendReasoningContent(event.Content)
91− return c.Messages.Update(ctx, *assistantMsg)
202+ return a.messages.Update(ctx, *assistantMsg)
92 case provider.EventContentDelta:
203 case provider.EventContentDelta:
93 assistantMsg.AppendContent(event.Content)
204 assistantMsg.AppendContent(event.Content)
94− return c.Messages.Update(ctx, *assistantMsg)
205+ return a.messages.Update(ctx, *assistantMsg)
95 case provider.EventError:
206 case provider.EventError:
96 if errors.Is(event.Error, context.Canceled) {
207 if errors.Is(event.Error, context.Canceled) {
208+ logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
97− return nil
209+ return context.Canceled
99 logging.ErrorPersist(event.Error.Error())
211 logging.ErrorPersist(event.Error.Error())
100 return event.Error
212 return event.Error
101 case provider.EventWarning:
213 case provider.EventWarning:
102 logging.WarnPersist(event.Info)
214 logging.WarnPersist(event.Info)
104 case provider.EventInfo:
215 case provider.EventInfo:
105 logging.InfoPersist(event.Info)
216 logging.InfoPersist(event.Info)
106 case provider.EventComplete:
217 case provider.EventComplete:
107 assistantMsg.SetToolCalls(event.Response.ToolCalls)
218 assistantMsg.SetToolCalls(event.Response.ToolCalls)
108 assistantMsg.AddFinish(event.Response.FinishReason)
219 assistantMsg.AddFinish(event.Response.FinishReason)
109− err := c.Messages.Update(ctx, *assistantMsg)
220+ if err := a.messages.Update(ctx, *assistantMsg); err != nil {
110− if err != nil {
221+ return fmt.Errorf("failed to update message: %w", err)
113− return c.TrackUsage(ctx, sessionID, c.model, event.Response.Usage)
223+ return a.TrackUsage(ctx, sessionID, a.model, event.Response.Usage)
116 return nil
226 return nil
229+// ExecuteTools runs all tool calls sequentially and returns the results
119−func (c *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall, tls []tools.BaseTool) ([]message.ToolResult, error) {
230+func (a *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall, tls []tools.BaseTool) ([]message.ToolResult, error) {
120− var wg sync.WaitGroup
121 toolResults := make([]message.ToolResult, len(toolCalls))
231 toolResults := make([]message.ToolResult, len(toolCalls))
122− mutex := &sync.Mutex{}
123− errChan := make(chan error, 1)
125 // Create a child context that can be canceled
233 // Create a child context that can be canceled
126 ctx, cancel := context.WithCancel(ctx)
234 ctx, cancel := context.WithCancel(ctx)
127 defer cancel()
235 defer cancel()
237+ // Check if already canceled before starting any execution
238+ if ctx.Err() != nil {
239+ // Mark all tools as canceled
129− for i, tc := range toolCalls {
240+ for i, toolCall := range toolCalls {
130− wg.Add(1)
241+ toolResults[i] = message.ToolResult{
131− go func(index int, toolCall message.ToolCall) {
242+ ToolCallID: toolCall.ID,
132− defer wg.Done()
243+ Content: "Tool execution canceled by user",
247+ return toolResults, ctx.Err()
134− // Check if context is already canceled
250+ for i, toolCall := range toolCalls {
251+ // Check for cancellation before executing each tool
135− select {
252+ select {
136− case <-ctx.Done():
253+ case <-ctx.Done():
137− mutex.Lock()
254+ // Mark this and all remaining tools as canceled
255+ for j := i; j < len(toolCalls); j++ {
138− toolResults[index] = message.ToolResult{
256+ toolResults[j] = message.ToolResult{
139− ToolCallID: toolCall.ID,
257+ ToolCallID: toolCalls[j].ID,
140− Content: "Tool execution canceled",
258+ Content: "Tool execution canceled by user",
141 IsError: true,
259 IsError: true,
145− // Send cancellation error to error channel if it's empty
147− case errChan <- ctx.Err():
262+ return toolResults, ctx.Err()
264+ // Continue processing
154− response := ""
267+ response := ""
155− isError := false
268+ isError := false
156− found := false
269+ found := false
271+ // Find and execute the appropriate tool
158− for _, tool := range tls {
272+ for _, tool := range tls {
159− if tool.Info().Name == toolCall.Name {
273+ if tool.Info().Name == toolCall.Name {
160− found = true
274+ found = true
161− toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
275+ toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
162− ID: toolCall.ID,
276+ ID: toolCall.ID,
163− Name: toolCall.Name,
277+ Name: toolCall.Name,
164− Input: toolCall.Input,
278+ Input: toolCall.Input,
167− if toolErr != nil {
281+ if toolErr != nil {
168− if errors.Is(toolErr, context.Canceled) {
282+ if errors.Is(toolErr, context.Canceled) {
169− response = "Tool execution canceled"
283+ response = "Tool execution canceled by user"
171− // Send cancellation error to error channel if it's empty
173− case errChan <- ctx.Err():
177− response = fmt.Sprintf("error running tool: %s", toolErr)
181− response = toolResult.Content
285+ response = fmt.Sprintf("Error running tool: %s", toolErr)
182− isError = toolResult.IsError
184− break
287+ isError = true
289+ response = toolResult.Content
290+ isError = toolResult.IsError
188− if !found {
296+ if !found {
189− response = fmt.Sprintf("tool not found: %s", toolCall.Name)
297+ response = fmt.Sprintf("Tool not found: %s", toolCall.Name)
190− isError = true
298+ isError = true
194− defer mutex.Unlock()
196− toolResults[index] = message.ToolResult{
197− ToolCallID: toolCall.ID,
204− // Wait for all goroutines to finish or context to be canceled
205− done := make(chan struct{})
211− select {
301+ toolResults[i] = message.ToolResult{
212− case <-done:
302+ ToolCallID: toolCall.ID,
213− // All tools completed successfully
303+ Content: response,
214− case err := <-errChan:
304+ IsError: isError,
215− // One of the tools encountered a cancellation
305+ }
216− return toolResults, err
218− // Context was canceled externally
219− return toolResults, ctx.Err()
222 return toolResults, nil
308 return toolResults, nil
311+// handleToolExecution processes tool calls and creates tool result messages
225−func (c *agent) handleToolExecution(
312+func (a *agent) handleToolExecution(
226 ctx context.Context,
313 ctx context.Context,
227 assistantMsg message.Message,
314 assistantMsg message.Message,
228) (*message.Message, error) {
315) (*message.Message, error) {
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{
324+ Content: "Tool execution canceled by user",
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)
335+ msg, err := a.messages.Create(bgCtx, assistantMsg.SessionID, message.CreateMessageParams{
340+ return nil, fmt.Errorf("failed to create cancelled tool message: %w", err)
342+ return &msg, ctx.Err()
344+ return nil, ctx.Err()
346+ // Continue processing
229 if len(assistantMsg.ToolCalls()) == 0 {
349 if len(assistantMsg.ToolCalls()) == 0 {
230 return nil, nil
350 return nil, nil
233− toolResults, err := c.ExecuteTools(ctx, assistantMsg.ToolCalls(), c.tools)
353+ toolResults, err := a.ExecuteTools(ctx, assistantMsg.ToolCalls(), a.tools)
234 if err != nil {
354 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)
364+ msg, createErr := a.messages.Create(bgCtx, assistantMsg.SessionID, message.CreateMessageParams{
368+ if createErr != nil {
369+ logging.ErrorPersist(fmt.Sprintf("Failed to create tool message after cancellation: %v", createErr))
235 return nil, err
374 return nil, err
237− parts := make([]message.ContentPart, 0)
377+ parts := make([]message.ContentPart, 0, len(toolResults))
238 for _, toolResult := range toolResults {
378 for _, toolResult := range toolResults {
239 parts = append(parts, toolResult)
379 parts = append(parts, toolResult)
241− msg, err := c.Messages.Create(ctx, assistantMsg.SessionID, message.CreateMessageParams{
382+ msg, err := a.messages.Create(ctx, assistantMsg.SessionID, message.CreateMessageParams{
242 Role: message.Tool,
383 Role: message.Tool,
243 Parts: parts,
384 Parts: parts,
387+ return nil, fmt.Errorf("failed to create tool message: %w", err)
246− return &msg, err
390+ return &msg, nil
393+// generate handles the main generation workflow
249−func (c *agent) generate(ctx context.Context, sessionID string, content string) error {
394+func (a *agent) generate(ctx context.Context, sessionID string, content string) error {
250 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
395 ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
397+ // Handle context cancellation at any point
398+ if err := ctx.Err(); err != nil {
399+ return ErrRequestCancelled
251− messages, err := c.Messages.List(ctx, sessionID)
402+ messages, err := a.messages.List(ctx, sessionID)
252 if err != nil {
403 if err != nil {
253− return err
404+ return fmt.Errorf("failed to list messages: %w", err)
256 if len(messages) == 0 {
407 if len(messages) == 0 {
408+ titleCtx := context.Background()
257− go c.handleTitleGeneration(ctx, sessionID, content)
409+ go a.handleTitleGeneration(titleCtx, sessionID, content)
260− userMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
412+ userMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
261 Role: message.User,
413 Role: message.User,
262 Parts: []message.ContentPart{
414 Parts: []message.ContentPart{
263 message.TextContent{
415 message.TextContent{
@@ -266,133 +418,125 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string)
268 if err != nil {
420 if err != nil {
269− return err
421+ return fmt.Errorf("failed to create user message: %w", err)
272 messages = append(messages, userMsg)
424 messages = append(messages, userMsg)
427+ // Check for cancellation before each iteration
275 case <-ctx.Done():
429 case <-ctx.Done():
276− assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
430+ return ErrRequestCancelled
277− Role: message.Assistant,
278− Parts: []message.ContentPart{},
283− assistantMsg.AddFinish("canceled")
284− c.Messages.Update(ctx, assistantMsg)
285− return context.Canceled
287 // Continue processing
432 // Continue processing
290− eventChan, err := c.agent.StreamResponse(ctx, messages, c.tools)
435+ eventChan, err := a.agent.StreamResponse(ctx, messages, a.tools)
291 if err != nil {
436 if err != nil {
292 if errors.Is(err, context.Canceled) {
437 if errors.Is(err, context.Canceled) {
293− assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
438+ return ErrRequestCancelled
294− Role: message.Assistant,
295− Parts: []message.ContentPart{},
300− assistantMsg.AddFinish("canceled")
301− c.Messages.Update(ctx, assistantMsg)
302− return context.Canceled
304− return err
440+ return fmt.Errorf("failed to stream response: %w", err)
307− assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{
443+ assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
308 Role: message.Assistant,
444 Role: message.Assistant,
309 Parts: []message.ContentPart{},
445 Parts: []message.ContentPart{},
310− Model: c.model.ID,
446+ Model: a.model.ID,
312 if err != nil {
448 if err != nil {
313− return err
449+ return fmt.Errorf("failed to create assistant message: %w", err)
316 ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
452 ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
454+ // Process events from the LLM provider
317 for event := range eventChan {
455 for event := range eventChan {
318− err = c.processEvent(ctx, sessionID, &assistantMsg, event)
456+ if err := a.processEvent(ctx, sessionID, &assistantMsg, event); err != nil {
320 if errors.Is(err, context.Canceled) {
457 if errors.Is(err, context.Canceled) {
458+ // Mark as canceled but don't create separate message
321 assistantMsg.AddFinish("canceled")
459 assistantMsg.AddFinish("canceled")
322− c.Messages.Update(ctx, assistantMsg)
460+ _ = a.messages.Update(context.Background(), assistantMsg)
323− return context.Canceled
461+ return ErrRequestCancelled
325 assistantMsg.AddFinish("error:" + err.Error())
463 assistantMsg.AddFinish("error:" + err.Error())
326− c.Messages.Update(ctx, assistantMsg)
464+ _ = a.messages.Update(ctx, assistantMsg)
327− return err
465+ return fmt.Errorf("event processing error: %w", err)
468+ // Check for cancellation during event processing
331 case <-ctx.Done():
470 case <-ctx.Done():
332 assistantMsg.AddFinish("canceled")
472 assistantMsg.AddFinish("canceled")
333− c.Messages.Update(ctx, assistantMsg)
473+ _ = a.messages.Update(context.Background(), assistantMsg)
334− return context.Canceled
474+ return ErrRequestCancelled
339− // Check for context cancellation before tool execution
479+ // Check for cancellation before tool execution
341 case <-ctx.Done():
481 case <-ctx.Done():
342− assistantMsg.AddFinish("canceled")
482+ assistantMsg.AddFinish("canceled_by_user")
343− c.Messages.Update(ctx, assistantMsg)
483+ _ = a.messages.Update(context.Background(), assistantMsg)
344− return context.Canceled
484+ return ErrRequestCancelled
346− // Continue processing
488+ // Execute any tool calls
349− msg, err := c.handleToolExecution(ctx, assistantMsg)
489+ toolMsg, err := a.handleToolExecution(ctx, assistantMsg)
350 if err != nil {
490 if err != nil {
351 if errors.Is(err, context.Canceled) {
491 if errors.Is(err, context.Canceled) {
352− assistantMsg.AddFinish("canceled")
492+ assistantMsg.AddFinish("canceled_by_user")
353− c.Messages.Update(ctx, assistantMsg)
493+ _ = a.messages.Update(context.Background(), assistantMsg)
354− return context.Canceled
494+ return ErrRequestCancelled
356− return err
496+ return fmt.Errorf("tool execution error: %w", err)
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)
503+ // If no tool calls, we're done
361 if len(assistantMsg.ToolCalls()) == 0 {
504 if len(assistantMsg.ToolCalls()) == 0 {
508+ // Add messages for next iteration
365 messages = append(messages, assistantMsg)
509 messages = append(messages, assistantMsg)
366− if msg != nil {
510+ if toolMsg != nil {
367− messages = append(messages, *msg)
511+ messages = append(messages, *toolMsg)
370− // Check for context cancellation after tool execution
514+ // Check for cancellation after tool execution
372 case <-ctx.Done():
516 case <-ctx.Done():
373− assistantMsg.AddFinish("canceled")
517+ return ErrRequestCancelled
374− c.Messages.Update(ctx, assistantMsg)
375− return context.Canceled
377− // Continue processing
380 return nil
522 return nil
525+// getAgentProviders initializes the LLM providers based on the chosen model
383func getAgentProviders(ctx context.Context, model models.Model) (provider.Provider, provider.Provider, error) {
526func getAgentProviders(ctx context.Context, model models.Model) (provider.Provider, provider.Provider, error) {
384 maxTokens := config.Get().Model.CoderMaxTokens
527 maxTokens := config.Get().Model.CoderMaxTokens
386 providerConfig, ok := config.Get().Providers[model.Provider]
529 providerConfig, ok := config.Get().Providers[model.Provider]
387 if !ok || providerConfig.Disabled {
530 if !ok || providerConfig.Disabled {
388− return nil, nil, errors.New("provider is not enabled")
531+ return nil, nil, ErrProviderNotEnabled
390 var agentProvider provider.Provider
534 var agentProvider provider.Provider
391 var titleGenerator provider.Provider
535 var titleGenerator provider.Provider
393 switch model.Provider {
538 switch model.Provider {
394 case models.ProviderOpenAI:
539 case models.ProviderOpenAI:
396 agentProvider, err = provider.NewOpenAIProvider(
540 agentProvider, err = provider.NewOpenAIProvider(
397 provider.WithOpenAISystemMessage(
541 provider.WithOpenAISystemMessage(
398 prompt.CoderOpenAISystemPrompt(),
542 prompt.CoderOpenAISystemPrompt(),
@@ -402,8 +546,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
402 provider.WithOpenAIKey(providerConfig.APIKey),
546 provider.WithOpenAIKey(providerConfig.APIKey),
404 if err != nil {
548 if err != nil {
405− return nil, nil, err
549+ return nil, nil, fmt.Errorf("failed to create OpenAI agent provider: %w", err)
407 titleGenerator, err = provider.NewOpenAIProvider(
552 titleGenerator, err = provider.NewOpenAIProvider(
408 provider.WithOpenAISystemMessage(
553 provider.WithOpenAISystemMessage(
409 prompt.TitlePrompt(),
554 prompt.TitlePrompt(),
@@ -413,10 +558,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
413 provider.WithOpenAIKey(providerConfig.APIKey),
558 provider.WithOpenAIKey(providerConfig.APIKey),
415 if err != nil {
560 if err != nil {
416− return nil, nil, err
561+ return nil, nil, fmt.Errorf("failed to create OpenAI title generator: %w", err)
418 case models.ProviderAnthropic:
564 case models.ProviderAnthropic:
420 agentProvider, err = provider.NewAnthropicProvider(
565 agentProvider, err = provider.NewAnthropicProvider(
421 provider.WithAnthropicSystemMessage(
566 provider.WithAnthropicSystemMessage(
422 prompt.CoderAnthropicSystemPrompt(),
567 prompt.CoderAnthropicSystemPrompt(),
@@ -426,8 +571,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
426 provider.WithAnthropicModel(model),
571 provider.WithAnthropicModel(model),
428 if err != nil {
573 if err != nil {
429− return nil, nil, err
574+ return nil, nil, fmt.Errorf("failed to create Anthropic agent provider: %w", err)
431 titleGenerator, err = provider.NewAnthropicProvider(
577 titleGenerator, err = provider.NewAnthropicProvider(
432 provider.WithAnthropicSystemMessage(
578 provider.WithAnthropicSystemMessage(
433 prompt.TitlePrompt(),
579 prompt.TitlePrompt(),
@@ -437,11 +583,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
437 provider.WithAnthropicModel(model),
583 provider.WithAnthropicModel(model),
439 if err != nil {
585 if err != nil {
440− return nil, nil, err
586+ return nil, nil, fmt.Errorf("failed to create Anthropic title generator: %w", err)
443 case models.ProviderGemini:
589 case models.ProviderGemini:
445 agentProvider, err = provider.NewGeminiProvider(
590 agentProvider, err = provider.NewGeminiProvider(
447 provider.WithGeminiSystemMessage(
592 provider.WithGeminiSystemMessage(
@@ -452,8 +597,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
452 provider.WithGeminiModel(model),
597 provider.WithGeminiModel(model),
454 if err != nil {
599 if err != nil {
455− return nil, nil, err
600+ return nil, nil, fmt.Errorf("failed to create Gemini agent provider: %w", err)
457 titleGenerator, err = provider.NewGeminiProvider(
603 titleGenerator, err = provider.NewGeminiProvider(
459 provider.WithGeminiSystemMessage(
605 provider.WithGeminiSystemMessage(
@@ -464,10 +610,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
464 provider.WithGeminiModel(model),
610 provider.WithGeminiModel(model),
466 if err != nil {
612 if err != nil {
467− return nil, nil, err
613+ return nil, nil, fmt.Errorf("failed to create Gemini title generator: %w", err)
469 case models.ProviderGROQ:
616 case models.ProviderGROQ:
471 agentProvider, err = provider.NewOpenAIProvider(
617 agentProvider, err = provider.NewOpenAIProvider(
472 provider.WithOpenAISystemMessage(
618 provider.WithOpenAISystemMessage(
473 prompt.CoderAnthropicSystemPrompt(),
619 prompt.CoderAnthropicSystemPrompt(),
@@ -478,8 +624,9 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
478 provider.WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
624 provider.WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
480 if err != nil {
626 if err != nil {
481− return nil, nil, err
627+ return nil, nil, fmt.Errorf("failed to create GROQ agent provider: %w", err)
483 titleGenerator, err = provider.NewOpenAIProvider(
630 titleGenerator, err = provider.NewOpenAIProvider(
484 provider.WithOpenAISystemMessage(
631 provider.WithOpenAISystemMessage(
485 prompt.TitlePrompt(),
632 prompt.TitlePrompt(),
@@ -490,11 +637,10 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
490 provider.WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
637 provider.WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
492 if err != nil {
639 if err != nil {
493− return nil, nil, err
640+ return nil, nil, fmt.Errorf("failed to create GROQ title generator: %w", err)
496 case models.ProviderBedrock:
643 case models.ProviderBedrock:
498 agentProvider, err = provider.NewBedrockProvider(
644 agentProvider, err = provider.NewBedrockProvider(
499 provider.WithBedrockSystemMessage(
645 provider.WithBedrockSystemMessage(
500 prompt.CoderAnthropicSystemPrompt(),
646 prompt.CoderAnthropicSystemPrompt(),
@@ -503,19 +649,21 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid
503 provider.WithBedrockModel(model),
649 provider.WithBedrockModel(model),
505 if err != nil {
651 if err != nil {
506− return nil, nil, err
652+ return nil, nil, fmt.Errorf("failed to create Bedrock agent provider: %w", err)
508 titleGenerator, err = provider.NewBedrockProvider(
655 titleGenerator, err = provider.NewBedrockProvider(
509 provider.WithBedrockSystemMessage(
656 provider.WithBedrockSystemMessage(
510 prompt.TitlePrompt(),
657 prompt.TitlePrompt(),
512− provider.WithBedrockMaxTokens(maxTokens),
659+ provider.WithBedrockMaxTokens(80),
513 provider.WithBedrockModel(model),
660 provider.WithBedrockModel(model),
515 if err != nil {
662 if err != nil {
516− return nil, nil, err
663+ return nil, nil, fmt.Errorf("failed to create Bedrock title generator: %w", err)
666+ return nil, nil, fmt.Errorf("unsupported provider: %s", model.Provider)
521 return agentProvider, titleGenerator, nil
669 return agentProvider, titleGenerator, nil