rcrewai 0.2.1 → 0.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.rubocop_todo.yml +99 -0
  4. data/CHANGELOG.md +24 -0
  5. data/README.md +2 -2
  6. data/Rakefile +53 -53
  7. data/bin/rcrewai +3 -3
  8. data/docs/mcp.md +109 -0
  9. data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
  10. data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
  11. data/docs/upgrading-to-0.3.md +163 -0
  12. data/examples/async_execution_example.rb +82 -81
  13. data/examples/hierarchical_crew_example.rb +68 -72
  14. data/examples/human_in_the_loop_example.rb +73 -74
  15. data/examples/mcp_example.rb +48 -0
  16. data/examples/native_tools_example.rb +64 -0
  17. data/examples/streaming_example.rb +56 -0
  18. data/lib/rcrewai/agent.rb +148 -287
  19. data/lib/rcrewai/async_executor.rb +43 -43
  20. data/lib/rcrewai/cli.rb +11 -11
  21. data/lib/rcrewai/configuration.rb +14 -9
  22. data/lib/rcrewai/crew.rb +56 -39
  23. data/lib/rcrewai/events.rb +30 -0
  24. data/lib/rcrewai/human_input.rb +104 -114
  25. data/lib/rcrewai/legacy_react_runner.rb +172 -0
  26. data/lib/rcrewai/llm_client.rb +1 -1
  27. data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
  28. data/lib/rcrewai/llm_clients/azure.rb +23 -128
  29. data/lib/rcrewai/llm_clients/base.rb +11 -7
  30. data/lib/rcrewai/llm_clients/google.rb +159 -95
  31. data/lib/rcrewai/llm_clients/ollama.rb +150 -106
  32. data/lib/rcrewai/llm_clients/openai.rb +140 -63
  33. data/lib/rcrewai/mcp/client.rb +101 -0
  34. data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
  35. data/lib/rcrewai/mcp/transport/http.rb +53 -0
  36. data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
  37. data/lib/rcrewai/mcp.rb +8 -0
  38. data/lib/rcrewai/memory.rb +45 -37
  39. data/lib/rcrewai/pricing.rb +34 -0
  40. data/lib/rcrewai/process.rb +86 -95
  41. data/lib/rcrewai/provider_schema.rb +38 -0
  42. data/lib/rcrewai/sse_parser.rb +55 -0
  43. data/lib/rcrewai/task.rb +56 -64
  44. data/lib/rcrewai/tool_runner.rb +132 -0
  45. data/lib/rcrewai/tool_schema.rb +97 -0
  46. data/lib/rcrewai/tools/base.rb +98 -37
  47. data/lib/rcrewai/tools/code_executor.rb +71 -74
  48. data/lib/rcrewai/tools/email_sender.rb +70 -78
  49. data/lib/rcrewai/tools/file_reader.rb +38 -30
  50. data/lib/rcrewai/tools/file_writer.rb +40 -38
  51. data/lib/rcrewai/tools/pdf_processor.rb +115 -130
  52. data/lib/rcrewai/tools/sql_database.rb +58 -55
  53. data/lib/rcrewai/tools/web_search.rb +26 -25
  54. data/lib/rcrewai/version.rb +2 -2
  55. data/lib/rcrewai.rb +18 -10
  56. data/rcrewai.gemspec +39 -39
  57. metadata +65 -47
@@ -12,7 +12,7 @@ module RCrewAI
12
12
  @timeout = options.fetch(:timeout, 300) # 5 minutes default
13
13
  @logger = Logger.new($stdout)
14
14
  @logger.level = options.fetch(:verbose, false) ? Logger::DEBUG : Logger::INFO
15
-
15
+
16
16
  # Create thread pool for task execution
17
17
  @thread_pool = Concurrent::ThreadPoolExecutor.new(
18
18
  min_threads: 1,
@@ -20,7 +20,7 @@ module RCrewAI
20
20
  max_queue: @max_concurrency * 2,
21
21
  fallback_policy: :caller_runs
22
22
  )
23
-
23
+
24
24
  @futures = {}
25
25
  @task_dependencies = {}
26
26
  @completed_tasks = Concurrent::Set.new
@@ -29,30 +29,30 @@ module RCrewAI
29
29
 
30
30
  def execute_tasks_async(tasks, dependency_graph = {})
31
31
  @logger.info "Starting async execution of #{tasks.length} tasks with max #{@max_concurrency} concurrent threads"
32
-
32
+
33
33
  @task_dependencies = dependency_graph
34
34
  execution_phases = organize_tasks_by_dependencies(tasks)
35
-
35
+
36
36
  start_time = Time.now
37
37
  results = []
38
-
38
+
39
39
  execution_phases.each_with_index do |phase_tasks, phase_index|
40
40
  @logger.info "Executing phase #{phase_index + 1}: #{phase_tasks.length} tasks"
41
-
41
+
42
42
  phase_results = execute_phase_concurrently(phase_tasks, phase_index + 1)
43
43
  results.concat(phase_results)
44
-
44
+
45
45
  # Check if we should continue
46
46
  failed_in_phase = phase_results.count { |r| r[:status] == :failed }
47
- if failed_in_phase > 0 && should_abort_after_failures?(failed_in_phase, phase_tasks.length)
47
+ if failed_in_phase.positive? && should_abort_after_failures?(failed_in_phase, phase_tasks.length)
48
48
  @logger.error "Aborting execution due to #{failed_in_phase} failures in phase #{phase_index + 1}"
49
49
  break
50
50
  end
51
51
  end
52
-
52
+
53
53
  total_time = Time.now - start_time
54
54
  @logger.info "Async execution completed in #{total_time.round(2)}s"
55
-
55
+
56
56
  format_async_results(results, total_time)
57
57
  end
58
58
 
@@ -60,38 +60,36 @@ module RCrewAI
60
60
  future = Concurrent::Future.execute(executor: @thread_pool) do
61
61
  execute_task_with_monitoring(task)
62
62
  end
63
-
63
+
64
64
  @futures[task] = future
65
65
  future
66
66
  end
67
67
 
68
68
  def wait_for_completion(futures, timeout = nil)
69
69
  timeout ||= @timeout
70
-
70
+
71
71
  results = []
72
72
  futures.each do |task, future|
73
- begin
74
- result = future.value(timeout)
75
- results << { task: task, result: result, status: :completed }
76
- rescue Concurrent::TimeoutError
77
- @logger.error "Task #{task.name} timed out after #{timeout}s"
78
- results << { task: task, result: "Task timed out", status: :timeout }
79
- rescue => e
80
- @logger.error "Task #{task.name} failed: #{e.message}"
81
- results << { task: task, result: e.message, status: :failed }
82
- end
73
+ result = future.value(timeout)
74
+ results << { task: task, result: result, status: :completed }
75
+ rescue Concurrent::TimeoutError
76
+ @logger.error "Task #{task.name} timed out after #{timeout}s"
77
+ results << { task: task, result: 'Task timed out', status: :timeout }
78
+ rescue StandardError => e
79
+ @logger.error "Task #{task.name} failed: #{e.message}"
80
+ results << { task: task, result: e.message, status: :failed }
83
81
  end
84
-
82
+
85
83
  results
86
84
  end
87
85
 
88
86
  def shutdown
89
- @logger.info "Shutting down async executor..."
87
+ @logger.info 'Shutting down async executor...'
90
88
  @thread_pool.shutdown
91
- unless @thread_pool.wait_for_termination(30)
92
- @logger.warn "Thread pool did not shut down gracefully, forcing shutdown"
93
- @thread_pool.kill
94
- end
89
+ return if @thread_pool.wait_for_termination(30)
90
+
91
+ @logger.warn 'Thread pool did not shut down gracefully, forcing shutdown'
92
+ @thread_pool.kill
95
93
  end
96
94
 
97
95
  def stats
@@ -136,7 +134,7 @@ module RCrewAI
136
134
 
137
135
  def execute_phase_concurrently(phase_tasks, phase_number)
138
136
  @logger.debug "Phase #{phase_number}: Launching #{phase_tasks.length} concurrent tasks"
139
-
137
+
140
138
  # Launch all tasks in this phase concurrently
141
139
  phase_futures = {}
142
140
  phase_tasks.each do |task|
@@ -146,7 +144,7 @@ module RCrewAI
146
144
 
147
145
  # Wait for all tasks in this phase to complete
148
146
  phase_results = wait_for_completion(phase_futures)
149
-
147
+
150
148
  # Update tracking sets
151
149
  phase_results.each do |result|
152
150
  case result[:status]
@@ -156,31 +154,33 @@ module RCrewAI
156
154
  @failed_tasks.add(result[:task])
157
155
  end
158
156
  end
159
-
160
- @logger.info "Phase #{phase_number} completed: #{phase_results.count { |r| r[:status] == :completed }}/#{phase_tasks.length} successful"
161
-
157
+
158
+ @logger.info "Phase #{phase_number} completed: #{phase_results.count do |r|
159
+ r[:status] == :completed
160
+ end}/#{phase_tasks.length} successful"
161
+
162
162
  phase_results.map { |r| r.merge(phase: phase_number) }
163
163
  end
164
164
 
165
165
  def execute_task_with_monitoring(task)
166
166
  thread_id = Thread.current.object_id
167
167
  @logger.debug "Task #{task.name} starting on thread #{thread_id}"
168
-
168
+
169
169
  start_time = Time.now
170
-
170
+
171
171
  begin
172
172
  # Add async context to task
173
173
  task.instance_variable_set(:@async_execution, true)
174
174
  task.instance_variable_set(:@thread_id, thread_id)
175
-
175
+
176
176
  # Execute the task
177
177
  result = task.execute
178
-
178
+
179
179
  execution_time = Time.now - start_time
180
180
  @logger.debug "Task #{task.name} completed in #{execution_time.round(2)}s on thread #{thread_id}"
181
-
181
+
182
182
  result
183
- rescue => e
183
+ rescue StandardError => e
184
184
  execution_time = Time.now - start_time
185
185
  @logger.error "Task #{task.name} failed after #{execution_time.round(2)}s on thread #{thread_id}: #{e.message}"
186
186
  raise e
@@ -189,7 +189,7 @@ module RCrewAI
189
189
 
190
190
  def should_abort_after_failures?(failed_count, total_count)
191
191
  failure_rate = failed_count.to_f / total_count
192
-
192
+
193
193
  # Abort if more than 50% of tasks in a phase fail
194
194
  failure_rate > 0.5
195
195
  end
@@ -198,7 +198,7 @@ module RCrewAI
198
198
  completed = results.count { |r| r[:status] == :completed }
199
199
  failed = results.count { |r| r[:status] == :failed }
200
200
  timed_out = results.count { |r| r[:status] == :timeout }
201
-
201
+
202
202
  {
203
203
  execution_mode: :async,
204
204
  total_time: total_time,
@@ -227,7 +227,7 @@ module RCrewAI
227
227
  module ClassMethods
228
228
  def execute_async(tasks, **options)
229
229
  executor = AsyncExecutor.new(**options)
230
-
230
+
231
231
  begin
232
232
  results = executor.execute_tasks_async(tasks)
233
233
  results
@@ -245,4 +245,4 @@ module RCrewAI
245
245
  @thread_id
246
246
  end
247
247
  end
248
- end
248
+ end
data/lib/rcrewai/cli.rb CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  module RCrewAI
4
4
  class CLI < Thor
5
- desc "new CREW_NAME", "Create a new AI crew"
5
+ desc 'new CREW_NAME', 'Create a new AI crew'
6
6
  def new(crew_name)
7
7
  puts "Creating new crew: #{crew_name}"
8
8
  Crew.create(crew_name)
9
9
  end
10
10
 
11
- desc "run", "Run the AI crew"
12
- option :crew, type: :string, required: true, desc: "Name of the crew to run"
11
+ desc 'run', 'Run the AI crew'
12
+ option :crew, type: :string, required: true, desc: 'Name of the crew to run'
13
13
  def run
14
14
  crew_name = options[:crew]
15
15
  puts "Running crew: #{crew_name}"
@@ -17,23 +17,23 @@ module RCrewAI
17
17
  crew.execute
18
18
  end
19
19
 
20
- desc "list", "List all available crews"
20
+ desc 'list', 'List all available crews'
21
21
  def list
22
- puts "Available crews:"
22
+ puts 'Available crews:'
23
23
  Crew.list.each do |crew|
24
24
  puts " - #{crew}"
25
25
  end
26
26
  end
27
27
 
28
- desc "agent SUBCOMMAND ...ARGS", "Manage agents"
29
- subcommand "agent", Agent::CLI
28
+ desc 'agent SUBCOMMAND ...ARGS', 'Manage agents'
29
+ subcommand 'agent', Agent::CLI
30
30
 
31
- desc "task SUBCOMMAND ...ARGS", "Manage tasks"
32
- subcommand "task", Task::CLI
31
+ desc 'task SUBCOMMAND ...ARGS', 'Manage tasks'
32
+ subcommand 'task', Task::CLI
33
33
 
34
- desc "version", "Show version"
34
+ desc 'version', 'Show version'
35
35
  def version
36
36
  puts "rcrewai version #{RCrewAI::VERSION}"
37
37
  end
38
38
  end
39
- end
39
+ end
@@ -2,10 +2,11 @@
2
2
 
3
3
  module RCrewAI
4
4
  class Configuration
5
- attr_accessor :llm_provider, :api_key, :model, :temperature, :max_tokens, :timeout
6
- attr_accessor :openai_api_key, :anthropic_api_key, :google_api_key, :azure_api_key
7
- attr_accessor :openai_model, :anthropic_model, :google_model, :azure_model
8
- attr_accessor :base_url, :api_version, :deployment_name
5
+ attr_accessor :llm_provider, :api_key, :model, :temperature, :max_tokens, :timeout,
6
+ :openai_api_key, :anthropic_api_key, :google_api_key, :azure_api_key,
7
+ :openai_model, :anthropic_model, :google_model, :azure_model,
8
+ :base_url, :api_version, :deployment_name,
9
+ :pricing, :ollama_native_tools, :log_level
9
10
 
10
11
  def initialize
11
12
  @llm_provider = :openai
@@ -13,13 +14,17 @@ module RCrewAI
13
14
  @temperature = 0.1
14
15
  @max_tokens = 4000
15
16
  @timeout = 120
16
-
17
+
17
18
  # Default models for each provider
18
19
  @openai_model = 'gpt-4'
19
20
  @anthropic_model = 'claude-3-sonnet-20240229'
20
21
  @google_model = 'gemini-pro'
21
22
  @azure_model = 'gpt-4'
22
-
23
+
24
+ @pricing = nil
25
+ @ollama_native_tools = nil
26
+ @log_level = :info
27
+
23
28
  # Load from environment variables
24
29
  load_from_env
25
30
  end
@@ -55,7 +60,7 @@ module RCrewAI
55
60
  end
56
61
 
57
62
  def validate!
58
- raise ConfigurationError, "LLM provider must be set" if @llm_provider.nil?
63
+ raise ConfigurationError, 'LLM provider must be set' if @llm_provider.nil?
59
64
  raise ConfigurationError, "API key must be set for #{@llm_provider}" if api_key.nil? || api_key.empty?
60
65
  raise ConfigurationError, "Model must be set for #{@llm_provider}" if model.nil? || model.empty?
61
66
  end
@@ -75,7 +80,7 @@ module RCrewAI
75
80
  @anthropic_api_key = ENV['ANTHROPIC_API_KEY'] || ENV['CLAUDE_API_KEY']
76
81
  @google_api_key = ENV['GOOGLE_API_KEY'] || ENV['GEMINI_API_KEY']
77
82
  @azure_api_key = ENV['AZURE_OPENAI_API_KEY']
78
-
83
+
79
84
  @api_key = ENV['LLM_API_KEY'] if @api_key.nil?
80
85
  @base_url = ENV['LLM_BASE_URL'] if @base_url.nil?
81
86
  @api_version = ENV['AZURE_API_VERSION'] if @api_version.nil?
@@ -97,4 +102,4 @@ module RCrewAI
97
102
  def self.reset_configuration!
98
103
  @configuration = Configuration.new
99
104
  end
100
- end
105
+ end
data/lib/rcrewai/crew.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'process'
4
4
  require_relative 'async_executor'
5
+ require_relative 'events'
5
6
 
6
7
  module RCrewAI
7
8
  class Crew
@@ -28,7 +29,12 @@ module RCrewAI
28
29
  @tasks << task
29
30
  end
30
31
 
31
- def execute(async: false, **async_options)
32
+ def execute(async: false, stream: nil, **async_options, &block)
33
+ sinks = []
34
+ sinks << block if block_given?
35
+ Array(stream).each { |s| sinks << s } if stream
36
+ @stream_sink = sinks.empty? ? nil : RCrewAI::Events.fan_out(sinks)
37
+
32
38
  if async
33
39
  execute_async(**async_options)
34
40
  else
@@ -36,9 +42,11 @@ module RCrewAI
36
42
  end
37
43
  end
38
44
 
45
+ attr_reader :stream_sink
46
+
39
47
  def execute_async(**options)
40
48
  puts "Executing crew: #{name} (async #{process_type} process)"
41
-
49
+
42
50
  case process_type
43
51
  when :sequential
44
52
  execute_sequential_async(**options)
@@ -53,13 +61,13 @@ module RCrewAI
53
61
 
54
62
  def execute_sync
55
63
  puts "Executing crew: #{name} (#{process_type} process)"
56
-
64
+
57
65
  # Create appropriate process instance
58
66
  @process_instance = create_process_instance
59
-
67
+
60
68
  # Execute using the process
61
69
  results = @process_instance.execute
62
-
70
+
63
71
  # Return formatted results
64
72
  format_execution_results(results)
65
73
  end
@@ -67,7 +75,7 @@ module RCrewAI
67
75
  def process=(new_process)
68
76
  @process_type = new_process.to_sym
69
77
  validate_process_type!
70
- @process_instance = nil # Reset process instance
78
+ @process_instance = nil # Reset process instance
71
79
  end
72
80
 
73
81
  def self.create(name)
@@ -84,7 +92,7 @@ module RCrewAI
84
92
 
85
93
  def self.list
86
94
  # List all available crews
87
- ["example_crew", "research_crew", "development_crew"]
95
+ %w[example_crew research_crew development_crew]
88
96
  end
89
97
 
90
98
  def save
@@ -95,10 +103,10 @@ module RCrewAI
95
103
  private
96
104
 
97
105
  def validate_process_type!
98
- valid_processes = [:sequential, :hierarchical, :consensual]
99
- unless valid_processes.include?(process_type)
100
- raise ConfigurationError, "Invalid process type: #{process_type}. Valid types: #{valid_processes.join(', ')}"
101
- end
106
+ valid_processes = %i[sequential hierarchical consensual]
107
+ return if valid_processes.include?(process_type)
108
+
109
+ raise ConfigurationError, "Invalid process type: #{process_type}. Valid types: #{valid_processes.join(', ')}"
102
110
  end
103
111
 
104
112
  def create_process_instance
@@ -116,11 +124,11 @@ module RCrewAI
116
124
 
117
125
  def execute_sequential_async(**options)
118
126
  executor = AsyncExecutor.new(**options)
119
-
127
+
120
128
  begin
121
129
  dependency_graph = build_dependency_graph
122
130
  results = executor.execute_tasks_async(tasks, dependency_graph)
123
-
131
+
124
132
  # Add crew metadata
125
133
  results.merge(
126
134
  crew: name,
@@ -134,38 +142,36 @@ module RCrewAI
134
142
  def execute_hierarchical_async(**options)
135
143
  # For hierarchical async, we need to coordinate through the manager
136
144
  manager_agent = find_manager_agent
137
- unless manager_agent
138
- raise Process::ProcessError, "Hierarchical async execution requires a manager agent"
139
- end
145
+ raise Process::ProcessError, 'Hierarchical async execution requires a manager agent' unless manager_agent
140
146
 
141
147
  puts "Manager #{manager_agent.name} coordinating async execution"
142
-
148
+
143
149
  executor = AsyncExecutor.new(**options)
144
-
150
+
145
151
  begin
146
152
  # Build execution phases respecting dependencies
147
153
  dependency_graph = build_dependency_graph
148
154
  phases = organize_tasks_into_phases(tasks, dependency_graph)
149
-
155
+
150
156
  results = []
151
157
  phases.each_with_index do |phase_tasks, phase_index|
152
158
  puts "Manager delegating phase #{phase_index + 1}: #{phase_tasks.length} tasks"
153
-
159
+
154
160
  # Create delegation contexts for each task
155
161
  phase_tasks.each do |task|
156
162
  unless task.agent
157
163
  # Manager assigns best agent for the task
158
164
  task.instance_variable_set(:@agent, find_best_agent_for_task(task))
159
165
  end
160
-
166
+
161
167
  # Add delegation context
162
168
  add_delegation_context(task, manager_agent)
163
169
  end
164
-
170
+
165
171
  # Execute phase concurrently
166
172
  phase_results = executor.execute_tasks_async(phase_tasks, {})
167
173
  results.concat(phase_results[:results])
168
-
174
+
169
175
  # Check if we should abort
170
176
  failed_count = phase_results[:results].count { |r| r[:status] == :failed }
171
177
  if failed_count > phase_tasks.length * 0.5
@@ -173,7 +179,7 @@ module RCrewAI
173
179
  break
174
180
  end
175
181
  end
176
-
182
+
177
183
  {
178
184
  crew: name,
179
185
  process: :async_hierarchical,
@@ -182,7 +188,13 @@ module RCrewAI
182
188
  completed_tasks: results.count { |r| r[:status] == :completed },
183
189
  failed_tasks: results.count { |r| r[:status] == :failed },
184
190
  results: results,
185
- success_rate: results.empty? ? 0 : (results.count { |r| r[:status] == :completed }.to_f / results.length * 100).round(1)
191
+ success_rate: if results.empty?
192
+ 0
193
+ else
194
+ (results.count do |r|
195
+ r[:status] == :completed
196
+ end.to_f / results.length * 100).round(1)
197
+ end
186
198
  }
187
199
  ensure
188
200
  executor.shutdown
@@ -192,11 +204,11 @@ module RCrewAI
192
204
  def execute_consensual_async(**options)
193
205
  # Simplified consensual async - execute in parallel with result aggregation
194
206
  executor = AsyncExecutor.new(**options)
195
-
207
+
196
208
  begin
197
209
  # All tasks can potentially run in parallel for consensual
198
210
  results = executor.execute_tasks_async(tasks, {})
199
-
211
+
200
212
  results.merge(
201
213
  crew: name,
202
214
  process: :async_consensual
@@ -239,30 +251,29 @@ module RCrewAI
239
251
  end
240
252
 
241
253
  def find_manager_agent
242
- agents.find { |agent| agent.is_manager? } ||
243
- agents.find { |agent| agent.allow_delegation }
254
+ agents.find(&:is_manager?) ||
255
+ agents.find(&:allow_delegation)
244
256
  end
245
257
 
246
258
  def find_best_agent_for_task(task)
247
259
  # Simple heuristic: match keywords
248
260
  task_keywords = extract_keywords(task.description.downcase)
249
-
261
+
250
262
  # Filter out manager agents first
251
263
  non_manager_agents = agents.reject(&:is_manager?)
252
264
  return nil if non_manager_agents.empty?
253
-
254
- best_agent = non_manager_agents.max_by do |agent|
265
+
266
+ non_manager_agents.max_by do |agent|
255
267
  agent_keywords = extract_keywords("#{agent.role} #{agent.goal}".downcase)
256
268
  common_keywords = (task_keywords & agent_keywords).length
257
269
  tool_bonus = agent.tools.any? ? 0.5 : 0
258
270
  common_keywords + tool_bonus
259
271
  end
260
-
261
- best_agent
262
272
  end
263
273
 
264
274
  def extract_keywords(text)
265
- stopwords = %w[the a an and or but in on at to for of with by is are was were be been being have has had do does did will would could should]
275
+ stopwords = %w[the a an and or but in on at to for of with by is are was were be been being have has had do does
276
+ did will would could should]
266
277
  text.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
267
278
  end
268
279
 
@@ -270,10 +281,10 @@ module RCrewAI
270
281
  delegation_context = {
271
282
  manager: manager_agent.name,
272
283
  delegation_reason: "Assigned by #{manager_agent.role} for optimal task execution",
273
- coordination_notes: "Part of async hierarchical execution",
284
+ coordination_notes: 'Part of async hierarchical execution',
274
285
  async_execution: true
275
286
  }
276
-
287
+
277
288
  task.instance_variable_set(:@delegation_context, delegation_context)
278
289
  end
279
290
 
@@ -285,8 +296,14 @@ module RCrewAI
285
296
  completed_tasks: results.count { |r| r[:status] == :completed },
286
297
  failed_tasks: results.count { |r| r[:status] == :failed },
287
298
  results: results,
288
- success_rate: results.empty? ? 0.0 : (results.count { |r| r[:status] == :completed }.to_f / results.length * 100).round(1)
299
+ success_rate: if results.empty?
300
+ 0.0
301
+ else
302
+ (results.count do |r|
303
+ r[:status] == :completed
304
+ end.to_f / results.length * 100).round(1)
305
+ end
289
306
  }
290
307
  end
291
308
  end
292
- end
309
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module Events
5
+ BASE_ATTRS = %i[type timestamp agent iteration].freeze
6
+
7
+ Event = Struct.new(*BASE_ATTRS, keyword_init: true)
8
+ TextDelta = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
9
+ TextDone = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
10
+ ToolCallStart = Struct.new(*BASE_ATTRS, :tool, :args, :call_id, keyword_init: true)
11
+ ToolCallResult = Struct.new(*BASE_ATTRS, :tool, :call_id, :result, :duration_ms, keyword_init: true)
12
+ ToolCallError = Struct.new(*BASE_ATTRS, :tool, :call_id, :error, keyword_init: true)
13
+ Thinking = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
14
+ Usage = Struct.new(*BASE_ATTRS, :prompt_tokens, :completion_tokens, :total_tokens, :cost_usd, keyword_init: true)
15
+ IterationStart = Struct.new(*BASE_ATTRS, :iteration_index, keyword_init: true)
16
+ IterationEnd = Struct.new(*BASE_ATTRS, :finish_reason, keyword_init: true)
17
+ Error = Struct.new(*BASE_ATTRS, :error, keyword_init: true)
18
+
19
+ def self.fan_out(sinks)
20
+ sinks = Array(sinks).compact
21
+ lambda do |event|
22
+ sinks.each do |s|
23
+ s.call(event)
24
+ rescue StandardError => e
25
+ Kernel.warn "[rcrewai] event sink raised: #{e.class}: #{e.message}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end