ruby_llm-agents 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/models/ruby_llm/agents/execution.rb +4 -0
- data/app/models/ruby_llm/agents/tool_execution.rb +25 -0
- data/lib/ruby_llm/agents/base_agent.rb +2 -0
- data/lib/ruby_llm/agents/core/configuration.rb +2 -0
- data/lib/ruby_llm/agents/core/errors.rb +3 -0
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +1 -3
- data/lib/ruby_llm/agents/results/base.rb +15 -0
- data/lib/ruby_llm/agents/tool.rb +169 -0
- data/lib/ruby_llm/agents/tool_context.rb +71 -0
- data/lib/ruby_llm/agents.rb +4 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00477ed5ab4c97d2b28bb0b697288882744f1c377613224361233e0b3a145273
|
|
4
|
+
data.tar.gz: bac4d24f3599fbf7f3045e5ac41b1e85d047d74491b5dcc1dfb1c47cc85a5f60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29ac67e031dc46d3b0d19f91843e6b2e034f0246445a5ad1d6ce453a2782f41948a92947a3a6e36d177d381756858185f87aa9b07d723123e1af3914f66ab136
|
|
7
|
+
data.tar.gz: d479f5076373eea1f30d8c797cd885d0788c7a081860a72fd69ee5dc3f77631860577eab91c14c09dd7f9fda709da41de90fb74b8a626b9094145f071e19259b
|
|
@@ -74,6 +74,10 @@ module RubyLLM
|
|
|
74
74
|
has_one :detail, class_name: "RubyLLM::Agents::ExecutionDetail",
|
|
75
75
|
foreign_key: :execution_id, dependent: :destroy
|
|
76
76
|
|
|
77
|
+
# Individual tool call records (real-time tracking)
|
|
78
|
+
has_many :tool_executions, class_name: "RubyLLM::Agents::ToolExecution",
|
|
79
|
+
foreign_key: :execution_id, dependent: :destroy
|
|
80
|
+
|
|
77
81
|
# Delegations so existing code keeps working transparently
|
|
78
82
|
delegate :system_prompt, :user_prompt, :assistant_prompt, :response, :error_message,
|
|
79
83
|
:messages_summary, :tool_calls, :attempts, :fallback_chain,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Tracks individual tool calls within an agent execution.
|
|
6
|
+
#
|
|
7
|
+
# Created in real-time as each tool runs (INSERT on start, UPDATE on complete),
|
|
8
|
+
# enabling live dashboard views and queryable tool-level analytics.
|
|
9
|
+
#
|
|
10
|
+
# @example Querying tool executions
|
|
11
|
+
# execution.tool_executions.where(status: "error")
|
|
12
|
+
# ToolExecution.where(tool_name: "bash").where("duration_ms > ?", 10_000)
|
|
13
|
+
#
|
|
14
|
+
class ToolExecution < ::ActiveRecord::Base
|
|
15
|
+
self.table_name = "ruby_llm_agents_tool_executions"
|
|
16
|
+
|
|
17
|
+
VALID_STATUSES = %w[running success error timed_out cancelled].freeze
|
|
18
|
+
|
|
19
|
+
belongs_to :execution, class_name: "RubyLLM::Agents::Execution"
|
|
20
|
+
|
|
21
|
+
validates :tool_name, presence: true
|
|
22
|
+
validates :status, presence: true, inclusion: {in: VALID_STATUSES}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -730,6 +730,8 @@ module RubyLLM
|
|
|
730
730
|
capture_response(response, context)
|
|
731
731
|
result = build_result(process_response(response), response, context)
|
|
732
732
|
context.output = result
|
|
733
|
+
rescue RubyLLM::Agents::CancelledError
|
|
734
|
+
context.output = Result.new(content: nil, cancelled: true)
|
|
733
735
|
rescue RubyLLM::UnauthorizedError, RubyLLM::ForbiddenError => e
|
|
734
736
|
raise_with_setup_hint(e, context)
|
|
735
737
|
rescue RubyLLM::ModelNotFoundError => e
|
|
@@ -387,6 +387,7 @@ module RubyLLM
|
|
|
387
387
|
:default_total_timeout,
|
|
388
388
|
:default_streaming,
|
|
389
389
|
:default_tools,
|
|
390
|
+
:default_tool_timeout,
|
|
390
391
|
:default_thinking,
|
|
391
392
|
:on_alert,
|
|
392
393
|
:persist_prompts,
|
|
@@ -639,6 +640,7 @@ module RubyLLM
|
|
|
639
640
|
# Streaming, tools, and thinking defaults
|
|
640
641
|
@default_streaming = false
|
|
641
642
|
@default_tools = []
|
|
643
|
+
@default_tool_timeout = nil
|
|
642
644
|
@default_thinking = nil
|
|
643
645
|
|
|
644
646
|
# Governance defaults
|
|
@@ -29,6 +29,9 @@ module RubyLLM
|
|
|
29
29
|
# Raised when an execution cannot be replayed
|
|
30
30
|
class ReplayError < Error; end
|
|
31
31
|
|
|
32
|
+
# Raised when an agent execution is cancelled via on_cancelled
|
|
33
|
+
class CancelledError < Error; end
|
|
34
|
+
|
|
32
35
|
# Raised when the TTS API returns an error response
|
|
33
36
|
class SpeechApiError < Error
|
|
34
37
|
attr_reader :status, :response_body
|
|
@@ -124,7 +124,7 @@ module RubyLLM
|
|
|
124
124
|
tenant.record_execution!(
|
|
125
125
|
cost: context.total_cost || 0,
|
|
126
126
|
tokens: context.total_tokens || 0,
|
|
127
|
-
error: context.
|
|
127
|
+
error: context.failed?
|
|
128
128
|
)
|
|
129
129
|
return
|
|
130
130
|
end
|
|
@@ -146,8 +146,6 @@ module RubyLLM
|
|
|
146
146
|
tenant_id: context.tenant_id
|
|
147
147
|
)
|
|
148
148
|
end
|
|
149
|
-
rescue => e
|
|
150
|
-
error("Failed to record spend: #{e.message}", context)
|
|
151
149
|
end
|
|
152
150
|
end
|
|
153
151
|
end
|
|
@@ -112,6 +112,11 @@ module RubyLLM
|
|
|
112
112
|
# @return [String, nil] The agent class that produced this result
|
|
113
113
|
attr_reader :agent_class_name
|
|
114
114
|
|
|
115
|
+
# @!group Cancellation
|
|
116
|
+
# @!attribute [r] cancelled
|
|
117
|
+
# @return [Boolean] Whether the execution was cancelled
|
|
118
|
+
attr_reader :cancelled
|
|
119
|
+
|
|
115
120
|
# @!group Debug
|
|
116
121
|
# @!attribute [r] trace
|
|
117
122
|
# @return [Array<Hash>, nil] Pipeline trace entries (when debug: true)
|
|
@@ -173,6 +178,9 @@ module RubyLLM
|
|
|
173
178
|
# Tracking
|
|
174
179
|
@agent_class_name = options[:agent_class_name]
|
|
175
180
|
|
|
181
|
+
# Cancellation
|
|
182
|
+
@cancelled = options[:cancelled] || false
|
|
183
|
+
|
|
176
184
|
# Debug trace
|
|
177
185
|
@trace = options[:trace]
|
|
178
186
|
|
|
@@ -222,6 +230,13 @@ module RubyLLM
|
|
|
222
230
|
!success?
|
|
223
231
|
end
|
|
224
232
|
|
|
233
|
+
# Returns whether the execution was cancelled
|
|
234
|
+
#
|
|
235
|
+
# @return [Boolean] true if cancelled
|
|
236
|
+
def cancelled?
|
|
237
|
+
cancelled == true
|
|
238
|
+
end
|
|
239
|
+
|
|
225
240
|
# Returns whether a fallback model was used
|
|
226
241
|
#
|
|
227
242
|
# @return [Boolean] true if chosen_model_id differs from model_id
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Agents
|
|
7
|
+
# Base class for tools that need access to the agent's execution context.
|
|
8
|
+
#
|
|
9
|
+
# Inherits from RubyLLM::Tool and adds:
|
|
10
|
+
# - `context` accessor: read agent params, tenant, execution ID
|
|
11
|
+
# - `timeout` DSL: per-tool timeout in seconds
|
|
12
|
+
# - Error handling: exceptions become error strings for the LLM
|
|
13
|
+
# - Tool execution tracking: records each tool call in the database
|
|
14
|
+
#
|
|
15
|
+
# Users implement `execute()` — the standard RubyLLM convention.
|
|
16
|
+
# This class overrides `call()` to wrap execution with its features.
|
|
17
|
+
#
|
|
18
|
+
# @example Defining a tool
|
|
19
|
+
# class BashTool < RubyLLM::Agents::Tool
|
|
20
|
+
# description "Run a shell command"
|
|
21
|
+
# timeout 30
|
|
22
|
+
#
|
|
23
|
+
# param :command, desc: "The command to run", required: true
|
|
24
|
+
#
|
|
25
|
+
# def execute(command:)
|
|
26
|
+
# context.container_id # reads agent param
|
|
27
|
+
# # ... run command ...
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @example Using with an agent
|
|
32
|
+
# class CodingAgent < ApplicationAgent
|
|
33
|
+
# param :container_id, required: true
|
|
34
|
+
# tools [BashTool]
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# CodingAgent.call(query: "list files", container_id: "abc123")
|
|
38
|
+
#
|
|
39
|
+
class Tool < RubyLLM::Tool
|
|
40
|
+
# The execution context, set before each call.
|
|
41
|
+
# Provides access to agent params, tenant, execution ID.
|
|
42
|
+
#
|
|
43
|
+
# @return [ToolContext, nil]
|
|
44
|
+
attr_reader :context
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# Sets or gets the per-tool timeout in seconds.
|
|
48
|
+
#
|
|
49
|
+
# @param value [Integer, nil] Timeout in seconds (setter)
|
|
50
|
+
# @return [Integer, nil] The configured timeout (getter)
|
|
51
|
+
def timeout(value = nil)
|
|
52
|
+
if value
|
|
53
|
+
@timeout = value
|
|
54
|
+
else
|
|
55
|
+
@timeout
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Wraps RubyLLM's call() with context, timeout, tracking, and error handling.
|
|
61
|
+
#
|
|
62
|
+
# RubyLLM's Chat calls tool.call(args) during the tool loop.
|
|
63
|
+
# We set up context, create a tracking record, apply timeout,
|
|
64
|
+
# then delegate to super (which validates args and calls execute).
|
|
65
|
+
#
|
|
66
|
+
# @param args [Hash] Tool arguments from the LLM
|
|
67
|
+
# @return [String, Tool::Halt] The tool result or a Halt signal
|
|
68
|
+
def call(args)
|
|
69
|
+
pipeline_context = Thread.current[:ruby_llm_agents_caller_context]
|
|
70
|
+
@context = pipeline_context ? ToolContext.new(pipeline_context) : nil
|
|
71
|
+
|
|
72
|
+
record = start_tool_tracking(pipeline_context, args)
|
|
73
|
+
|
|
74
|
+
check_cancelled!(pipeline_context)
|
|
75
|
+
|
|
76
|
+
timeout_seconds = self.class.timeout
|
|
77
|
+
timeout_seconds ||= RubyLLM::Agents.configuration.default_tool_timeout
|
|
78
|
+
|
|
79
|
+
result = if timeout_seconds
|
|
80
|
+
Timeout.timeout(timeout_seconds) { super }
|
|
81
|
+
else
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
complete_tool_tracking(record, result, status: "success")
|
|
86
|
+
result
|
|
87
|
+
rescue Timeout::Error
|
|
88
|
+
complete_tool_tracking(record, nil, status: "timed_out", error: "Timed out after #{timeout_seconds}s")
|
|
89
|
+
"TIMEOUT: Tool did not complete within #{timeout_seconds}s."
|
|
90
|
+
rescue RubyLLM::Agents::CancelledError
|
|
91
|
+
complete_tool_tracking(record, nil, status: "cancelled")
|
|
92
|
+
raise # Let cancellation propagate to BaseAgent
|
|
93
|
+
rescue => e
|
|
94
|
+
complete_tool_tracking(record, nil, status: "error", error: e.message)
|
|
95
|
+
"ERROR (#{e.class}): #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Creates a "running" ToolExecution record before the tool runs.
|
|
101
|
+
# Silently skips if no execution_id or ToolExecution is not available.
|
|
102
|
+
#
|
|
103
|
+
# @param pipeline_context [Pipeline::Context, nil]
|
|
104
|
+
# @param args [Hash] The tool arguments
|
|
105
|
+
# @return [ToolExecution, nil]
|
|
106
|
+
def start_tool_tracking(pipeline_context, args)
|
|
107
|
+
return nil unless pipeline_context&.execution_id
|
|
108
|
+
return nil unless defined?(ToolExecution)
|
|
109
|
+
|
|
110
|
+
@tool_iteration = (@tool_iteration || 0) + 1
|
|
111
|
+
|
|
112
|
+
ToolExecution.create!(
|
|
113
|
+
execution_id: pipeline_context.execution_id,
|
|
114
|
+
tool_name: name,
|
|
115
|
+
iteration: @tool_iteration,
|
|
116
|
+
status: "running",
|
|
117
|
+
input: normalize_input(args),
|
|
118
|
+
started_at: Time.current
|
|
119
|
+
)
|
|
120
|
+
rescue => e
|
|
121
|
+
# Don't let tracking failures break tool execution
|
|
122
|
+
Rails.logger.debug("[RubyLLM::Agents::Tool] Tracking failed: #{e.message}") if defined?(Rails) && Rails.logger
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Updates the ToolExecution record after the tool completes.
|
|
127
|
+
#
|
|
128
|
+
# @param record [ToolExecution, nil]
|
|
129
|
+
# @param result [Object, nil] The tool result
|
|
130
|
+
# @param status [String] Final status
|
|
131
|
+
# @param error [String, nil] Error message
|
|
132
|
+
def complete_tool_tracking(record, result, status:, error: nil)
|
|
133
|
+
return unless record
|
|
134
|
+
|
|
135
|
+
completed_at = Time.current
|
|
136
|
+
duration_ms = record.started_at ? ((completed_at - record.started_at) * 1000).to_i : nil
|
|
137
|
+
output_str = result.is_a?(RubyLLM::Tool::Halt) ? result.content.to_s : result.to_s
|
|
138
|
+
|
|
139
|
+
record.update!(
|
|
140
|
+
status: status,
|
|
141
|
+
output: truncate_output(output_str),
|
|
142
|
+
output_bytes: output_str.bytesize,
|
|
143
|
+
error_message: error,
|
|
144
|
+
completed_at: completed_at,
|
|
145
|
+
duration_ms: duration_ms
|
|
146
|
+
)
|
|
147
|
+
rescue => e
|
|
148
|
+
Rails.logger.debug("[RubyLLM::Agents::Tool] Tracking update failed: #{e.message}") if defined?(Rails) && Rails.logger
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def check_cancelled!(pipeline_context)
|
|
152
|
+
return unless pipeline_context
|
|
153
|
+
on_cancelled = pipeline_context[:on_cancelled]
|
|
154
|
+
return unless on_cancelled.respond_to?(:call)
|
|
155
|
+
raise CancelledError, "Execution cancelled" if on_cancelled.call
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def normalize_input(args)
|
|
159
|
+
return {} if args.nil?
|
|
160
|
+
args.respond_to?(:to_h) ? args.to_h : {}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def truncate_output(str)
|
|
164
|
+
max = RubyLLM::Agents.configuration.try(:tool_result_max_length) || 10_000
|
|
165
|
+
(str.length > max) ? str[0, max] + "... (truncated)" : str
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Read-only wrapper around Pipeline::Context for tool authors.
|
|
6
|
+
#
|
|
7
|
+
# Exposes agent params and execution metadata to tools without
|
|
8
|
+
# leaking pipeline internals. Supports both method-style and
|
|
9
|
+
# hash-style access to agent params.
|
|
10
|
+
#
|
|
11
|
+
# @example Method-style access
|
|
12
|
+
# context.container_id # reads agent param
|
|
13
|
+
# context.tenant_id # fixed attribute
|
|
14
|
+
#
|
|
15
|
+
# @example Hash-style access
|
|
16
|
+
# context[:container_id]
|
|
17
|
+
#
|
|
18
|
+
class ToolContext
|
|
19
|
+
# Execution record ID — links tool calls to the agent execution
|
|
20
|
+
#
|
|
21
|
+
# @return [Integer, nil]
|
|
22
|
+
def id
|
|
23
|
+
@ctx.execution_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Tenant ID from the pipeline context
|
|
27
|
+
#
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
def tenant_id
|
|
30
|
+
@ctx.tenant_id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Agent class name
|
|
34
|
+
#
|
|
35
|
+
# @return [String, nil]
|
|
36
|
+
def agent_type
|
|
37
|
+
@ctx.agent_class&.name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Hash-style access to agent params
|
|
41
|
+
#
|
|
42
|
+
# @param key [Symbol, String] The param key
|
|
43
|
+
# @return [Object, nil]
|
|
44
|
+
def [](key)
|
|
45
|
+
@agent_options[key.to_sym] || @agent_options[key.to_s]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(pipeline_context)
|
|
49
|
+
@ctx = pipeline_context
|
|
50
|
+
@agent_options = @ctx.agent_instance&.send(:options) || {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Method-style access to agent params
|
|
56
|
+
def method_missing(method_name, *args)
|
|
57
|
+
key = method_name.to_sym
|
|
58
|
+
if @agent_options.key?(key) || @agent_options.key?(key.to_s)
|
|
59
|
+
self[key]
|
|
60
|
+
else
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
66
|
+
key = method_name.to_sym
|
|
67
|
+
@agent_options.key?(key) || @agent_options.key?(key.to_s) || super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/ruby_llm/agents.rb
CHANGED
|
@@ -26,6 +26,10 @@ require_relative "agents/base_agent"
|
|
|
26
26
|
# Agent-as-Tool adapter
|
|
27
27
|
require_relative "agents/agent_tool"
|
|
28
28
|
|
|
29
|
+
# Tool base class and context for coding agents
|
|
30
|
+
require_relative "agents/tool_context"
|
|
31
|
+
require_relative "agents/tool"
|
|
32
|
+
|
|
29
33
|
# Infrastructure - Budget & Utilities
|
|
30
34
|
require_relative "agents/infrastructure/circuit_breaker"
|
|
31
35
|
require_relative "agents/infrastructure/budget_tracker"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- adham90
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- app/models/ruby_llm/agents/tenant/resettable.rb
|
|
99
99
|
- app/models/ruby_llm/agents/tenant/trackable.rb
|
|
100
100
|
- app/models/ruby_llm/agents/tenant_budget.rb
|
|
101
|
+
- app/models/ruby_llm/agents/tool_execution.rb
|
|
101
102
|
- app/services/ruby_llm/agents/agent_registry.rb
|
|
102
103
|
- app/views/layouts/ruby_llm/agents/application.html.erb
|
|
103
104
|
- app/views/ruby_llm/agents/agents/_config_agent.html.erb
|
|
@@ -326,6 +327,8 @@ files:
|
|
|
326
327
|
- lib/ruby_llm/agents/routing/class_methods.rb
|
|
327
328
|
- lib/ruby_llm/agents/routing/result.rb
|
|
328
329
|
- lib/ruby_llm/agents/text/embedder.rb
|
|
330
|
+
- lib/ruby_llm/agents/tool.rb
|
|
331
|
+
- lib/ruby_llm/agents/tool_context.rb
|
|
329
332
|
- lib/ruby_llm/agents/track_report.rb
|
|
330
333
|
- lib/ruby_llm/agents/tracker.rb
|
|
331
334
|
- lib/tasks/ruby_llm_agents.rake
|