Class: LittleGhost::CodeMode::Broker

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/code_mode/broker.rb

Overview

Keeps tool authorization and execution in the trusted parent process.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent: nil, registry: nil, context: RunContext.new, events: [], parent_operation_id: nil, parent_trace_context: nil, except: nil, dispatch: nil, max_calls: nil) ⇒ Broker

Builds a broker for an Agent's Tool registry. except removes names from the code-mode catalog. dispatch is a trusted adapter hook used by custom hosts; ordinary Agent integrations should let the broker dispatch through agent.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/little_ghost/code_mode/broker.rb', line 14

def initialize(agent: nil, registry: nil, context: RunContext.new, events: [],
  parent_operation_id: nil, parent_trace_context: nil, except: nil, dispatch: nil, max_calls: nil)
  @agent = agent
  runtime = agent&.runtime
  @task_runner = runtime ? runtime.task_runner : Support::TaskRunner.new
  @registry = registry || agent&.tool_registry
  @context = context
  @events = events
  @parent_operation_id = parent_operation_id
  @parent_trace_context = parent_trace_context
  @except = Array(except).map(&:to_s).freeze
  @dispatch = dispatch
  @max_calls = max_calls && Integer(max_calls)
  @call_count = 0
  @call_mutex = Mutex.new
  @delivery_mutex = Mutex.new
  @presentation_content = []
  @artifacts = []
  @fallback_references = []
  @presentation_budget = Artifacts::PresentationBudget.new
end

Instance Attribute Details

#context=(value) ⇒ Object (writeonly)

:nodoc:



48
49
50
# File 'lib/little_ghost/code_mode/broker.rb', line 48

def context=(value)
  @context = value
end

#task_runnerObject (readonly)

:nodoc:



50
51
52
# File 'lib/little_ghost/code_mode/broker.rb', line 50

def task_runner
  @task_runner
end

Instance Method Details

#bind(context:, events:, parent_operation_id:, parent_trace_context: nil) ⇒ Object

:nodoc:



52
53
54
55
56
57
58
# File 'lib/little_ghost/code_mode/broker.rb', line 52

def bind(context:, events:, parent_operation_id:, parent_trace_context: nil) # :nodoc:
  @context = context
  @events = events
  @parent_operation_id = parent_operation_id
  @parent_trace_context = parent_trace_context
  self
end

#call(name, arguments = {}, id: SecureRandom.uuid) ⇒ Object

Calls an available Tool by model-visible name. The returned CallResult has id, decoded value, and model-safe error fields. arguments remains untrusted until the ordinary Tool path validates and authorizes it.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/little_ghost/code_mode/broker.rb', line 64

def call(name, arguments = {}, id: SecureRandom.uuid)
  @call_mutex.synchronize do
    @call_count += 1
    raise ToolError, "Code-mode tool call budget exceeded" if @max_calls && @call_count > @max_calls
  end
  name = String(name).delete_prefix("tools.")
  available = catalog.any? { |specification| specification.fetch(:name, specification["name"]).to_s == name }
  raise ToolError, "Tool is not available in code mode: #{name}" unless available
  raise ToolError, "Tool arguments must be an object" unless arguments.is_a?(Hash)

  execution_result = nil
  result = if @dispatch
    dispatched = @dispatch.call(Call.new(id:, name:, arguments:))
    if dispatched.respond_to?(:presentation_content) && dispatched.respond_to?(:artifacts)
      record_delivery(dispatched.presentation_content, dispatched.artifacts)
    end
    dispatched
  elsif @agent
    use = Content::ToolUse.new(id:, name:, input: arguments)
    emit_brokered_tool_call(use)
    executed = @agent.dispatch_tools(
      [use],
      context: @context,
      events: @events,
      parent_operation_id: @parent_operation_id,
      parent_trace_context: @parent_trace_context
    ).first
    record_delivery(executed.presentation_content, executed.artifacts)
    execution_result = executed.execution_result
  else
    tool = @registry.fetch(name)
    execution = tool.execute(arguments, context: @context)
    record_delivery(execution.presentation_content, execution.artifacts)
    return CallResult.new(id:, value: execution.value, error: execution.error? ? execution.content : nil)
  end

  return result if result.is_a?(CallResult)

  execution_result ||= result
  content = execution_result.respond_to?(:content) ? execution_result.content : execution_result
  value = execution_result.respond_to?(:value) ? execution_result.value : decode(content)
  status = execution_result.respond_to?(:status) ? execution_result.status : :success
  CallResult.new(id:, value:, error: (content if status == :error))
rescue ToolError => error
  CallResult.new(id:, value: nil, error: error.message)
end

#catalogObject

Returns the Tool specifications available to model-authored code. Excluded Tools, code-mode controls, and subagent controls stay outside this catalog.



39
40
41
42
43
44
45
46
# File 'lib/little_ghost/code_mode/broker.rb', line 39

def catalog
  specifications = @registry&.specifications || []
  specifications.reject do |specification|
    name = specification.fetch(:name, specification["name"]).to_s
    tool = @registry.fetch(name)
    @except.include?(name) || %w[exec wait stop].include?(name) || subagent_control?(tool)
  end
end

#drain_deliveryObject

:nodoc:



111
112
113
114
115
116
117
118
119
120
# File 'lib/little_ghost/code_mode/broker.rb', line 111

def drain_delivery # :nodoc:
  @delivery_mutex.synchronize do
    delivery = [@presentation_content.freeze, @artifacts.freeze, @fallback_references.uniq.freeze]
    @presentation_content = []
    @artifacts = []
    @fallback_references = []
    @presentation_budget = Artifacts::PresentationBudget.new
    delivery
  end
end