rcrewai 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68a381d21e047e37972fda09fd7e9faa93da77e6fe01a040a4c77b9ee0066aca
4
- data.tar.gz: 3818acc6b9b6eb71d27d4099cb1c1fc574adbdd0a3a8128389803c051c364774
3
+ metadata.gz: fb90e26000ddf4a2907b06150fdc87b1707fc701c6c61ba35db33339ed6f38d4
4
+ data.tar.gz: ba3435eb1918be328ce65faf2c3000553833b09778eb3b06f2a6191cae31aab4
5
5
  SHA512:
6
- metadata.gz: 65d796bde314db55466f7f4f043db9eeb772dee3e72914d2bc27947eb6dfe84ce246b89a6181d473c3f7d175d353fbc9531914c8baa5b7b010e02229324bac27
7
- data.tar.gz: b2fffaab35dc672b12d2a5aea5fd0b02e6519b2f4a5ca98daaf66bef740551e2e674f4cc39967f3d987ed9dfe1c13e7dec4c8a30e5b86e2140242e347dd96a11
6
+ metadata.gz: 992ac3d7368cb7690f11294b78fe876fcea29d48cffd001a355471a351c22c321563fbd390a0688951be6bcbd0f177322509e59cdd19cf8073e228784d96b503
7
+ data.tar.gz: 92e7db7609d4503693396f85735fd48633cb5204a34d2431e57ffcb7c758e693bff5b1282ed716899f4242bd9a26b0483f278c991dedb13b21d05894a59b7105
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-07-06
11
+
12
+ Replaces the placeholder agent memory with a **cognitive memory** system —
13
+ semantic recall (embeddings + cosine), optional SQLite persistence, and four
14
+ memory types. This is the one area where CrewAI advanced past what the gem
15
+ originally ported. Backward compatible: the `Memory` public API is unchanged and
16
+ the zero-config default behaves as before.
17
+
18
+ ### Added
19
+ - Cognitive memory: `RCrewAI::Memory` is now a semantic, optionally-persistent, multi-type memory system, replacing the previous word-overlap placeholder. It composes four memory types — `ShortTermMemory` (recent executions, capped), `LongTermMemory` (durable, deduped insights), `EntityMemory` (facts about entities seen in work), and `ToolMemory` (tool-call history) — behind the original `Memory` public API.
20
+ - Semantic recall: pass `Agent.new(memory: { embedder: RCrewAI::Knowledge::Embedder.new })` to recall conceptually related past work via embeddings + cosine similarity. Without an embedder, recall falls back to lexical (word-overlap) similarity — and embedding failures fall back gracefully, so memory never breaks execution.
21
+ - Persistent memory: `RCrewAI::Memory::SqliteStore` persists memories to SQLite (vectors packed as floats, metadata as JSON) so recall survives restarts. Default store is `InMemoryStore` (volatile). The store interface is pluggable.
22
+ - Memory scoping: each agent's memory is scoped to its name by default, so agents sharing a persistent store don't cross-read; override via `memory: { scope: ... }`.
23
+ - `RCrewAI::Similarity` — shared cosine + lexical similarity helpers (extracted from `Knowledge::Store`, now used by both Knowledge and Memory).
24
+ - `sqlite3` added as a runtime dependency (required lazily; only needed for `SqliteStore`).
25
+
26
+ ### Changed
27
+ - `Agent.new(memory:)` accepts a pre-built `Memory`, an options hash (`{ embedder:, store:, scope:, short_term_limit: }`), or nothing (zero-config default, unchanged behavior).
28
+
10
29
  ## [0.5.0] - 2026-07-03
11
30
 
12
31
  Polish release completing the roadmap backlog: crew lifecycle hooks, batch
@@ -179,7 +198,8 @@ output, guardrails, planning, and training/testing. See `ROADMAP.md`.
179
198
  - CLI usage documentation
180
199
  - Real-world use cases and examples
181
200
 
182
- [Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.5.0...HEAD
201
+ [Unreleased]: https://github.com/gkosmo/rcrewAI/compare/v0.6.0...HEAD
202
+ [0.6.0]: https://github.com/gkosmo/rcrewAI/compare/v0.5.0...v0.6.0
183
203
  [0.5.0]: https://github.com/gkosmo/rcrewAI/compare/v0.4.0...v0.5.0
184
204
  [0.4.0]: https://github.com/gkosmo/rcrewAI/compare/v0.3.0...v0.4.0
185
205
  [0.3.0]: https://github.com/gkosmo/rcrewAI/compare/v0.1.0...v0.3.0
data/README.md CHANGED
@@ -13,7 +13,7 @@ RCrewAI is a Ruby implementation of the CrewAI framework, allowing you to create
13
13
  - **🤖 Intelligent Agents**: AI agents with reasoning loops, memory, and tool usage capabilities
14
14
  - **🔗 Multi-LLM Support**: OpenAI, Anthropic (Claude), Google (Gemini), Azure OpenAI, and Ollama
15
15
  - **🛠️ Rich Tool Ecosystem**: Web search, file operations, SQL, email, code execution, PDF processing, and custom tools
16
- - **🧠 Agent Memory**: Short-term and long-term memory for learning from past executions
16
+ - **🧠 Cognitive Memory**: Semantic recall (embeddings + cosine) with optional SQLite persistence and short-term/long-term/entity/tool memory types
17
17
  - **🤝 Human-in-the-Loop**: Interactive approval workflows, human guidance, and collaborative decision making
18
18
  - **⚡ Advanced Task System**: Dependencies, retries, async/concurrent execution, and context sharing
19
19
  - **🏗️ Hierarchical Teams**: Manager agents that coordinate and delegate tasks to specialist agents
@@ -374,6 +374,31 @@ Embeddings default to OpenAI's `text-embedding-3-small`; pass a custom
374
374
  `embedder:` (anything responding to `embed(texts)`) or vector store to swap the
375
375
  backend.
376
376
 
377
+ ## 🧠 Cognitive Memory
378
+
379
+ Agents remember what they've done and recall it semantically on future tasks.
380
+ Memory is zero-config by default (in-memory, lexical recall); add an embedder
381
+ for semantic recall and a SQLite store for persistence:
382
+
383
+ ```ruby
384
+ embedder = RCrewAI::Knowledge::Embedder.new
385
+ store = RCrewAI::Memory::SqliteStore.new(path: '~/.rcrewai/memory.db')
386
+
387
+ agent = RCrewAI::Agent.new(
388
+ name: 'engineer', role: '...', goal: '...',
389
+ memory: { embedder: embedder, store: store } # both optional
390
+ )
391
+ ```
392
+
393
+ - **Semantic recall** — with an embedder, an agent recalls conceptually related
394
+ past work even when the wording differs (falls back to word-overlap without one).
395
+ - **Persistence** — a `SqliteStore` makes memory survive restarts.
396
+ - **Memory types** — `agent.memory.short_term` / `long_term` / `entity` / `tool`.
397
+ - **Scoping** — memory is scoped per agent so a shared store doesn't cross-read.
398
+
399
+ Memory is best-effort: embedding failures fall back to lexical similarity, so it
400
+ never breaks agent execution.
401
+
377
402
  ## 🌊 Flows
378
403
 
379
404
  Beyond crews, RCrewAI has **Flows** — an event-driven workflow engine for
data/ROADMAP.md CHANGED
@@ -19,8 +19,7 @@ Since CrewAI's `1.0`, the framework grew a second pillar (**Flows**) plus
19
19
  **Training/Testing**. RCrewAI now implements all of these — see the matrix below.
20
20
 
21
21
  **Status: complete.** All milestone issues (#5–#12) shipped in v0.4.0, and all
22
- backlog items (#15–#20) are done and merged to `main` (awaiting the next
23
- release). There is no outstanding roadmap work.
22
+ backlog items (#15–#20) shipped in v0.5.0. There is no outstanding roadmap work.
24
23
 
25
24
  ## Parity matrix
26
25
 
@@ -0,0 +1,118 @@
1
+ # Cognitive Memory: Semantic, Persistent, Multi-Type Agent Memory
2
+
3
+ **Date:** 2026-07-06
4
+ **Status:** Approved design (pending implementation)
5
+ **Target version:** `rcrewai` 0.6.0 (current is 0.5.0)
6
+ **Scope:** Replace the placeholder `RCrewAI::Memory` with a cognitive memory system modeled on CrewAI's memory taxonomy — short-term (semantic recent recall), long-term (persistent insights), entity memory, and tool-usage memory — backed by embeddings and a persistent SQLite vector store. No new external dependencies; reuses the in-repo `Knowledge::Embedder` and cosine-search infrastructure.
7
+
8
+ ---
9
+
10
+ ## Motivation
11
+
12
+ The current `RCrewAI::Memory` is a placeholder. It matches "relevant" past executions with word-overlap similarity (`common_words / total_words`) plus a hardcoded keyword/stopword list and a `classify_task_type` that greps the description for words like "research" or "write". Nothing is embedded; nothing is persisted; everything is lost when the process exits. It is the weakest module in the codebase and it undercuts the framework's core promise — agents that learn from what they've done.
13
+
14
+ Meanwhile CrewAI has advanced *past* the version this project ported: its 2026 "cognitive memory" work adds semantic recall, distinct memory types (short-term, long-term, entity), and persistence. This is genuine parity work, not gold-plating — and the infrastructure to do it well already exists in-repo (`Knowledge::Embedder`, cosine `Knowledge::Store`), so we compose rather than build from scratch.
15
+
16
+ ## Goals
17
+
18
+ 1. **Semantic recall.** Replace word-overlap with embedding-based similarity so an agent recalls conceptually related past work, not just keyword-overlapping work.
19
+ 2. **Persistence.** Memory survives process restarts via a SQLite-backed vector store (no external service).
20
+ 3. **Memory types.** Model CrewAI's taxonomy: short-term, long-term, entity, and tool-usage memory, each with clear write/read semantics.
21
+ 4. **Backward compatibility.** The existing `Memory` public API (`add_execution`, `add_tool_usage`, `relevant_executions`, `tool_usage_for`, `clear_*!`, `stats`) keeps working, so `Agent` and the runners need no changes to function.
22
+ 5. **No new hard dependencies.** SQLite via Ruby's stdlib-adjacent `sqlite3` gem (already common in Ruby projects) — but the store is pluggable, and the default remains in-memory so the gem works with zero setup.
23
+
24
+ ## Non-goals
25
+
26
+ - External vector databases (Chroma/Qdrant/pgvector). The pluggable store interface leaves room for them later; we ship SQLite + in-memory.
27
+ - Reranking, query rewriting, or summarization of memories (CrewAI has some of this; it's a follow-up).
28
+ - Multi-tenant / shared cross-agent memory servers.
29
+
30
+ ## Decisions captured during brainstorming
31
+
32
+ | # | Decision | Choice |
33
+ |---|----------|--------|
34
+ | 1 | Similarity mechanism | Embeddings + cosine, reusing `Knowledge::Embedder` and the cosine math already in `Knowledge::Store`. |
35
+ | 2 | Persistence | Pluggable vector store; ship `InMemoryStore` (default) and `SqliteStore`. |
36
+ | 3 | Memory taxonomy | Four types: `ShortTermMemory`, `LongTermMemory`, `EntityMemory`, `ToolMemory`. |
37
+ | 4 | Backward compat | Keep the `Memory` facade and its current method signatures; delegate internally to the new types. |
38
+ | 5 | Default behavior | Zero-config: in-memory store, embeddings only if an embedder is available/keyed; graceful fallback to the current lexical similarity when no embedder. |
39
+ | 6 | Embedding failures | Never fatal. If embedding fails (no key, network), fall back to lexical similarity so agents still run. |
40
+ | 7 | Namespacing | Memories are scoped by agent (role/name) so one agent's memory doesn't leak into another, matching CrewAI's per-role collections. |
41
+
42
+ ## Architecture
43
+
44
+ ```
45
+ RCrewAI::Memory (facade — preserves today's public API)
46
+ ├── ShortTermMemory recent executions, semantic recall, capped, volatile-by-default
47
+ ├── LongTermMemory durable insights from successful executions, persistent
48
+ ├── EntityMemory facts about entities (people, systems, concepts) extracted from work
49
+ └── ToolMemory tool-call history + outcomes (replaces @tool_usage)
50
+
51
+ Each memory type holds:
52
+ - a VectorStore (InMemoryStore | SqliteStore) ← persistence + search
53
+ - an Embedder (Knowledge::Embedder | nil) ← semantic vectors
54
+ - a lexical fallback (current word-overlap) ← when no embedder
55
+ ```
56
+
57
+ ### Storage: `Memory::VectorStore`
58
+
59
+ A small interface (mirrors `Knowledge::Store` so they can converge later):
60
+
61
+ ```ruby
62
+ store.add(id:, text:, vector:, metadata:) # upsert a record
63
+ store.search(vector, k:, scope:) # cosine top-k, filtered by scope
64
+ store.all(scope:) # enumerate (for stats / lexical fallback)
65
+ store.delete(scope:) # clear a scope
66
+ ```
67
+
68
+ - **`InMemoryStore`** — array of records; cosine in Ruby (lift the existing private cosine out of `Knowledge::Store` into a shared `Similarity` helper). Default.
69
+ - **`SqliteStore`** — one row per memory: `id, scope, type, text, vector (blob), metadata (json), created_at`. Vectors stored as packed floats (`Array#pack('e*')`). Cosine computed in Ruby over candidate rows filtered by `scope`/`type` (fine for the thousands-of-memories scale agents produce; a real ANN index is a later optimization). Opened at a configurable path (default `~/.rcrewai/memory.db` or `:memory:`).
70
+
71
+ ### Embeddings
72
+
73
+ Reuse `Knowledge::Embedder`. The memory system takes an optional `embedder:`; when present, `add_*` embeds the text and stores the vector, and recall embeds the query. When absent (no key, or explicitly disabled), the store keeps `vector: nil` and recall falls back to the current lexical similarity over `store.all`. This keeps the zero-config path working.
74
+
75
+ ### The four memory types
76
+
77
+ - **ShortTermMemory** — every execution is written here; recall returns the top-k semantically similar recent items. Capped (default 100) and, by default, uses the in-memory store (volatile). This is the direct upgrade of today's `@short_term` + `relevant_executions`.
78
+ - **LongTermMemory** — successful executions promote a distilled "insight" record (task type, what worked, result summary) into a persistent store, deduped by similarity so we don't store near-identical insights repeatedly. Upgrade of today's `@long_term`.
79
+ - **EntityMemory** — optional lightweight entity extraction (heuristic first: capitalized noun phrases / quoted terms; pluggable LLM extractor later) so agents accumulate facts about recurring entities. New capability.
80
+ - **ToolMemory** — replaces `@tool_usage`; same API (`add_tool_usage`, `tool_usage_for`) but persistable and searchable.
81
+
82
+ ### Facade: `RCrewAI::Memory`
83
+
84
+ Keeps today's signatures, delegating to the types:
85
+
86
+ ```ruby
87
+ Memory.new(embedder: nil, store: nil, scope: nil, short_term_limit: 100)
88
+
89
+ add_execution(task, result, execution_time) # -> ShortTerm + (if success) LongTerm + Entity
90
+ add_tool_usage(tool_name, params, result) # -> ToolMemory
91
+ relevant_executions(task, limit = 3) # -> semantic recall across Short+Long, formatted string|nil
92
+ tool_usage_for(tool_name, limit = 5) # -> ToolMemory
93
+ clear_short_term! / clear_all! / stats # -> delegate
94
+ ```
95
+
96
+ `Agent#initialize` gains optional `memory:` config (embedder/store/persistence) but defaults to the zero-config in-memory, lexical-fallback behavior — so existing code is unchanged.
97
+
98
+ ## Backward compatibility & migration
99
+
100
+ - The `Memory` public API is preserved exactly; `agent.rb` and the runners are untouched functionally.
101
+ - Default construction (`Memory.new` with no args) behaves like today from the caller's perspective — just with better recall when an embedder is configured, and identical lexical recall when not.
102
+ - `relevant_executions` still returns the same formatted-string-or-nil shape consumed at `agent.rb:241`.
103
+
104
+ ## Testing strategy
105
+
106
+ - **Unit**: each memory type with a fake deterministic embedder (as `knowledge_spec.rb` already does) — assert semantic recall ranks the conceptually-closest item first, not the keyword-overlapping one.
107
+ - **Persistence**: `SqliteStore` round-trip — write memories, reopen the DB, recall survives.
108
+ - **Fallback**: with no embedder, recall still returns results via lexical similarity (proves the graceful path).
109
+ - **Facade compat**: the existing `memory_spec.rb` expectations continue to pass (or are updated only where the placeholder behavior was clearly a bug).
110
+ - **Agent integration**: an agent with `reasoning`/knowledge off recalls a semantically-related prior execution injected into its context.
111
+
112
+ ## Dependencies
113
+
114
+ - `sqlite3` gem — added as a runtime dependency, but only required lazily inside `SqliteStore` so the gem loads and the in-memory path works even if it's absent. (If we'd rather avoid the dependency entirely, the fallback is a JSON-file-backed store; SQLite is preferred for query/scale.)
115
+
116
+ ## Rollout
117
+
118
+ Ship in `0.6.0`. `docs/upgrading-to-0.6.md` documents opt-in persistence and embeddings; the default path is unchanged, so it's a no-action upgrade for existing users.
@@ -0,0 +1,101 @@
1
+ # Upgrading to RCrewAI 0.6
2
+
3
+ RCrewAI 0.6 replaces the placeholder memory with a **cognitive memory** system:
4
+ semantic recall (embeddings + cosine), optional SQLite persistence, and four
5
+ memory types (short-term, long-term, entity, tool).
6
+
7
+ **0.6 is backward compatible.** The `RCrewAI::Memory` public API is unchanged,
8
+ and the default (`Memory.new` with no config) behaves like before from the
9
+ caller's perspective — just with better recall once an embedder is configured.
10
+ Everything below is opt-in.
11
+
12
+ ---
13
+
14
+ ## 1. What you must do
15
+
16
+ Nothing. Upgrade the gem; existing code keeps working:
17
+
18
+ ```ruby
19
+ gem 'rcrewai', '~> 0.6'
20
+ ```
21
+
22
+ Note: `0.6` adds `sqlite3` as a runtime dependency (used only if you opt into
23
+ persistence). It is required lazily, so the in-memory default works even if the
24
+ native extension isn't present.
25
+
26
+ ---
27
+
28
+ ## 2. What you can do (new capabilities)
29
+
30
+ ### 2a. Semantic recall via embeddings
31
+
32
+ By default, memory recall uses lexical (word-overlap) similarity — same as
33
+ before. Pass an embedder to get semantic recall, so an agent remembers
34
+ conceptually related work even when the wording differs:
35
+
36
+ ```ruby
37
+ embedder = RCrewAI::Knowledge::Embedder.new # OpenAI text-embedding-3-small
38
+ agent = RCrewAI::Agent.new(
39
+ name: 'engineer', role: '...', goal: '...',
40
+ memory: { embedder: embedder }
41
+ )
42
+ ```
43
+
44
+ If embedding fails (no API key, network error), recall automatically falls back
45
+ to lexical similarity — memory never breaks agent execution.
46
+
47
+ ### 2b. Persistent memory (SQLite)
48
+
49
+ Give memory a SQLite store and it survives restarts:
50
+
51
+ ```ruby
52
+ store = RCrewAI::Memory::SqliteStore.new(path: '~/.rcrewai/memory.db')
53
+ agent = RCrewAI::Agent.new(
54
+ name: 'engineer', role: '...', goal: '...',
55
+ memory: { embedder: embedder, store: store }
56
+ )
57
+ ```
58
+
59
+ The default store is in-memory (volatile). Any object implementing the store
60
+ interface (`add` / `search` / `all` / `delete` / `delete_record`) can be used —
61
+ e.g. a future Chroma/pgvector adapter.
62
+
63
+ ### 2c. Memory scoping
64
+
65
+ Each agent's memory is scoped to its name by default, so agents sharing a
66
+ persistent store don't read each other's memories. Override with
67
+ `memory: { scope: 'custom' }` if you want deliberate sharing.
68
+
69
+ ### 2d. Memory types (direct access)
70
+
71
+ The `Memory` facade exposes the underlying types for advanced use:
72
+
73
+ ```ruby
74
+ agent.memory.short_term # recent executions, semantic recall, capped
75
+ agent.memory.long_term # durable insights from successful executions, deduped
76
+ agent.memory.entity # facts about entities (people, systems) seen in work
77
+ agent.memory.tool # tool-call history + outcomes
78
+
79
+ agent.memory.entity.entities # => ["Alice", "AWS", ...]
80
+ agent.memory.long_term.recall('...', limit: 3)
81
+ ```
82
+
83
+ ### 2e. Pre-built Memory
84
+
85
+ You can also construct and pass a `Memory` directly:
86
+
87
+ ```ruby
88
+ memory = RCrewAI::Memory.new(scope: 'shared', embedder: embedder, store: store)
89
+ agent = RCrewAI::Agent.new(name: 'a', role: '...', goal: '...', memory: memory)
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Behavior notes
95
+
96
+ - `add_execution` writes to short-term always, and (on success) promotes a
97
+ deduped insight to long-term and extracts entities.
98
+ - `relevant_executions(task, limit)` recalls across short- and long-term and
99
+ returns the same formatted-string-or-nil shape as before.
100
+ - `clear_short_term!` leaves long-term insights intact; `clear_all!` wipes
101
+ everything.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Cognitive Memory — semantic recall + persistence.
5
+ #
6
+ # Agents remember past executions and recall them by *meaning*, not just shared
7
+ # words. With a SQLite store, memory survives restarts.
8
+ #
9
+ # This example uses a fake, deterministic embedder so it runs WITHOUT an API
10
+ # key. In real use you'd pass `RCrewAI::Knowledge::Embedder.new` (OpenAI).
11
+ #
12
+ # Run:
13
+ # ruby examples/cognitive_memory_example.rb
14
+
15
+ require_relative '../lib/rcrewai'
16
+ require 'tmpdir'
17
+
18
+ RCrewAI.configure(validate: false) do |c|
19
+ c.llm_provider = :openai
20
+ c.api_key = 'demo-key'
21
+ end
22
+
23
+ # Fake embedder: maps text to a concept vector by keyword. Any object
24
+ # responding to embed(texts) -> [[float, ...], ...] works.
25
+ class ConceptEmbedder
26
+ def embed(texts)
27
+ texts.map do |t|
28
+ l = t.downcase
29
+ [
30
+ l.match?(/payment|billing|invoice|charge/) ? 1.0 : 0.0,
31
+ l.match?(/auth|login|session|token/) ? 1.0 : 0.0,
32
+ l.match?(/deploy|release|ci|pipeline/) ? 1.0 : 0.0
33
+ ]
34
+ end
35
+ end
36
+ end
37
+
38
+ Task = Struct.new(:name, :description)
39
+
40
+ Dir.mktmpdir do |dir|
41
+ store = RCrewAI::Memory::SqliteStore.new(path: File.join(dir, 'memory.db'))
42
+ agent = RCrewAI::Agent.new(
43
+ name: 'engineer', role: 'Senior engineer', goal: 'Ship reliable software',
44
+ memory: { embedder: ConceptEmbedder.new, store: store }
45
+ )
46
+
47
+ # Record a few past executions.
48
+ agent.memory.add_execution(Task.new('t1', 'fixed the billing invoice bug'),
49
+ 'adjusted the payment retry logic', 1.2)
50
+ agent.memory.add_execution(Task.new('t2', 'patched the login session flow'),
51
+ 'rotated the auth tokens', 0.8)
52
+ agent.memory.add_execution(Task.new('t3', 'sped up the release pipeline'),
53
+ 'parallelized the CI stages', 2.0)
54
+
55
+ puts '== Semantic recall =='
56
+ # Query shares NO words with the billing execution, but is conceptually close.
57
+ query = Task.new('q', 'a customer got double-charged on checkout')
58
+ puts agent.memory.relevant_executions(query, 1)
59
+
60
+ puts "\n== Persistence (reopen the DB in a fresh agent) =="
61
+ store2 = RCrewAI::Memory::SqliteStore.new(path: File.join(dir, 'memory.db'))
62
+ agent2 = RCrewAI::Agent.new(name: 'engineer', role: 'r', goal: 'g',
63
+ memory: { embedder: ConceptEmbedder.new, store: store2 })
64
+ puts agent2.memory.relevant_executions(query, 1)
65
+ puts "stats: #{agent2.memory.stats.inspect}"
66
+ end
data/lib/rcrewai/agent.rb CHANGED
@@ -39,7 +39,7 @@ module RCrewAI
39
39
  @reasoning = options.fetch(:reasoning, false)
40
40
  @max_reasoning_attempts = options.fetch(:max_reasoning_attempts, 3)
41
41
  @respect_context_window = options.fetch(:respect_context_window, false)
42
- @memory = Memory.new
42
+ @memory = build_memory(options[:memory])
43
43
  @rate_limiter = options[:max_rpm] ? RateLimiter.new(max_rpm: options[:max_rpm]) : nil
44
44
  @llm_client = wrap_with_rate_limiter(build_llm_client(options[:llm]))
45
45
  @knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
@@ -220,6 +220,17 @@ module RCrewAI
220
220
  RateLimiter::ThrottledClient.new(client, @rate_limiter)
221
221
  end
222
222
 
223
+ # Builds the agent's memory. Accepts a pre-built Memory, an options hash
224
+ # (`{ embedder:, store:, scope:, short_term_limit: }`), or nil for the
225
+ # zero-config default. Memory is scoped to the agent's name so agents don't
226
+ # share recall (matters when a persistent store is shared).
227
+ def build_memory(memory)
228
+ return memory if memory.is_a?(Memory)
229
+
230
+ opts = memory.is_a?(Hash) ? memory : {}
231
+ Memory.new(scope: opts.fetch(:scope, name), **opts.slice(:embedder, :store, :short_term_limit))
232
+ end
233
+
223
234
  # Accepts a pre-built Knowledge::Base via +knowledge:+ or an array of
224
235
  # sources via +knowledge_sources:+ (wrapped in a Base). Returns nil if
225
236
  # neither is given.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../similarity'
4
+
3
5
  module RCrewAI
4
6
  module Knowledge
5
7
  # In-memory vector store with cosine-similarity search. The default backing
@@ -38,20 +40,8 @@ module RCrewAI
38
40
 
39
41
  private
40
42
 
41
- def cosine_similarity(a, b)
42
- dot = 0.0
43
- norm_a = 0.0
44
- norm_b = 0.0
45
- a.each_index do |i|
46
- ai = a[i].to_f
47
- bi = (b[i] || 0).to_f
48
- dot += ai * bi
49
- norm_a += ai * ai
50
- norm_b += bi * bi
51
- end
52
- return 0.0 if norm_a.zero? || norm_b.zero?
53
-
54
- dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
43
+ def cosine_similarity(vec_a, vec_b)
44
+ Similarity.cosine(vec_a, vec_b)
55
45
  end
56
46
  end
57
47
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative '../similarity'
5
+ require_relative 'in_memory_store'
6
+
7
+ module RCrewAI
8
+ class Memory
9
+ # Shared behavior for the memory types: embed-on-write (when an embedder is
10
+ # present) and semantic recall with a lexical fallback when it isn't.
11
+ # Records are namespaced per (agent) scope + a type suffix so different
12
+ # memory types don't collide in a shared store.
13
+ class BaseMemory
14
+ def initialize(scope:, embedder: nil, store: nil, limit: nil)
15
+ @scope = "#{scope}:#{type_suffix}"
16
+ @embedder = embedder
17
+ @store = store || InMemoryStore.new
18
+ @limit = limit
19
+ @seq = 0
20
+ end
21
+
22
+ def record(text, metadata = {})
23
+ add(text, metadata)
24
+ end
25
+
26
+ def recall(query, limit: 3)
27
+ records = search_records(query, limit)
28
+ records.map { |r| format(r) }
29
+ end
30
+
31
+ def count
32
+ @store.all(scope: @scope).length
33
+ end
34
+
35
+ def clear!
36
+ @store.delete(scope: @scope)
37
+ end
38
+
39
+ protected
40
+
41
+ # Subclasses override to namespace their records.
42
+ def type_suffix
43
+ 'base'
44
+ end
45
+
46
+ def format(record)
47
+ { text: record[:text], metadata: record[:metadata] }
48
+ end
49
+
50
+ def add(text, metadata)
51
+ vector = embed(text)
52
+ id = next_id(text)
53
+ @store.add(id: id, text: text, vector: vector, scope: @scope, metadata: stringify(metadata))
54
+ evict_if_needed
55
+ id
56
+ end
57
+
58
+ def search_records(query, limit)
59
+ all = @store.all(scope: @scope)
60
+ return [] if all.empty?
61
+
62
+ query_vector = embed(query)
63
+ if query_vector && all.any? { |r| r[:vector] }
64
+ @store.search(query_vector, k: limit, scope: @scope)
65
+ else
66
+ lexical_search(query, all, limit)
67
+ end
68
+ end
69
+
70
+ def lexical_search(query, records, limit)
71
+ records
72
+ .map { |r| [r, Similarity.lexical(query, r[:text])] }
73
+ .sort_by { |(_r, score)| -score }
74
+ .first(limit)
75
+ .map(&:first)
76
+ end
77
+
78
+ def embed(text)
79
+ return nil unless @embedder
80
+
81
+ @embedder.embed([text]).first
82
+ rescue StandardError
83
+ nil # embedding is best-effort; fall back to lexical
84
+ end
85
+
86
+ def evict_if_needed
87
+ return unless @limit
88
+
89
+ records = @store.all(scope: @scope)
90
+ return if records.length <= @limit
91
+
92
+ # records carry a monotonic :seq in metadata; drop the oldest.
93
+ oldest = records.min_by { |r| r[:metadata]['seq'].to_i }
94
+ @store.delete_record(id: oldest[:id], scope: @scope) if @store.respond_to?(:delete_record)
95
+ end
96
+
97
+ def next_id(text)
98
+ @seq += 1
99
+ Digest::SHA256.hexdigest("#{@scope}:#{@seq}:#{text}")[0, 24]
100
+ end
101
+
102
+ def stringify(metadata)
103
+ (metadata || {}).transform_keys(&:to_s).merge('seq' => @seq)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_memory'
4
+
5
+ module RCrewAI
6
+ class Memory
7
+ # Facts about entities (people, systems, concepts) accumulated from work.
8
+ # Entities are extracted heuristically (capitalized tokens / acronyms); an
9
+ # LLM-backed extractor can be swapped in later.
10
+ class EntityMemory < BaseMemory
11
+ # Skip sentence-initial common words that happen to be capitalized.
12
+ COMMON = %w[The A An I In On At To For Of With By And Or But It This That Who Where When].freeze
13
+
14
+ def initialize(scope:, embedder: nil, store: nil, limit: nil)
15
+ super
16
+ @entities = []
17
+ end
18
+
19
+ # Records a full observation and indexes the entities it mentions.
20
+ def observe(text)
21
+ found = extract_entities(text)
22
+ @entities.concat(found)
23
+ add(text, { 'entities' => found })
24
+ end
25
+
26
+ def entities
27
+ @entities.uniq
28
+ end
29
+
30
+ protected
31
+
32
+ def type_suffix
33
+ 'entity'
34
+ end
35
+
36
+ def format(record)
37
+ record[:text]
38
+ end
39
+
40
+ private
41
+
42
+ def extract_entities(text)
43
+ text.to_s.scan(/\b([A-Z][a-zA-Z0-9]{1,})\b/).flatten.reject { |w| COMMON.include?(w) }.uniq
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../similarity'
4
+
5
+ module RCrewAI
6
+ class Memory
7
+ # Default, volatile vector store: records live in a Hash keyed by scope.
8
+ # Records: { id:, text:, vector:, metadata: }. Cosine search in Ruby.
9
+ class InMemoryStore
10
+ def initialize
11
+ @scopes = Hash.new { |h, k| h[k] = {} }
12
+ end
13
+
14
+ def add(id:, text:, vector:, scope:, metadata: {})
15
+ @scopes[scope][id] = { id: id, text: text, vector: vector, metadata: metadata || {} }
16
+ end
17
+
18
+ def all(scope:)
19
+ @scopes[scope].values
20
+ end
21
+
22
+ def search(vector, k:, scope:)
23
+ @scopes[scope].values
24
+ .reject { |r| r[:vector].nil? }
25
+ .map { |r| [r, Similarity.cosine(vector, r[:vector])] }
26
+ .sort_by { |(_r, score)| -score }
27
+ .first(k)
28
+ .map(&:first)
29
+ end
30
+
31
+ def delete(scope:)
32
+ @scopes.delete(scope)
33
+ end
34
+
35
+ def delete_record(id:, scope:)
36
+ @scopes[scope].delete(id)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_memory'
4
+
5
+ module RCrewAI
6
+ class Memory
7
+ # Durable insights promoted from successful executions. Dedupes
8
+ # near-identical insights so the store doesn't fill with paraphrases.
9
+ class LongTermMemory < BaseMemory
10
+ DEDUPE_THRESHOLD = 0.92
11
+
12
+ def record(text, metadata = {})
13
+ return nil if duplicate?(text)
14
+
15
+ add(text, metadata)
16
+ end
17
+
18
+ protected
19
+
20
+ def type_suffix
21
+ 'long_term'
22
+ end
23
+
24
+ private
25
+
26
+ def duplicate?(text)
27
+ existing = @store.all(scope: @scope)
28
+ return false if existing.empty?
29
+
30
+ query_vector = embed(text)
31
+ if query_vector && existing.any? { |r| r[:vector] }
32
+ existing.any? { |r| r[:vector] && Similarity.cosine(query_vector, r[:vector]) >= DEDUPE_THRESHOLD }
33
+ else
34
+ existing.any? { |r| Similarity.lexical(text, r[:text]) >= DEDUPE_THRESHOLD }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_memory'
4
+
5
+ module RCrewAI
6
+ class Memory
7
+ # Recent executions with semantic recall. Capped and volatile-by-default.
8
+ class ShortTermMemory < BaseMemory
9
+ def initialize(scope:, embedder: nil, store: nil, limit: 100)
10
+ super
11
+ end
12
+
13
+ protected
14
+
15
+ def type_suffix
16
+ 'short_term'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../similarity'
5
+
6
+ module RCrewAI
7
+ class Memory
8
+ # Persistent vector store backed by SQLite. Vectors are packed as
9
+ # little-endian floats; metadata is JSON. Cosine is computed in Ruby over
10
+ # rows filtered by scope — adequate for the thousands-of-memories scale an
11
+ # agent produces; an ANN index is a later optimization.
12
+ #
13
+ # The sqlite3 gem is required lazily so the rest of the library (and the
14
+ # in-memory store) works even if it isn't installed.
15
+ class SqliteStore
16
+ DEFAULT_PATH = File.join(Dir.home, '.rcrewai', 'memory.db')
17
+
18
+ def initialize(path: DEFAULT_PATH)
19
+ require 'sqlite3'
20
+ ensure_parent_dir(path) unless path == ':memory:'
21
+ @db = SQLite3::Database.new(path)
22
+ @db.results_as_hash = true
23
+ create_schema
24
+ end
25
+
26
+ def add(id:, text:, vector:, scope:, metadata: {})
27
+ @db.execute(
28
+ 'INSERT INTO memories (id, scope, text, vector, metadata) VALUES (?, ?, ?, ?, ?) ' \
29
+ 'ON CONFLICT(id) DO UPDATE SET scope=excluded.scope, text=excluded.text, ' \
30
+ 'vector=excluded.vector, metadata=excluded.metadata',
31
+ [id, scope, text, pack_vector(vector), JSON.generate(metadata || {})]
32
+ )
33
+ end
34
+
35
+ def all(scope:)
36
+ @db.execute('SELECT * FROM memories WHERE scope = ?', [scope]).map { |row| to_record(row) }
37
+ end
38
+
39
+ def search(vector, k:, scope:)
40
+ all(scope: scope)
41
+ .reject { |r| r[:vector].nil? }
42
+ .map { |r| [r, Similarity.cosine(vector, r[:vector])] }
43
+ .sort_by { |(_r, score)| -score }
44
+ .first(k)
45
+ .map(&:first)
46
+ end
47
+
48
+ def delete(scope:)
49
+ @db.execute('DELETE FROM memories WHERE scope = ?', [scope])
50
+ end
51
+
52
+ def delete_record(id:, scope:)
53
+ @db.execute('DELETE FROM memories WHERE id = ? AND scope = ?', [id, scope])
54
+ end
55
+
56
+ private
57
+
58
+ def create_schema
59
+ @db.execute(<<~SQL)
60
+ CREATE TABLE IF NOT EXISTS memories (
61
+ id TEXT PRIMARY KEY,
62
+ scope TEXT NOT NULL,
63
+ text TEXT,
64
+ vector BLOB,
65
+ metadata TEXT,
66
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
67
+ )
68
+ SQL
69
+ @db.execute('CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories (scope)')
70
+ end
71
+
72
+ def ensure_parent_dir(path)
73
+ require 'fileutils'
74
+ FileUtils.mkdir_p(File.dirname(path))
75
+ end
76
+
77
+ def pack_vector(vector)
78
+ return nil if vector.nil?
79
+
80
+ vector.map(&:to_f).pack('e*')
81
+ end
82
+
83
+ def unpack_vector(blob)
84
+ return nil if blob.nil?
85
+
86
+ blob.unpack('e*')
87
+ end
88
+
89
+ def to_record(row)
90
+ {
91
+ id: row['id'],
92
+ text: row['text'],
93
+ vector: unpack_vector(row['vector']),
94
+ metadata: JSON.parse(row['metadata'] || '{}')
95
+ }
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_memory'
4
+
5
+ module RCrewAI
6
+ class Memory
7
+ # Tool-call history and outcomes. Replaces the old @tool_usage array;
8
+ # persistable and searchable like the other memory types.
9
+ class ToolMemory < BaseMemory
10
+ def record_call(tool_name, params, result)
11
+ success = !result.to_s.downcase.include?('error')
12
+ text = "#{tool_name}(#{format_params(params)}) -> #{result}"
13
+ add(text, { 'tool' => tool_name, 'success' => success, 'result' => result.to_s })
14
+ end
15
+
16
+ # Most-recent-first usage records for a given tool.
17
+ def usage_for(tool_name, limit: 5)
18
+ @store.all(scope: @scope)
19
+ .select { |r| r[:metadata]['tool'] == tool_name }
20
+ .sort_by { |r| -r[:metadata]['seq'].to_i }
21
+ .first(limit)
22
+ .map { |r| r[:text] }
23
+ end
24
+
25
+ protected
26
+
27
+ def type_suffix
28
+ 'tool'
29
+ end
30
+
31
+ private
32
+
33
+ def format_params(params)
34
+ (params || {}).map { |k, v| "#{k}=#{v}" }.join(', ')
35
+ end
36
+ end
37
+ end
38
+ end
@@ -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,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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RCrewAI
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/rcrewai.rb CHANGED
@@ -22,7 +22,15 @@ 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'
26
34
  require_relative 'rcrewai/rate_limiter'
27
35
  require_relative 'rcrewai/context_window'
28
36
  require_relative 'rcrewai/multimodal'
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.5.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
@@ -350,7 +365,9 @@ files:
350
365
  - docs/tutorials/multiple-crews.md
351
366
  - docs/upgrading-to-0.3.md
352
367
  - docs/upgrading-to-0.4.md
368
+ - docs/upgrading-to-0.6.md
353
369
  - examples/async_execution_example.rb
370
+ - examples/cognitive_memory_example.rb
354
371
  - examples/flow_example.rb
355
372
  - examples/hierarchical_crew_example.rb
356
373
  - examples/human_in_the_loop_example.rb
@@ -393,6 +410,13 @@ files:
393
410
  - lib/rcrewai/mcp/transport/http.rb
394
411
  - lib/rcrewai/mcp/transport/stdio.rb
395
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
396
420
  - lib/rcrewai/multimodal.rb
397
421
  - lib/rcrewai/output_schema.rb
398
422
  - lib/rcrewai/planning.rb
@@ -400,6 +424,7 @@ files:
400
424
  - lib/rcrewai/process.rb
401
425
  - lib/rcrewai/provider_schema.rb
402
426
  - lib/rcrewai/rate_limiter.rb
427
+ - lib/rcrewai/similarity.rb
403
428
  - lib/rcrewai/sse_parser.rb
404
429
  - lib/rcrewai/task.rb
405
430
  - lib/rcrewai/tool_runner.rb