Module: Mistri::Providers::Anthropic::Serializer

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

Overview

Serializes protocol messages into Anthropic Messages API wire shapes.

Wire rules that matter: consecutive tool results merge into one user turn (parallel tool calls demand their results together), thinking blocks replay with their signature and must never be altered, redacted thinking replays as its opaque payload, and cache_control marks the last system block and the last user message so the stable prefix and the growing history both cache.

Class Method Summary collapse

Class Method Details

.block(block) ⇒ Object

Returns nil for a block the API would reject (empty text, unusable thinking), so callers filter_map it out.



60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/mistri/providers/anthropic/serializer.rb', line 60

def block(block)
  case block
  when Content::Text then text_block(block)
  when Content::Thinking then thinking_block(block)
  when Content::Image
    { type: "image",
      source: { type: "base64", media_type: block.mime_type, data: block.data } }
  when ToolCall
    { type: "tool_use", id: block.id, name: block.name, input: block.arguments }
  else
    raise SchemaError, "cannot serialize #{block.class} for Anthropic"
  end
end

.mark_last_user_turn(wire) ⇒ Object



93
94
95
96
97
98
99
100
101
102
# File 'lib/mistri/providers/anthropic/serializer.rb', line 93

def mark_last_user_turn(wire)
  last_user = wire.rindex { |turn| turn[:role] == "user" }
  return unless last_user

  content = wire[last_user][:content]
  return unless content.is_a?(Array) && content.any?

  content.last[:cache_control] =
    { type: "ephemeral" }
end

.message(msg) ⇒ Object



44
45
46
# File 'lib/mistri/providers/anthropic/serializer.rb', line 44

def message(msg)
  { role: msg.role.to_s, content: msg.content.filter_map { |block| block(block) } }
end

.messages(history, cache: false) ⇒ Object



25
26
27
28
29
30
31
32
# File 'lib/mistri/providers/anthropic/serializer.rb', line 25

def messages(history, cache: false)
  turns = history.reject(&:system?).chunk_while { |a, b| a.tool? && b.tool? }
  wire = turns.map do |group|
    group.first.tool? ? tool_results(group) : message(group.first)
  end
  mark_last_user_turn(wire) if cache
  wire
end

.system_blocks(system, cache:) ⇒ Object



17
18
19
20
21
22
23
# File 'lib/mistri/providers/anthropic/serializer.rb', line 17

def system_blocks(system, cache:)
  return nil if system.nil? || system.empty?

  blocks = [{ type: "text", text: system }]
  blocks.last[:cache_control] = { type: "ephemeral" } if cache
  blocks
end

.text_block(block) ⇒ Object

The API rejects empty text content blocks.



75
76
77
# File 'lib/mistri/providers/anthropic/serializer.rb', line 75

def text_block(block)
  block.text.empty? ? nil : { type: "text", text: block.text }
end

.thinking_block(block) ⇒ Object

Thinking replays only with its signature. Redacted thinking carries its opaque payload; a normal thinking block missing its signature (an aborted turn cut before signature_delta) cannot replay, so it degrades to its text, or drops when even that is empty.



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

def thinking_block(block)
  return { type: "redacted_thinking", data: block.signature } if block.redacted?
  if block.signature
    return { type: "thinking", thinking: block.thinking,
             signature: block.signature }
  end

  block.thinking.empty? ? nil : { type: "text", text: block.thinking }
end

.tool_results(group) ⇒ Object



48
49
50
51
52
53
54
55
56
# File 'lib/mistri/providers/anthropic/serializer.rb', line 48

def tool_results(group)
  { role: "user", content: group.map do |msg|
    blocks = msg.content.filter_map { |block| block(block) }
    # The API rejects an empty tool_result; a space stands in for a
    # tool that returned nothing.
    blocks = [{ type: "text", text: " " }] if blocks.empty?
    { type: "tool_result", tool_use_id: msg.tool_call_id, content: blocks }
  end }
end

.tools(definitions) ⇒ Object



34
35
36
37
38
39
40
41
42
# File 'lib/mistri/providers/anthropic/serializer.rb', line 34

def tools(definitions)
  definitions.map do |tool|
    spec = tool.transform_keys(&:to_sym)
    wire = { name: spec[:name], description: spec[:description],
             input_schema: spec[:input_schema] }
    wire[:eager_input_streaming] = true if spec[:eager_input_streaming]
    wire
  end
end