on main help sign in
fix(agent): cancelation logic
commit5e698b2ec6b4d2e3251b6d3cfd1a47aae87d3527 authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 3 Oct 2025, 14:14 (1 year ago) parentb45dcfe
4 files changed+78−46
go.mod+18−24 go.sum+6−8 internal/agent/agent.go+48−14 internal/tui/page/chat/chat.go+6
go.mod+18−24
@@ -15,6 +15,7 @@ require (
1515 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250910155747-997384b0b35e
1616 github.com/charmbracelet/catwalk v0.6.4-0.20251002104711-f8c6c1e5b4a5
1717 github.com/charmbracelet/fang v0.4.2
18+ github.com/charmbracelet/fantasy v0.0.0-20251003110041-a581de4d1d81
1819 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
1920 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
2021 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
@@ -50,30 +51,8 @@ require (
5051 cloud.google.com/go v0.116.0 // indirect
5152 cloud.google.com/go/auth v0.9.3 // indirect
5253 cloud.google.com/go/compute/metadata v0.5.0 // indirect
53− github.com/charmbracelet/x/json v0.2.0 // indirect
54− github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
55− github.com/google/go-cmp v0.7.0 // indirect
56− github.com/google/s2a-go v0.1.8 // indirect
57− github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
58− github.com/gorilla/websocket v1.5.3 // indirect
59− github.com/openai/openai-go/v2 v2.3.0 // indirect
60− go.opencensus.io v0.24.0 // indirect
61− golang.org/x/crypto v0.41.0 // indirect
62− google.golang.org/genai v1.26.0 // indirect
63− google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
64− google.golang.org/grpc v1.66.2 // indirect
65− google.golang.org/protobuf v1.36.8 // indirect
66− gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
67−)
68−
69−require (
70− github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
71− github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d
72− github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
73−)
74−
75−require (
7654 github.com/andybalholm/cascadia v1.3.3 // indirect
55+ github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
7756 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
7857 github.com/aymerick/douceur v0.2.0 // indirect
7958 github.com/bahlo/generic-list-go v0.2.0 // indirect
@@ -82,6 +61,7 @@ require (
8261 github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef
8362 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect
8463 github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5
64+ github.com/charmbracelet/x/json v0.2.0 // indirect
8565 github.com/charmbracelet/x/powernap v0.0.0-20250919153222-1038f7e6fef4
8666 github.com/charmbracelet/x/term v0.2.1
8767 github.com/charmbracelet/x/termios v0.1.1 // indirect
@@ -93,7 +73,13 @@ require (
9373 github.com/dustin/go-humanize v1.0.1 // indirect
9474 github.com/fsnotify/fsnotify v1.9.0 // indirect
9575 github.com/go-logfmt/logfmt v0.6.0 // indirect
76+ github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
77+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
78+ github.com/google/go-cmp v0.7.0 // indirect
79+ github.com/google/s2a-go v0.1.8 // indirect
80+ github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
9681 github.com/gorilla/css v1.0.1 // indirect
82+ github.com/gorilla/websocket v1.5.3 // indirect
9783 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
9884 github.com/inconshreveable/mousetrap v1.1.0 // indirect
9985 github.com/klauspost/compress v1.18.0 // indirect
@@ -112,6 +98,7 @@ require (
11298 github.com/muesli/mango-pflag v0.1.0 // indirect
11399 github.com/muesli/roff v0.1.0 // indirect
114100 github.com/ncruces/julianday v1.0.0 // indirect
101+ github.com/openai/openai-go/v2 v2.3.0 // indirect
115102 github.com/pierrec/lz4/v4 v4.1.22 // indirect
116103 github.com/pmezard/go-difflib v1.0.0 // indirect
117104 github.com/posthog/posthog-go v1.6.10
@@ -132,14 +119,21 @@ require (
132119 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
133120 github.com/yuin/goldmark v1.7.8 // indirect
134121 github.com/yuin/goldmark-emoji v1.0.5 // indirect
122+ go.opencensus.io v0.24.0 // indirect
135123 go.uber.org/multierr v1.11.0 // indirect
124+ golang.org/x/crypto v0.41.0 // indirect
136125 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
137− golang.org/x/image v0.26.0 // indirect
126+ golang.org/x/image v0.27.0 // indirect
138127 golang.org/x/net v0.43.0 // indirect
139128 golang.org/x/sync v0.17.0 // indirect
140129 golang.org/x/sys v0.36.0 // indirect
141130 golang.org/x/term v0.34.0 // indirect
142131 golang.org/x/text v0.29.0
132+ google.golang.org/genai v1.26.0 // indirect
133+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
134+ google.golang.org/grpc v1.66.2 // indirect
135+ google.golang.org/protobuf v1.36.8 // indirect
136+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
143137 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
144138 gopkg.in/yaml.v3 v3.0.1 // indirect
145139 mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
go.sum+6−8
@@ -51,14 +51,12 @@ github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqI
5151github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
5252github.com/charmbracelet/fang v0.4.2 h1:nWr7Tb82/TTNNGMGG35aTZ1X68loAOQmpb0qxkKXjas=
5353github.com/charmbracelet/fang v0.4.2/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
54−github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77 h1:YHuUqaojkeu00YtQeXPqM/1RNJH/jqGNaQYFwa7JQTk=
55−github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
56−github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c h1:+gCA3Sv7g1jnZ96Em7j9u61H2O/1SkBAZ1LM9yd2bM8=
57−github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
58−github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380 h1:UYWO3cutUTV9ZLOH4hrXFy9hg4E1pVbLZsZLA1OA3+g=
59−github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
6054github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d h1:HS9qu7CgQGPnPcNoHIZXGWGFS8SaxaBXYytLdakm+jY=
6155github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
56+github.com/charmbracelet/fantasy v0.0.0-20251003104828-c9bb5e0cf237 h1:OgqeFnDwgYOVwLoOZpvmBFEh+j7G4f5gOzkozHUMbqo=
57+github.com/charmbracelet/fantasy v0.0.0-20251003104828-c9bb5e0cf237/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
58+github.com/charmbracelet/fantasy v0.0.0-20251003110041-a581de4d1d81 h1:TFQ9P46252Db/B/eDLHtL5Le9Fm0d6LtYDr59mquN7w=
59+github.com/charmbracelet/fantasy v0.0.0-20251003110041-a581de4d1d81/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
6260github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
6361github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
6462github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
@@ -328,8 +326,8 @@ golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sU
328326golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
329327golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
330328golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
331−golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
332−golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
329+golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
330+golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
333331golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
334332golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
335333golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
internal/agent/agent.go+48−14
@@ -3,6 +3,7 @@ package agent
33import (
44 "context"
55 _ "embed"
6+ "encoding/json"
67 "errors"
78 "fmt"
89 "log/slog"
@@ -177,7 +178,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
177178 // Before each step create the new assistant message
178179 PrepareStep: func(callContext context.Context, options ai.PrepareStepFunctionOptions) (_ context.Context, prepared ai.PrepareStepResult, err error) {
179180 var assistantMsg message.Message
180− assistantMsg, err = a.messages.Create(genCtx, call.SessionID, message.CreateMessageParams{
181+ assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
181182 Role: message.Assistant,
182183 Parts: []message.ContentPart{},
183184 Model: a.largeModel.ModelCfg.Model,
@@ -187,7 +188,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
187188 return callContext, prepared, err
188189 }
189190
190− callContext = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
191+ callContext = context.WithValue(callContext, tools.MessageIDContextKey, assistantMsg.ID)
191192
192193 currentAssistant = &assistantMsg
193194
@@ -200,7 +201,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
200201 queuedCalls, _ := a.messageQueue.Get(call.SessionID)
201202 a.messageQueue.Del(call.SessionID)
202203 for _, queued := range queuedCalls {
203− userMessage, createErr := a.createUserMessage(genCtx, queued)
204+ userMessage, createErr := a.createUserMessage(callContext, queued)
204205 if createErr != nil {
205206 return callContext, prepared, createErr
206207 }
@@ -291,12 +292,15 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
291292 IsError: isError,
292293 Metadata: result.ClientMetadata,
293294 }
294− a.messages.Create(context.Background(), currentAssistant.SessionID, message.CreateMessageParams{
295+ _, err := a.messages.Create(context.Background(), currentAssistant.SessionID, message.CreateMessageParams{
295296 Role: message.Tool,
296297 Parts: []message.ContentPart{
297298 toolResult,
298299 },
299300 })
301+ if err != nil {
302+ return err
303+ }
300304 return a.messages.Update(genCtx, *currentAssistant)
301305 },
302306 OnStepFinish: func(stepResult ai.StepResult) error {
@@ -328,16 +332,29 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
328332 }
329333 toolCalls := currentAssistant.ToolCalls()
330334 toolResults := currentAssistant.ToolResults()
335+ // INFO: we use the parent context here because the genCtx has been cancelled
336+ msgs, createErr := a.messages.List(ctx, currentSession.ID)
337+ if createErr != nil {
338+ return nil, createErr
339+ }
331340 for _, tc := range toolCalls {
332341 if !tc.Finished {
333342 tc.Finished = true
334343 tc.Input = "{}"
335344 }
336345 currentAssistant.AddToolCall(tc)
346+
337347 found := false
338− for _, tr := range toolResults {
339− if tr.ToolCallID == tc.ID {
340− found = true
348+ for _, msg := range msgs {
349+ if msg.Role == message.Tool {
350+ for _, tr := range toolResults {
351+ if tr.ToolCallID == tc.ID {
352+ found = true
353+ break
354+ }
355+ }
356+ }
357+ if found {
341358 break
342359 }
343360 }
@@ -348,12 +365,21 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
348365 } else if isPermissionErr {
349366 content = "Permission denied"
350367 }
351− currentAssistant.AddToolResult(message.ToolResult{
368+ toolResult := message.ToolResult{
352369 ToolCallID: tc.ID,
353370 Name: tc.Name,
354371 Content: content,
355372 IsError: true,
373+ }
374+ _, createErr = a.messages.Create(context.Background(), currentAssistant.SessionID, message.CreateMessageParams{
375+ Role: message.Tool,
376+ Parts: []message.ContentPart{
377+ toolResult,
378+ },
356379 })
380+ if createErr != nil {
381+ return nil, createErr
382+ }
357383 }
358384 }
359385 if isCancelErr {
@@ -363,13 +389,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
363389 } else {
364390 currentAssistant.AddFinish(message.FinishReasonError, "API Error", err.Error())
365391 }
366− // INFO: we use the parent context here because the genCtx might have been cancelled
392+ // INFO: we use the parent context here because the genCtx has been cancelled
367393 updateErr := a.messages.Update(ctx, *currentAssistant)
368394 if updateErr != nil {
369395 return nil, updateErr
370396 }
371− }
372− if err != nil {
373397 return nil, err
374398 }
375399 wg.Wait()
@@ -541,22 +565,32 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
541565 return
542566 }
543567
568+ var maxOutput int64 = 40
569+ if a.smallModel.CatwalkCfg.CanReason {
570+ maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
571+ }
572+
544573 agent := ai.NewAgent(a.smallModel.Model,
545− ai.WithSystemPrompt(string(titlePrompt)),
546− ai.WithMaxOutputTokens(40),
574+ ai.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
575+ ai.WithMaxOutputTokens(maxOutput),
547576 )
548577
549578 resp, err := agent.Stream(ctx, ai.AgentStreamCall{
550− Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s", prompt),
579+ Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", prompt),
551580 })
552581 if err != nil {
553582 slog.Error("error generating title", "err", err)
554583 return
555584 }
556585
586+ data, _ := json.Marshal(resp)
587+
588+ slog.Info("Title Response")
589+ slog.Info(string(data))
557590 title := resp.Response.Content.Text()
558591
559592 title = strings.ReplaceAll(title, "\n", " ")
593+ slog.Info(title)
560594
561595 // remove thinking tags if present
562596 if idx := strings.Index(title, "</think>"); idx > 0 {
internal/tui/page/chat/chat.go+6
@@ -2,6 +2,7 @@ package chat
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "time"
78
@@ -751,6 +752,11 @@ func (p *chatPage) sendMessage(text string, attachments []message.Attachment) te
751752 cmds = append(cmds, func() tea.Msg {
752753 _, err := p.app.AgentCoordinator.Run(context.Background(), session.ID, text, attachments...)
753754 if err != nil {
755+ isCancelErr := errors.Is(err, context.Canceled)
756+ isPermissionErr := errors.Is(err, permission.ErrorPermissionDenied)
757+ if isCancelErr || isPermissionErr {
758+ return nil
759+ }
754760 return util.InfoMsg{
755761 Type: util.InfoTypeError,
756762 Msg: err.Error(),