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:



313
314
315
316
# File 'lib/little_ghost/content.rb', line 313

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.



244
245
246
247
248
249
250
251
252
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
# File 'lib/little_ghost/content.rb', line 244

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.



230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/little_ghost/content.rb', line 230

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.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/little_ghost/content.rb', line 285

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 if block.details
    end
  when Image
    binary("image", block.data, media_type: block.media_type)
  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}
  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:



318
319
320
321
322
# File 'lib/little_ghost/content.rb', line 318

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