Skip to main content

Cortadel × CrewAI

Cortadel is self-hosted long-term memory for AI agents — a bi-temporal graph store with hybrid BM25 + vector search. This package makes it a drop-in replacement for CrewAI's built-in memory, so a crew remembers what it learned in previous runs instead of starting from zero every kickoff. Point it at a Cortadel server and the retrieval, embedding, deduplication, entity extraction, and versioning all happen server-side.

Install

pip install cortadel-crewai

Quickstart

from crewai import Agent, Crew, Task
from cortadel_crewai import CortadelMemory

# One Cortadel client is bound to one user id, so one CortadelMemory == one user.
memory = CortadelMemory(
    base_url="http://localhost:3001",
    user_id="e2e-crewai-demo",
)

researcher = Agent(
    role="Research Analyst",
    goal="Answer questions using everything the team already knows",
    backstory="You have a long memory and hate repeating work.",
)

task = Task(
    description="What deployment target did we settle on, and why?",
    expected_output="A short answer citing the earlier decision.",
    agent=researcher,
)

crew = Crew(agents=[researcher], tasks=[task], memory=memory)
print(crew.kickoff())

That is the whole integration. Before each task the agent recalls from Cortadel; anything it chooses to save goes back to Cortadel.

What you get

1. CortadelMemory — automatic memory

A real subclass of crewai.Memory, accepted anywhere CrewAI takes a memory instance: Crew(memory=...), Agent(memory=...), or a scoped view via memory.scope("/agent/researcher").

It overrides the text-level API that every CrewAI consumer actually calls:

CrewAI call Cortadel call
recall(query, limit=N)Agent before each task, LiteAgent, Flow, RecallMemoryTool search(query, SearchOptions(top_k=N, mode, rerank))
remember(content, ...) / remember_many([...])RememberTool and any explicit save add(text, AddOptions(...))
remember_conversation(messages) (added by this package) add_conversation(messages, ConversationOptions(...))
list_records(limit, offset) list(ListOptions(page, size))
forget(record_ids=[...]) delete(ids)

Because it is a genuine Memory, CrewAI's own built-in Search memory / Save to memory tools (create_memory_tools) route to Cortadel automatically too.

2. cortadel_tools() — agent-invoked memory

Native crewai.tools.BaseTool subclasses with pydantic args_schema models, for when the agent should decide when to remember:

from crewai import Agent
from cortadel_crewai import cortadel_tools

agent = Agent(
    role="Support Engineer",
    goal="Resolve tickets without asking the customer to repeat themselves",
    backstory="You look things up before you ask.",
    tools=cortadel_tools(base_url="http://localhost:3001", user_id="e2e-crewai-demo"),
)

This gives the agent search_memory and add_memories. Works with or without CortadelMemory.

3. CortadelConversationListener — turn-level capture

An event-bus listener (crewai.events.BaseEventListener) that hands each completed task to Cortadel's extraction pipeline, so durable facts are distilled without the agent deciding anything:

from cortadel_crewai import CortadelConversationListener

listener = CortadelConversationListener(user_id="e2e-crewai-demo", session_id="nightly-run")
crew.kickoff()   # each finished task is persisted via add_conversation

Keep a reference to the listener for as long as you want it active.

Configuration

CortadelMemory accepts every crewai.Memory field (read_only, root_scope, default_importance, …) plus:

Option Type Default Meaning
base_url str $CORTADEL_BASE_URL or http://localhost:3001 Cortadel server URL
user_id str $CORTADEL_USER_ID User that owns the memories. Required — one instance per user
api_key str | None $CORTADEL_API_KEY Bearer token; omit when the server runs with auth disabled
app_name str "cortadel-crewai" Recorded for access logging on searches
client SyncCortadelClient | None None Inject a pre-built client instead of constructing one
search_mode str "hybrid" hybrid, text, or vector
rerank str | None None Set to "cross_encoder" to rerank with Cortadel's cross-encoder
memory_type str | None None Pin/filter a cognitive type: episodic, semantic, procedural
infer bool True False stores text verbatim and skips entity/category extraction
raise_on_error bool False True propagates Cortadel errors instead of degrading
on_error Callable[[BaseException], None] | None None Called with the exception on every Cortadel failure
dedupe_window int 0 Suppress the last N memory ids already returned by recall

cortadel_tools() takes base_url / user_id / api_key / app_name / client / raise_on_error / on_error. CortadelConversationListener takes those seven plus session_id (groups every fact extracted during a run), project (e.g. a repository name), tags (applied to every extracted fact), is_agent_memory (True extracts facts about the assistant — usually what a self-improving crew wants) and min_output_chars (default 1; skip tasks whose output is shorter).

The search_memory tool takes query and top_k (default 10, clamped to Cortadel's 1–50 range). CortadelMemory.recall() keeps CrewAI's own parameter name, limit, because it overrides crewai.Memory.recall and the framework calls it by keyword (recall(query, limit=5) in crewai/agent/core.py); it maps straight onto SearchOptions.top_k.

Degrading gracefully

By default every Cortadel call is guarded: if the server is unreachable, the call logs a warning to the cortadel_crewai logger and returns an empty result. An agent runs without memory rather than crashing — a memory outage should never take the crew down. Set raise_on_error=True to opt into hard failures.

on_error is an observer, not a handler: it is called with the exception on every failure, including the ones raise_on_error=True goes on to re-raise. Setting it replaces the default warning log, so route it wherever you actually watch (Sentry, a metric, your own logger):

memory = CortadelMemory(user_id="e2e-crewai-demo", on_error=lambda exc: sentry_sdk.capture_exception(exc))

A callback that raises is swallowed and logged, so a broken observer can never mask the original failure. On the listener, note where a re-raised error lands: its handler runs on CrewAI's event-bus thread pool, so the error surfaces on the bus Future, not in the task that produced it.

One wrinkle worth knowing: a degraded remember() returns an unpersisted placeholder record (record.id == "", record.metadata["cortadel_persisted"] is False) rather than None. CrewAI's own Save to memory tool reads record.scope / record.importance straight off that return value with no None check, so returning None would turn an unreachable server into an AttributeError inside the agent loop. remember_many() still returns only what was actually stored, so a fully degraded batch is [].

Avoiding re-injected memories

CrewAI re-recalls on every turn (Agent recalls the task description, then the whole message history at kickoff), so the same facts can land in the prompt repeatedly. Set dedupe_window=N to suppress the last N memory ids returned by recall. Note that the window counts ids, not recalls: with dedupe_window=50 and ten hits per recall it spans roughly the last five recalls. It defaults to 0 (off), because suppression is wrong when an agent legitimately asks the same question twice.

What does not map

Stated plainly, because two memory systems are meeting here and only one of them can be in charge:

  • Cortadel is a flat per-user namespace. CrewAI's scope hierarchy (/crew/research/...) and its category taxonomy are sent to Cortadel as AddOptions.metadata for provenance, but they are not retrieval filtersrecall(scope=..., categories=...) does not narrow the search. Cortadel does its own organisation (entity graph, server-inferred categories).
  • depth="deep" is ignored. CrewAI's deep recall drives a local LLM flow over a vector store; Cortadel already fuses BM25 and vector arms with RRF server-side. Running both would pay for a second, weaker retrieval brain.
  • update(), reset(), reset_all() are inert. Cortadel is bi-temporal — corrections are new writes that supersede old versions server-side, and there is no bulk-reset endpoint. To delete, pass explicit ids to forget(record_ids=[...]).
  • forget() without record_ids deletes nothing and returns 0, rather than guessing at criteria the server cannot express. With ids it returns len(record_ids), not a server-side count — Cortadel's delete answers with a confirmation string, so deleting two ids of which one never existed still reports 2.
  • list_records(offset=...) is approximate. Cortadel paginates by page/size with no row offset, so the offset is floored to a page boundary: list_records(limit=50, offset=25) returns records 0–49. Pass an offset that is a multiple of limit for an exact window.
  • Automatic post-task saves go through a CrewAI-side LLM first. This package does not override extract_memories(), so CrewAI's own auto-save (base_agent_executormemory.extract_memories(raw)memory.remember_many(...)) distils the task text with its LLM (Memory.llm, default gpt-5.4-mini) before Cortadel sees it. With no LLM key configured that step raises, CrewAI logs "Failed to save to memory", and the auto-save silently no-ops — recall still works, but nothing is written. To write without a CrewAI-side LLM, use CortadelConversationListener (which calls add_conversation directly and lets Cortadel do the extraction), cortadel_tools(), or an explicit memory.remember(...).
  • Agent(memory=...) does not survive Crew.copy(). BaseAgent.copy() re-validates the agent through a model_dump(), and CrewAI's discriminated union turns a CortadelMemory back into a plain crewai.Memory. Crew.copy() copies every agent this way, and it is used by kickoff_for_each(), train() and test(). Crew-level memory is unaffected (Crew.copy() reattaches the live instance), so with those three entry points rely on Crew(memory=...), not Agent(memory=...). A plain kickoff() keeps both.
  • Metadata does not round-trip. The Cortadel Python SDK always returns None for MemoryListItem.metadata / MemoryDetail.metadata, so the CrewAI bookkeeping written on save cannot be read back through this SDK.
  • StorageBackend is not implemented. CrewAI's storage protocol searches by query_embedding: list[float]; Cortadel's API takes a text query and embeds it itself, so that seam cannot be served faithfully. This package plugs in one level up, at the text API.

Running the tests

The suite is fully offline — no server, no API keys, no network.

uv sync --extra test
uv run pytest -q

Requirements

  • Python ≥ 3.11, < 3.14. The upper bound mirrors crewai's own range; the lower bound is one version above it because 3.10 cannot actually be installed — crewai pulls chromadb, whose Python-3.10 branch resolves to onnxruntime 1.24.3, and that release ships no cp310 wheel and no sdist at all.
  • crewai ≥ 1.10.0 — this implements the unified crewai.Memory surface, and 1.10.0 is the release that introduced it. crewai.memory.unified_memory and crewai.memory.types do not exist in 1.0.0–1.9.3, so this package cannot import on those versions. 1.10.0 is also the release that removed the older ExternalMemory + crewai.memory.storage.interface.Storage seam (which shipped right through 1.9.3); that seam is not supported here. Only crewai 1.15.15 has been tested — 1.10.0 is the true minimum, not a verified-working floor.
  • cortadel ≥ 1.0, < 2.0
  • A running Cortadel server: hosted at https://app.cortadel.ai, or self-host with docker compose uphttp://localhost:3001.

Links

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cortadel_crewai-0.1.0.tar.gz (25.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cortadel_crewai-0.1.0-py3-none-any.whl (28.5 kB view details)

Uploaded Python 3

File details

Details for the file cortadel_crewai-0.1.0.tar.gz.

File metadata

  • Download URL: cortadel_crewai-0.1.0.tar.gz
  • Upload date:
  • Size: 25.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cortadel_crewai-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0f63391256ebb47fe2148b9daf7f715ad64828fc2cf3dcd7fb724fa6ba03e027
MD5 3b1919ebd5e8df894e085b9b030ef59c
BLAKE2b-256 084336750a34abf81bcfe3490543cdbed989b70d9166425d8674337f2b118b9b

See more details on using hashes here.

File details

Details for the file cortadel_crewai-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for cortadel_crewai-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 519a3f0cc5db8cc322e0ac08e5505cf3928be82eed8d9c746a17794a8b155b30
MD5 033c89767ce782869e43a27534ec813b
BLAKE2b-256 4f1b0f1af92c4164335a0d85a01b61bb55576cf6d4b0f1fee3b138ff4eae89f3

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page