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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6925b14509a50c3bbf5efb8d0e37f5a38b4d2f52f13f3c7ecd7af588f832c5c1
4
- data.tar.gz: 4332d03ebaf4bd94f3e314656e7f6755475c5ad45661a8c1e76e6e324d37ec2e
3
+ metadata.gz: 00477ed5ab4c97d2b28bb0b697288882744f1c377613224361233e0b3a145273
4
+ data.tar.gz: bac4d24f3599fbf7f3045e5ac41b1e85d047d74491b5dcc1dfb1c47cc85a5f60
5
5
  SHA512:
6
- metadata.gz: e12ea68a14b7c9ec683ae7b07a14d9d422922fc8e5233d7296fd42bc6100097bb929da87b6c52fe55929779a17c04e59253fa490c40147558392a32a79023fa6
7
- data.tar.gz: 9d0d5d249ee0bb6bac741d5c1087ade27ba0a6d032184bb01e1ecfc0d43f79453fbd12cd22e9e4f3eeca7b457bfb242ebca7ebc4e010f44387b9ee8a13bf0865
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
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "3.9.0"
7
+ VERSION = "3.10.0"
8
8
  end
9
9
  end
@@ -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.error?
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
@@ -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.9.0
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