Module: Mistri::Providers::Gemini::Serializer

Defined in:
lib/mistri/providers/gemini/serializer.rb

Overview

Serializes protocol messages into generateContent contents.

Wire rules that matter: roles are user and model, consecutive tool results merge into one user turn of functionResponse parts, and thought signatures echo back verbatim on the exact part they arrived with, but only for messages this provider produced; a foreign signature would be rejected. Thinking summaries are output-only and never replay. Consecutive user turns stay separate: Gemini accepts them, and mixing a text part into a functionResponse turn makes it answer an empty candidate. Completed tool exchanges from another provider become ordinary text: Gemini rejects foreign functionCall history because it has no Gemini thought signature.

Class Method Summary collapse

Class Method Details

.assistant_parts(msg) ⇒ Object



109
110
111
112
113
114
115
116
117
118
# File 'lib/mistri/providers/gemini/serializer.rb', line 109

def assistant_parts(msg)
  own = msg.provider == :gemini
  msg.content.filter_map do |block|
    case block
    when Content::Text then signed({ text: block.text }, block.signature, own)
    when ToolCall
      own ? native_function_call(block) : foreign_function_call(block)
    end
  end
end

.contents(history) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/mistri/providers/gemini/serializer.rb', line 21

def contents(history)
   = (history)
  groups = history.reject(&:system?).chunk_while do |a, b|
    a.tool? && b.tool? && native_tool_result?(a, ) ==
      native_tool_result?(b, )
  end
  groups.filter_map do |group|
    if group.first.tool?
      if native_tool_result?(group.first, )
        tool_turn(group, )
      else
        foreign_tool_turn(group)
      end
    else
      turn(group.first)
    end
  end
end

.foreign_function_call(block) ⇒ Object



128
129
130
131
132
133
134
135
136
# File 'lib/mistri/providers/gemini/serializer.rb', line 128

def foreign_function_call(block)
  arguments = if block.arguments_error
                "unavailable (#{block.arguments_error})"
              else
                JSON.generate(block.arguments)
              end
  { text: "Tool call #{JSON.generate(block.name)} " \
          "(call #{JSON.generate(block.id)}) with arguments: #{arguments}" }
end

.foreign_tool_turn(group) ⇒ Object



78
79
80
81
82
83
84
85
86
87
# File 'lib/mistri/providers/gemini/serializer.rb', line 78

def foreign_tool_turn(group)
  { role: "user", parts: group.map do |msg|
    raise SchemaError, "Gemini tool results need tool_name" unless msg.tool_name

    label = msg.tool_error? ? "error" : "result"
    text = "Tool #{JSON.generate(msg.tool_name)} #{label} " \
           "(call #{JSON.generate(msg.tool_call_id)}): #{result_text(msg)}"
    { text: text }
  end }
end

.native_function_call(block) ⇒ Object



120
121
122
123
124
125
126
# File 'lib/mistri/providers/gemini/serializer.rb', line 120

def native_function_call(block)
  arguments = ToolArguments.replay_object(block)
  signature = block.signature if arguments.equal?(block.arguments)
  call = { name: block.name, args: arguments }
  call[:id] = block.provider_call_id if block.provider_call_id
  signed({ functionCall: call }, signature, true)
end

.native_tool_result?(message, result_metadata) ⇒ Boolean

Returns:

  • (Boolean)


138
139
140
# File 'lib/mistri/providers/gemini/serializer.rb', line 138

def native_tool_result?(message, )
  .dig(message, :provider) == :gemini
end

.result_text(msg) ⇒ Object

Non-text blocks in a tool result have no functionResponse encoding; note the omission rather than dropping it silently.



91
92
93
94
95
# File 'lib/mistri/providers/gemini/serializer.rb', line 91

def result_text(msg)
  omitted = msg.content.count { |block| !block.is_a?(Content::Text) }
  text = msg.text.to_s
  omitted.positive? ? "#{text}\n[#{omitted} non-text block(s) omitted]".strip : text
end

.signed(part, signature, own) ⇒ Object



162
163
164
165
# File 'lib/mistri/providers/gemini/serializer.rb', line 162

def signed(part, signature, own)
  part[:thoughtSignature] = signature if own && signature
  part
end

.system_instruction(system) ⇒ Object



40
41
42
43
44
# File 'lib/mistri/providers/gemini/serializer.rb', line 40

def system_instruction(system)
  return nil if system.nil? || system.empty?

  { parts: [{ text: system }] }
end

.tool_result_metadata(history) ⇒ Object

Legacy histories may reuse call_N across turns. Correlating each result to the nearest preceding occurrence keeps a later Gemini call from rewriting an earlier foreign result's wire semantics.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/mistri/providers/gemini/serializer.rb', line 145

def (history)
  pending = Hash.new { |calls, id| calls[id] = [] }
   = {}.compare_by_identity
  history.each do |message|
    if message.assistant?
      message.tool_calls.each do |call|
        pending[call.id] << { provider: message.provider,
                              provider_call_id: call.provider_call_id }.freeze
      end
    elsif message.tool?
      matched = pending[message.tool_call_id].pop
      [message] = matched if matched
    end
  end
  
end

.tool_turn(group, result_metadata = {}.compare_by_identity) ⇒ Object

Gemini pairs a functionResponse to its call by NAME; a wrong name silently mismatches, so a missing one fails loudly instead.



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/mistri/providers/gemini/serializer.rb', line 64

def tool_turn(group,  = {}.compare_by_identity)
  { role: "user", parts: group.map do |msg|
    unless msg.tool_name
      raise SchemaError, "Gemini tool results need tool_name to pair with their call"
    end

    key = msg.tool_error? ? "error" : "result"
    response = { name: msg.tool_name, response: { key => result_text(msg) } }
    wire_id = .dig(msg, :provider_call_id)
    response[:id] = wire_id if wire_id
    { functionResponse: response }
  end }
end

.tools(definitions) ⇒ Object



46
47
48
49
50
51
52
53
# File 'lib/mistri/providers/gemini/serializer.rb', line 46

def tools(definitions)
  declarations = definitions.map do |tool|
    spec = tool.transform_keys(&:to_sym)
    { name: spec[:name], description: spec[:description],
      parametersJsonSchema: spec[:input_schema] }
  end
  [{ functionDeclarations: declarations }]
end

.turn(msg) ⇒ Object



55
56
57
58
59
60
# File 'lib/mistri/providers/gemini/serializer.rb', line 55

def turn(msg)
  parts = msg.assistant? ? assistant_parts(msg) : user_parts(msg)
  return nil if parts.empty?

  { role: msg.assistant? ? "model" : "user", parts: parts }
end

.user_parts(msg) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/mistri/providers/gemini/serializer.rb', line 97

def user_parts(msg)
  msg.content.map do |block|
    case block
    when Content::Text then { text: block.text }
    when Content::Image
      { inlineData: { mimeType: block.mime_type, data: block.data } }
    else
      raise SchemaError, "cannot serialize #{block.class} for Gemini user input"
    end
  end
end