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,94 @@
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
+
16
+ config.performance do |perf|
17
+ perf.max_iterations = 5
18
+ perf.timeout = 30
19
+ end
20
+ end
21
+
22
+ # Define a simple search tool
23
+ class SearchTool < Soka::AgentTool
24
+ desc 'Search the web for information'
25
+
26
+ params do
27
+ requires :query, String, desc: 'The query to search for'
28
+ end
29
+
30
+ def call(query:)
31
+ puts "SearchTool call: #{query}"
32
+ # This is a simulated search result
33
+ case query.downcase
34
+ when /weather/
35
+ 'Today in Taipei: Sunny, Temperature 28°C, Humidity 65%'
36
+ when /news/
37
+ 'Today\'s headline: AI technology reaches new milestone'
38
+ else
39
+ "Searching for information about '#{query}'..."
40
+ end
41
+ end
42
+ end
43
+
44
+ # Define time tool
45
+ class TimeTool < Soka::AgentTool
46
+ desc 'Get current time and date'
47
+
48
+ def call
49
+ puts 'TimeTool call'
50
+ Time.now.strftime('%Y-%m-%d %H:%M:%S')
51
+ end
52
+ end
53
+
54
+ # Define Agent
55
+ class DemoAgent < Soka::Agent
56
+ tool SearchTool
57
+ tool TimeTool
58
+ end
59
+
60
+ # Use Agent
61
+ agent = DemoAgent.new
62
+
63
+ puts '=== Soka Demo Agent ==='
64
+ puts
65
+
66
+ # Example 1: Ask about weather
67
+ puts 'Question: What\'s the weather like in Taipei today?'
68
+ puts '-' * 50
69
+
70
+ agent.run('What\'s the weather like in Taipei today?') do |event|
71
+ case event.type
72
+ when :thought
73
+ puts "šŸ’­ Thinking: #{event.content}"
74
+ when :action
75
+ puts "šŸ”§ Action: Using tool #{event.content[:tool]}"
76
+ when :observation
77
+ puts "šŸ‘€ Observation: #{event.content}"
78
+ when :final_answer
79
+ puts "āœ… Answer: #{event.content}"
80
+ end
81
+ end
82
+
83
+ puts
84
+ puts '=' * 50
85
+ puts
86
+
87
+ # Example 2: Ask about time
88
+ puts 'Question: What time is it now?'
89
+ puts '-' * 50
90
+
91
+ result = agent.run('What time is it now?')
92
+ puts "āœ… Answer: #{result.final_answer}"
93
+ puts "šŸ“Š Confidence: #{(result.confidence_score * 100).round(1)}%"
94
+ puts "ā±ļø Iterations: #{result.iterations}"
@@ -0,0 +1,120 @@
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
+
16
+ config.performance do |perf|
17
+ perf.max_iterations = 5
18
+ perf.timeout = 30
19
+ end
20
+ end
21
+
22
+ # Simple calculation tool
23
+ class CalculatorTool < Soka::AgentTool
24
+ desc 'Perform calculations'
25
+
26
+ params do
27
+ requires :expression, String, desc: 'Mathematical expression to calculate'
28
+ end
29
+
30
+ def call(expression:)
31
+ result = eval(expression) # rubocop:disable Security/Eval
32
+ "Result: #{expression} = #{result}"
33
+ rescue StandardError => e
34
+ "Error: #{e.message}"
35
+ end
36
+ end
37
+
38
+ # Weather tool
39
+ class WeatherTool < Soka::AgentTool
40
+ desc 'Get weather information'
41
+
42
+ params do
43
+ requires :location, String, desc: 'Location to get weather for'
44
+ end
45
+
46
+ def call(location:)
47
+ # Simulated weather data
48
+ "Weather in #{location}: Sunny, 22°C"
49
+ end
50
+ end
51
+
52
+ # Define streaming agent
53
+ class StreamingAgent < Soka::Agent
54
+ tool CalculatorTool
55
+ tool WeatherTool
56
+ end
57
+
58
+ # Main program
59
+ puts '=== Soka Event Handling Example ==='
60
+ puts "Demonstrating event-based responses\n\n"
61
+ puts "NOTE: Streaming is not yet fully integrated with the event system\n"
62
+
63
+ agent = StreamingAgent.new
64
+
65
+ # Example 1: Event-based response handling
66
+ puts 'Example 1: Simple calculation with event handling'
67
+ puts '-' * 50
68
+
69
+ agent.run('Calculate 123 * 456') do |event|
70
+ case event.type
71
+ when :thought
72
+ puts "šŸ’­ Thinking: #{event.content}"
73
+ when :action
74
+ puts "šŸ”§ Action: Using #{event.content[:tool]}"
75
+ when :observation
76
+ puts "šŸ‘€ Result: #{event.content}"
77
+ when :final_answer
78
+ puts "āœ… Final: #{event.content}"
79
+ when :error
80
+ puts "āŒ Error: #{event.content}"
81
+ end
82
+ end
83
+
84
+ puts "\n" + '=' * 50 + "\n"
85
+
86
+ # Example 2: Multi-step task with event handling
87
+ puts 'Example 2: Multi-step task with event handling'
88
+ puts '-' * 50
89
+
90
+ agent.run('What is the weather in Tokyo? Also calculate 15% tip on $85.50') do |event|
91
+ case event.type
92
+ when :thought
93
+ puts "šŸ’­ Thinking: #{event.content}"
94
+ when :action
95
+ puts "šŸ”§ Action: #{event.content[:tool]}"
96
+ puts " Input: #{event.content[:params]}" if event.content[:params]
97
+ when :observation
98
+ puts "šŸ‘€ Result: #{event.content}"
99
+ when :final_answer
100
+ puts "āœ… Complete: #{event.content}"
101
+ end
102
+ end
103
+
104
+ puts "\n" + '=' * 50 + "\n"
105
+
106
+ # Example 3: Without event handling (direct result)
107
+ puts 'Example 3: Without event handling (direct result)'
108
+ puts '-' * 50
109
+
110
+ result = agent.run('Calculate the area of a circle with radius 5')
111
+ puts "Direct result: #{result.final_answer}"
112
+ puts "Confidence: #{(result.confidence_score * 100).round(1)}%"
113
+ puts "Iterations: #{result.iterations}"
114
+
115
+ puts "\n=== Event Handling Benefits ==="
116
+ puts "1. Real-time feedback on agent's thinking process"
117
+ puts "2. Visibility into tool usage and results"
118
+ puts "3. Can track progress during complex tasks"
119
+ puts "4. Debugging support with detailed event logs"
120
+ puts "\nNote: True streaming of LLM responses is not yet integrated with events"
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'soka'
6
+ require 'dotenv/load'
7
+ require 'singleton'
8
+
9
+ # Configure Soka
10
+ Soka.setup do |config|
11
+ config.ai do |ai|
12
+ ai.provider = :gemini
13
+ ai.model = 'gemini-2.5-flash-lite'
14
+ ai.api_key = ENV.fetch('GEMINI_API_KEY', nil)
15
+ end
16
+
17
+ config.performance do |perf|
18
+ perf.max_iterations = 10
19
+ perf.timeout = 30
20
+ end
21
+ end
22
+
23
+ # Calculator tool - Simple math calculations
24
+ class CalculatorTool < Soka::AgentTool
25
+ desc 'Perform basic math operations'
26
+
27
+ params do
28
+ requires :expression, String, desc: "Mathematical expression, e.g., '2 + 2' or '10 * 5'"
29
+ end
30
+
31
+ def call(expression:)
32
+ # Simple math expression evaluation (production should use safer methods)
33
+ result = eval(expression) # rubocop:disable Security/Eval
34
+ "Calculation result: #{expression} = #{result}"
35
+ rescue StandardError => e
36
+ "Calculation error: #{e.message}"
37
+ end
38
+ end
39
+
40
+ # Shared memory manager
41
+ class SharedMemory
42
+ include Singleton
43
+
44
+ def initialize
45
+ @storage = {}
46
+ end
47
+
48
+ def store(key, value)
49
+ @storage[key] = value
50
+ end
51
+
52
+ def retrieve(key)
53
+ @storage[key]
54
+ end
55
+
56
+ def exists?(key)
57
+ @storage.key?(key)
58
+ end
59
+ end
60
+
61
+ # Memory tool - Store and retrieve information
62
+ class MemoryTool < Soka::AgentTool
63
+ desc 'Remember important information'
64
+
65
+ params do
66
+ requires :key, String, desc: 'Information key'
67
+ requires :value, String, desc: 'Content to remember'
68
+ end
69
+
70
+ def call(key:, value:)
71
+ SharedMemory.instance.store(key, value)
72
+ "Remembered: #{key} = #{value}"
73
+ end
74
+ end
75
+
76
+ # Recall tool - Retrieve previously remembered information
77
+ class RecallTool < Soka::AgentTool
78
+ desc 'Recall previously remembered information'
79
+
80
+ params do
81
+ requires :key, String, desc: 'Information key to recall'
82
+ end
83
+
84
+ def call(key:)
85
+ if SharedMemory.instance.exists?(key)
86
+ value = SharedMemory.instance.retrieve(key)
87
+ "Recalled: #{key} = #{value}"
88
+ else
89
+ "No memory found for '#{key}'"
90
+ end
91
+ end
92
+ end
93
+
94
+ # Define Agent
95
+ class MemoryAgent < Soka::Agent
96
+ tool CalculatorTool
97
+ tool MemoryTool
98
+ tool RecallTool
99
+ end
100
+
101
+ # Main program
102
+ puts '=== Soka Memory Example ==='
103
+ puts "Testing Agent memory functionality\n\n"
104
+
105
+ # Program starts
106
+
107
+ # Method 1: Using Soka::Memory class
108
+ puts '>>> Method 1: Using Soka::Memory class'
109
+ soka_memory = Soka::Memory.new
110
+ soka_memory.add(role: 'user', content: 'My name is Xiao Ming, I am 25 years old')
111
+ soka_memory.add(role: 'assistant', content: 'Nice to meet you, Xiao Ming! I will remember that you are 25 years old.')
112
+
113
+ # Method 2: Using Array (direct format)
114
+ puts '>>> Method 2: Using Array (direct format)'
115
+ memory_array = [{
116
+ role: 'user',
117
+ content: 'My name is Xiao Ming, I am 25 years old'
118
+ }, {
119
+ role: 'assistant',
120
+ content: 'Nice to meet you, Xiao Ming! I will remember that you are 25 years old.'
121
+ }]
122
+
123
+ # Both methods work - Soka will convert Array to Memory object automatically
124
+ memory = soka_memory # You can use either initial_memory_obj or initial_memory_array
125
+
126
+ # Example of using array format (uncomment to use this instead)
127
+ # memory = memory_array
128
+
129
+ # Test case 1: Basic calculation and memory
130
+ puts '\nTest 1: Basic calculation and memory'
131
+ # Agent with initial memory
132
+ agent_with_memory = MemoryAgent.new(memory: memory)
133
+ result = agent_with_memory.run(
134
+ "Please calculate 15 * 8, and remember this calculation result with key 'first_calc'"
135
+ )
136
+ puts "Agent: #{result.final_answer}"
137
+ puts "Confidence: #{(result.confidence_score * 100).round(1)}%"
138
+ puts "Iterations: #{result.iterations}"
139
+ puts '-' * 50
140
+
141
+ # Test case 2: Recall previous information
142
+ puts "\nTest 2: Recall previous calculation"
143
+ # Continue using the same agent, as it already has memory
144
+ result = agent_with_memory.run(
145
+ "Please recall what the value of 'first_calc' is?"
146
+ )
147
+ puts "Agent: #{result.final_answer}"
148
+ puts '-' * 50
149
+
150
+ # Test case 3: Use remembered information for new calculations
151
+ puts "\nTest 3: Complex memory operations"
152
+ result = agent_with_memory.run(
153
+ "Please remember that pi is 3.14159, then calculate the area of a circle with radius 5 (using pi * r * r), and remember the result as 'circle_area'"
154
+ )
155
+ puts "Agent: #{result.final_answer}"
156
+ puts '-' * 50
157
+
158
+ # Test case 4: Recall information from initial memory
159
+ puts "\nTest 4: Recall initial information"
160
+ result = agent_with_memory.run(
161
+ 'Do you remember my name and age?'
162
+ )
163
+ puts "Agent: #{result.final_answer}"
164
+ puts '-' * 50
165
+
166
+ # Display complete conversation history
167
+ puts "\n=== Complete Conversation History ==="
168
+ agent_with_memory.memory.messages.each_with_index do |msg, idx|
169
+ puts "#{idx + 1}. [#{msg[:role]}]: #{msg[:content][0..100]}#{'…' if msg[:content].length > 100}"
170
+ end
171
+
172
+ # Display the last thinking process
173
+ puts "\n=== Last Thinking Process ==="
174
+ if result.thoughts&.any?
175
+ result.thoughts.each_with_index do |thought_item, idx|
176
+ step = thought_item[:step] || (idx + 1)
177
+ content = thought_item[:thought] || thought_item[:content] || ''
178
+ puts "#{step}. #{content[0..200]}#{'…' if content.length > 200}"
179
+ end
180
+ else
181
+ puts 'No thinking process recorded'
182
+ end
@@ -0,0 +1,140 @@
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
+ # Simple database tool
18
+ class DatabaseTool < Soka::AgentTool
19
+ desc 'Query database'
20
+
21
+ params do
22
+ requires :query, String, desc: 'SQL query to execute'
23
+ end
24
+
25
+ def call(query:)
26
+ # Simulated database query
27
+ if query.downcase.include?('select')
28
+ "Found 5 records matching your query"
29
+ else
30
+ "Query executed successfully"
31
+ end
32
+ end
33
+ end
34
+
35
+ # Agent with hooks
36
+ class HookedAgent < Soka::Agent
37
+ tool DatabaseTool
38
+
39
+ # Hook called before action
40
+ before_action :log_request
41
+
42
+ # Hook called after action
43
+ after_action :log_response
44
+
45
+ # Hook called on error
46
+ on_error :handle_error
47
+
48
+ private
49
+
50
+ def log_request(input)
51
+ puts "šŸ”µ [BEFORE] Starting request: #{input}"
52
+ puts "šŸ”µ [BEFORE] Memory size: #{@memory.messages.size} messages"
53
+ puts "šŸ”µ [BEFORE] Available tools: #{@tools.map { |t| t.class.name }.join(', ')}"
54
+ puts '-' * 50
55
+ end
56
+
57
+ def log_response(result)
58
+ puts '-' * 50
59
+ puts "🟢 [AFTER] Request completed"
60
+ puts "🟢 [AFTER] Final answer: #{result.final_answer[0..100]}..."
61
+ puts "🟢 [AFTER] Iterations: #{result.iterations}"
62
+ puts "🟢 [AFTER] Confidence: #{(result.confidence_score * 100).round(1)}%"
63
+ puts "🟢 [AFTER] Memory size: #{@memory.messages.size} messages"
64
+ end
65
+
66
+ def handle_error(error, input)
67
+ puts "šŸ”“ [ERROR] An error occurred!"
68
+ puts "šŸ”“ [ERROR] Input: #{input}"
69
+ puts "šŸ”“ [ERROR] Error: #{error.message}"
70
+ puts "šŸ”“ [ERROR] Backtrace: #{error.backtrace.first(3).join("\n")}"
71
+
72
+ # Return :continue to continue execution with error result
73
+ # Return :stop to re-raise the error
74
+ :continue
75
+ end
76
+ end
77
+
78
+ # Main program
79
+ puts '=== Soka Hooks Example ==='
80
+ puts "Demonstrating lifecycle hooks\n\n"
81
+
82
+ agent = HookedAgent.new
83
+
84
+ # Example 1: Normal execution with hooks
85
+ puts 'Example 1: Normal execution showing all hooks'
86
+ puts '=' * 50
87
+
88
+ result = agent.run('Query the database for user statistics')
89
+ puts "\nResult: #{result.final_answer}" if result.respond_to?(:final_answer)
90
+
91
+ puts "\n" + '=' * 50 + "\n"
92
+
93
+ # Example 2: Multiple calls showing memory growth
94
+ puts 'Example 2: Multiple calls showing memory tracking'
95
+ puts '=' * 50
96
+
97
+ queries = [
98
+ 'Select all active users',
99
+ 'Count total orders this month',
100
+ 'Find top selling products'
101
+ ]
102
+
103
+ queries.each_with_index do |query, index|
104
+ puts "\n--- Query #{index + 1} ---"
105
+ agent.run(query)
106
+ sleep(0.5) # Small delay for visibility
107
+ end
108
+
109
+ puts "\n" + '=' * 50 + "\n"
110
+
111
+ # Example 3: Error handling
112
+ puts 'Example 3: Error handling with hooks'
113
+ puts '=' * 50
114
+
115
+ # Create a tool that will trigger an error
116
+ class ErrorTool < Soka::AgentTool
117
+ desc 'This tool simulates errors'
118
+
119
+ def call
120
+ raise StandardError, 'This is a simulated tool error!'
121
+ end
122
+ end
123
+
124
+ class ErrorHandlingAgent < HookedAgent
125
+ tool ErrorTool
126
+ end
127
+
128
+ error_agent = ErrorHandlingAgent.new
129
+ result = error_agent.run('Use the error tool to test error handling')
130
+ puts "\nError handled gracefully: #{result.failed? ? 'Yes' : 'No'}"
131
+ puts "Final answer: #{result.final_answer}" if result.final_answer
132
+ puts "Error message: #{result.error}" if result.error
133
+
134
+ puts "\n=== Hook Benefits ==="
135
+ puts "1. Logging and monitoring of agent activity"
136
+ puts "2. Pre-processing of inputs before execution"
137
+ puts "3. Post-processing of results"
138
+ puts "4. Custom error handling and recovery"
139
+ puts "5. Performance tracking and metrics"
140
+ puts "6. Integration with external systems"
@@ -0,0 +1,85 @@
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
+ # Tool that can demonstrate different scenarios
18
+ class DemoTool < Soka::AgentTool
19
+ desc 'A tool that can succeed or fail based on input'
20
+
21
+ params do
22
+ requires :action, String, inclusion: %w[success fail],
23
+ desc: 'Whether to succeed or fail'
24
+ end
25
+
26
+ def call(action:)
27
+ case action
28
+ when 'success'
29
+ 'Operation completed successfully!'
30
+ when 'fail'
31
+ raise StandardError, 'Tool execution failed as requested!'
32
+ end
33
+ end
34
+ end
35
+
36
+ # Agent with error handling
37
+ class ErrorDemoAgent < Soka::Agent
38
+ tool DemoTool
39
+
40
+ # This hook now handles ALL errors (both Tool and Agent-level)
41
+ on_error :handle_errors
42
+
43
+ private
44
+
45
+ def handle_errors(error, input)
46
+ puts "\nšŸ”“ Error Handler Triggered!"
47
+ puts " Error Type: #{error.class}"
48
+ puts " Error Message: #{error.message}"
49
+ puts " Original Input: #{input}"
50
+
51
+ # Return :continue to get an error result but continue execution
52
+ # Return :stop to re-raise the error and halt
53
+ :continue
54
+ end
55
+ end
56
+
57
+ # Run demonstrations
58
+ puts "=== Soka Error Handling Example ==="
59
+ puts "Demonstrating that ALL errors now trigger the on_error hook\n\n"
60
+
61
+ agent = ErrorDemoAgent.new
62
+
63
+ # Example 1: Successful operation
64
+ puts "1. Successful Operation:"
65
+ puts "-" * 40
66
+ result = agent.run('Use the demo tool with action=success')
67
+ puts "Status: #{result.status}"
68
+ puts "Success?: #{result.successful?}"
69
+ puts "Answer: #{result.final_answer}"
70
+
71
+ # Example 2: Tool error (NOW triggers on_error)
72
+ puts "\n2. Tool Error (triggers on_error hook):"
73
+ puts "-" * 40
74
+ result = agent.run('Use the demo tool with action=fail')
75
+ puts "\nResult after error handling:"
76
+ puts "Status: #{result.status}"
77
+ puts "Failed?: #{result.failed?}"
78
+ puts "Error: #{result.error}"
79
+ puts "Answer: #{result.final_answer || 'No answer due to error'}"
80
+
81
+ puts "\n=== Key Points ==="
82
+ puts "āœ… ALL errors now trigger the on_error hook (Tool and Agent errors)"
83
+ puts "āœ… Tool errors are wrapped as Soka::ToolError"
84
+ puts "āœ… Return :continue from on_error to continue with error result"
85
+ puts "āœ… Return :stop from on_error to re-raise and halt execution"