fleet-memory/hindsight-integrations/crewai/hindsight_crewai/_compat.py
Ben 41db2960c5
feat: add CrewAI integration for persistent crew memory (#319)
* feat: add CrewAI integration for persistent crew memory

Implements a CrewAI ExternalMemory storage backend that maps CrewAI's
Storage interface (save/search/reset) to Hindsight's retain/recall/delete
APIs, giving crews long-term memory with fact extraction, entity tracking,
and temporal awareness across runs.

Key features:
- HindsightStorage: drop-in Storage backend for CrewAI ExternalMemory
- HindsightReflectTool: BaseTool exposing Hindsight's reflect API
- Per-agent memory banks with customizable bank resolver
- Async compatibility layer for CrewAI's threading model
- 35 unit tests, docs site page, example script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move CrewAI example to hindsight-cookbook

Move research_crew.py example from hindsight-integrations/crewai/examples/
to the cookbook repo and update the integration README to link there instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add GitHub Actions test job for CrewAI integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add uv.lock for frozen installs in CI

The test-crewai-integration CI job uses `uv sync --frozen` which
requires a committed lock file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:05:52 +01:00

51 lines
1.7 KiB
Python

"""Async compatibility helpers.
CrewAI runs inside an async event loop. The Hindsight client's sync
methods internally call ``loop.run_until_complete()``, which fails
when a loop is already running or when ``asyncio.get_event_loop()``
returns a foreign loop from another thread.
This module provides a dedicated worker thread with a persistent
event loop for all Hindsight API calls, ensuring the aiohttp session
stays bound to a single, stable loop.
"""
from __future__ import annotations
import asyncio
import concurrent.futures
import threading
from typing import Any, Callable
_thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
_thread_init_lock = threading.Lock()
_initialized_threads: set[int] = set()
def _ensure_thread_loop() -> None:
"""Ensure the current thread has a persistent event loop."""
tid = threading.get_ident()
if tid not in _initialized_threads:
with _thread_init_lock:
if tid not in _initialized_threads:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
_initialized_threads.add(tid)
def call_sync(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
"""Call a sync Hindsight client method safely.
Runs the call in a dedicated thread pool where each thread has
its own persistent event loop. This avoids:
- Nested ``run_until_complete`` when CrewAI's loop is running
- Cross-thread loop references that cause "Event loop is closed"
- aiohttp session/loop binding issues
"""
def _run() -> Any:
_ensure_thread_loop()
return fn(*args, **kwargs)
future = _thread_pool.submit(_run)
return future.result(timeout=60)