Class: Riffer::Tools::Runtime

Inherits:
Object
  • Object
show all
Defined in:
lib/riffer/tools/runtime.rb,
sig/generated/riffer/tools/runtime.rbs

Overview

Handles tool call execution for an agent, composing with a Riffer::Runner for concurrency. Subclass and override dispatch_tool_call to customize dispatch (e.g. HTTP, gRPC).

Direct Known Subclasses

Fibers, Inline, Threaded

Defined Under Namespace

Classes: Fibers, Inline, Threaded

Instance Method Summary collapse

Constructor Details

#initialize(runner:) ⇒ Runtime

-- : (runner: Riffer::Runner) -> void

Parameters:



14
15
16
17
# File 'lib/riffer/tools/runtime.rb', line 14

def initialize(runner:)
  raise NotImplementedError, "#{self.class} is abstract — use a subclass like Riffer::Tools::Runtime::Inline" if instance_of?(Riffer::Tools::Runtime)
  @runner = runner
end

Instance Method Details

#around_tool_call(tool_call, context:, assistant_message: nil) { ... } ⇒ Riffer::Tools::Response

Hook wrapping each tool call; override in subclasses to instrument or customize. Must yield to continue.

class InstrumentedRuntime < Riffer::Tools::Runtime::Inline
private

def around_tool_call(tool_call, context:, assistant_message: nil)
  start = Time.now
  result = yield
  Rails.logger.info("Tool #{tool_call.name} took #{Time.now - start}s")
  result
end
end

-- : (Riffer::Messages::Assistant::ToolCall, context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) { () -> Riffer::Tools::Response } -> Riffer::Tools::Response

Parameters:

Yields:

Yield Returns:

Returns:



55
56
57
# File 'lib/riffer/tools/runtime.rb', line 55

def around_tool_call(tool_call, context:, assistant_message: nil)
  yield
end

#capture_tool_arguments(span, tool_call) ⇒ void

This method returns an undefined value.

-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Messages::Assistant::ToolCall) -> void



151
152
153
154
155
156
# File 'lib/riffer/tools/runtime.rb', line 151

def capture_tool_arguments(span, tool_call)
  return unless capture_tool_content?(span)

  arguments = tool_call.arguments
  span.set_attribute("gen_ai.tool.call.arguments", arguments) if arguments
end

#capture_tool_content?(span) ⇒ Boolean

-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> bool

Parameters:

Returns:

  • (Boolean)


168
169
170
# File 'lib/riffer/tools/runtime.rb', line 168

def capture_tool_content?(span)
  Riffer.config.tracing.capture_messages && span.recording?
end

#capture_tool_result(span, result) ⇒ void

This method returns an undefined value.

-- : ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tools::Response) -> void



160
161
162
163
164
# File 'lib/riffer/tools/runtime.rb', line 160

def capture_tool_result(span, result)
  return unless capture_tool_content?(span)

  span.set_attribute("gen_ai.tool.call.result", result.content)
end

#dispatch_tool_call(tool_call, tools:, context:, assistant_message: nil) ⇒ Riffer::Tools::Response

-- : (Riffer::Messages::Assistant::ToolCall, tools: Array, context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) -> Riffer::Tools::Response

Parameters:

Returns:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/riffer/tools/runtime.rb', line 74

def dispatch_tool_call(tool_call, tools:, context:, assistant_message: nil)
  tool_class = tools.find { |tc| tc.name == tool_call.name }

  if tool_class.nil?
    return Riffer::Tools::Response.error(
      "Unknown tool '#{tool_call.name}'",
      type: :unknown_tool
    )
  end

  tool_instance = tool_class.new
  arguments = parse_arguments(tool_call.arguments)

  tool_instance.call_with_validation(context: context, **arguments)
rescue Riffer::TimeoutError => e
  Riffer::Tools::Response.error(e.message, type: :timeout_error)
rescue Riffer::ValidationError => e
  Riffer::Tools::Response.error(e.message, type: :validation_error)
rescue Riffer::ToolExecutionError => e
  Riffer::Tools::Response.error(e.message, type: :execution_error)
rescue RuntimeError => e
  Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
end

#execute(tool_calls, tools:, context:, assistant_message: nil, tags: {}) ⇒ Array[[ Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response ]]

Executes a batch of tool calls, returning [tool_call, response] pairs.

: (Array, tools: Array, context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?, ?tags: Hash[String, String]) -> Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]

Parameters:

Returns:



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/riffer/tools/runtime.rb', line 22

def execute(tool_calls, tools:, context:, assistant_message: nil, tags: {})
  # Each Runner worker runs in its own thread/fiber, where the OTEL context
  # starts empty — capture here so the execute_tool span parents correctly.
  # tags are an ordinary local captured in the block, so they reach the
  # worker's span without re-propagation.
  trace_context = Riffer::Tracing.current_context
  @runner.map(tool_calls, context: context) do |tool_call|
    Riffer::Tracing.with_context(trace_context) do
      instrument_tool_call(tool_call, tags) do
        around_tool_call(tool_call, context: context, assistant_message: assistant_message) do
          dispatch_tool_call(tool_call, tools: tools, context: context, assistant_message: assistant_message)
        end
      end
    end
  end
end

#in_tool_span(tool_call, tags = {}) ⇒ void

This method returns an undefined value.

Emitted outside around_tool_call so host enrichment spans nest beneath it.

: [R] (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span)) -> R } -> R



109
110
111
112
113
114
115
116
117
118
119
# File 'lib/riffer/tools/runtime.rb', line 109

def in_tool_span(tool_call, tags = {})
  Riffer::Tracing.in_span("execute_tool #{tool_call.name}", attributes: tool_span_attributes(tool_call, tags), kind: :internal) do |span|
    capture_tool_arguments(span, tool_call)
    yield span
  rescue => error
    # The backend records the exception and error status on the re-raise;
    # error.type is the one semconv attribute it doesn't set.
    span.set_attribute("error.type", error.class.name)
    raise
  end
end

#instrument_tool_call(tool_call, tags = {}) { ... } ⇒ [ Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response ]

-- : (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) { () -> Riffer::Tools::Response } -> [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]

Parameters:

Yields:

Yield Returns:

Returns:



63
64
65
66
67
68
69
70
# File 'lib/riffer/tools/runtime.rb', line 63

def instrument_tool_call(tool_call, tags = {})
  result = in_tool_span(tool_call, tags) do |span|
    response = yield
    record_tool_outcome(span, response)
    response
  end
  [tool_call, result] #: [Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]
end

#parse_arguments(arguments) ⇒ Hash[Symbol, untyped]

-- : (String?) -> Hash[Symbol, untyped]

Parameters:

  • (String, nil)

Returns:

  • (Hash[Symbol, untyped])


100
101
102
103
104
# File 'lib/riffer/tools/runtime.rb', line 100

def parse_arguments(arguments)
  return {} if arguments.nil? || arguments.empty?

  JSON.parse(arguments, symbolize_names: true)
end

#record_tool_outcome(span, result) ⇒ void

This method returns an undefined value.

A returned error Response is a handled outcome, so its status stays unset — an error span status is reserved for a raised exception.

: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tools::Response) -> void



143
144
145
146
147
# File 'lib/riffer/tools/runtime.rb', line 143

def record_tool_outcome(span, result)
  error_type = result.error_type
  span.set_attribute("error.type", error_type.to_s) if error_type
  capture_tool_result(span, result)
end

#tag_attributes(tags) ⇒ Hash[String, String]

Maps normalized tags to their namespaced span attribute form. An empty map yields an empty hash, so merging it is a no-op.

: (Hash[String, String]) -> Hash[String, String]

Parameters:

  • (Hash[String, String])

Returns:

  • (Hash[String, String])


135
136
137
# File 'lib/riffer/tools/runtime.rb', line 135

def tag_attributes(tags)
  tags.transform_keys { |key| "riffer.tag.#{key}" }
end

#tool_span_attributes(tool_call, tags = {}) ⇒ Hash[String, untyped]

-- : (Riffer::Messages::Assistant::ToolCall, ?Hash[String, String]) -> Hash[String, untyped]

Parameters:

Returns:

  • (Hash[String, untyped])


123
124
125
126
127
128
129
# File 'lib/riffer/tools/runtime.rb', line 123

def tool_span_attributes(tool_call, tags = {})
  {
    "gen_ai.operation.name" => "execute_tool",
    "gen_ai.tool.name" => tool_call.name,
    "gen_ai.tool.call.id" => tool_call.call_id
  }.merge(tag_attributes(tags))
end