rcrewai 0.4.0 → 0.6.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.
@@ -1,202 +1,99 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require 'digest'
5
4
 
6
5
  module RCrewAI
6
+ # Cognitive memory facade. Preserves the original public API
7
+ # (add_execution / add_tool_usage / relevant_executions / tool_usage_for /
8
+ # clear_*! / stats) while delegating to semantic, optionally-persistent
9
+ # memory types (short-term, long-term, entity, tool).
10
+ #
11
+ # Zero-config: `Memory.new` uses an in-memory store and, when no embedder is
12
+ # available, falls back to lexical similarity — so existing code behaves as
13
+ # before, just with better recall once an embedder is configured.
7
14
  class Memory
8
- attr_reader :short_term, :long_term, :tool_usage
9
-
10
- def initialize
11
- @short_term = [] # Recent executions, limited size
12
- @long_term = {} # Persistent memory, keyed by task type/similarity
13
- @tool_usage = [] # Tool usage history
14
- @max_short_term = 100
15
- @similarity_threshold = 0.7
15
+ def initialize(scope: 'default', embedder: nil, store: nil, short_term_limit: 100)
16
+ @short_term = ShortTermMemory.new(scope: scope, embedder: embedder, store: store, limit: short_term_limit)
17
+ @long_term = LongTermMemory.new(scope: scope, embedder: embedder, store: store)
18
+ @entity = EntityMemory.new(scope: scope, embedder: embedder, store: store)
19
+ @tool = ToolMemory.new(scope: scope, embedder: embedder, store: store)
16
20
  end
17
21
 
22
+ # --- original API --------------------------------------------------------
23
+
18
24
  def add_execution(task, result, execution_time)
19
- execution_data = {
20
- task_name: task.name,
21
- task_description: task.description,
22
- task_type: classify_task_type(task),
23
- result: result,
24
- execution_time: execution_time,
25
- timestamp: Time.now,
26
- success: !result.to_s.downcase.include?('failed'),
27
- hash: generate_task_hash(task)
25
+ success = !result.to_s.downcase.include?('failed')
26
+ text = "Task: #{task.name}\nDescription: #{task.description}\nResult: #{truncate(result, 300)}"
27
+ metadata = {
28
+ 'task' => task.name,
29
+ 'success' => success,
30
+ 'execution_time' => execution_time,
31
+ 'result' => result.to_s
28
32
  }
29
33
 
30
- # Add to short-term memory
31
- @short_term.unshift(execution_data)
32
- @short_term = @short_term.first(@max_short_term)
33
-
34
- # Add to long-term memory if successful
35
- return unless execution_data[:success]
36
-
37
- task_type = execution_data[:task_type]
38
- @long_term[task_type] ||= []
39
- @long_term[task_type] << execution_data
40
-
41
- # Keep only best executions for each type
42
- @long_term[task_type] = @long_term[task_type]
43
- .sort_by { |e| [e[:success] ? 0 : 1, -e[:execution_time]] }
44
- .first(10)
34
+ @short_term.record(text, metadata)
35
+ if success
36
+ @long_term.record(text, metadata)
37
+ @entity.observe("#{task.description} #{result}")
38
+ end
39
+ nil
45
40
  end
46
41
 
47
42
  def add_tool_usage(tool_name, params, result)
48
- usage_data = {
49
- tool_name: tool_name,
50
- params: params,
51
- result: result,
52
- timestamp: Time.now,
53
- success: !result.to_s.downcase.include?('error')
54
- }
55
-
56
- @tool_usage.unshift(usage_data)
57
- @tool_usage = @tool_usage.first(50) # Keep last 50 tool usages
43
+ @tool.record_call(tool_name, params, result)
44
+ nil
58
45
  end
59
46
 
47
+ # Returns a formatted string of the most relevant past executions, or nil.
60
48
  def relevant_executions(task, limit = 3)
61
- task_type = classify_task_type(task)
62
- task_hash = generate_task_hash(task)
63
-
64
- # Get similar executions from both short and long term memory
65
- candidates = []
66
-
67
- # Check short-term for exact or similar matches
68
- @short_term.each do |execution|
69
- if execution[:hash] == task_hash
70
- candidates << { execution: execution, similarity: 1.0 }
71
- elsif execution[:task_type] == task_type
72
- similarity = calculate_similarity(task, execution)
73
- candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
74
- end
75
- end
49
+ query = "#{task.name} #{task.description}"
50
+ recalled = (@short_term.recall(query, limit: limit) + @long_term.recall(query, limit: limit))
51
+ seen = {}
52
+ unique = recalled.reject { |r| seen[r[:text]].tap { seen[r[:text]] = true } }
53
+ return nil if unique.empty?
76
54
 
77
- # Check long-term memory
78
- @long_term[task_type]&.each do |execution|
79
- similarity = calculate_similarity(task, execution)
80
- candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
81
- end
82
-
83
- # Sort by similarity and success, return top results
84
- relevant = candidates
85
- .sort_by { |c| [-c[:similarity], c[:execution][:success] ? 0 : 1] }
86
- .first(limit)
87
- .map { |c| format_execution_for_context(c[:execution]) }
88
-
89
- relevant.empty? ? nil : relevant.join("\n---\n")
55
+ unique.first(limit).map { |r| format_execution(r) }.join("\n---\n")
90
56
  end
91
57
 
92
58
  def tool_usage_for(tool_name, limit = 5)
93
- @tool_usage
94
- .select { |usage| usage[:tool_name] == tool_name }
95
- .first(limit)
96
- .map { |usage| format_tool_usage_for_context(usage) }
97
- .join("\n")
59
+ @tool.usage_for(tool_name, limit: limit).join("\n")
98
60
  end
99
61
 
100
62
  def clear_short_term!
101
- @short_term.clear
63
+ @short_term.clear!
102
64
  end
103
65
 
104
66
  def clear_all!
105
- @short_term.clear
106
- @long_term.clear
107
- @tool_usage.clear
67
+ @short_term.clear!
68
+ @long_term.clear!
69
+ @entity.clear!
70
+ @tool.clear!
108
71
  end
109
72
 
110
73
  def stats
111
74
  {
112
- short_term_count: @short_term.length,
113
- long_term_types: @long_term.keys.length,
114
- long_term_total: @long_term.values.flatten.length,
115
- tool_usage_count: @tool_usage.length,
116
- success_rate: calculate_success_rate
75
+ short_term_count: @short_term.count,
76
+ long_term_total: @long_term.count,
77
+ entity_count: @entity.entities.length,
78
+ tool_usage_count: @tool.count
117
79
  }
118
80
  end
119
81
 
120
- private
121
-
122
- def classify_task_type(task)
123
- description = task.description.downcase
124
-
125
- if description.include?('research') || description.include?('find') || description.include?('search')
126
- return :research
127
- end
128
- if description.include?('analyze') || description.include?('examine') || description.include?('study')
129
- return :analysis
130
- end
131
- if description.include?('write') || description.include?('create') || description.include?('compose')
132
- return :writing
133
- end
134
- if description.include?('code') || description.include?('program') || description.include?('develop')
135
- return :coding
136
- end
137
- if description.include?('plan') || description.include?('strategy') || description.include?('organize')
138
- return :planning
139
- end
140
-
141
- :general
142
- end
143
-
144
- def generate_task_hash(task)
145
- content = "#{task.name}:#{task.description}"
146
- Digest::SHA256.hexdigest(content)[0..16]
147
- end
148
-
149
- def calculate_similarity(task, execution)
150
- # Simple similarity based on common words and task type
151
- task_words = extract_keywords(task.description)
152
- execution_words = extract_keywords(execution[:task_description])
153
-
154
- common_words = (task_words & execution_words).length
155
- total_words = (task_words | execution_words).length
156
-
157
- return 0.0 if total_words.zero?
82
+ # --- new surface (optional direct access) --------------------------------
158
83
 
159
- word_similarity = common_words.to_f / total_words
84
+ attr_reader :short_term, :long_term, :entity, :tool
160
85
 
161
- # Boost similarity if task types match
162
- type_bonus = classify_task_type(task) == execution[:task_type] ? 0.2 : 0.0
163
-
164
- [word_similarity + type_bonus, 1.0].min
165
- end
166
-
167
- def extract_keywords(text)
168
- # Simple keyword extraction - remove common words
169
- stopwords = %w[the a an and or but in on at to for of with by]
170
- text.downcase.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
171
- end
172
-
173
- def format_execution_for_context(execution)
174
- success_indicator = execution[:success] ? '✓' : '✗'
175
- <<~CONTEXT
176
- #{success_indicator} Task: #{execution[:task_name]}
177
- Description: #{execution[:task_description]}
178
- Result: #{execution[:result][0..200]}#{'...' if execution[:result].length > 200}
179
- Time: #{execution[:execution_time].round(2)}s
180
- Date: #{execution[:timestamp].strftime('%Y-%m-%d %H:%M')}
181
- CONTEXT
182
- end
86
+ private
183
87
 
184
- def format_tool_usage_for_context(usage)
185
- success_indicator = usage[:success] ? '✓' : '✗'
186
- params_str = usage[:params].map { |k, v| "#{k}=#{v}" }.join(', ')
187
- <<~CONTEXT
188
- #{success_indicator} Tool: #{usage[:tool_name]}
189
- Params: #{params_str}
190
- Result: #{usage[:result][0..100]}#{'...' if usage[:result].to_s.length > 100}
191
- Date: #{usage[:timestamp].strftime('%Y-%m-%d %H:%M')}
192
- CONTEXT
88
+ def format_execution(record)
89
+ meta = record[:metadata] || {}
90
+ indicator = meta['success'] == false ? '✗' : '✓'
91
+ "#{indicator} #{record[:text]}"
193
92
  end
194
93
 
195
- def calculate_success_rate
196
- return 0.0 if @short_term.empty?
197
-
198
- successful = @short_term.count { |e| e[:success] }
199
- (successful.to_f / @short_term.length * 100).round(1)
94
+ def truncate(text, limit)
95
+ str = text.to_s
96
+ str.length > limit ? "#{str[0, limit]}..." : str
200
97
  end
201
98
  end
202
99
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RCrewAI
6
+ # Builds multimodal message content (text + images) in the OpenAI
7
+ # chat-completions format:
8
+ # [{ type: 'text', text: '...' },
9
+ # { type: 'image_url', image_url: { url: '...' } }]
10
+ #
11
+ # Local image paths are base64-encoded into data URLs; URLs pass through.
12
+ # Only OpenAI-style multimodal is supported today; other providers raise.
13
+ module Multimodal
14
+ SUPPORTED_PROVIDERS = %i[openai azure].freeze
15
+
16
+ MIME_TYPES = {
17
+ '.png' => 'image/png',
18
+ '.jpg' => 'image/jpeg',
19
+ '.jpeg' => 'image/jpeg',
20
+ '.gif' => 'image/gif',
21
+ '.webp' => 'image/webp'
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ # Returns an OpenAI-style content-parts array for the given text and
27
+ # attachments. With no attachments this is a single text part.
28
+ def content_parts(text, attachments)
29
+ parts = [{ type: 'text', text: text.to_s }]
30
+ Array(attachments).each { |att| parts << image_part(att) }
31
+ parts
32
+ end
33
+
34
+ def supported_provider?(provider)
35
+ SUPPORTED_PROVIDERS.include?(provider.to_sym)
36
+ end
37
+
38
+ def ensure_supported_provider!(provider)
39
+ return if supported_provider?(provider)
40
+
41
+ raise UnsupportedProviderError,
42
+ "multimodal attachments are not supported for provider #{provider}"
43
+ end
44
+
45
+ def image_part(attachment)
46
+ type = attachment[:type] || attachment['type']
47
+ raise UnsupportedAttachmentError, "unsupported attachment type: #{type.inspect}" unless type.to_sym == :image
48
+
49
+ url = attachment[:url] || attachment['url']
50
+ path = attachment[:path] || attachment['path']
51
+ resolved = url || data_url_for(path)
52
+
53
+ { type: 'image_url', image_url: { url: resolved } }
54
+ end
55
+
56
+ def data_url_for(path)
57
+ raise UnsupportedAttachmentError, 'image attachment needs a :url or :path' unless path
58
+
59
+ mime = MIME_TYPES[File.extname(path).downcase] || 'application/octet-stream'
60
+ encoded = Base64.strict_encode64(File.binread(path))
61
+ "data:#{mime};base64,#{encoded}"
62
+ end
63
+
64
+ class UnsupportedAttachmentError < RCrewAI::Error; end
65
+ class UnsupportedProviderError < RCrewAI::Error; end
66
+ end
67
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ # Thread-safe requests-per-minute throttle. Records call timestamps in a
5
+ # rolling 60-second window; `acquire` blocks (sleeps) until a slot is free.
6
+ #
7
+ # The clock and sleeper are injectable so tests can drive time deterministically
8
+ # without touching the wall clock. `max_rpm` of nil or 0 means unlimited.
9
+ class RateLimiter
10
+ WINDOW = 60.0
11
+
12
+ def initialize(max_rpm:, clock: nil, sleeper: nil)
13
+ @max_rpm = max_rpm
14
+ @clock = clock || -> { current_time }
15
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
16
+ @calls = []
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ # Blocks until making a call would keep us within max_rpm, then records it.
21
+ def acquire
22
+ return record_unlimited if unlimited?
23
+
24
+ loop do
25
+ wait = @mutex.synchronize do
26
+ prune(@clock.call)
27
+ if @calls.length < @max_rpm
28
+ @calls << @clock.call
29
+ return
30
+ end
31
+ # Time until the oldest in-window call ages out.
32
+ (@calls.first + WINDOW) - @clock.call
33
+ end
34
+
35
+ @sleeper.call(wait) if wait.positive?
36
+ end
37
+ end
38
+
39
+ # Number of calls currently inside the window (useful for tests/metrics).
40
+ def recent_count
41
+ @mutex.synchronize do
42
+ prune(@clock.call)
43
+ @calls.length
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def unlimited?
50
+ @max_rpm.nil? || @max_rpm.zero?
51
+ end
52
+
53
+ def record_unlimited
54
+ @mutex.synchronize { @calls << @clock.call }
55
+ nil
56
+ end
57
+
58
+ def prune(now)
59
+ cutoff = now - WINDOW
60
+ @calls.reject! { |t| t <= cutoff }
61
+ end
62
+
63
+ def current_time
64
+ # Fully-qualified: bare `Process` would resolve to RCrewAI::Process here.
65
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
66
+ end
67
+
68
+ # Wraps an LLM client so every #chat acquires a rate-limiter slot first.
69
+ # All other messages delegate to the wrapped client unchanged.
70
+ class ThrottledClient
71
+ def initialize(client, limiter)
72
+ @client = client
73
+ @limiter = limiter
74
+ end
75
+
76
+ def chat(**kwargs, &block)
77
+ @limiter.acquire
78
+ @client.chat(**kwargs, &block)
79
+ end
80
+
81
+ def respond_to_missing?(name, include_private = false)
82
+ @client.respond_to?(name, include_private)
83
+ end
84
+
85
+ def method_missing(name, *args, **kwargs, &block)
86
+ if @client.respond_to?(name)
87
+ @client.public_send(name, *args, **kwargs, &block)
88
+ else
89
+ super
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ # Similarity measures shared across Knowledge and Memory. `cosine` compares
5
+ # embedding vectors; `lexical` is the word-overlap fallback used when no
6
+ # embedder is available.
7
+ module Similarity
8
+ STOPWORDS = %w[the a an and or but in on at to for of with by is are was were be].freeze
9
+
10
+ module_function
11
+
12
+ def cosine(vec_a, vec_b)
13
+ dot = 0.0
14
+ norm_a = 0.0
15
+ norm_b = 0.0
16
+ length = [vec_a.length, vec_b.length].max
17
+ length.times do |i|
18
+ ai = (vec_a[i] || 0).to_f
19
+ bi = (vec_b[i] || 0).to_f
20
+ dot += ai * bi
21
+ norm_a += ai * ai
22
+ norm_b += bi * bi
23
+ end
24
+ return 0.0 if norm_a.zero? || norm_b.zero?
25
+
26
+ dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
27
+ end
28
+
29
+ # Jaccard-style overlap of content words. Cheap, no embeddings.
30
+ def lexical(text_a, text_b)
31
+ words_a = keywords(text_a)
32
+ words_b = keywords(text_b)
33
+ union = (words_a | words_b).length
34
+ return 0.0 if union.zero?
35
+
36
+ (words_a & words_b).length.to_f / union
37
+ end
38
+
39
+ def keywords(text)
40
+ text.to_s.downcase.split(/\W+/).reject { |w| w.length < 3 || STOPWORDS.include?(w) }
41
+ end
42
+ end
43
+ end
data/lib/rcrewai/task.rb CHANGED
@@ -9,7 +9,7 @@ module RCrewAI
9
9
  include AsyncExtensions
10
10
  include HumanInteractionExtensions
11
11
  attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async,
12
- :raw_result, :structured_output
12
+ :raw_result, :structured_output, :attachments
13
13
  attr_accessor :result, :status, :start_time, :end_time, :execution_time
14
14
 
15
15
  def initialize(name:, description:, agent: nil, **options)
@@ -21,6 +21,7 @@ module RCrewAI
21
21
  @tools = options[:tools] || [] # Additional tools for this specific task
22
22
  @async = options[:async] || false # Whether task can run asynchronously
23
23
  @callback = options[:callback] # Callback function after completion
24
+ @attachments = options[:attachments] || [] # Multimodal inputs (images)
24
25
 
25
26
  # Output processing (0.4.0)
26
27
  @output_schema = options[:output_schema] # JSON-schema for structured output
@@ -27,7 +27,7 @@ module RCrewAI
27
27
  emit(Events::IterationStart, iteration: iter, iteration_index: iter)
28
28
 
29
29
  response = @llm.chat(
30
- messages: msgs,
30
+ messages: fit_context(msgs),
31
31
  tools: @tools.map(&:json_schema),
32
32
  stream: ->(e) { @sink.call(retag(e, iter)) }
33
33
  )
@@ -80,6 +80,12 @@ module RCrewAI
80
80
 
81
81
  private
82
82
 
83
+ # Trims the message list to the model's context window when the agent
84
+ # supports it; a no-op otherwise.
85
+ def fit_context(messages)
86
+ @agent.respond_to?(:fit_context) ? @agent.fit_context(messages) : messages
87
+ end
88
+
83
89
  def tool_result_message(call_id, content)
84
90
  { role: 'tool', tool_call_id: call_id, content: content }
85
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCrewAI
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/rcrewai.rb CHANGED
@@ -22,7 +22,18 @@ require_relative 'rcrewai/events'
22
22
  require_relative 'rcrewai/sse_parser'
23
23
  require_relative 'rcrewai/pricing'
24
24
  require_relative 'rcrewai/llm_client'
25
+ require_relative 'rcrewai/similarity'
25
26
  require_relative 'rcrewai/memory'
27
+ require_relative 'rcrewai/memory/in_memory_store'
28
+ require_relative 'rcrewai/memory/sqlite_store'
29
+ require_relative 'rcrewai/memory/base_memory'
30
+ require_relative 'rcrewai/memory/short_term_memory'
31
+ require_relative 'rcrewai/memory/long_term_memory'
32
+ require_relative 'rcrewai/memory/entity_memory'
33
+ require_relative 'rcrewai/memory/tool_memory'
34
+ require_relative 'rcrewai/rate_limiter'
35
+ require_relative 'rcrewai/context_window'
36
+ require_relative 'rcrewai/multimodal'
26
37
  require_relative 'rcrewai/knowledge'
27
38
  require_relative 'rcrewai/human_input'
28
39
  require_relative 'rcrewai/tool_schema'
data/rcrewai.gemspec CHANGED
@@ -59,6 +59,7 @@ Gem::Specification.new do |spec|
59
59
  spec.add_dependency 'nokogiri', '~> 1.15'
60
60
  spec.add_dependency 'pdf-reader', '~> 2.11'
61
61
  spec.add_dependency 'ruby-openai', '~> 6.3'
62
+ spec.add_dependency 'sqlite3', '~> 2.0'
62
63
  spec.add_dependency 'thor', '~> 1.3'
63
64
 
64
65
  # Development dependencies
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcrewai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gkosmo
@@ -149,6 +149,20 @@ dependencies:
149
149
  - - "~>"
150
150
  - !ruby/object:Gem::Version
151
151
  version: '6.3'
152
+ - !ruby/object:Gem::Dependency
153
+ name: sqlite3
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '2.0'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '2.0'
152
166
  - !ruby/object:Gem::Dependency
153
167
  name: thor
154
168
  requirement: !ruby/object:Gem::Requirement
@@ -342,6 +356,7 @@ files:
342
356
  - docs/mcp.md
343
357
  - docs/superpowers/plans/2026-05-11-llm-modernization.md
344
358
  - docs/superpowers/specs/2026-05-11-llm-modernization-design.md
359
+ - docs/superpowers/specs/2026-07-06-cognitive-memory-design.md
345
360
  - docs/tutorials/advanced-agents.md
346
361
  - docs/tutorials/custom-tools.md
347
362
  - docs/tutorials/deployment.md
@@ -349,17 +364,26 @@ files:
349
364
  - docs/tutorials/index.md
350
365
  - docs/tutorials/multiple-crews.md
351
366
  - docs/upgrading-to-0.3.md
367
+ - docs/upgrading-to-0.4.md
368
+ - docs/upgrading-to-0.6.md
352
369
  - examples/async_execution_example.rb
370
+ - examples/cognitive_memory_example.rb
371
+ - examples/flow_example.rb
353
372
  - examples/hierarchical_crew_example.rb
354
373
  - examples/human_in_the_loop_example.rb
374
+ - examples/knowledge_rag_example.rb
355
375
  - examples/mcp_example.rb
356
376
  - examples/native_tools_example.rb
377
+ - examples/planning_and_training_example.rb
357
378
  - examples/streaming_example.rb
379
+ - examples/structured_output_example.rb
358
380
  - lib/rcrewai.rb
359
381
  - lib/rcrewai/agent.rb
382
+ - lib/rcrewai/agent_augmentations.rb
360
383
  - lib/rcrewai/async_executor.rb
361
384
  - lib/rcrewai/cli.rb
362
385
  - lib/rcrewai/configuration.rb
386
+ - lib/rcrewai/context_window.rb
363
387
  - lib/rcrewai/crew.rb
364
388
  - lib/rcrewai/events.rb
365
389
  - lib/rcrewai/flow.rb
@@ -386,11 +410,21 @@ files:
386
410
  - lib/rcrewai/mcp/transport/http.rb
387
411
  - lib/rcrewai/mcp/transport/stdio.rb
388
412
  - lib/rcrewai/memory.rb
413
+ - lib/rcrewai/memory/base_memory.rb
414
+ - lib/rcrewai/memory/entity_memory.rb
415
+ - lib/rcrewai/memory/in_memory_store.rb
416
+ - lib/rcrewai/memory/long_term_memory.rb
417
+ - lib/rcrewai/memory/short_term_memory.rb
418
+ - lib/rcrewai/memory/sqlite_store.rb
419
+ - lib/rcrewai/memory/tool_memory.rb
420
+ - lib/rcrewai/multimodal.rb
389
421
  - lib/rcrewai/output_schema.rb
390
422
  - lib/rcrewai/planning.rb
391
423
  - lib/rcrewai/pricing.rb
392
424
  - lib/rcrewai/process.rb
393
425
  - lib/rcrewai/provider_schema.rb
426
+ - lib/rcrewai/rate_limiter.rb
427
+ - lib/rcrewai/similarity.rb
394
428
  - lib/rcrewai/sse_parser.rb
395
429
  - lib/rcrewai/task.rb
396
430
  - lib/rcrewai/tool_runner.rb