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.

Class Method Summary collapse

Class Method Details

.assistant_parts(msg) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
# File 'lib/mistri/providers/gemini/serializer.rb', line 81

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
      signed({ functionCall: { name: block.name, args: block.arguments } },
             block.signature, own)
    end
  end
end

.contents(history) ⇒ Object



19
20
21
22
23
24
# File 'lib/mistri/providers/gemini/serializer.rb', line 19

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

.result_text(msg) ⇒ Object

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



63
64
65
66
67
# File 'lib/mistri/providers/gemini/serializer.rb', line 63

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



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

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

.system_instruction(system) ⇒ Object



26
27
28
29
30
# File 'lib/mistri/providers/gemini/serializer.rb', line 26

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

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

.tool_turn(group) ⇒ Object

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



50
51
52
53
54
55
56
57
58
59
# File 'lib/mistri/providers/gemini/serializer.rb', line 50

def tool_turn(group)
  { 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

    { functionResponse: { name: msg.tool_name,
                          response: { "result" => result_text(msg) } } }
  end }
end

.tools(definitions) ⇒ Object



32
33
34
35
36
37
38
39
# File 'lib/mistri/providers/gemini/serializer.rb', line 32

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

.turn(msg) ⇒ Object



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

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



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/mistri/providers/gemini/serializer.rb', line 69

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