Module: LittleGhost::Content

Defined in:
lib/little_ghost/content.rb

Overview

Content gives messages a shared vocabulary for text, attachments, tool calls, tool results, and model reasoning. The same blocks move between agents, providers, tools, and sessions without leaking a provider's wire format.

block = LittleGhost::Content::Text.new(text: "Hello")
LittleGhost::Content.normalize("Hello") == block # => true

Every block serializes through Content.serialize. Binary data uses strict base64 encoding in the serialized form.

Defined Under Namespace

Classes: Document, Image, Reasoning, Text, ToolResult, ToolUse

Constant Summary collapse

Serializable =

:nodoc:

Module.new do # :nodoc:
  def to_h = Content.serialize(self)
  def to_json(*arguments) = JSON.generate(to_h, *arguments)
end

Class Method Summary collapse

Class Method Details

.binary(type, data, **attributes) ⇒ Object

:nodoc:



324
325
326
327
# File 'lib/little_ghost/content.rb', line 324

def binary(type, data, **attributes) # :nodoc:
  {"type" => type, "data" => Base64.strict_encode64(data), "encoding" => "base64"}
    .merge(attributes.transform_keys(&:to_s))
end

.from_hash(value) ⇒ Object

Reconstructs a content block from its serialized hash.



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/little_ghost/content.rb', line 253

def from_hash(value)
  hash = value.transform_keys(&:to_sym)
  type = hash.delete(:type)&.to_sym
  encoding = hash.delete(:encoding)
  if encoding.to_s == "base64"
    encoded = hash.delete(:data)
    raise ArgumentError, "base64 data is required" unless encoded.is_a?(String)

    decoded = Base64.strict_decode64(encoded)
    if type == :reasoning
      hash[:redacted_content] = decoded
    else
      hash[:data] = decoded
    end
  end
  if type == :tool_result
    hash[:status] = hash[:status].to_sym if hash[:status]
    if hash[:content].is_a?(Array)
      hash[:content] = hash[:content].map do |block|
        if block.is_a?(Hash) && (block.key?(:type) || block.key?("type"))
          normalize(block)
        else
          block
        end
      end
    end
  end
  klass = {
    text: Text,
    image: Image,
    document: Document,
    tool_use: ToolUse,
    tool_result: ToolResult,
    reasoning: Reasoning
  }.fetch(type) { raise ArgumentError, "Unsupported content type: #{type.inspect}" }
  klass.new(**hash)
rescue ArgumentError, KeyError => error
  raise ArgumentError, "Invalid #{type || "content"} block: #{error.message}"
end

.normalize(value) ⇒ Object

Accepts an existing block, a String, or a serialized Hash.



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/little_ghost/content.rb', line 239

def normalize(value)
  case value
  when Text, Image, Document, ToolUse, ToolResult, Reasoning
    value
  when String
    Text.new(text: value)
  when Hash
    from_hash(value)
  else
    raise ArgumentError, "Unsupported content block: #{value.class}"
  end
end

.serialize(block) ⇒ Object

Produces the JSON-safe representation of block.



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/little_ghost/content.rb', line 294

def serialize(block)
  case block
  when Text then {"type" => "text", "text" => block.text}
  when Reasoning
    {"type" => "reasoning", "text" => block.text}.tap do |value|
      value["signature"] = block.signature if block.signature
      if block.redacted_content
        value["data"] = Base64.strict_encode64(block.redacted_content)
        value["encoding"] = "base64"
      end
      value["details"] = block.details.map(&:to_h) if block.details
    end
  when Image
    binary("image", block.data, media_type: block.media_type).tap do |value|
      value["name"] = block.name if block.name
    end
  when Document
    binary("document", block.data, media_type: block.media_type, name: block.name)
  when ToolUse
    {"type" => "tool_use", "id" => block.id, "name" => block.name, "input" => block.input.to_h}
  when ToolResult
    {
      "type" => "tool_result", "tool_use_id" => block.tool_use_id,
      "content" => serialize_tool_result_content(block.content), "status" => block.status.to_s
    }
  else
    raise ArgumentError, "Unsupported content block: #{block.class}"
  end
end

.serialize_tool_result_content(content) ⇒ Object

:nodoc:



329
330
331
332
333
# File 'lib/little_ghost/content.rb', line 329

def serialize_tool_result_content(content) # :nodoc:
  return content unless content.is_a?(Array)

  content.map { |block| block.respond_to?(:to_h) ? block.to_h : block }
end