Class: Assistant::RestoreToolResultBuilder

Inherits:
Object
  • Object
show all
Defined in:
app/services/assistant/restore_tool_result_builder.rb

Overview

Builds the +restore_tool_result+ tool: retrieves the full, uncompressed
output of an earlier tool result that ContextCompactor truncated to save
context. The original is stashed in +assistant_messages.compacted_content+
keyed by the message id, so — unlike a stateless compression proxy —
there is no hash store; the message id shown in the truncated result's
+_restore+ marker is the key.

Gated by +ContextCompactor.restore_enabled?+ (env SUNNY_TOOL_RESULT_RESTORE):
no point offering the tool when nothing is being stashed to restore.

Class Method Summary collapse

Class Method Details

.tool(conversation_id) ⇒ RubyLLM::Tool

Parameters:

  • conversation_id (Integer)

    the current conversation; scopes the
    lookup so one tenant's Sunny can never read another's tool output.

Returns:

  • (RubyLLM::Tool)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'app/services/assistant/restore_tool_result_builder.rb', line 18

def tool(conversation_id)
  captured_conversation_id = conversation_id

  Class.new(RubyLLM::Tool) do
    description <<~DESC.squish
      Retrieve the full, uncompressed output of an earlier tool result that was
      truncated to save context. When a tool result shows a "_restore" field with a
      message_id, call this with that message_id to get the complete original output —
      use it when the truncated version dropped rows, fields, or detail you now need.
    DESC

    parameters type: 'object',
           properties: {
             message_id: {
               type: 'integer',
               description: 'The assistant_message id from the truncated result\'s "_restore" field.'
             }
           },
           required: %w[message_id]

    define_method(:name) { 'restore_tool_result' }

    define_method(:execute) do |message_id:, **_|
      id = message_id.to_i
      return { error: 'message_id is required' }.to_json if id.zero?

      # Conversation-scoped: never a bare AssistantMessage.find, or one
      # tenant's Sunny could read another conversation's tool output.
      original = AssistantMessage
                 .where(assistant_conversation_id: captured_conversation_id, id: id)
                 .pick(:compacted_content)

      if original.blank?
        { error: "No stored original for message #{id} (never truncated, already expired, or not in this conversation)." }.to_json
      else
        Rails.logger.info("[Sunny] tool-result restore hit: msg=#{id} conv=#{captured_conversation_id} chars=#{original.length}")
        original
      end
    rescue StandardError => e
      Rails.logger.warn("[Sunny] restore_tool_result failed: #{e.class}: #{e.message}")
      { error: "restore failed: #{e.message}" }.to_json
    end
  end.new
end