fix: append error tool message when tool call is cancelled (#2492)

Chris Chen created

When a user cancels a tool call by pressing Esc twice, the assistant
message with tool_calls was remaining in the conversation history but
no tool response followed. This caused 400 Bad Request errors from
model providers on the next message.

The fix uses the parent context (ctx) instead of the request context
(genCtx) when updating messages during tool call tracking and tool
result creation. This ensures that even if the request is cancelled
mid-stream, the message updates and creations succeed.

Additionally, the error message was updated from 'Tool execution
canceled by user' to 'Error: user cancelled assistant tool calling'
to better indicate the cancellation occurred during the assistant
tool calling phase.

Fixes #529
Fixes #1206

Change summary

internal/agent/agent.go | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)

Detailed changes

internal/agent/agent.go 🔗

@@ -364,7 +364,9 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 				Finished:         false,
 			}
 			currentAssistant.AddToolCall(toolCall)
-			return a.messages.Update(genCtx, *currentAssistant)
+			// Use parent ctx instead of genCtx to ensure the update succeeds
+			// even if the request is canceled mid-stream
+			return a.messages.Update(ctx, *currentAssistant)
 		},
 		OnRetry: func(err *fantasy.ProviderError, delay time.Duration) {
 			// TODO: implement
@@ -378,11 +380,15 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 				Finished:         true,
 			}
 			currentAssistant.AddToolCall(toolCall)
-			return a.messages.Update(genCtx, *currentAssistant)
+			// Use parent ctx instead of genCtx to ensure the update succeeds
+			// even if the request is canceled mid-stream
+			return a.messages.Update(ctx, *currentAssistant)
 		},
 		OnToolResult: func(result fantasy.ToolResultContent) error {
 			toolResult := a.convertToToolResult(result)
-			_, createMsgErr := a.messages.Create(genCtx, currentAssistant.SessionID, message.CreateMessageParams{
+			// Use parent ctx instead of genCtx to ensure the message is created
+			// even if the request is canceled mid-stream
+			_, createMsgErr := a.messages.Create(ctx, currentAssistant.SessionID, message.CreateMessageParams{
 				Role: message.Tool,
 				Parts: []message.ContentPart{
 					toolResult,
@@ -485,7 +491,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			}
 			content := "There was an error while executing the tool"
 			if isCancelErr {
-				content = "Tool execution canceled by user"
+				content = "Error: user cancelled assistant tool calling"
 			} else if isPermissionErr {
 				content = "User denied permission"
 			}