fix(cursor): preserve tool call context in multi-turn conversations

When an assistant message appears after tool results without a pending
user message, append it to the last turn's assistant text instead of
dropping it. Also add bakeToolResultsIntoTurns() to merge tool results
into turn context when no active H2 session exists for resume, ensuring
the model sees the full tool interaction history in follow-up requests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
黄姜恒
2026-03-25 17:15:24 +08:00
parent c1083cbfc6
commit 8afef43887

View File

@@ -321,6 +321,13 @@ func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
}
e.mu.Unlock()
// If tool results exist but no session to resume, bake them into turns
// so the model sees tool interaction context in the new conversation.
if len(parsed.ToolResults) > 0 {
log.Debugf("cursor: no session to resume, baking %d tool results into turns", len(parsed.ToolResults))
bakeToolResultsIntoTurns(parsed)
}
params := buildRunRequestParams(parsed)
requestBytes := cursorproto.EncodeRunRequest(params)
framedRequest := cursorproto.FrameConnectMessage(requestBytes, 0)
@@ -898,12 +905,22 @@ func parseOpenAIRequest(payload []byte) *parsedOpenAIRequest {
pendingUser = extractTextContent(msg.Get("content"))
p.Images = extractImages(msg.Get("content"))
case "assistant":
assistantText := extractTextContent(msg.Get("content"))
if pendingUser != "" {
p.Turns = append(p.Turns, cursorproto.TurnData{
UserText: pendingUser,
AssistantText: extractTextContent(msg.Get("content")),
AssistantText: assistantText,
})
pendingUser = ""
} else if len(p.Turns) > 0 && assistantText != "" {
// Assistant message after tool results (no pending user) —
// append to the last turn's assistant text to preserve context.
last := &p.Turns[len(p.Turns)-1]
if last.AssistantText != "" {
last.AssistantText += "\n" + assistantText
} else {
last.AssistantText = assistantText
}
}
}
}
@@ -922,6 +939,27 @@ func parseOpenAIRequest(payload []byte) *parsedOpenAIRequest {
return p
}
// bakeToolResultsIntoTurns merges tool results into the last turn's assistant text
// when there's no active H2 session to resume. This ensures the model sees the
// full tool interaction context in a new conversation.
func bakeToolResultsIntoTurns(parsed *parsedOpenAIRequest) {
if len(parsed.ToolResults) == 0 || len(parsed.Turns) == 0 {
return
}
last := &parsed.Turns[len(parsed.Turns)-1]
var toolContext strings.Builder
for _, tr := range parsed.ToolResults {
toolContext.WriteString("\n\n[Tool Result]\n")
toolContext.WriteString(tr.Content)
}
if last.AssistantText != "" {
last.AssistantText += toolContext.String()
} else {
last.AssistantText = toolContext.String()
}
parsed.ToolResults = nil // consumed
}
func extractTextContent(content gjson.Result) string {
if content.Type == gjson.String {
return content.String()