LiveKit integration
Add long-term agent memory to LiveKit voice agents
The zep-livekit package adds long-term agent memory to LiveKit voice agents. It wraps LiveKit’s Agent so that completed conversation turns are persisted to Zep and relevant context is injected before each response. Choose between user thread memory or structured knowledge graph memory.
Core benefits
- Persistent voice memory: Each completed turn is stored in Zep and contributes to the user’s temporal knowledge graph
- Automatic context injection: Relevant context is retrieved and added as a system message before the agent’s next response
- Two access patterns:
ZepUserAgentfor thread-based conversation memory,ZepGraphAgentfor direct knowledge graph access - Drop-in replacement: Both classes subclass LiveKit’s
Agentand accept all standardAgentparameters
How it works
LiveKit’s AgentSession owns the audio pipeline — speech-to-text, voice activity detection, turn detection, and text-to-speech. Zep does not touch audio. Instead, the Zep agent hooks into LiveKit’s turn lifecycle and runs a write-then-read cycle on each completed user turn:
- Persist the turn — when LiveKit fires
on_user_turn_completed, the user message is written to Zep (a thread forZepUserAgent, the graph forZepGraphAgent). Assistant responses are captured separately via theconversation_item_addedsession event. - Retrieve context —
ZepUserAgentfolds persistence and retrieval into a singlethread.add_messages(..., return_context=True)round-trip;ZepGraphAgentwrites the message to the graph, then runs hybrid search across edges, nodes, and episodes. - Inject context — the retrieved context is wrapped in a context template and added to the turn as a system message, so the LLM’s next response is grounded in prior conversation.
Allow time for indexing: Turns are ingested and knowledge is extracted asynchronously, so facts from the current turn are not searchable within that same turn. Context retrieved on a given turn reflects knowledge extracted from earlier turns.
Installation
Requires Python 3.11+, zep-livekit>=0.2.0, LiveKit Agents v1.0+ (not v0.x), and zep-cloud>=3.23.0, plus a Zep Cloud API key. The examples use the v1.0 AgentSession API. Get your API key from app.getzep.com.
Set up your environment variables:
LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET come from a LiveKit Cloud project or a self-hosted LiveKit server. They configure the LiveKit infrastructure your agent connects to and are unrelated to Zep.
Upgrading from zep-livekit 0.1.x
One breaking change affects existing code: both agents wrap injected context in the shared DEFAULT_CONTEXT_TEMPLATE (<ZEP_CONTEXT>...</ZEP_CONTEXT>) rather than the per-agent “Relevant user context:” and “Relevant knowledge from memory:” prefixes. Pass context_template to override the wrapper.
See the package changelog for the full list of changes.
Agent types
ZepUserAgent: Uses user threads for conversation memory with automatic context injectionZepGraphAgent: Reads and writes a knowledge graph, optionally shaped by custom entity models
Identity and isolation
The example below derives a stable user_id from your application’s auth system and scopes the thread_id (and graph_id) to the LiveKit room. Use a stable, durable user ID — do not derive user_id from the room name. A room is a per-session construct, so a room-derived user_id fragments a returning user’s history across rooms and prevents Zep from accumulating long-term memory for that person.
Scope thread_id or graph_id to the room when you want per-session isolation while still attributing every session to the same long-lived user.
Provisioning users and threads
ensure_user and ensure_thread are idempotent create-then-catch-conflict helpers. Both return True when the resource is newly created and False when it already exists; genuine failures (auth, network, 5xx) raise. Call them before the first turn — for example during account or session onboarding — so misconfiguration surfaces loudly.
The optional on_created hook fires exactly once, only when the user is genuinely new — use it to seed initial facts, set custom instructions, or configure an ontology.
ZepUserAgent also accepts first_name, last_name, email, and on_created directly and lazily calls the same helpers on the first turn, cached per agent instance. The lazy path logs and swallows failures rather than raising into the voice session — convenient for prototyping, but prefer the explicit helpers when provisioning failures need to surface.
ZepGraphAgent does not accept on_created: it is scoped to a standalone graph_id, not a Zep user, so there is no “user created” event to hook into. Passing it raises TypeError.
User memory agent
ZepUserAgent stores each turn in a Zep thread and injects a context block before the next response.
Automatic memory integration: ZepUserAgent captures each voice turn and injects relevant context from previous conversations, enabling continuity across sessions without manual memory management.
ZepUserAgent configuration
ZepUserAgent accepts the following parameters in addition to all standard LiveKit Agent parameters (stt, llm, tts, instructions, tools, chat_ctx, etc.):
The context_mode parameter is deprecated and ignored; the Zep V3 context block returns a structured format and no longer accepts a mode selector.
Knowledge graph agent
ZepGraphAgent writes each turn directly to a knowledge graph and retrieves context with hybrid search over edges (facts), nodes (entities), and episodes. You can optionally shape the graph with custom entity models.
Search filters: The search_filters parameter constrains which results the agent retrieves. Use node_labels to filter by entity types defined in your ontology.
Graph memory context: ZepGraphAgent writes each turn to the graph and injects relevant facts, entities, and episodes as context, grounding responses in prior conversations.
ZepGraphAgent configuration
ZepGraphAgent accepts the following parameters in addition to all standard LiveKit Agent parameters:
ZepGraphAgent has no on_created parameter — it is graph-scoped, with no Zep user to provision. Passing on_created raises TypeError.
Customizing retrieved context
Both agents wrap injected context in DEFAULT_CONTEXT_TEMPLATE — a <ZEP_CONTEXT>...</ZEP_CONTEXT> block shared across Zep integrations — before adding it as a system message. Override with context_template: it must contain a literal {context} placeholder, substituted via plain string replacement (never str.format), so context text containing {, }, or % is always safe to inject.
To replace the retrieval logic itself — a filtered graph search, a different graph, or multi-source context assembly — pass context_builder. On ZepUserAgent the builder is an async callable receiving a frozen ContextInput (zep, user_id, thread_id, user_message, session) and returning the context string, or None to skip injection:
When context_builder is set on ZepUserAgent, message persistence and the builder run concurrently for lower latency, with per-side failure isolation: a builder error is logged and skips injection for that turn but does not stop persistence, and a persistence error is logged but a successful builder result is still injected.
ZepGraphAgent takes the analogous context_builder typed as GraphContextBuilder, receiving a GraphContextInput (zep, graph_id, user_message, session). Setting it fully replaces the built-in hybrid search rather than running concurrently with anything — graph message persistence happens independently, earlier in the turn.
Graph search tool
In addition to the context injected automatically every turn, create_graph_search_tool builds a model-callable LiveKit function tool (via function_tool(raw_schema=...), returning a RawFunctionTool) that lets the agent search a Zep graph on demand. Exactly one of graph_id or user_id is required: graph_id targets a shared standalone graph, user_id targets that user’s personal graph. Register it through the standard tools=[...] parameter:
The tool exposes every graph.search parameter to the model by default:
Use pinned_params to fix a parameter to a constant value (hidden from the model, always sent), or hidden_params to hide a parameter without pinning it (Zep’s server-side default applies). search_filters and bfs_origin_node_uuids are constructor-only and never exposed to the model.
Zep failures are caught and returned as an error string to the model — the tool never raises into the voice session.
Size limits
- Zep rejects direct thread-message payloads over 4,096 characters; LiveKit agents truncate message content to 4,000 characters before writing it, logging lengths only and never content.
- Zep rejects direct
graph.addpayloads over 10,000 characters; LiveKit graph agents truncate graph payloads to 9,900 characters before callinggraph.add.
Best practices
- Use a stable user ID — derive
user_idfrom your auth system, not the room name, so a returning user’s memory accumulates instead of fragmenting across sessions - Scope sessions with the thread or graph — use the room name for
thread_idorgraph_idwhen you want per-session isolation, keepinguser_idconstant - Let LiveKit own audio —
AgentSessionhandles STT, VAD, turn detection, and TTS; Zep only persists turns and injects context - Allow time for indexing — Zep extracts knowledge asynchronously, so facts from a turn are not instantly searchable
Next steps
- Explore customizing graph structure for advanced knowledge organization
- Learn about searching the graph and how to tune search
- See code examples for additional patterns