Module: LittleGhost::CodeMode::Ruby::Host

Defined in:
lib/little_ghost/code_mode/ruby/host.rb

Overview

:nodoc:

Constant Summary collapse

SOURCE =
<<~'RUBY'
    require "json"
    STDOUT.sync = true
    begin
    read_exactly = lambda do |length|
      value = +"".b
      value << (STDIN.read(length - value.bytesize) || raise("incomplete protocol frame")) while value.bytesize < length
      value
    end
    read_frame = lambda do
      length = read_exactly.call(4).unpack1("N")
      raise "protocol frame too large" if length > 64 * 1024 * 1024
      JSON.parse(read_exactly.call(length))
    end
    write_frame = lambda do |value|
      payload = JSON.generate(value)
      raise "protocol frame too large" if payload.bytesize > 64 * 1024 * 1024
      STDOUT.write([payload.bytesize].pack("N"))
      STDOUT.write(payload)
      STDOUT.flush
    end
    request = read_frame.call
    catalog = request.fetch("catalog")
    write_lock = Mutex.new
    queues_lock = Mutex.new
    response_queues = {}
    calls = 0
    max_calls = request.fetch("tool_calls")
    emit = ->(value) { write_lock.synchronize { write_frame.call(value) } }
    reader = Thread.new do
      loop do
        response = read_frame.call
        if response["id"]
          queue = queues_lock.synchronize { response_queues[response["id"]] }
          queue << response if queue
        end
      end
    end
    invoke = lambda do |name, arguments|
      id, queue = queues_lock.synchronize do
        calls += 1
        raise "tool call limit exceeded" if calls > max_calls
        id = "call-#{calls}"
        queue = Queue.new
        response_queues[id] = queue
        [id, queue]
      end
      write_lock.synchronize do
        write_frame.call(type: "call", id: id, name: name, arguments: arguments)
      end
      response = queue.pop
      queues_lock.synchronize { response_queues.delete(id) }
      raise response.fetch("error") if response["error"]
      response["value"]
    end
    tools = Object.new
    tools.define_singleton_method(:call) { |name, arguments = {}| invoke.call(name.to_s, arguments) }
    catalog.each do |specification|
      name = specification.fetch("name")
      method_name = name.gsub(/[^a-zA-Z0-9_]/, "_").sub(/\A(?=\d)/, "tool_")
      tools.define_singleton_method(method_name) { |**arguments| invoke.call(name, arguments) }
    end
    concurrency = request.fetch("concurrency")
    tools.define_singleton_method(:parallel) do |*operations|
      raise ArgumentError, "parallel accepts callables" unless operations.all? { |operation| operation.respond_to?(:call) }
      operations.each_slice(concurrency).flat_map do |batch|
        batch.map { |operation| Thread.new { operation.call } }.map(&:value)
      end
    end
    Object.const_set(:ALL_TOOLS, catalog.freeze) unless Object.const_defined?(:ALL_TOOLS)
    Object.const_set(:FRAME, request["frame"].freeze) if request["frame"] && !Object.const_defined?(:FRAME)
    context = Object.new
    finished = false
    finish_value = nil
    context.define_singleton_method(:tools) { tools }
    context.define_singleton_method(:text) { |value| emit.call(type: "text", value: value.to_s); nil }
    context.define_singleton_method(:finish) do |value = nil|
      finished = true
      finish_value = value
      throw :little_ghost_finish
    end
    value = catch(:little_ghost_finish) { context.instance_eval(request.fetch("source"), "(code-mode)", 1) }
    value = finish_value if finished
    emit.call(type: "done", value: value)
  rescue SignalException
    exit 0
  rescue Exception => error
    STDERR.puts("#{error.class}: #{error.message}")
    write_frame&.call(type: "error", error: "#{error.class}: #{error.message}")
    exit 1
    end
RUBY