Module: Models::AssistantConversationMessageReplayable

Extended by:
ActiveSupport::Concern
Included in:
AssistantConversation
Defined in:
app/concerns/models/assistant_conversation_message_replayable.rb

Overview

Rebuilds an LLM-ready message history from persisted AssistantMessage
rows for AssistantConversation: compaction cutoff/summary injection,
orphaned tool-call/result filtering (providers reject unpaired blocks),
consecutive-user-message superseding, and provider-specific thinking /
thought-signature handling for Anthropic and Gemini replays.

Instance Method Summary collapse

Instance Method Details

#drop_unpaired_tool_results!Integer

Delete tool-result messages whose tool_use no longer exists, so the next
replay cannot rebuild a payload the provider will refuse.

The per-request guard in #replay_messages_onto_chat already refuses to emit
these, but only for the request it is building. This is the durable version:
once the provider has rejected a turn there is no value in keeping a result
nothing can answer for, and leaving it means every future turn depends on the
guard holding (AppSignal #3808, conversation 4640).

A tool result whose parent tool-call record is missing is unanswerable by
definition — the row that named it is gone — and the message body is a tool
payload the model cannot interpret without the call that produced it.

Returns:

  • (Integer)

    number of messages removed



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'app/concerns/models/assistant_conversation_message_replayable.rb', line 36

def drop_unpaired_tool_results!
  results = assistant_messages.where(role: 'tool').to_a
  return 0 if results.empty?

  answered = locally_answered_ids(results)
  unpaired = results.reject { answered.include?(it.id) }
  return 0 if unpaired.empty?

  Rails.logger.warn(
    "[AssistantConversation##{id}] dropping #{unpaired.size} tool result(s) with no surviving tool_use: " \
    "#{unpaired.map(&:id).join(', ')}"
  )
  unpaired.each(&:destroy)
  unpaired.size
end

#has_thinking_history?Boolean

True when any assistant message in this conversation contains thinking data.
Used by ChatService to force thinking mode on subsequent turns so Anthropic
doesn't reject the request for missing thinking blocks.

Returns:

  • (Boolean)


16
17
18
19
20
# File 'app/concerns/models/assistant_conversation_message_replayable.rb', line 16

def has_thinking_history?
  assistant_messages.where(role: 'assistant')
                    .where.not(thinking_text: nil)
                    .exists?
end