@@ -64,7 +64,10 @@ var titlePrompt []byte
64var summaryPrompt []byte
64var summaryPrompt []byte
66// Used to remove <think> tags from generated titles.
66// Used to remove <think> tags from generated titles.
67−var thinkTagRegex = regexp.MustCompile(`<think>.*?</think>`)
68+ thinkTagRegex = regexp.MustCompile(`(?s)<think>.*?</think>`)
69+ orphanThinkTagRegex = regexp.MustCompile(`</?think>`)
69type SessionAgentCall struct {
72type SessionAgentCall struct {
70 SessionID string
73 SessionID string
@@ -769,6 +772,16 @@ If not, please feel free to ignore. Again do not mention this message to the use
775+ // Collect all tool call IDs present in assistant messages.
776+ knownToolCallIDs := make(map[string]struct{})
777+ for _, m := range msgs {
778+ if m.Role == message.Assistant {
779+ for _, tc := range m.ToolCalls() {
780+ knownToolCallIDs[tc.ID] = struct{}{}
772 for _, m := range msgs {
785 for _, m := range msgs {
773 if len(m.Parts) == 0 {
786 if len(m.Parts) == 0 {
@@ -778,6 +791,35 @@ If not, please feel free to ignore. Again do not mention this message to the use
778 if m.Role == message.Assistant && len(m.ToolCalls()) == 0 && m.Content().Text == "" && m.ReasoningContent().String() == "" {
791 if m.Role == message.Assistant && len(m.ToolCalls()) == 0 && m.Content().Text == "" && m.ReasoningContent().String() == "" {
794+ if m.Role == message.Tool {
795+ // Filter out tool results that have no matching tool call. An orphaned
796+ // result causes every subsequent API call to fail validation.
797+ aiMsgs := m.ToAIMessage()
798+ if len(aiMsgs) == 0 {
801+ var validParts []fantasy.MessagePart
802+ for _, part := range aiMsgs[0].Content {
803+ tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
805+ validParts = append(validParts, part)
808+ if _, known := knownToolCallIDs[tr.ToolCallID]; known {
809+ validParts = append(validParts, part)
811+ slog.Warn("Dropping orphaned tool result with no matching tool call",
812+ "tool_call_id", tr.ToolCallID,
816+ if len(validParts) > 0 {
818+ msg.Content = validParts
819+ history = append(history, msg)
781 history = append(history, m.ToAIMessage()...)
823 history = append(history, m.ToAIMessage()...)
@@ -898,6 +940,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
899 // Remove thinking tags if present.
941 // Remove thinking tags if present.
900 title = thinkTagRegex.ReplaceAllString(title, "")
942 title = thinkTagRegex.ReplaceAllString(title, "")
943+ title = orphanThinkTagRegex.ReplaceAllString(title, "")
902 title = strings.TrimSpace(title)
945 title = strings.TrimSpace(title)
903 title = cmp.Or(title, DefaultSessionName)
946 title = cmp.Or(title, DefaultSessionName)