rcrewai 0.2.0 → 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 +33 -1
  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 +55 -36
  57. metadata +86 -50
@@ -22,77 +22,77 @@ module RCrewAI
22
22
 
23
23
  def request_approval(message, **options)
24
24
  interaction = create_interaction(:approval, message, options)
25
-
25
+
26
26
  return handle_auto_approval(interaction) if @auto_approve
27
-
27
+
28
28
  display_approval_request(interaction)
29
29
  response = get_user_input(interaction)
30
-
30
+
31
31
  result = process_approval_response(response, interaction)
32
32
  record_interaction(interaction, response, result)
33
-
33
+
34
34
  result
35
35
  end
36
36
 
37
37
  def request_input(prompt, **options)
38
38
  interaction = create_interaction(:input, prompt, options)
39
-
39
+
40
40
  display_input_request(interaction)
41
41
  response = get_user_input(interaction)
42
-
42
+
43
43
  result = process_input_response(response, interaction)
44
44
  record_interaction(interaction, response, result)
45
-
45
+
46
46
  result
47
47
  end
48
48
 
49
49
  def request_choice(prompt, choices, **options)
50
50
  interaction = create_interaction(:choice, prompt, options.merge(choices: choices))
51
-
51
+
52
52
  display_choice_request(interaction)
53
53
  response = get_user_input(interaction)
54
-
54
+
55
55
  result = process_choice_response(response, interaction)
56
56
  record_interaction(interaction, response, result)
57
-
57
+
58
58
  result
59
59
  end
60
60
 
61
61
  def request_review(content, **options)
62
62
  interaction = create_interaction(:review, content, options)
63
-
63
+
64
64
  display_review_request(interaction)
65
65
  response = get_user_input(interaction)
66
-
66
+
67
67
  result = process_review_response(response, interaction)
68
68
  record_interaction(interaction, response, result)
69
-
69
+
70
70
  result
71
71
  end
72
72
 
73
73
  def confirm_action(action_description, **options)
74
74
  interaction = create_interaction(:confirmation, action_description, options)
75
-
75
+
76
76
  return handle_auto_approval(interaction) if @auto_approve
77
-
77
+
78
78
  display_confirmation_request(interaction)
79
79
  response = get_user_input(interaction)
80
-
80
+
81
81
  result = process_confirmation_response(response, interaction)
82
82
  record_interaction(interaction, response, result)
83
-
83
+
84
84
  result
85
85
  end
86
86
 
87
87
  def get_feedback(prompt, **options)
88
88
  interaction = create_interaction(:feedback, prompt, options)
89
-
89
+
90
90
  display_feedback_request(interaction)
91
91
  response = get_user_input(interaction)
92
-
92
+
93
93
  result = { feedback: response, timestamp: Time.now }
94
94
  record_interaction(interaction, response, result)
95
-
95
+
96
96
  result
97
97
  end
98
98
 
@@ -131,122 +131,110 @@ module RCrewAI
131
131
  end
132
132
 
133
133
  def display_approval_request(interaction)
134
- puts "\n" + "="*60
135
- puts "🤝 HUMAN APPROVAL REQUIRED"
136
- puts "="*60
134
+ puts "\n#{'=' * 60}"
135
+ puts '🤝 HUMAN APPROVAL REQUIRED'
136
+ puts '=' * 60
137
137
  puts "Request: #{interaction[:content]}"
138
-
139
- if interaction[:options][:context]
140
- puts "\nContext: #{interaction[:options][:context]}"
141
- end
142
-
143
- if interaction[:options][:consequences]
144
- puts "\nConsequences: #{interaction[:options][:consequences]}"
145
- end
146
-
138
+
139
+ puts "\nContext: #{interaction[:options][:context]}" if interaction[:options][:context]
140
+
141
+ puts "\nConsequences: #{interaction[:options][:consequences]}" if interaction[:options][:consequences]
142
+
147
143
  puts "\nDo you approve this action? (yes/no)"
148
- print "> "
144
+ print '> '
149
145
  end
150
146
 
151
147
  def display_input_request(interaction)
152
- puts "\n" + "="*60
153
- puts "💬 HUMAN INPUT REQUESTED"
154
- puts "="*60
148
+ puts "\n#{'=' * 60}"
149
+ puts '💬 HUMAN INPUT REQUESTED'
150
+ puts '=' * 60
155
151
  puts "Prompt: #{interaction[:content]}"
156
-
157
- if interaction[:options][:help_text]
158
- puts "\nHelp: #{interaction[:options][:help_text]}"
159
- end
160
-
161
- if interaction[:options][:examples]
162
- puts "\nExamples: #{interaction[:options][:examples].join(', ')}"
163
- end
164
-
152
+
153
+ puts "\nHelp: #{interaction[:options][:help_text]}" if interaction[:options][:help_text]
154
+
155
+ puts "\nExamples: #{interaction[:options][:examples].join(', ')}" if interaction[:options][:examples]
156
+
165
157
  puts "\nPlease provide your input:"
166
- print "> "
158
+ print '> '
167
159
  end
168
160
 
169
161
  def display_choice_request(interaction)
170
- puts "\n" + "="*60
171
- puts "🎯 HUMAN CHOICE REQUIRED"
172
- puts "="*60
162
+ puts "\n#{'=' * 60}"
163
+ puts '🎯 HUMAN CHOICE REQUIRED'
164
+ puts '=' * 60
173
165
  puts "Question: #{interaction[:content]}"
174
166
  puts "\nAvailable choices:"
175
-
167
+
176
168
  interaction[:options][:choices].each_with_index do |choice, index|
177
169
  puts " #{index + 1}. #{choice}"
178
170
  end
179
-
171
+
180
172
  puts "\nPlease select a choice (enter number or text):"
181
- print "> "
173
+ print '> '
182
174
  end
183
175
 
184
176
  def display_review_request(interaction)
185
- puts "\n" + "="*60
186
- puts "👀 HUMAN REVIEW REQUESTED"
187
- puts "="*60
188
- puts "Content to review:"
189
- puts "-" * 40
177
+ puts "\n#{'=' * 60}"
178
+ puts '👀 HUMAN REVIEW REQUESTED'
179
+ puts '=' * 60
180
+ puts 'Content to review:'
181
+ puts '-' * 40
190
182
  puts interaction[:content]
191
- puts "-" * 40
192
-
183
+ puts '-' * 40
184
+
193
185
  if interaction[:options][:review_criteria]
194
186
  puts "\nReview criteria: #{interaction[:options][:review_criteria].join(', ')}"
195
187
  end
196
-
188
+
197
189
  puts "\nPlease review and provide feedback (or type 'approve' to approve as-is):"
198
- print "> "
190
+ print '> '
199
191
  end
200
192
 
201
193
  def display_confirmation_request(interaction)
202
- puts "\n" + "="*60
203
- puts "⚠️ CONFIRMATION REQUIRED"
204
- puts "="*60
194
+ puts "\n#{'=' * 60}"
195
+ puts '⚠️ CONFIRMATION REQUIRED'
196
+ puts '=' * 60
205
197
  puts "Action: #{interaction[:content]}"
206
-
207
- if interaction[:options][:risk_level]
208
- puts "Risk Level: #{interaction[:options][:risk_level]}"
209
- end
210
-
211
- if interaction[:options][:details]
212
- puts "Details: #{interaction[:options][:details]}"
213
- end
214
-
198
+
199
+ puts "Risk Level: #{interaction[:options][:risk_level]}" if interaction[:options][:risk_level]
200
+
201
+ puts "Details: #{interaction[:options][:details]}" if interaction[:options][:details]
202
+
215
203
  puts "\nConfirm this action? (yes/no)"
216
- print "> "
204
+ print '> '
217
205
  end
218
206
 
219
207
  def display_feedback_request(interaction)
220
- puts "\n" + "="*60
221
- puts "📝 FEEDBACK REQUESTED"
222
- puts "="*60
208
+ puts "\n#{'=' * 60}"
209
+ puts '📝 FEEDBACK REQUESTED'
210
+ puts '=' * 60
223
211
  puts "Request: #{interaction[:content]}"
224
-
212
+
225
213
  puts "\nPlease provide your feedback:"
226
- print "> "
214
+ print '> '
227
215
  end
228
216
 
229
217
  def get_user_input(interaction)
230
218
  start_time = Time.now
231
-
219
+
232
220
  begin
233
- if STDIN.tty?
221
+ if $stdin.tty?
234
222
  # Interactive terminal input with timeout
235
223
  response = nil
236
224
  input_thread = Thread.new do
237
- response = STDIN.gets&.chomp
225
+ response = $stdin.gets&.chomp
238
226
  end
239
-
227
+
240
228
  unless input_thread.join(interaction[:timeout])
241
229
  input_thread.kill
242
230
  puts "\n⏰ Input timed out after #{interaction[:timeout]} seconds"
243
231
  return nil
244
232
  end
245
-
233
+
246
234
  response
247
235
  else
248
236
  # Non-interactive mode (e.g., scripts, tests)
249
- puts "⚠️ Non-interactive mode detected, using default response"
237
+ puts '⚠️ Non-interactive mode detected, using default response'
250
238
  get_default_response(interaction[:type])
251
239
  end
252
240
  rescue Interrupt
@@ -270,20 +258,20 @@ module RCrewAI
270
258
  end
271
259
  end
272
260
 
273
- def process_approval_response(response, interaction)
261
+ def process_approval_response(response, _interaction)
274
262
  return { approved: false, reason: 'No response provided' } if response.nil?
275
-
263
+
276
264
  response_clean = response.strip.downcase
277
-
265
+
278
266
  approved = if @approval_keywords.any? { |keyword| response_clean.include?(keyword) }
279
267
  true
280
268
  elsif @rejection_keywords.any? { |keyword| response_clean.include?(keyword) }
281
269
  false
282
270
  else
283
271
  # Try to interpret ambiguous responses
284
- response_clean.length > 0 && !response_clean.start_with?('n')
272
+ response_clean.length.positive? && !response_clean.start_with?('n')
285
273
  end
286
-
274
+
287
275
  {
288
276
  approved: approved,
289
277
  response: response,
@@ -294,10 +282,10 @@ module RCrewAI
294
282
 
295
283
  def process_input_response(response, interaction)
296
284
  return { input: nil, valid: false, reason: 'No response provided' } if response.nil?
297
-
285
+
298
286
  # Validate input if validation rules provided
299
287
  validation_result = validate_input(response, interaction[:options][:validation] || {})
300
-
288
+
301
289
  {
302
290
  input: response,
303
291
  valid: validation_result[:valid],
@@ -309,10 +297,10 @@ module RCrewAI
309
297
 
310
298
  def process_choice_response(response, interaction)
311
299
  return { choice: nil, valid: false, reason: 'No response provided' } if response.nil?
312
-
300
+
313
301
  choices = interaction[:options][:choices]
314
302
  response_clean = response.strip
315
-
303
+
316
304
  # Try to match by number
317
305
  if response_clean.match?(/^\d+$/)
318
306
  choice_index = response_clean.to_i - 1
@@ -327,7 +315,7 @@ module RCrewAI
327
315
  }
328
316
  end
329
317
  end
330
-
318
+
331
319
  # Try to match by text
332
320
  selected_choice = choices.find { |choice| choice.downcase.include?(response_clean.downcase) }
333
321
  if selected_choice
@@ -339,7 +327,7 @@ module RCrewAI
339
327
  timestamp: Time.now
340
328
  }
341
329
  end
342
-
330
+
343
331
  # Invalid selection
344
332
  {
345
333
  choice: nil,
@@ -350,11 +338,11 @@ module RCrewAI
350
338
  }
351
339
  end
352
340
 
353
- def process_review_response(response, interaction)
341
+ def process_review_response(response, _interaction)
354
342
  return { feedback: nil, approved: false, reason: 'No response provided' } if response.nil?
355
-
343
+
356
344
  response_clean = response.strip.downcase
357
-
345
+
358
346
  # Check if it's a simple approval
359
347
  if @approval_keywords.any? { |keyword| response_clean == keyword }
360
348
  return {
@@ -364,7 +352,7 @@ module RCrewAI
364
352
  timestamp: Time.now
365
353
  }
366
354
  end
367
-
355
+
368
356
  # Otherwise treat as feedback
369
357
  {
370
358
  feedback: response,
@@ -381,36 +369,38 @@ module RCrewAI
381
369
 
382
370
  def validate_input(input, validation_rules)
383
371
  return { valid: true, reason: 'No validation rules' } if validation_rules.empty?
384
-
372
+
385
373
  # Check minimum length
386
374
  if validation_rules[:min_length] && input.length < validation_rules[:min_length]
387
375
  return { valid: false, reason: "Input too short (minimum #{validation_rules[:min_length]} characters)" }
388
376
  end
389
-
377
+
390
378
  # Check maximum length
391
379
  if validation_rules[:max_length] && input.length > validation_rules[:max_length]
392
380
  return { valid: false, reason: "Input too long (maximum #{validation_rules[:max_length]} characters)" }
393
381
  end
394
-
382
+
395
383
  # Check pattern matching
396
384
  if validation_rules[:pattern] && !input.match?(validation_rules[:pattern])
397
385
  return { valid: false, reason: "Input doesn't match required pattern" }
398
386
  end
399
-
387
+
400
388
  # Check required keywords
401
389
  if validation_rules[:required_keywords]
402
- missing_keywords = validation_rules[:required_keywords].reject { |keyword| input.downcase.include?(keyword.downcase) }
390
+ missing_keywords = validation_rules[:required_keywords].reject do |keyword|
391
+ input.downcase.include?(keyword.downcase)
392
+ end
403
393
  unless missing_keywords.empty?
404
394
  return { valid: false, reason: "Missing required keywords: #{missing_keywords.join(', ')}" }
405
395
  end
406
396
  end
407
-
397
+
408
398
  { valid: true, reason: 'Input passes validation' }
409
399
  end
410
400
 
411
401
  def process_input_value(input, options)
412
402
  return input unless options[:type]
413
-
403
+
414
404
  case options[:type]
415
405
  when :integer
416
406
  input.to_i
@@ -431,19 +421,19 @@ module RCrewAI
431
421
 
432
422
  def extract_suggestions(feedback)
433
423
  suggestions = []
434
-
424
+
435
425
  # Simple pattern matching for common suggestion indicators
436
426
  suggestion_patterns = [
437
427
  /(?:should|could|might want to|consider|suggest|recommend)\s+(.+?)(?:\.|$)/i,
438
428
  /(?:change|modify|update|fix|improve)\s+(.+?)(?:\.|$)/i,
439
429
  /(?:add|include|remove|delete)\s+(.+?)(?:\.|$)/i
440
430
  ]
441
-
431
+
442
432
  suggestion_patterns.each do |pattern|
443
433
  matches = feedback.scan(pattern)
444
434
  suggestions.concat(matches.flatten)
445
435
  end
446
-
436
+
447
437
  suggestions.map(&:strip).reject(&:empty?).uniq
448
438
  end
449
439
 
@@ -462,20 +452,20 @@ module RCrewAI
462
452
  interaction[:result] = result
463
453
  interaction[:completed_at] = Time.now
464
454
  interaction[:duration] = interaction[:completed_at] - interaction[:timestamp]
465
-
455
+
466
456
  @interactions << interaction
467
457
  @input_history << {
468
458
  type: interaction[:type],
469
459
  response: response,
470
460
  timestamp: Time.now
471
461
  }
472
-
462
+
473
463
  @logger.info "Human interaction completed: #{interaction[:type]} - #{result[:reason] || 'Success'}"
474
464
  end
475
465
 
476
466
  def calculate_session_duration
477
467
  return 0 if @interactions.empty?
478
-
468
+
479
469
  first = @interactions.first[:timestamp]
480
470
  last = @interactions.last[:completed_at] || @interactions.last[:timestamp]
481
471
  last - first
@@ -517,4 +507,4 @@ module RCrewAI
517
507
  )
518
508
  end
519
509
  end
520
- end
510
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'events'
4
+
5
+ module RCrewAI
6
+ # Behavior-preserving extraction of the prompt-parsed `USE_TOOL[]` /
7
+ # `FINAL_ANSWER[]` loop that lived in Agent. Used as a fallback when an
8
+ # agent's tools have no DSL schemas declared OR the configured LLM does
9
+ # not support native function calling.
10
+ class LegacyReactRunner
11
+ DEFAULT_MAX_ITERATIONS = 10
12
+
13
+ def initialize(agent:, llm:, tools:, max_iterations: DEFAULT_MAX_ITERATIONS, event_sink: nil)
14
+ @agent = agent
15
+ @llm = llm
16
+ @tools = tools
17
+ @max_iterations = max_iterations
18
+ @sink = event_sink || ->(_) {}
19
+ end
20
+
21
+ def run(messages:)
22
+ msgs = messages.dup
23
+ history = []
24
+ iter = 0
25
+ total_usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
26
+ last_reasoning = nil
27
+ last_action_result = nil
28
+
29
+ while iter < @max_iterations
30
+ iter += 1
31
+ emit(Events::IterationStart, iteration: iter, iteration_index: iter)
32
+
33
+ response = @llm.chat(messages: msgs)
34
+ accumulate_usage(total_usage, response[:usage])
35
+ reasoning = response[:content] || ''
36
+ last_reasoning = reasoning
37
+
38
+ action_result, iteration_history = parse_and_execute_actions(reasoning, iter)
39
+ history.concat(iteration_history)
40
+ last_action_result = action_result
41
+
42
+ msgs << { role: 'assistant', content: reasoning }
43
+ msgs << { role: 'user', content: action_result } if action_result && !action_result.empty?
44
+
45
+ finish_reason = response[:finish_reason]
46
+ emit(Events::IterationEnd, iteration: iter, finish_reason: finish_reason)
47
+
48
+ next unless task_complete?(reasoning, action_result) || finish_reason == :stop
49
+
50
+ final = extract_final_result(reasoning, action_result)
51
+ return finalize(content: final, history: history, iter: iter,
52
+ finish_reason: finish_reason || :stop, usage: total_usage)
53
+ end
54
+
55
+ final = extract_final_result(last_reasoning || '', last_action_result) ||
56
+ 'Task execution reached limits without clear completion'
57
+ finalize(content: final, history: history, iter: iter,
58
+ finish_reason: :max_iterations, usage: total_usage)
59
+ end
60
+
61
+ private
62
+
63
+ def parse_and_execute_actions(reasoning, iter)
64
+ results = []
65
+ iteration_history = []
66
+ reasoning.scan(/USE_TOOL\[(\w+)\]\(([^)]*)\)/).each do |tool_name, params_str|
67
+ params = parse_tool_params(params_str)
68
+ tool = find_tool(tool_name)
69
+
70
+ emit(Events::ToolCallStart, iteration: iter, tool: tool_name,
71
+ args: params, call_id: nil)
72
+
73
+ if tool.nil?
74
+ err = "tool not found: #{tool_name}"
75
+ emit(Events::ToolCallError, iteration: iter, tool: tool_name, call_id: nil, error: err)
76
+ results << "Tool #{tool_name} failed: #{err}"
77
+ next
78
+ end
79
+
80
+ started = monotonic_ms
81
+ begin
82
+ result = tool.execute(**params)
83
+ duration = monotonic_ms - started
84
+ @agent.memory.add_tool_usage(tool_name, params, result) if @agent.respond_to?(:memory) && @agent.memory
85
+ emit(Events::ToolCallResult, iteration: iter, tool: tool_name,
86
+ call_id: nil, result: result, duration_ms: duration)
87
+ iteration_history << { tool: tool_name, args: params, result: result, duration_ms: duration }
88
+ results << "Tool #{tool_name} result: #{result}"
89
+ rescue StandardError => e
90
+ emit(Events::ToolCallError, iteration: iter, tool: tool_name,
91
+ call_id: nil, error: e.message)
92
+ results << "Tool #{tool_name} failed: #{e.message}"
93
+ end
94
+ end
95
+
96
+ [results.join("\n"), iteration_history]
97
+ end
98
+
99
+ def parse_tool_params(params_str)
100
+ params = {}
101
+ return params if params_str.strip.empty?
102
+
103
+ params_str.split(',').map(&:strip).each do |pair|
104
+ key, value = pair.split('=', 2).map(&:strip)
105
+ next unless key && value
106
+
107
+ value = value.gsub(/^["']|["']$/, '')
108
+ params[key.to_sym] = value
109
+ end
110
+ params
111
+ end
112
+
113
+ def find_tool(name)
114
+ @tools.find do |t|
115
+ t.name == name || t.class.name.split('::').last.downcase == name.downcase
116
+ end
117
+ end
118
+
119
+ def task_complete?(reasoning, _action_result)
120
+ reasoning.include?('FINAL_ANSWER[') ||
121
+ reasoning.downcase.include?('task complete') ||
122
+ reasoning.downcase.include?('finished')
123
+ end
124
+
125
+ def extract_final_result(reasoning, action_result)
126
+ if (match = reasoning.match(/FINAL_ANSWER\[(.*?)\]$/m))
127
+ return match[1].strip
128
+ end
129
+
130
+ lines = reasoning.split("\n").map(&:strip).reject(&:empty?)
131
+ final_lines = lines.last(3).join(' ')
132
+ return final_lines if final_lines.length > 20
133
+
134
+ action_result
135
+ end
136
+
137
+ def emit(klass, iteration:, **attrs)
138
+ type_sym = klass.name.split('::').last
139
+ .gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
140
+ .sub(/^_/, '').to_sym
141
+ @sink.call(klass.new(
142
+ type: type_sym,
143
+ timestamp: Time.now,
144
+ agent: @agent.respond_to?(:name) ? @agent.name : nil,
145
+ iteration: iteration,
146
+ **attrs
147
+ ))
148
+ end
149
+
150
+ def accumulate_usage(total, partial)
151
+ return unless partial.is_a?(Hash)
152
+
153
+ total[:prompt_tokens] += partial[:prompt_tokens] || 0
154
+ total[:completion_tokens] += partial[:completion_tokens] || 0
155
+ total[:total_tokens] += partial[:total_tokens] || 0
156
+ end
157
+
158
+ def finalize(content:, history:, iter:, finish_reason:, usage:)
159
+ {
160
+ content: content,
161
+ tool_calls_history: history,
162
+ usage: usage,
163
+ iterations: iter,
164
+ finish_reason: finish_reason
165
+ }
166
+ end
167
+
168
+ def monotonic_ms
169
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) * 1000).to_i
170
+ end
171
+ end
172
+ end
@@ -38,4 +38,4 @@ module RCrewAI
38
38
  client.complete(prompt: prompt, **options)
39
39
  end
40
40
  end
41
- end
41
+ end