feat(crewai): Add GenAI memory operation tracing for unified memory system#3713
feat(crewai): Add GenAI memory operation tracing for unified memory system#3713nagkumar91 wants to merge 1 commit intotraceloop:mainfrom
Conversation
…set) Add GenAI memory semantic convention spans for CrewAI's unified memory system: - Memory.remember() → update_memory span - Captures importance, scope, namespace, update_strategy (merge) - Records memory ID from returned MemoryRecord - Memory.recall() → search_memory span - Captures query (opt-in), scope, result count - Infers memory type from categories - Memory.forget() → delete_memory span - Captures scope, individual record ID when deleting single records - Reports deleted_count from return value - Memory.reset() → delete_memory span - Scope-level deletion with reset indicator All wrappers: - Set gen_ai.operation.name, gen_ai.system, gen_ai.provider.name - Infer gen_ai.memory.scope from MemoryScope._root path - Record gen_ai.client.operation.duration metric - Set error.type on failures - Gate content/query capture behind OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT Aligned with GenAI memory semantic conventions: open-telemetry/semantic-conventions#3250 11 new tests covering all 4 operations + content capture + error handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
|
📝 WalkthroughWalkthroughThe pull request introduces GenAI memory instrumentation for the CrewAI instrumentor, adding wrappers for memory operations (remember, recall, forget, reset) that create OpenTelemetry spans with semantic attributes. It includes environment-controlled content capture, per-operation instruments, error handling, and comprehensive test coverage for the new functionality. Changes
Sequence Diagram(s)sequenceDiagram
participant App as CrewAI App
participant Wrapper as Memory Wrapper
participant Tracer as Tracer/Span
participant Mem as Memory Engine
App->>Wrapper: Call memory operation<br/>(remember/recall/forget/reset)
Wrapper->>Tracer: Start span
Wrapper->>Tracer: Set operation attribute
Wrapper->>Tracer: Set scope/type/provider attributes
rect rgba(100, 150, 200, 0.5)
Wrapper->>Tracer: Capture content<br/>(if OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT)
end
Wrapper->>Mem: Execute operation
alt Operation Success
Mem-->>Wrapper: Return result<br/>(memory ids, counts, etc.)
Wrapper->>Tracer: Set result attributes
else Operation Error
Mem-->>Wrapper: Raise exception
Wrapper->>Tracer: Record error status
Wrapper->>Tracer: Record error type attribute
end
Wrapper->>Tracer: End span
Wrapper-->>App: Return result or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Important
Looks good to me! 👍
Reviewed everything up to ae95a6b in 14 seconds. Click for details.
- Reviewed
554lines of code in2files - Skipped
0files when reviewing. - Skipped posting
0draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
Workflow ID: wflow_xXZ8pjZxEtTZtE4w
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py (3)
351-352: Define a constant for"gen_ai.memory.id"instead of repeating raw string literals.
"gen_ai.memory.id"is used as a raw string at line 352 and again at line 431 (inwrap_memory_forget). This is inconsistent with the constants block at the top of the file and violates the guideline to leverage the semconv package (or explicit constants) for all attribute keys.♻️ Proposed fix
Add to the constants block (lines 21–37):
+_GEN_AI_MEMORY_ID = getattr(GenAIAttributes, "GEN_AI_MEMORY_ID", "gen_ai.memory.id")Then replace both raw string usages:
- set_span_attribute(span, "gen_ai.memory.id", str(result.id)) + set_span_attribute(span, _GEN_AI_MEMORY_ID, str(result.id))- set_span_attribute(span, "gen_ai.memory.id", str(record_ids[0])) + set_span_attribute(span, _GEN_AI_MEMORY_ID, str(record_ids[0]))Based on learnings: "Instrumentation packages should leverage the semantic conventions package to generate spans and tracing data compliant with OpenTelemetry semantic conventions."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py` around lines 351 - 352, Add a named constant for the attribute key used for memory IDs instead of the raw string literal; update the constants block at the top of the file (alongside existing constants) with something like GEN_AI_MEMORY_ID = "gen_ai.memory.id" and then replace the raw string usages in set_span_attribute(span, "gen_ai.memory.id", ...) inside the function where result.id is set and in wrap_memory_forget where "gen_ai.memory.id" is referenced to use the new GEN_AI_MEMORY_ID constant; ensure imports/namespace match existing style and run tests to confirm no regressions.
74-89: Narrow the exception guard to avoid swallowing programming errors.The intent is to tolerate older CrewAI versions that don't have
unified_memory. Butexcept Exception: passwill also silently absorb bugs like incorrect argument counts towrap_function_wrapper, name typos, or any exception raised during wrapping — making them very hard to diagnose. OnlyImportErrorandAttributeErrorneed to be tolerated here.🛠 Proposed fix
- except Exception: - # CrewAI versions before unified_memory may not have these classes - pass + except (ImportError, AttributeError): + # Older CrewAI versions may not have unified_memory + pass🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py` around lines 74 - 89, The broad except Exception is hiding real bugs during instrumentation; change the catch around the wrap_function_wrapper calls (wrapping "crewai.memory.unified_memory" methods Memory.remember, Memory.recall, Memory.forget, Memory.reset) to only tolerate ImportError and AttributeError (e.g., use except (ImportError, AttributeError): pass) so missing unified_memory in older CrewAI is allowed but other errors during wrap_function_wrapper remain visible.
310-480: Consider extracting the repeated span/timing/error boilerplate into a shared context manager.Each of the four memory wrappers repeats ~20 lines of identical scaffolding (timing, span creation with
GEN_AI_SYSTEM, setting_GEN_AI_OPERATION_NAME/_GEN_AI_PROVIDER_NAME, try/except/finally with_record_memory_duration). A small context manager would eliminate the duplication and make future changes (e.g., adding a new metric label) apply in one place.♻️ Example refactor sketch
from contextlib import contextmanager `@contextmanager` def _memory_span(tracer, duration_histogram, operation): error_type = None start = time.time() with tracer.start_as_current_span( f"{operation} {_PROVIDER}", kind=SpanKind.CLIENT, attributes={GenAIAttributes.GEN_AI_SYSTEM: _PROVIDER}, ) as span: set_span_attribute(span, _GEN_AI_OPERATION_NAME, operation) set_span_attribute(span, _GEN_AI_PROVIDER_NAME, _PROVIDER) try: yield span span.set_status(Status(StatusCode.OK)) except Exception as ex: error_type = _set_memory_error(span, ex) raise finally: _record_memory_duration(duration_histogram, time.time() - start, operation, error_type)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py` around lines 310 - 480, The four wrappers (wrap_memory_remember, wrap_memory_recall, wrap_memory_forget, wrap_memory_reset) duplicate span creation, timing, status/error handling and final _record_memory_duration; extract that logic into a context manager (e.g. _memory_span(tracer, duration_histogram, operation)) that starts the span with GenAIAttributes.GEN_AI_SYSTEM and sets _GEN_AI_OPERATION_NAME and _GEN_AI_PROVIDER_NAME, yields the span for function-specific attributes, sets StatusCode.OK on success, calls _set_memory_error(span, ex) in the except block, and always calls _record_memory_duration(duration_histogram, elapsed, operation, error_type) in finally; then refactor each wrapper to call tracer/_memory_span and only set the per-operation attributes (scope, type, content, ids, etc.) and invoke wrapped(*args, **kwargs) inside the context manager.packages/opentelemetry-instrumentation-crewai/tests/test_memory_instrumentation.py (2)
179-189:test_recall_erroris missing the span status assertion present intest_remember_error.
test_remember_error(line 145) checks bothspan.status.status_code == StatusCode.ERRORandattrs[_ERROR_TYPE].test_recall_erroronly checks_ERROR_TYPE, leaving the status-code path untested for the recall wrapper.💚 Proposed fix
span, attrs = _get_span(exporter) + assert span.status.status_code == StatusCode.ERROR assert attrs[_ERROR_TYPE] == "TimeoutError"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opentelemetry-instrumentation-crewai/tests/test_memory_instrumentation.py` around lines 179 - 189, The test_recall_error currently asserts only the error type attribute but omits the span status assertion; update test_recall_error (which invokes wrap_memory_recall) to mirror test_remember_error by fetching span via _get_span(exporter) and asserting span.status.status_code == StatusCode.ERROR in addition to the existing assert attrs[_ERROR_TYPE] == "TimeoutError" so the recall wrapper's error status path is covered.
37-49: Replace the hand-rolled_InMemoryExporterwith the SDK-providedInMemorySpanExporter.
opentelemetry.sdk.trace.export.in_memory_span_exporter.InMemorySpanExporteralready provides this exact functionality (includingget_finished_spans()). The custom class adds maintenance surface with no benefit.♻️ Proposed fix
-from opentelemetry.sdk.trace.export import ( - SimpleSpanProcessor, - SpanExporter, - SpanExportResult, -) +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter-class _InMemoryExporter(SpanExporter): - def __init__(self): - self.spans = [] - - def export(self, spans): - self.spans.extend(spans) - return SpanExportResult.SUCCESS - - def shutdown(self): - pass - - def get_finished_spans(self): - return list(self.spans)Update the fixture:
`@pytest.fixture`() def exporter(tracer_provider): - exp = _InMemoryExporter() + exp = InMemorySpanExporter() tracer_provider.add_span_processor(SimpleSpanProcessor(exp)) return exp🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opentelemetry-instrumentation-crewai/tests/test_memory_instrumentation.py` around lines 37 - 49, Replace the custom _InMemoryExporter class in tests (class _InMemoryExporter, its methods export/shutdown/get_finished_spans) with the SDK-provided InMemorySpanExporter: import InMemorySpanExporter from opentelemetry.sdk.trace.export.in_memory_span_exporter and use that exporter in the test fixtures where _InMemoryExporter is constructed so you no longer define export/shutdown/get_finished_spans yourself; keep usage of get_finished_spans() as-is since the SDK class provides it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py`:
- Line 325: The code currently hardcodes the memory update strategy by calling
set_span_attribute(span, _GEN_AI_MEMORY_UPDATE_STRATEGY, "merge"); instead,
derive the actual strategy from the runtime context (e.g., inspect the
Memory.remember() call arguments or the Memory instance—check
kwargs.get("strategy") or getattr(memory_instance, "strategy", None)) and only
call set_span_attribute when a concrete strategy value is present; if you cannot
determine it, omit setting the _GEN_AI_MEMORY_UPDATE_STRATEGY attribute to avoid
reporting incorrect telemetry.
- Around line 342-345: The content-capture guard currently skips keyword-only
calls and contains dead code; change the if to check content capture and
presence of content in either args or kwargs (e.g., if _capture_content() and
(args or "content" in kwargs):), then set content = args[0] if args else
kwargs.get("content"), and finally if content and isinstance(content, str): call
set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content); this removes the
unreachable branches and ensures keyword argument captures work for the content
capture logic.
- Around line 263-273: The _infer_memory_scope function currently reads a
non-existent instance._root and should be replaced to extract the scope from the
kwargs passed into Memory methods: change _infer_memory_scope to accept kwargs,
read scope = kwargs.get("scope"), validate it's a string, split/strip it and
return the first component if it's one of
("user","agent","session","team","global"), otherwise return "agent"; then
update all call sites to pass kwargs (i.e. call _infer_memory_scope(kwargs))
inside wrap_memory_remember, wrap_memory_recall, wrap_memory_forget, and
wrap_memory_reset so telemetry uses the actual scope parameter.
---
Nitpick comments:
In
`@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py`:
- Around line 351-352: Add a named constant for the attribute key used for
memory IDs instead of the raw string literal; update the constants block at the
top of the file (alongside existing constants) with something like
GEN_AI_MEMORY_ID = "gen_ai.memory.id" and then replace the raw string usages in
set_span_attribute(span, "gen_ai.memory.id", ...) inside the function where
result.id is set and in wrap_memory_forget where "gen_ai.memory.id" is
referenced to use the new GEN_AI_MEMORY_ID constant; ensure imports/namespace
match existing style and run tests to confirm no regressions.
- Around line 74-89: The broad except Exception is hiding real bugs during
instrumentation; change the catch around the wrap_function_wrapper calls
(wrapping "crewai.memory.unified_memory" methods Memory.remember, Memory.recall,
Memory.forget, Memory.reset) to only tolerate ImportError and AttributeError
(e.g., use except (ImportError, AttributeError): pass) so missing unified_memory
in older CrewAI is allowed but other errors during wrap_function_wrapper remain
visible.
- Around line 310-480: The four wrappers (wrap_memory_remember,
wrap_memory_recall, wrap_memory_forget, wrap_memory_reset) duplicate span
creation, timing, status/error handling and final _record_memory_duration;
extract that logic into a context manager (e.g. _memory_span(tracer,
duration_histogram, operation)) that starts the span with
GenAIAttributes.GEN_AI_SYSTEM and sets _GEN_AI_OPERATION_NAME and
_GEN_AI_PROVIDER_NAME, yields the span for function-specific attributes, sets
StatusCode.OK on success, calls _set_memory_error(span, ex) in the except block,
and always calls _record_memory_duration(duration_histogram, elapsed, operation,
error_type) in finally; then refactor each wrapper to call tracer/_memory_span
and only set the per-operation attributes (scope, type, content, ids, etc.) and
invoke wrapped(*args, **kwargs) inside the context manager.
In
`@packages/opentelemetry-instrumentation-crewai/tests/test_memory_instrumentation.py`:
- Around line 179-189: The test_recall_error currently asserts only the error
type attribute but omits the span status assertion; update test_recall_error
(which invokes wrap_memory_recall) to mirror test_remember_error by fetching
span via _get_span(exporter) and asserting span.status.status_code ==
StatusCode.ERROR in addition to the existing assert attrs[_ERROR_TYPE] ==
"TimeoutError" so the recall wrapper's error status path is covered.
- Around line 37-49: Replace the custom _InMemoryExporter class in tests (class
_InMemoryExporter, its methods export/shutdown/get_finished_spans) with the
SDK-provided InMemorySpanExporter: import InMemorySpanExporter from
opentelemetry.sdk.trace.export.in_memory_span_exporter and use that exporter in
the test fixtures where _InMemoryExporter is constructed so you no longer define
export/shutdown/get_finished_spans yourself; keep usage of get_finished_spans()
as-is since the SDK class provides it.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.pypackages/opentelemetry-instrumentation-crewai/tests/test_memory_instrumentation.py
| def _infer_memory_scope(instance) -> str: | ||
| """Infer memory scope from the Memory instance or its MemoryScope wrapper.""" | ||
| # MemoryScope has a _root attribute like "/agent/1" or "/user/123" | ||
| root = getattr(instance, "_root", None) | ||
| if root: | ||
| parts = root.strip("/").split("/") | ||
| if parts: | ||
| first = parts[0].lower() | ||
| if first in ("user", "agent", "session", "team", "global"): | ||
| return first | ||
| return "agent" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the instrumentation file mentioned in the review
find . -name "instrumentation.py" -path "*crewai*" | head -5Repository: traceloop/openllmetry
Length of output: 168
🏁 Script executed:
# Search for Memory and MemoryScope references in the codebase
rg "class Memory|class MemoryScope" -A 5Repository: traceloop/openllmetry
Length of output: 47
🏁 Script executed:
# Look for unified_memory references
rg "unified_memory\|MemoryScope\|_root" --type pyRepository: traceloop/openllmetry
Length of output: 47
🏁 Script executed:
# Read the instrumentation.py file to see the complete context and usage
cat -n ./packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py | head -300Repository: traceloop/openllmetry
Length of output: 14435
🏁 Script executed:
# Search for calls to _infer_memory_scope
rg "_infer_memory_scope" --type py -B 3 -A 3Repository: traceloop/openllmetry
Length of output: 5424
🏁 Script executed:
# Check dependencies for CrewAI version
find . -name "pyproject.toml" -o -name "requirements.txt" | xargs grep -l "crewai"Repository: traceloop/openllmetry
Length of output: 204
🏁 Script executed:
# Check the crewai dependency version in the CrewAI instrumentation package
cat ./packages/opentelemetry-instrumentation-crewai/pyproject.toml | grep -A 20 "dependencies"Repository: traceloop/openllmetry
Length of output: 926
🏁 Script executed:
# Now let's search for the Memory class usage and see what object is actually passed to _infer_memory_scope
# Look at the memory wrapper functions
sed -n '300,450p' ./packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.pyRepository: traceloop/openllmetry
Length of output: 6426
🏁 Script executed:
# Look for tests of the memory instrumentation to see how Memory objects are used
find . -name "*test*" -type f -path "*crewai*" | head -10Repository: traceloop/openllmetry
Length of output: 234
🏁 Script executed:
# Look for any mocks or references to Memory structure in tests
rg "Memory\|_root\|MemoryScope" --type py -A 2 -B 2 ./packages/opentelemetry-instrumentation-crewai/tests/ 2>/dev/null || echo "No tests directory found"Repository: traceloop/openllmetry
Length of output: 89
🏁 Script executed:
# Check if there's documentation or comments about Memory structure elsewhere
rg "unified_memory\|MemoryScope" --type py ./packages/opentelemetry-instrumentation-crewai/Repository: traceloop/openllmetry
Length of output: 47
🏁 Script executed:
# Check the memory instrumentation tests to see how Memory objects are used
cat ./packages/opentelemetry-instrumentation-crewai/tests/test_memory_instrumentation.pyRepository: traceloop/openllmetry
Length of output: 7888
🏁 Script executed:
# Let's check if there's any documentation or comments about Memory's _root attribute
# in the instrumentation code
rg "Memory\|_root" --type py -B 3 -A 3 ./packages/opentelemetry-instrumentation-crewai/Repository: traceloop/openllmetry
Length of output: 47
🏁 Script executed:
# Let's also check the git history to see if there are comments about this
git log --oneline -n 20 -- ./packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py 2>/dev/null || echo "Git history not available"Repository: traceloop/openllmetry
Length of output: 147
🌐 Web query:
CrewAI Memory class _root attribute MemoryScope unified_memory
💡 Result:
In CrewAI’s unified memory system, MemoryScope is essentially a scoped “view” over a single underlying Memory instance: it limits operations (remember/recall/forget, etc.) to one branch of the scope tree (e.g. /agent/researcher). The private MemoryScope._root attribute is the internal reference to that underlying root Memory object that actually performs the storage/retrieval; you typically shouldn’t access _root directly—use the public API like memory.scope("/path"). [1]
unified_memory is also used by CrewAI as the source_type emitted in memory-related events for this unified system. [1]
If you’re seeing inconsistent behavior/imports (e.g., from crewai import Memory not working), it may be because your installed crewai package version differs from what the docs/repo show; CrewAI community reports mismatches between “separate memory classes” vs the newer unified-memory exports (crewai.memory.unified_memory.Memory, MemoryScope, etc.). [2]
Sources: [1] CrewAI Memory docs (unified memory, MemoryScope, unified_memory events) https://docs.crewai.com/ko/concepts/memory, [2] CrewAI community thread showing unified-memory module exports vs older imports https://community.crewai.com/t/was-getting-error-while-importing-memory-using-the-same-method-displayed-in-docs/7344
🌐 Web query:
crewai 0.80.0 Memory class unified_memory source code
💡 Result:
In crewai==0.80.0 there is no unified_memory module / Memory class under crewai.memory.unified_memory (those paths 404 at the 0.80.0 tag). [2]
The Memory class that exists in 0.80.0 is the base class at:
src/crewai/memory/memory.py[1]
Source code (as of tag 0.80.0):
from typing import Any, Dict, Optional, List
from crewai.memory.storage.rag_storage import RAGStorage
class Memory:
""" Base class for memory, now supporting agent tags and generic metadata.
"""
def __init__(self, storage: RAGStorage):
self.storage = storage
def save(
self,
value: Any,
metadata: Optional[Dict[str, Any]] = None,
agent: Optional[str] = None,
) -> None:
metadata = metadata or {}
if agent:
metadata["agent"] = agent
self.storage.save(value, metadata)
def search(self, query: str) -> List[Dict[str, Any]]:
return self.storage.search(query)
``` [1]
**Sources:** [1] [2]
---
🌐 Web query:
`CrewAI Memory class attributes scope root unified_memory`
💡 Result:
In CrewAI’s **unified `Memory`** system:
- **`scope`**: a hierarchical “filesystem-like” path that partitions memories (e.g. `/`, `/project/alpha`, `/agent/researcher`). Recalls can be limited to a scope/branch, and `memory.scope("/agent/researcher")` returns a scoped view (a `MemoryScope`) that only reads/writes within that subtree. [1]
- **`root`**: the **root scope is `/`** (the top of the scope tree). [1]
- **`unified_memory`**: memory operations emit events tagged with **`source_type="unified_memory"`** (useful for filtering memory-related events in listeners). [2]
**Sources:** CrewAI Memory docs (scopes/root + `MemoryScope`) [1], CrewAI Memory docs (events `source_type="unified_memory"`) [2].
---
🌐 Web query:
`crewai unified_memory Memory class version support when added`
💡 Result:
CrewAI’s **unified memory `Memory` class** (i.e., `from crewai import Memory`, implemented in `crewai.memory.unified_memory`) was introduced by PR **“New Unified Memory System” `#4420`**, which was **merged on Feb 14, 2026**. [1]
The first CrewAI release that explicitly includes this change is the **pre-release `1.10.0a1` (Feb 19, 2026)**, whose release notes list **“New Unified Memory System”**. [2]
This unified system replaces the older separate memory types (Short/Long-term, Entity, External) with a single `Memory` API, as reflected in the current docs. [1][3]
**Sources**
[1] PR `#4420` (merged Feb 14, 2026) – crewAIInc/crewAI
[2] GitHub Releases page (shows `1.10.0a1`, Feb 19, 2026, includes “New Unified Memory System”) – crewAIInc/crewAI
[3] CrewAI docs: Memory concept page describing the unified `Memory` class – docs.crewai.com
---
🌐 Web query:
`site:github.com crewai memory.unified_memory Memory _root attribute`
💡 Result:
In the current CrewAI codebase, `crewai.memory.unified_memory.Memory` **does not have** a `_root` attribute—it’s a regular Python class (not a Pydantic RootModel). Its internal state is kept in attributes like `_config`, `_storage`, `_llm_instance`, etc. [1]
If you’re trying to work with the “root” of the memory hierarchy, CrewAI models that as a **scope path string**, where the root scope is `"/"` (and APIs take `scope` / `scope_prefix`, e.g., storage `reset(scope_prefix=...)`). [2]
**Sources**: [1], [2]
---
</details>
**Replace `_infer_memory_scope` to extract scope from `kwargs` instead of relying on a non-existent `instance._root` attribute.**
The CrewAI `Memory` class (both in versions 0.80.0 and the current unified_memory system) does **not** have a `_root` attribute. The current implementation will always return `"agent"` as a fallback, silently producing incorrect scope values in telemetry for all non-agent memory operations.
The scope information is already available in the `kwargs` passed to `Memory.remember()`, `Memory.recall()`, `Memory.forget()`, and `Memory.reset()` as the `scope` parameter. Extract and parse the scope from `kwargs` instead:
```python
def _infer_memory_scope(kwargs) -> str:
"""Infer memory scope from the scope kwarg."""
scope = kwargs.get("scope")
if scope and isinstance(scope, str):
parts = scope.strip("/").split("/")
if parts:
first = parts[0].lower()
if first in ("user", "agent", "session", "team", "global"):
return first
return "agent"Then update all call sites to pass kwargs instead of instance:
_infer_memory_scope(kwargs)inwrap_memory_remember_infer_memory_scope(kwargs)inwrap_memory_recall_infer_memory_scope(kwargs)inwrap_memory_forget_infer_memory_scope(kwargs)inwrap_memory_reset
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py`
around lines 263 - 273, The _infer_memory_scope function currently reads a
non-existent instance._root and should be replaced to extract the scope from the
kwargs passed into Memory methods: change _infer_memory_scope to accept kwargs,
read scope = kwargs.get("scope"), validate it's a string, split/strip it and
return the first component if it's one of
("user","agent","session","team","global"), otherwise return "agent"; then
update all call sites to pass kwargs (i.e. call _infer_memory_scope(kwargs))
inside wrap_memory_remember, wrap_memory_recall, wrap_memory_forget, and
wrap_memory_reset so telemetry uses the actual scope parameter.
| set_span_attribute(span, _GEN_AI_PROVIDER_NAME, _PROVIDER) | ||
| set_span_attribute(span, _GEN_AI_MEMORY_SCOPE, _infer_memory_scope(instance)) | ||
| set_span_attribute(span, _GEN_AI_MEMORY_TYPE, _infer_memory_type(kwargs)) | ||
| set_span_attribute(span, _GEN_AI_MEMORY_UPDATE_STRATEGY, "merge") |
There was a problem hiding this comment.
Hardcoded "merge" strategy may produce inaccurate telemetry.
_GEN_AI_MEMORY_UPDATE_STRATEGY is set to "merge" regardless of what CrewAI's Memory.remember() actually does. If CrewAI uses other strategies (e.g., "overwrite", "append") depending on instance configuration or call arguments, every span will report the wrong strategy. Either read this value from the instance/kwargs, or omit the attribute until the actual strategy is observable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py`
at line 325, The code currently hardcodes the memory update strategy by calling
set_span_attribute(span, _GEN_AI_MEMORY_UPDATE_STRATEGY, "merge"); instead,
derive the actual strategy from the runtime context (e.g., inspect the
Memory.remember() call arguments or the Memory instance—check
kwargs.get("strategy") or getattr(memory_instance, "strategy", None)) and only
call set_span_attribute when a concrete strategy value is present; if you cannot
determine it, omit setting the _GEN_AI_MEMORY_UPDATE_STRATEGY attribute to avoid
reporting incorrect telemetry.
| if _capture_content() and args: | ||
| content = args[0] if args else kwargs.get("content") | ||
| if content and isinstance(content, str): | ||
| set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content) |
There was a problem hiding this comment.
Content capture silently fails for keyword-only callers, and contains dead code.
Two issues:
-
The guard
if _capture_content() and args:skips capture entirely when content is passed as a keyword argument (e.g.,remember(content="...")), even if content capture is enabled. Thekwargs.get("content")fallback inside the block is unreachable becauseargsis always truthy at that point. -
The inner ternary
args[0] if args else kwargs.get("content")is dead code: the outer condition already guaranteesargsis truthy.
🐛 Proposed fix
- # Content (opt-in)
- if _capture_content() and args:
- content = args[0] if args else kwargs.get("content")
- if content and isinstance(content, str):
- set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content)
+ # Content (opt-in)
+ if _capture_content():
+ content = args[0] if args else kwargs.get("content")
+ if content and isinstance(content, str):
+ set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if _capture_content() and args: | |
| content = args[0] if args else kwargs.get("content") | |
| if content and isinstance(content, str): | |
| set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content) | |
| # Content (opt-in) | |
| if _capture_content(): | |
| content = args[0] if args else kwargs.get("content") | |
| if content and isinstance(content, str): | |
| set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/opentelemetry-instrumentation-crewai/opentelemetry/instrumentation/crewai/instrumentation.py`
around lines 342 - 345, The content-capture guard currently skips keyword-only
calls and contains dead code; change the if to check content capture and
presence of content in either args or kwargs (e.g., if _capture_content() and
(args or "content" in kwargs):), then set content = args[0] if args else
kwargs.get("content"), and finally if content and isinstance(content, str): call
set_span_attribute(span, _GEN_AI_MEMORY_CONTENT, content); this removes the
unreachable branches and ensures keyword argument captures work for the content
capture logic.
Summary
Adds memory operation tracing to the CrewAI instrumentor, aligned with the GenAI memory semantic conventions proposed in open-telemetry/semantic-conventions#3250.
The existing instrumentor traces
Crew.kickoff,Agent.execute_task,Task.execute_sync, andLLM.call— but does not trace memory operations. CrewAI's new unified memory system (crewai.memory.unified_memory.Memory) has first-classremember(),recall(),forget(), andreset()methods that are critical for observability.What changed
New wrappers in
instrumentation.py:Memory.remember()update_memoryMemory.recall()search_memoryMemory.forget()delete_memoryMemory.reset()delete_memoryAll wrappers emit:
gen_ai.operation.name— memory operation typegen_ai.system/gen_ai.provider.name—crewaigen_ai.memory.scope— inferred fromMemoryScope._rootpath (user/agent/session/team/global)gen_ai.memory.type— inferred from categories (short_term/long_term/entity)gen_ai.client.operation.duration— histogram metricerror.type— on failuresOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTGraceful degradation:
Memory wrapping is in a try/except — older CrewAI versions without
unified_memorywon't break.How has this been tested?
11 new tests in
tests/test_memory_instrumentation.py:TestMemoryRemember: 5 tests (basic, scope inference, content capture, no-content default, error)TestMemoryRecall: 3 tests (basic + result count, query capture, error)TestMemoryForget: 2 tests (scope-based + single record)TestMemoryReset: 1 test (scope-level delete)Related PRs
Important
Adds memory operation tracing for CrewAI's unified memory system, with tests for span attributes and error handling.
Memory.remember(),Memory.recall(),Memory.forget(), andMemory.reset()ininstrumentation.py.gen_ai.operation.name,gen_ai.system,gen_ai.memory.scope, anderror.type.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.unified_memory.test_memory_instrumentation.pywith 11 tests for memory operations.This description was created by
for ae95a6b. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
Release Notes
New Features
Tests