LENA
LangGraph-powered, model-agnostic agent harness.
LENA runs any AI model — GPT-5.5, Claude, Gemini, or local Ollama — through a single LangGraph state machine. Approximately 70% of the core logic was ported from a Claude Code plugin, so the orchestration patterns are battle-tested. Swap the model; the graph stays the same.
Table of Contents
Architecture
LangGraph State Machine
Every task flows through a StateGraph compiled from lena/runtime/graph.py:
session_init
└── vector_recall
└── router_node
├── [orchestrate + parallel tasks] → branch_executor (×N, parallel fan-out)
│ └── merge_node → synthesizer
├── [orchestrate, no tasks] → bd_register → executor
└── [direct / fallback] → executor
├── [action == review] → generator → critic → gate ⟲
└── [default] → synthesizer
└── consolidation_node → END
Parallel fan-out (up to 4 branches) uses LangGraph Send objects returned from the conditional edge function — each branch runs branch_executor independently and converges at merge_node.
Routing
Routing is handled by skills/lena/routing_score.py — a zero-LLM, deterministic heuristic scorer. It scores a task across six categories (task shape, domain breadth, concreteness, risk, validation need, intent verbs) and produces:
| Field | Meaning |
|---|
routing | direct, orchestrate, or fallback path |
confidence | 0–100 percentage |
domains | matched domain tags (frontend, backend, database, …) |
action | execute, execute_log, clarify_or_orchestrate, or force_orchestrate |
The threshold is configured in lena.config.yaml (routing.threshold, default 70). If the scorer exits non-zero, times out, or returns unparseable JSON, the router falls back to a safe default and lets the executor proceed.
ModelAdapter Protocol
lena/adapters/base.py defines a ModelAdapter Protocol:
class ModelAdapter(Protocol):
name: str
last_usage: dict[str, int]
def complete(
self,
messages: list[Message],
cache_breakpoints: list[int] | None = None,
model: str = "",
**kwargs,
) -> str: ...
Concrete adapters (anthropic.py, openai.py, gemini.py, ollama.py) are selected at runtime by pattern-matching the model string against lena.config.yaml. Every adapter is wrapped in MetricsAdapter to track token usage.
3-Layer Memory
| Layer | Backend | Purpose |
|---|
| Working memory | mem0 OSS + pgvector | Per-session episodic recall (vector_recall node) |
| Temporal knowledge graph | Zep OSS + Postgres | Long-term entity and fact timeline |
| Team moat | pgvector (lena_team_memory table) | Shared organizational memory, 768-dim nomic-embed-text embeddings |
Event Bus
lena/events.py exports a module-level bus: LenaEventBus — a thread-safe async pub/sub queue. Both UIs subscribe to it and render live updates. Events include NODE_ENTER, NODE_EXIT, TASK_COMPLETE, TASK_ERROR, TOKEN_USAGE, ROUTING_DECISION, and FEEDBACK_LOOP.
Prerequisites
- Python 3.11 or later
- Docker Compose (runs Postgres/pgvector, Redis, Zep, Langfuse)
- Ollama with
nomic-embed-text pulled (used for embeddings by mem0 and the team moat)
- An OpenAI API key (the only paid external dependency; required only when using
gpt-*, o1-*, or o3-* models)
Installation
1. Clone and install
git clone https://github.com/your-org/lena.git
cd lena
pip install -e .
2. Pull the embedding model
ollama pull nomic-embed-text
3. Set environment variables
Copy the table in the Environment Variables section into a .env file or your shell profile.
4. Start infrastructure
docker compose up -d
This starts:
- Postgres 16 + pgvector on
localhost:5432 (creates lena, zep, and langfuse databases automatically)
- Redis 7 on
localhost:6379
- Zep on
localhost:8000
- Langfuse (web + worker) on
localhost:3000
5. Run database migrations