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, own: true) ⇒ Object

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



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

def block(block, own: true)
  case block
  when Content::Text then text_block(block)
  when Content::Thinking then thinking_block(block, own:)
  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: ToolArguments.replay_object(block) }
  else
    raise SchemaError, "cannot serialize #{block.class} for Anthropic"
  end
end

.mark_last_user_turn(wire) ⇒ Object



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

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
47
48
# File 'lib/mistri/providers/anthropic/serializer.rb', line 44

def message(msg)
  own = msg.provider == :anthropic
  { role: msg.role.to_s,
    content: msg.content.filter_map { |content| block(content, own:) } }
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.



80
81
82
# File 'lib/mistri/providers/anthropic/serializer.rb', line 80

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

.thinking_block(block, own: true) ⇒ 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.



88
89
90
91
92
93
94
95
96
# File 'lib/mistri/providers/anthropic/serializer.rb', line 88

def thinking_block(block, own: true)
  return { type: "redacted_thinking", data: block.signature } if own && block.redacted?
  if own && 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



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

def tool_results(group)
  { role: "user", content: group.map do |msg|
    blocks = msg.content.filter_map { |content| block(content, own: false) }
    # The API rejects an empty tool_result; a space stands in for a
    # tool that returned nothing.
    blocks = [{ type: "text", text: " " }] if blocks.empty?
    result = { type: "tool_result", tool_use_id: msg.tool_call_id, content: blocks }
    result[:is_error] = true if msg.tool_error?
    result
  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