Fix: do not delete tool calls or lose them on copy

This commit is contained in:
Grail Finder
2026-02-28 10:23:03 +03:00
parent 2580360f91
commit d79760a289
3 changed files with 44 additions and 69 deletions

109
bot.go
View File

@@ -136,6 +136,9 @@ func processMessageTag(msg *models.RoleMsg) *models.RoleMsg {
// filterMessagesForCharacter returns messages visible to the specified character. // filterMessagesForCharacter returns messages visible to the specified character.
// If CharSpecificContextEnabled is false, returns all messages. // If CharSpecificContextEnabled is false, returns all messages.
func filterMessagesForCharacter(messages []models.RoleMsg, character string) []models.RoleMsg { func filterMessagesForCharacter(messages []models.RoleMsg, character string) []models.RoleMsg {
if strings.Contains(cfg.CurrentAPI, "chat") {
return messages
}
if cfg == nil || !cfg.CharSpecificContextEnabled || character == "" { if cfg == nil || !cfg.CharSpecificContextEnabled || character == "" {
return messages return messages
} }
@@ -158,82 +161,52 @@ func filterMessagesForCharacter(messages []models.RoleMsg, character string) []m
return filtered return filtered
} }
func cleanToolCalls(messages []models.RoleMsg) []models.RoleMsg {
// If AutoCleanToolCallsFromCtx is false, keep tool call messages in context
if cfg != nil && !cfg.AutoCleanToolCallsFromCtx {
return consolidateAssistantMessages(messages)
}
cleaned := make([]models.RoleMsg, 0, len(messages))
for i := range messages {
// recognize the message as the tool call and remove it
// tool call in last msg should stay
if messages[i].ToolCallID == "" || i == len(messages)-1 {
cleaned = append(cleaned, messages[i])
}
}
return consolidateAssistantMessages(cleaned)
}
// consolidateAssistantMessages merges consecutive assistant messages into a single message
func consolidateAssistantMessages(messages []models.RoleMsg) []models.RoleMsg { func consolidateAssistantMessages(messages []models.RoleMsg) []models.RoleMsg {
if len(messages) == 0 { if len(messages) == 0 {
return messages return messages
} }
consolidated := make([]models.RoleMsg, 0, len(messages)) result := make([]models.RoleMsg, 0, len(messages))
currentAssistantMsg := models.RoleMsg{} for i := range messages {
isBuildingAssistantMsg := false // Non-assistant messages are appended as-is
for i := 0; i < len(messages); i++ { if messages[i].Role != cfg.AssistantRole {
msg := messages[i] result = append(result, messages[i])
// assistant role only continue
if msg.Role == cfg.AssistantRole { }
// If this is an assistant message, start or continue building // Assistant message: start a new block or merge with the last one
if !isBuildingAssistantMsg { if len(result) == 0 || result[len(result)-1].Role != cfg.AssistantRole {
// Start accumulating assistant message // First assistant in a block: append a copy (avoid mutating input)
currentAssistantMsg = msg.Copy() result = append(result, messages[i].Copy())
isBuildingAssistantMsg = true continue
} else { }
// Continue accumulating - append content to the current assistant message // Merge with the last assistant message
if currentAssistantMsg.IsContentParts() || msg.IsContentParts() { last := &result[len(result)-1]
// Handle structured content // If either message has structured content, unify to ContentParts
if !currentAssistantMsg.IsContentParts() { if last.IsContentParts() || messages[i].IsContentParts() {
// Preserve the original ToolCallID before conversion // Convert last to ContentParts if needed, preserving ToolCallID
originalToolCallID := currentAssistantMsg.ToolCallID if !last.IsContentParts() {
// Convert existing content to content parts toolCallID := last.ToolCallID
currentAssistantMsg = models.NewMultimodalMsg(currentAssistantMsg.Role, []interface{}{models.TextContentPart{Type: "text", Text: currentAssistantMsg.Content}}) *last = models.NewMultimodalMsg(last.Role, []interface{}{
// Restore the original ToolCallID to preserve tool call linking models.TextContentPart{Type: "text", Text: last.Content},
currentAssistantMsg.ToolCallID = originalToolCallID })
} last.ToolCallID = toolCallID
if msg.IsContentParts() { }
currentAssistantMsg.ContentParts = append(currentAssistantMsg.ContentParts, msg.GetContentParts()...) // Add current message's content to last
} else if msg.Content != "" { if messages[i].IsContentParts() {
currentAssistantMsg.AddTextPart(msg.Content) last.ContentParts = append(last.ContentParts, messages[i].GetContentParts()...)
} } else if messages[i].Content != "" {
} else { last.AddTextPart(messages[i].Content)
// Simple string content
if currentAssistantMsg.Content != "" {
currentAssistantMsg.Content += "\n" + msg.Content
} else {
currentAssistantMsg.Content = msg.Content
}
// ToolCallID is already preserved since we're not creating a new message object when just concatenating content
}
} }
} else { } else {
// This is not an assistant message // Both simple strings: concatenate with newline
// If we were building an assistant message, add it to the result if last.Content != "" && messages[i].Content != "" {
if isBuildingAssistantMsg { last.Content += "\n" + messages[i].Content
consolidated = append(consolidated, currentAssistantMsg) } else if messages[i].Content != "" {
isBuildingAssistantMsg = false last.Content = messages[i].Content
} }
// Add the non-assistant message // ToolCallID is already preserved in last
consolidated = append(consolidated, msg)
} }
} }
// Don't forget the last assistant message if we were building one return result
if isBuildingAssistantMsg {
consolidated = append(consolidated, currentAssistantMsg)
}
return consolidated
} }
// GetLogLevel returns the current log level as a string // GetLogLevel returns the current log level as a string
@@ -982,7 +955,7 @@ func cleanChatBody() {
} }
// Tool request cleaning is now configurable via AutoCleanToolCallsFromCtx (default false) // Tool request cleaning is now configurable via AutoCleanToolCallsFromCtx (default false)
// /completion msg where part meant for user and other part tool call // /completion msg where part meant for user and other part tool call
chatBody.Messages = cleanToolCalls(chatBody.Messages) // chatBody.Messages = cleanToolCalls(chatBody.Messages)
chatBody.Messages = consolidateAssistantMessages(chatBody.Messages) chatBody.Messages = consolidateAssistantMessages(chatBody.Messages)
} }

View File

@@ -16,7 +16,7 @@ var (
shellHistory []string shellHistory []string
shellHistoryPos int = -1 shellHistoryPos int = -1
thinkingCollapsed = false thinkingCollapsed = false
toolCollapsed = false toolCollapsed = true
statusLineTempl = "help (F12) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)" statusLineTempl = "help (F12) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
focusSwitcher = map[tview.Primitive]tview.Primitive{} focusSwitcher = map[tview.Primitive]tview.Primitive{}
) )

View File

@@ -283,6 +283,8 @@ func (m *RoleMsg) Copy() RoleMsg {
KnownTo: m.KnownTo, KnownTo: m.KnownTo,
Stats: m.Stats, Stats: m.Stats,
HasContentParts: m.HasContentParts, HasContentParts: m.HasContentParts,
ToolCall: m.ToolCall,
IsShellCommand: m.IsShellCommand,
} }
} }