soka 0.0.1.beta2

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +365 -0
  4. data/CHANGELOG.md +31 -0
  5. data/CLAUDE.md +213 -0
  6. data/LICENSE +21 -0
  7. data/README.md +650 -0
  8. data/Rakefile +10 -0
  9. data/examples/1_basic.rb +94 -0
  10. data/examples/2_event_handling.rb +120 -0
  11. data/examples/3_memory.rb +182 -0
  12. data/examples/4_hooks.rb +140 -0
  13. data/examples/5_error_handling.rb +85 -0
  14. data/examples/6_retry.rb +164 -0
  15. data/examples/7_tool_conditional.rb +180 -0
  16. data/examples/8_multi_provider.rb +112 -0
  17. data/lib/soka/agent.rb +130 -0
  18. data/lib/soka/agent_tool.rb +146 -0
  19. data/lib/soka/agent_tools/params_validator.rb +139 -0
  20. data/lib/soka/agents/dsl_methods.rb +140 -0
  21. data/lib/soka/agents/hook_manager.rb +68 -0
  22. data/lib/soka/agents/llm_builder.rb +32 -0
  23. data/lib/soka/agents/retry_handler.rb +74 -0
  24. data/lib/soka/agents/tool_builder.rb +78 -0
  25. data/lib/soka/configuration.rb +60 -0
  26. data/lib/soka/engines/base.rb +67 -0
  27. data/lib/soka/engines/concerns/prompt_template.rb +130 -0
  28. data/lib/soka/engines/concerns/response_processor.rb +103 -0
  29. data/lib/soka/engines/react.rb +136 -0
  30. data/lib/soka/engines/reasoning_context.rb +92 -0
  31. data/lib/soka/llm.rb +85 -0
  32. data/lib/soka/llms/anthropic.rb +124 -0
  33. data/lib/soka/llms/base.rb +114 -0
  34. data/lib/soka/llms/concerns/response_parser.rb +47 -0
  35. data/lib/soka/llms/concerns/streaming_handler.rb +78 -0
  36. data/lib/soka/llms/gemini.rb +106 -0
  37. data/lib/soka/llms/openai.rb +97 -0
  38. data/lib/soka/memory.rb +83 -0
  39. data/lib/soka/result.rb +136 -0
  40. data/lib/soka/test_helpers.rb +162 -0
  41. data/lib/soka/thoughts_memory.rb +112 -0
  42. data/lib/soka/version.rb +5 -0
  43. data/lib/soka.rb +49 -0
  44. data/sig/soka.rbs +4 -0
  45. metadata +158 -0
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'soka'
6
+ require 'dotenv/load'
7
+
8
+ # Configure Soka
9
+ Soka.setup do |config|
10
+ config.ai do |ai|
11
+ ai.provider = :gemini
12
+ ai.model = 'gemini-2.5-flash-lite'
13
+ ai.api_key = ENV.fetch('GEMINI_API_KEY', nil)
14
+ end
15
+ end
16
+
17
+ # Flaky API tool that fails randomly
18
+ class FlakyApiTool < Soka::AgentTool
19
+ desc 'Call unreliable API'
20
+
21
+ params do
22
+ requires :action, String, desc: 'Action to perform'
23
+ end
24
+
25
+ def initialize
26
+ super
27
+ @call_count = 0
28
+ end
29
+
30
+ def call(action:)
31
+ @call_count += 1
32
+ puts " šŸ”„ API call attempt ##{@call_count} for: #{action}"
33
+
34
+ # Fail 60% of the time on first 2 attempts
35
+ if @call_count <= 2 && rand < 0.6
36
+ raise StandardError, "Network timeout on attempt #{@call_count}"
37
+ end
38
+
39
+ "Success: #{action} completed after #{@call_count} attempts"
40
+ end
41
+ end
42
+
43
+ # Rate limited tool
44
+ class RateLimitedTool < Soka::AgentTool
45
+ desc 'API with rate limiting'
46
+
47
+ params do
48
+ requires :query, String, desc: 'Query to execute'
49
+ end
50
+
51
+ def initialize
52
+ super
53
+ @last_call = Time.now - 10
54
+ end
55
+
56
+ def call(query:)
57
+ time_since_last = Time.now - @last_call
58
+
59
+ if time_since_last < 1
60
+ raise StandardError, 'Rate limit exceeded. Please wait 1 second between calls.'
61
+ end
62
+
63
+ @last_call = Time.now
64
+ "Query result for: #{query}"
65
+ end
66
+ end
67
+
68
+ # Agent with retry configuration
69
+ class RetryAgent < Soka::Agent
70
+ tool FlakyApiTool
71
+ tool RateLimitedTool
72
+
73
+ # The framework has built-in retry with exponential backoff
74
+ end
75
+
76
+ # Agent with custom configuration
77
+ class CustomRetryAgent < Soka::Agent
78
+ tool FlakyApiTool
79
+
80
+ # Configure max iterations (which affects retry behavior)
81
+ max_iterations 2
82
+ end
83
+
84
+ # Main program
85
+ puts '=== Soka Retry Example ==='
86
+ puts "Demonstrating retry mechanisms\n\n"
87
+
88
+ # Example 1: Basic retry with exponential backoff
89
+ puts 'Example 1: Retry with exponential backoff'
90
+ puts '-' * 50
91
+
92
+ agent = RetryAgent.new
93
+ puts "Calling flaky API (may fail and retry)..."
94
+
95
+ result = agent.run('Call the flaky API to fetch user data')
96
+ puts "\nFinal result: #{result.final_answer}"
97
+ puts "Success after retries: #{!result.failed?}"
98
+
99
+ puts "\n" + '=' * 50 + "\n"
100
+
101
+ # Example 2: Simple retry configuration
102
+ puts 'Example 2: Simple retry configuration'
103
+ puts '-' * 50
104
+
105
+ simple_agent = CustomRetryAgent.new
106
+ puts "Using simple retry (max 2 attempts)..."
107
+
108
+ # Reset the tool's call count
109
+ simple_agent.tools.first.instance_variable_set(:@call_count, 0)
110
+
111
+ result = simple_agent.run('Perform critical operation')
112
+ puts "\nResult: #{result.final_answer}"
113
+
114
+ puts "\n" + '=' * 50 + "\n"
115
+
116
+ # Example 3: Rate limiting with retry
117
+ puts 'Example 3: Handling rate limits'
118
+ puts '-' * 50
119
+
120
+ agent = RetryAgent.new
121
+ puts "Making rapid API calls (will hit rate limit)..."
122
+
123
+ queries = ['Query 1', 'Query 2', 'Query 3']
124
+ queries.each do |query|
125
+ puts "\nExecuting: #{query}"
126
+ result = agent.run("Use the rate limited API for: #{query}")
127
+ puts "Result: #{result.final_answer[0..50]}..."
128
+ sleep(0.5) # Small delay between queries
129
+ end
130
+
131
+ puts "\n" + '=' * 50 + "\n"
132
+
133
+ # Example 4: No retry configuration
134
+ puts 'Example 4: Agent without retry (for comparison)'
135
+ puts '-' * 50
136
+
137
+ class NoRetryAgent < Soka::Agent
138
+ tool FlakyApiTool
139
+ end
140
+
141
+ no_retry_agent = NoRetryAgent.new
142
+ puts "Calling flaky API without retry..."
143
+
144
+ begin
145
+ # Reset call count
146
+ no_retry_agent.tools.first.instance_variable_set(:@call_count, 0)
147
+ result = no_retry_agent.run('Try once without retry')
148
+ puts "Success on first try!"
149
+ rescue => e
150
+ puts "Failed immediately: #{e.message}"
151
+ end
152
+
153
+ puts "\n=== Retry Benefits ==="
154
+ puts "1. Handles transient network failures"
155
+ puts "2. Deals with rate limiting gracefully"
156
+ puts "3. Improves reliability of AI agents"
157
+ puts "4. Configurable backoff strategies"
158
+ puts "5. Selective retry based on error types"
159
+
160
+ puts "\n=== Retry Strategies ==="
161
+ puts "1. Fixed delay: Wait same time between retries"
162
+ puts "2. Exponential backoff: Increasing delays (1s, 2s, 4s...)"
163
+ puts "3. Linear backoff: Linear increase (1s, 2s, 3s...)"
164
+ puts "4. Custom logic: Define your own retry behavior"
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'soka'
6
+ require 'dotenv/load'
7
+
8
+ # Configure Soka
9
+ Soka.setup do |config|
10
+ config.ai do |ai|
11
+ ai.provider = :gemini
12
+ ai.model = 'gemini-2.5-flash-lite'
13
+ ai.api_key = ENV.fetch('GEMINI_API_KEY', nil)
14
+ end
15
+ end
16
+
17
+ # Development-only debugging tool
18
+ class DebugTool < Soka::AgentTool
19
+ desc 'Debug internal state (dev only)'
20
+
21
+ def call
22
+ "Debug info: Memory size=#{@agent&.memory&.messages&.size || 0}, Time=#{Time.now}"
23
+ end
24
+ end
25
+
26
+ # Production monitoring tool
27
+ class MonitoringTool < Soka::AgentTool
28
+ desc 'Monitor system metrics'
29
+
30
+ def call
31
+ "System metrics: CPU=#{rand(10..90)}%, Memory=#{rand(30..70)}%, Uptime=#{rand(1..100)}h"
32
+ end
33
+ end
34
+
35
+ # Admin-only tool
36
+ class AdminTool < Soka::AgentTool
37
+ desc 'Perform admin operations'
38
+
39
+ params do
40
+ requires :operation, String, desc: 'Admin operation to perform'
41
+ end
42
+
43
+ def call(operation:)
44
+ "Admin operation '#{operation}' completed successfully"
45
+ end
46
+ end
47
+
48
+ # Feature flag tool
49
+ class FeatureFlagTool < Soka::AgentTool
50
+ desc 'Check feature flags'
51
+
52
+ params do
53
+ requires :flag, String, desc: 'Feature flag name'
54
+ end
55
+
56
+ def call(flag:)
57
+ # Simulate feature flag service
58
+ enabled = %w[new_ui dark_mode beta_features].include?(flag)
59
+ "Feature '#{flag}' is #{enabled ? 'enabled' : 'disabled'}"
60
+ end
61
+ end
62
+
63
+ # Environment-aware agent
64
+ class ConditionalAgent < Soka::Agent
65
+ # Always available tools
66
+ tool FeatureFlagTool
67
+
68
+ # Conditional tools based on environment
69
+ tool DebugTool, if: -> { ENV['ENVIRONMENT'] == 'development' }
70
+ tool MonitoringTool, if: -> { %w[staging production].include?(ENV['ENVIRONMENT']) }
71
+ tool AdminTool, if: -> { ENV['USER_ROLE'] == 'admin' }
72
+ end
73
+
74
+ # Dynamic condition agent
75
+ class DynamicConditionAgent < Soka::Agent
76
+ # Load tool based on time of day
77
+ tool DebugTool, if: -> { Time.now.hour.between?(9, 17) } # Business hours only
78
+
79
+ # Load based on environment variable
80
+ tool MonitoringTool, if: -> { ENV['ENABLE_MONITORING'] == 'true' }
81
+
82
+ # Multiple conditions
83
+ tool AdminTool, if: -> { ENV['USER_ROLE'] == 'admin' && ENV['SECURE_MODE'] == 'true' }
84
+ end
85
+
86
+ # Main program
87
+ puts '=== Soka Conditional Tool Example ==='
88
+ puts "Demonstrating conditional tool loading\n\n"
89
+
90
+ # Example 1: Environment-based tools
91
+ puts 'Example 1: Environment-based tool loading'
92
+ puts '-' * 50
93
+
94
+ environments = %w[development staging production]
95
+
96
+ environments.each do |env|
97
+ ENV['ENVIRONMENT'] = env
98
+ agent = ConditionalAgent.new
99
+
100
+ puts "\nEnvironment: #{env}"
101
+ puts "Loaded tools: #{agent.tools.map { |t| t.class.name }.join(', ')}"
102
+
103
+ # Try to use monitoring
104
+ result = agent.run('Check system monitoring metrics')
105
+ puts "Result: #{result.final_answer}"
106
+ end
107
+
108
+ puts "\n" + '=' * 50 + "\n"
109
+
110
+ # Example 2: Role-based tools
111
+ puts 'Example 2: Role-based tool access'
112
+ puts '-' * 50
113
+
114
+ roles = %w[user admin guest]
115
+
116
+ roles.each do |role|
117
+ ENV['USER_ROLE'] = role
118
+ ENV['ENVIRONMENT'] = 'production'
119
+ agent = ConditionalAgent.new
120
+
121
+ puts "\nUser role: #{role}"
122
+ puts "Available tools: #{agent.tools.map { |t| t.class.name }.join(', ')}"
123
+
124
+ if role == 'admin'
125
+ result = agent.run('Perform admin operation: restart_service')
126
+ puts "Admin result: #{result.final_answer}"
127
+ else
128
+ puts "Admin tools not available for #{role} role"
129
+ end
130
+ end
131
+
132
+ puts "\n" + '=' * 50 + "\n"
133
+
134
+ # Example 3: Dynamic conditions
135
+ puts 'Example 3: Dynamic condition checking'
136
+ puts '-' * 50
137
+
138
+ # Set up dynamic conditions
139
+ ENV['ENABLE_MONITORING'] = 'true'
140
+ ENV['USER_ROLE'] = 'admin'
141
+ ENV['SECURE_MODE'] = 'true'
142
+
143
+ dynamic_agent = DynamicConditionAgent.new
144
+ puts "Current time: #{Time.now}"
145
+ puts "Business hours tool available: #{Time.now.hour.between?(9, 17)}"
146
+ puts "Monitoring enabled: #{ENV['ENABLE_MONITORING']}"
147
+ puts "Admin with secure mode: #{ENV['USER_ROLE'] == 'admin' && ENV['SECURE_MODE'] == 'true'}"
148
+ puts "\nLoaded tools: #{dynamic_agent.tools.map { |t| t.class.name }.join(', ')}"
149
+
150
+ result = dynamic_agent.run('List all available debugging options')
151
+ puts "\nResult: #{result.final_answer}"
152
+
153
+ puts "\n" + '=' * 50 + "\n"
154
+
155
+ # Example 4: Feature flags
156
+ puts 'Example 4: Feature flag integration'
157
+ puts '-' * 50
158
+
159
+ agent = ConditionalAgent.new
160
+ features = %w[new_ui dark_mode legacy_support beta_features]
161
+
162
+ puts "Checking feature flags:"
163
+ features.each do |feature|
164
+ result = agent.run("Check if feature flag '#{feature}' is enabled")
165
+ puts "- #{feature}: #{result.final_answer.include?('enabled') ? 'āœ“' : 'āœ—'}"
166
+ end
167
+
168
+ puts "\n=== Conditional Tool Benefits ==="
169
+ puts "1. Environment-specific functionality"
170
+ puts "2. Role-based access control"
171
+ puts "3. Feature flag integration"
172
+ puts "4. Resource optimization"
173
+ puts "5. Security through tool isolation"
174
+
175
+ puts "\n=== Use Cases ==="
176
+ puts "1. Debug tools only in development"
177
+ puts "2. Admin tools for privileged users"
178
+ puts "3. Monitoring in production only"
179
+ puts "4. Beta features behind flags"
180
+ puts "5. Time-based tool availability"
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'soka'
6
+ require 'dotenv/load'
7
+
8
+ # Configure Soka with all providers
9
+ Soka.setup do |config|
10
+ # Default configuration can be overridden per agent
11
+ config.ai do |ai|
12
+ ai.provider = :gemini
13
+ ai.model = 'gemini-2.5-flash-lite'
14
+ end
15
+ end
16
+
17
+ # Simple analysis tool
18
+ class AnalysisTool < Soka::AgentTool
19
+ desc 'Analyze text or data'
20
+
21
+ params do
22
+ requires :content, String, desc: 'Content to analyze'
23
+ end
24
+
25
+ def call(content:)
26
+ word_count = content.split.size
27
+ char_count = content.length
28
+ "Analysis: #{word_count} words, #{char_count} characters"
29
+ end
30
+ end
31
+
32
+ # Gemini Agent
33
+ class GeminiAgent < Soka::Agent
34
+ provider :gemini
35
+ model 'gemini-2.5-flash-lite'
36
+ api_key ENV.fetch('GEMINI_API_KEY', nil)
37
+
38
+ tool AnalysisTool
39
+ end
40
+
41
+ # OpenAI Agent
42
+ class OpenAIAgent < Soka::Agent
43
+ provider :openai
44
+ model 'gpt-4o-mini'
45
+ api_key ENV.fetch('OPENAI_API_KEY', nil)
46
+
47
+ tool AnalysisTool
48
+ end
49
+
50
+ # Anthropic Agent
51
+ class AnthropicAgent < Soka::Agent
52
+ provider :anthropic
53
+ model 'claude-3-5-haiku-latest'
54
+ api_key ENV.fetch('ANTHROPIC_API_KEY', nil)
55
+
56
+ tool AnalysisTool
57
+ end
58
+
59
+ # Main program
60
+ puts '=== Soka Multi-Provider Example ==='
61
+ puts "Demonstrating multiple LLM providers\n\n"
62
+
63
+ # Example 1: Different providers for same task
64
+ puts 'Example 1: Same task with different providers'
65
+ puts '-' * 50
66
+
67
+ task = 'Analyze this text: "Soka is a Ruby framework for building AI agents"'
68
+
69
+ # Try with Gemini
70
+ if ENV['GEMINI_API_KEY']
71
+ puts "\nšŸ”· Using Gemini:"
72
+ gemini_agent = GeminiAgent.new
73
+ result = gemini_agent.run(task)
74
+ puts "Response: #{result.final_answer}"
75
+ puts "Provider: Gemini (#{gemini_agent.llm.model})"
76
+ else
77
+ puts "\nšŸ”· Gemini: Skipped (no API key)"
78
+ end
79
+
80
+ # Try with OpenAI
81
+ if ENV['OPENAI_API_KEY']
82
+ puts "\n🟧 Using OpenAI:"
83
+ openai_agent = OpenAIAgent.new
84
+ result = openai_agent.run(task)
85
+ puts "Response: #{result.final_answer}"
86
+ puts "Provider: OpenAI (#{openai_agent.llm.model})"
87
+ else
88
+ puts "\n🟧 OpenAI: Skipped (no API key)"
89
+ end
90
+
91
+ # Try with Anthropic
92
+ if ENV['ANTHROPIC_API_KEY']
93
+ puts "\nšŸ”¶ Using Anthropic:"
94
+ anthropic_agent = AnthropicAgent.new
95
+ result = anthropic_agent.run(task)
96
+ puts "Response: #{result.final_answer}"
97
+ puts "Provider: Anthropic (#{anthropic_agent.llm.model})"
98
+ else
99
+ puts "\nšŸ”¶ Anthropic: Skipped (no API key)"
100
+ end
101
+
102
+ puts "\n=== Multi-Provider Benefits ==="
103
+ puts '1. Flexibility to choose best model for each task'
104
+ puts '2. Cost optimization (use cheaper models when appropriate)'
105
+ puts '3. Fallback options for reliability'
106
+ puts '4. Access to provider-specific features'
107
+ puts '5. Compare outputs across different models'
108
+
109
+ puts "\n=== Provider Comparison ==="
110
+ puts 'Gemini: Fast, cost-effective, good for general tasks'
111
+ puts 'OpenAI: Powerful GPT models, great for complex reasoning'
112
+ puts 'Anthropic: Claude models, excellent for analysis and writing'
data/lib/soka/agent.rb ADDED
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Soka
4
+ # Base class for AI agents that use ReAct pattern
5
+ class Agent
6
+ include Agents::RetryHandler
7
+ include Agents::ToolBuilder
8
+ include Agents::HookManager
9
+ include Agents::DSLMethods
10
+ include Agents::LLMBuilder
11
+
12
+ attr_reader :llm, :tools, :memory, :thoughts_memory, :engine
13
+
14
+ # Initialize a new Agent instance
15
+ # @param memory [Memory, Array, nil] The memory instance to use (defaults to new Memory)
16
+ # Can be a Memory instance or an Array of message hashes
17
+ # @param engine [Class] The engine class to use (defaults to Engine::React)
18
+ # @param options [Hash] Configuration options
19
+ # @option options [Integer] :max_iterations Maximum iterations for reasoning
20
+ # @option options [Integer] :timeout Timeout in seconds for operations
21
+ # @option options [Symbol] :provider LLM provider override
22
+ # @option options [String] :model LLM model override
23
+ # @option options [String] :api_key LLM API key override
24
+ def initialize(memory: nil, engine: Engines::React, **options)
25
+ @memory = initialize_memory(memory)
26
+ @thoughts_memory = ThoughtsMemory.new
27
+ @engine = engine
28
+
29
+ # Initialize components
30
+ @llm = build_llm(options)
31
+ @tools = build_tools
32
+
33
+ # Apply configuration with clear defaults
34
+ apply_configuration(options)
35
+ end
36
+
37
+ # Apply configuration options with defaults
38
+ # @param options [Hash] Configuration options
39
+ def apply_configuration(options)
40
+ @max_iterations = options.fetch(:max_iterations) { self.class._max_iterations || 10 }
41
+ @timeout = options.fetch(:timeout) { self.class._timeout || 30 }
42
+ end
43
+
44
+ # Run the agent with the given input
45
+ # @param input [String] The input query or task
46
+ # @yield [event] Optional block to handle events during execution
47
+ # @return [Result] The result of the agent's reasoning
48
+ def run(input, &)
49
+ validate_input(input)
50
+ execute_reasoning(input, &)
51
+ rescue ArgumentError
52
+ raise # Re-raise ArgumentError without handling
53
+ rescue StandardError => e
54
+ handle_error(e, input)
55
+ end
56
+
57
+ private
58
+
59
+ # Initialize memory from various input formats
60
+ # @param memory [Memory, Array, nil] The memory input
61
+ # @return [Memory] The initialized memory instance
62
+ def initialize_memory(memory)
63
+ case memory
64
+ when Memory
65
+ memory
66
+ when Array
67
+ Memory.new(memory)
68
+ when nil
69
+ Memory.new
70
+ else
71
+ raise ArgumentError, "Invalid memory type: #{memory.class}. Expected Memory, Array, or nil"
72
+ end
73
+ end
74
+
75
+ # Validate the input is not empty
76
+ # @param input [String] The input to validate
77
+ # @raise [ArgumentError] If input is empty
78
+ def validate_input(input)
79
+ raise ArgumentError, 'Input cannot be empty' if input.to_s.strip.empty?
80
+ end
81
+
82
+ # Execute the reasoning process with hooks
83
+ # @param input [String] The input query
84
+ # @yield [event] Optional block to handle events
85
+ # @return [Result] The reasoning result
86
+ def execute_reasoning(input, &)
87
+ run_hooks(:before_action, input)
88
+
89
+ engine_result = perform_reasoning(input, &)
90
+ result = convert_engine_result(engine_result)
91
+
92
+ finalize_result(input, result)
93
+ result
94
+ end
95
+
96
+ # Perform the actual reasoning using the engine
97
+ # @param input [String] The input query
98
+ # @yield [event] Optional block to handle events
99
+ # @return [EngineResult] The raw engine result
100
+ def perform_reasoning(input, &)
101
+ engine_instance = @engine.new(self, @llm, @tools, @max_iterations)
102
+ with_retry { engine_instance.reason(input, &) }
103
+ end
104
+
105
+ # Finalize the result by updating memories and running hooks
106
+ # @param input [String] The original input
107
+ # @param result [Result] The result to finalize
108
+ def finalize_result(input, result)
109
+ update_memories(input, result)
110
+ run_hooks(:after_action, result)
111
+ end
112
+
113
+ # Handle errors during execution
114
+ # @param error [StandardError] The error that occurred
115
+ # @param input [String] The original input
116
+ # @return [Result] An error result
117
+ # @raise [StandardError] Re-raises if on_error hook returns :stop
118
+ def handle_error(error, input)
119
+ error_action = run_hooks(:on_error, error, input)
120
+ raise error if error_action == :stop
121
+
122
+ build_error_result(input, error)
123
+ end
124
+
125
+ # Tool building methods are in ToolBuilder module
126
+ # Retry handling methods are in RetryHandler module
127
+ # LLM building methods are in LLMBuilder module
128
+ # Hook management methods are in HookManager module
129
+ end
130
+ end