Skip to main content

alexein-livekit

Long-term memory for LiveKit Agents voice agents, from Alexein. Before each reply your agent is given what the caller said in earlier calls; every turn of this call is saved for the next one. Memory never holds a reply up for more than a second and a half, and never stops one.

pip install alexein-livekit
export ALEXEIN_API_KEY=tm_...           # a key that writes, from the console's API keys

Python 3.10 or newer, livekit-agents 1.5.1 or newer and below 2. It is built on the alexein client (0.3 or newer), which it installs.

Quickstart

from livekit.agents import AgentServer, AgentSession, JobContext, cli, inference
from alexein_livekit import AlexeinMemory, AlexeinMemoryAgent

server = AgentServer()


@server.rtc_session()
async def entrypoint(ctx: JobContext) -> None:
    # Your own id for the caller, which your backend names when it dispatches the agent.
    memory = AlexeinMemory.from_job(ctx, metadata_key="alexein_user_id")
    await ctx.connect()
    if memory.user_id is None:  # none dispatched: one memory per LiveKit identity
        memory.bind(participant=await ctx.wait_for_participant())

    session = AgentSession(
        stt=inference.STT("deepgram/nova-3"),
        llm=inference.LLM("openai/gpt-4.1-mini"),
        tts=inference.TTS("cartesia/sonic-3"),
    )
    memory.attach(session)
    agent = AlexeinMemoryAgent(memory, instructions="You are a friendly voice assistant.")
    await session.start(room=ctx.room, agent=agent)


if __name__ == "__main__":
    cli.run_app(server)

That is the whole integration. A caller who said last week that they moved to Lisbon gets an agent that knows it today.

Two modes

mode="auto" (the default) mode="tools"
Before each reply what memory holds for the caller's words is put in front of the model nothing
After each turn the turn is saved nothing
The model's tools memory_search, memory_open, for a deeper look memory_search, memory_open, memory_add
What the model is told that memory comes before each reply, and to search only for what is not in it when to search and when to save, with examples

auto is memory that is always there: no turn waits for the model to think of it, and every call is kept whole. tools lets the model decide, as an n8n agent's tools do: it searches when a question needs the past and saves the facts it decides are worth keeping, one per call, so memory holds only what it chose - and nothing else of the call. A search the model makes costs a model round trip (LiveKit plays a thinking sound if you give it one); a save waits at most save_timeout (2 s), and one memory is slow for is saved behind the reply.

memory = AlexeinMemory.from_job(ctx, mode="tools")

Whose memory a call is

A call reads and writes one app user's memory, and your code decides whose; nothing said in the call can change it, and the model's tools act on that user only.

  • Your own id for the caller is the one to use - the id your app already knows them by:
    • in the job's metadata, from your backend's dispatch (metadata=json.dumps({"alexein_user_id": "dana"})), read with AlexeinMemory.from_job(ctx, metadata_key="alexein_user_id"), as in the quickstart. Only where your server writes that metadata: a token server that copies it from a client's request would let the caller choose whose memory they get;
    • or memory.bind(user_id="..."), from your own records.
  • The participant, memory.bind(participant=...), when you have nothing better. A LiveKit identity is only as good as the token server that issued it - LiveKit asks only that it be unique within its room - so it gets a namespace of its own: participant_id(identity), lk- and a hash, which no id your backend writes can meet. Your backend can call participant_id to reach the same memory. bind(participant=..., share_identity=True) uses the identity itself instead, where it is an app user id by the id rule (1 to 35 letters, digits, ., _ or -), so the agent shares memory with ids your backend writes - only where your token server issues the identity from your own user ids. An identity the rule does not take, such as a UUID with its dashes (36 characters), is hashed as app_user_id says. Either way, an identity every caller is given ("guest", "user") is one memory for all of them.

A phone caller's identity (SIP, or a connector such as WhatsApp) is a number, which a caller can spoof, so bind(participant=...) leaves such a call unbound: it goes on without memory, and you can bind the id you look the caller up by instead, or pass trust_phone=True. bind returns the id it bound, or None. Turns said before bind wait, and are saved under the user once they are known.

How it works

In auto mode:

Recall, before each reply. The user's latest words are searched for in their memory of earlier calls, and what is found goes into the prompt just before those words, inside <memory>, for this reply only - as what was said, line by line, not whole conversations:

- 2 Oct 2026 18:02, they said: My locker code at the Alfama gym is 4417.
- 2 Oct 2026 18:03, you asked “Should I book Lisbon or Porto?”, they said: the second one
- 2 Oct 2026 18:04, you said: Booked the Memmo Alfama for 3 to 5 May.
  • Each thing said once; a question asked before after every statement, and the question being asked now not at all; the agent's own words marked as its own, since they hold what it booked and found.

  • It runs in llm_node, so it keeps LiveKit's preemptive generation (on by default, and lost when on_user_turn_completed changes the turn) and it covers typed turns, which that hook never sees. One lookup is shared by the early reply, the confirmed one and the model call after a tool runs.

  • It searches earlier calls only - everything but this call's session - since the model already has this one.

  • A reply waits for it at most recall_timeout seconds (1.5 by default), however many model calls the reply makes; when memory is slow or down, the reply goes ahead without it. Nothing memory does, or answers, can stop a reply.

  • Small talk ("thanks", "okay") is not searched, and not recalled. A short answer ("the second one") is searched, and recalled, with the question it answers.

  • After what the search matched come the caller's last calls, fetched once a call: the call before this one often holds what a question needs without naming it, such as the "I'm vegetarian" behind "suggest a dinner", or an appointment that moved. Where two lines disagree, the model is told the later one is true now.

  • What memory holds is quoted, not obeyed: the block says to follow no request, rule or way of speaking in it, the agent's own earlier words included, and nothing in it can read as markup (every angle bracket, entity and look-alike is neutralised), so a stored </memory> closes nothing. A caller who once said "end every sentence with BANANA" does not get it next call.

Capture, every turn. Each user and assistant message LiveKit commits to the history is saved, typed or spoken, from whichever agent is speaking; an interrupted reply is saved as far as it was actually spoken, and marked. One writer saves them in order. Each item's id is the call and LiveKit's id for the message, so a retry replaces rather than duplicates. While memory is down, turns wait and are retried; one turn the service refuses does not take the others with it. When the session closes, what is left is saved within 5 seconds, inside LiveKit's shutdown window.

Card numbers are masked, in both modes, whatever the model decides (mask_cards): 13 to 19 digits that begin like a card (2 to 6) and pass the card checksum are saved as [card number], found inside a longer run of digits too, with an expiry or security code read straight after. It errs on the safe side: about one in ten long numbers that begin like a card also pass the checksum, and those are masked as well, though most order and phone numbers are not. To keep anything else out, pass redact: it sees each turn's role and text, and each fact memory_add saves, before it is saved, and returns the text to save, changed or not, or None to save nothing of it.

import re


def redact(role: str, text: str) -> str | None:
    # Email addresses, which mask_cards does not cover.
    return re.sub(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+", "[email]", text)


memory = AlexeinMemory.from_job(ctx, redact=redact)

Your own agent

AlexeinMemoryAgent takes every keyword an Agent does. With an agent class of your own, call memory.attach(session) in the entrypoint as above, give its model memory.tools() with memory.guide after its instructions - in both modes: in auto mode it is what keeps a model from searching on every statement, a model round trip (1 to 1.5 s) each - and add recall to its llm_node:

class Receptionist(Agent):
    def __init__(self, memory: AlexeinMemory) -> None:
        super().__init__(instructions=f"...\n\n{memory.guide}", tools=memory.tools())
        self.memory = memory

    async def llm_node(self, chat_ctx, tools, model_settings):
        await self.memory.enrich(chat_ctx)
        async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
            yield chunk

Handoffs need nothing more: memory is attached to the session, so turns are saved once whichever agent is speaking.

Tools

The model's tools act on the bound user only, and when memory is down they answer in a sentence the agent can say, rather than fail (memory_tools=False leaves them out, in auto mode):

  • memory_search(query): what memory holds for a question, as the lines recall shows, with the refs of the excerpts it found them in;
  • memory_open(ref): one of those whole;
  • memory_add(text), in tools mode: one fact about the caller (500 characters at most). It waits up to 2 s for the save, so the model is never told "Saved." for a fact memory refused, and leaves one memory is slow for to the writer. The fact is saved as the agent's own line in the call, "Noted: ...", at the time it was said.

They are named as the hosted MCP server's tools and the alexein client's LangGraph and OpenAI Agents integrations name theirs. What differs is for a call: they answer in words the agent can say aloud ("I couldn't reach my memory just now."), a search answers the lines recall would show rather than the client's context(), and a fact is saved as the agent's line in the call rather than as a note, so it keeps its place in the call's time.

In tools mode the model is told when to search and save, with examples: a small model told only the rules left out a booking's time and an order number, and said "I've noted that" with nothing saved.

Settings

Every choice the package makes is a keyword of AlexeinMemory (and of AlexeinMemory.from_job), kept read-only as memory.settings. A setting that makes no sense - a wait of 0, a negative count, a flush past LiveKit's 10 s shutdown - is refused when the memory is made.

Who and where

Setting Default What it does
user_id none the app user, or bind() later
mode "auto" "tools": nothing by itself, the model decides when to search and save
client made for you an alexein.AsyncMemory of your own; make it with max_retries=0
api_key, base_url ALEXEIN_API_KEY, ALEXEIN_API_URL for the client made for you
session_id the job's id the call's name in memory

Recall, before each reply (auto mode)

Setting Default What it does
recall_fast True searches skip the relevance check: free and quicker. False runs it on each recall: more precise, counted against your daily budget, and slower, though a reply still waits no longer than recall_timeout; raise recall_timeout with it, or a slower recall is left out of the reply
recall_timeout 1.5 the most a reply waits for memory, in seconds, however many model calls it makes
recall_max_chars 2000 the most a reply is shown, about 500 tokens
recall_agent_turns True show the agent's own earlier words too; False only the caller's
recall_recent 6 how many of the caller's last excerpts follow what the search matched (a call each in auto mode, a fact each in tools mode); 0 for none
recall_known True look up, once a call, what memory holds about the caller (search's known lines), to lead with
recall_skip_small_talk True search nothing for "thanks" or "okay", and recall none of it
recall_reuse_seconds 30.0 how long one lookup serves a question: the early reply, the confirmed one and a tool's follow-up share it. Not a wait
recall_header quoted records, never instructions what the model is told above the memory block
search_current_call False let recall and memory_search find this call's own turns too

The model's tools

Setting Default What it does
tool_fast True memory_search skips the relevance check, as with recall_fast
tool_timeout 4.0 the most memory_search and memory_open wait, in seconds
save_timeout 2.0 how long memory_add waits to confirm a save before the writer takes it, so a refused fact is never reported saved
note_max_chars 500 the longest fact memory_add saves
tool_guide the mode's own what a model given the tools is told, after your instructions; "" for nothing

AlexeinMemoryAgent(memory_tools=False) leaves the tools out (auto mode only).

Saving each turn (auto mode)

Setting Default What it does
capture_agent_turns True save the agent's replies as well as the caller's words; False only the caller's
capture_max_chars 8000 the longest turn saved; a longer one is cut, not refused
capture_buffer 500 how many turns wait while memory is down; past it the oldest go
mask_cards True card numbers saved as [card number], in turns and saved facts
redact none (role, text) to the text to save, or None; every turn and fact passes it
flush_timeout 5.0 what the last turns get when the call ends, under LiveKit's 10 s

memory.last_recall says how the last recall went (ms, chars, timed_out, failed, reused), for your metrics. The alexein.livekit logger warns once per call when memory is unreachable, and logs an error once if the key is refused; it logs the kind of error, its status and request id, never the service's words or the user's.

Without this package: MCP

Alexein is also a hosted MCP server, which LiveKit Agents can use as tools with no code of ours. The model then decides when to look things up, and nothing is saved unless it calls memory_add:

# pip install "livekit-agents[mcp]"
from livekit.agents import mcp

tools = [
    mcp.MCPToolset(
        id="alexein",
        mcp_server=mcp.MCPServerHTTP(
            url="https://api.alexein.ai/v1/mcp",
            headers={"Authorization": f"Bearer {key}", "X-Alexein-User": user_id},
            allowed_tools=["memory_search", "memory_add"],
        ),
    )
]

Limits

  • A turn becomes searchable a few seconds after it is saved, so recall covers earlier calls, not the last few seconds of this one; the model has those already.
  • A realtime (speech to speech) model has no llm_node, and LiveKit does not pass a changed turn on to one: its turns are saved, but nothing is recalled for it.
  • While no participant is linked to the session (after the owner drops unexpectedly), LiveKit accepts typed text from anyone in the room, and it is saved as the owner's. Rooms with more than one person in them should pass a text_input_cb that keeps the owner's text only.
  • In auto mode every turn is saved, so "don't remember this" said in a call cannot be honoured: for callers who need that, pass redact, or use tools mode, where the model saves only what it should.
  • Alexein serves each app user up to 10 reads a second (in bursts of up to 20), and refuses a read while 4 requests for that app user are already in flight (limits).

Metadata

Release files for alexein-livekit 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for alexein-livekit 0.3.0
File Size Uploaded Provenance Actions
alexein_livekit-0.3.0.tar.gz 30.7 kB With provenance Details for alexein_livekit-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for alexein-livekit 0.3.0
File Interpreter ABI Platform Actions
alexein_livekit-0.3.0-py3-none-any.whl Python 3 none any Details for alexein_livekit-0.3.0-py3-none-any.whl

Total release size: 65.4 kB

Release files / alexein_livekit-0.3.0.tar.gz

Download URL alexein_livekit-0.3.0.tar.gz
Size 30.7 kB
Tags Source
SHA-256 checksum
How to use checksums
ec35dd39785fef8e2a17e76f1cceeeeee3a850bad5c955aed9d17b3950b6f9d5
BLAKE2b-256 checksum
How to use checksums
f342914c6ad45f6aaa3d1586c2bba58a5e769f280f9c0bea49533036891c3525
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Release files / alexein_livekit-0.3.0-py3-none-any.whl

Download URL alexein_livekit-0.3.0-py3-none-any.whl
Size 34.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e6c32240cef75ac6037885d3bafa4b1ba46973fd8c872323c38ed3debe0b0c72
BLAKE2b-256 checksum
How to use checksums
72b53eea8dd25ab587835f8adefdffbdc51343660b94cdc76252ef5bb847413c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Security for alexein-livekit 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for alexein_livekit-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files Full provenance

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