A LiveKit Agents 1.x plugin that gives your voice agents persistent, cross-session memory powered by Alchemyst AI.
Your agent remembers users across conversations — preferences, past requests, personal details — and uses that context to deliver personalised responses without any manual state management.
Voice agents are stateless by default. Every new session starts from scratch. This plugin fixes that:
- A user says "I'm allergic to peanuts" in session 1 → the agent remembers it in session 47.
- A returning caller doesn't have to repeat their account details.
- The agent builds rapport over time — "Welcome back! Last time we discussed your trip to Tokyo."
- Automatic RAG injection — relevant memories are semantically retrieved and injected into the chat context before each LLM response, using LiveKit's recommended
onUserTurnCompletedhook - Auto-persist — completed user/assistant turns are stored to Alchemyst via session events
- LLM tools — exposes
remember,recall, andforgettools so the model can manage memory explicitly mid-conversation - Per-session scoping — memories are scoped by
groupName, session ID, and user identity - Provider-agnostic — works with any LLM (OpenAI, Gemini, etc.) via LiveKit Inference or direct plugins
- Two integration patterns — simple
onUserTurnCompletedhook (recommended) or fullllmNodeoverride (advanced)
npm install livekit-plugin-alchemyst
# or
pnpm add livekit-plugin-alchemystPeer dependencies (install separately if not already present):
npm install @livekit/agents zodUses LiveKit's official onUserTurnCompleted hook for memory injection. The default pipeline handles all LLM calls — no custom stream wrapping needed.
import 'dotenv/config';
import { createAlchemystPlugin } from 'livekit-plugin-alchemyst';
import {
type JobContext,
cli,
defineAgent,
inference,
ServerOptions,
voice,
llm,
} from '@livekit/agents';
import * as deepgram from '@livekit/agents-plugin-deepgram';
import * as cartesia from '@livekit/agents-plugin-cartesia';
import * as silero from '@livekit/agents-plugin-silero';
// 1. Create the plugin (shared across all sessions in this worker)
const alchemyst = createAlchemystPlugin({
apiKey: process.env.ALCHEMYST_API_KEY,
groupNames: ['voice-agent'],
});
// 2. Define your agent with memory injection
class MyAgent extends voice.Agent {
constructor() {
super({
instructions: 'You are a helpful voice assistant with persistent memory.',
tools: alchemyst.getTools(), // adds remember / recall / forget
});
}
// Inject relevant memories before the LLM generates a response
override async onUserTurnCompleted(
turnCtx: llm.ChatContext,
newMessage: llm.ChatMessage,
): Promise<void> {
const userText = newMessage.textContent;
if (!userText) return;
const memories = await alchemyst.search(userText);
if (memories.length > 0) {
turnCtx.addMessage({
role: 'system',
content: memories.map((m, i) => `${i + 1}. ${m.content}`).join('\n'),
});
}
}
}
// 3. Wire up the session
export default defineAgent({
entry: async (ctx: JobContext) => {
await ctx.connect();
// Wait for participant and scope memory to their identity
let participant = ctx.room.remoteParticipants.values().next().value;
if (!participant) {
participant = await new Promise((resolve) => {
ctx.room.once('participantConnected', resolve);
});
}
alchemyst.bindSession(ctx.room.name!, participant?.identity ?? 'anonymous');
const session = new voice.AgentSession({
vad: await silero.VAD.load(),
stt: new deepgram.STT(),
tts: new cartesia.TTS(),
llm: new inference.LLM({ model: 'openai/gpt-4.1-mini' }),
});
await session.start({ room: ctx.room, agent: new MyAgent() });
// Auto-persist completed turns to memory
let lastUserText: string | null = null;
session.on('conversation_item_added', (ev) => {
if (ev.item.role === 'user') {
lastUserText = ev.item.textContent ?? null;
} else if (ev.item.role === 'assistant' && lastUserText && ev.item.textContent) {
alchemyst.addTurn(lastUserText, ev.item.textContent).catch(console.error);
lastUserText = null;
}
});
},
});See examples/agent.ts for the full working example with VAD, turn detection, and the LiveKit CLI bootstrap.
Remember customer details, past issues, and preferences across calls so users never have to repeat themselves.
override async onUserTurnCompleted(turnCtx, newMessage) {
const memories = await alchemyst.search(newMessage.textContent ?? '');
if (memories.length > 0) {
turnCtx.addMessage({
role: 'system',
content: `Customer history:\n${memories.map(m => m.content).join('\n')}`,
});
}
}"Hi, I called last week about my billing issue." Agent already knows the ticket number, the plan, and the resolution status.
Build a voice assistant that learns user preferences, routines, and interests over time.
const alchemyst = createAlchemystPlugin({
groupNames: ['companion', 'preferences'],
maxMemories: 10, // inject more context for personalization
});"What should I have for dinner?" Agent remembers you're vegetarian, allergic to nuts, and loved the pasta recipe it suggested last time.
Track prospect details, past conversations, and buying signals across multiple touchpoints.
const alchemyst = createAlchemystPlugin({
groupNames: ['sales', 'prospects'],
similarityThreshold: 0.6, // cast a wider net for relevant context
});Prospect calls back a week later — the agent picks up right where the conversation left off, referencing the pricing tier they were interested in.
Scope memory per organization and user so tenants never leak data to each other.
alchemyst.bindSession(
`org-${orgId}-room-${ctx.room.name}`, // session scoped to org
participant.identity, // user scoped to participant
);| Variable | Description |
|---|---|
LIVEKIT_URL |
Your LiveKit server URL (wss://...) |
LIVEKIT_API_KEY |
LiveKit API key |
LIVEKIT_API_SECRET |
LiveKit API secret |
DEEPGRAM_API_KEY |
Deepgram API key (STT) |
CARTESIA_API_KEY |
Cartesia API key (TTS) |
ALCHEMYST_API_KEY |
Alchemyst AI key — get one at platform.getalchemystai.com |
The example uses LiveKit Inference for the LLM (
inference.LLM), which routes through LiveKit Cloud and requires no separate OpenAI/Gemini key. To use a direct provider instead, swap inopenai.LLM()orgoogle.LLM()and add the corresponding API key.
All options are optional. Pass them to createAlchemystPlugin(config):
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
ALCHEMYST_API_KEY env |
Alchemyst AI API key |
userId |
string |
"anonymous" |
User identifier (typically participant identity) |
sessionId |
string |
auto-generated | Session identifier (typically room name) |
similarityThreshold |
number |
0.5 |
Cosine similarity threshold for memory retrieval (0–1) |
maxMemories |
number |
5 |
Maximum memories returned per search |
groupNames |
string[] |
["voice-agent"] |
Group tags for storing and filtering memories |
memorySystemPromptTemplate |
string |
see below | Template for injected memory; use {{memories}} placeholder |
autoPersist |
boolean |
true |
Auto-store turns; set false to use the remember tool exclusively |
logger |
PluginLogger |
console |
Custom logger (info, warn, error, debug) |
## Relevant memories from previous conversations
The following context was automatically retrieved from long-term memory.
Use it to personalise your responses, but do not mention that you are
using stored memories unless the user explicitly asks.
{{memories}}
Returns an AlchemystPluginInstance:
| Method / Property | Description |
|---|---|
bindSession(sessionId, userId?) |
Re-scope memory to a new session and user. Call once per participant. |
getTools() |
Returns remember, recall, forget tool definitions for new Agent({ tools }) |
search(query) |
Semantic search over stored memories. Returns MemoryEntry[]. |
addTurn(userText, assistantText) |
Persist a completed conversation turn to memory. |
createLLMNode(llm, opts?) |
(Advanced) Returns an llmNode function that handles memory injection and persistence internally. |
delete(memoryId) |
Delete a specific memory by ID. |
deleteSession() |
Delete all memories for the current session. |
sessionId |
Current session ID (read-only). |
userId |
Current user ID (read-only). |
When you pass alchemyst.getTools() to your agent, the model gains access to:
| Tool | Parameters | Description |
|---|---|---|
remember |
content, tags? |
Store a fact or preference in persistent memory |
recall |
query, limit? |
Semantic search over stored memories |
forget |
memoryId |
Permanently delete a memory by ID |
The simplest and most robust approach. Memory is injected once per user turn before the default pipeline calls the LLM. Turn persistence is handled via session events.
This is the official LiveKit RAG pattern.
For full control over the LLM call, use createLLMNode() to override the pipeline's llmNode. This manages its own innerLLM.chat() call, injects memories, wraps the stream, and persists turns internally.
class AdvancedAgent extends voice.Agent {
constructor(private myLLM: llm.LLM) {
super({ instructions: '...', tools: alchemyst.getTools() });
}
override async llmNode(chatCtx, toolCtx, modelSettings) {
return alchemyst.createLLMNode(this.myLLM)(chatCtx, toolCtx, modelSettings);
}
}Note: When using this pattern, the LLM passed to
AgentSessionis bypassed — make sure to pass the same instance to both, or use the override as the sole LLM entry point.
User speaks → STT → onUserTurnCompleted (memory search + inject) → LLM → TTS → Agent speaks
↓
conversation_item_added event
↓
addTurn() → Alchemyst API
- User turn completes —
onUserTurnCompletedfires with the transcribed text - Memory search — the user's utterance is used as a semantic query against stored memories (filtered by
groupName) - Context injection — matching memories are added to the chat context as a system message
- LLM responds — the default pipeline calls the LLM with the enriched context
- Turn persistence — the
conversation_item_addedevent pairs user + assistant messages and callsaddTurn()to store them
# 1. Install dependencies
pnpm install
# 2. Configure environment
cp .env.example .env
# edit .env with your API keys
# 3. Download model files (VAD, turn detection)
npx tsx examples/agent.ts download-files
# 4. Run the agent
npx tsx examples/agent.ts devThen open the LiveKit Agents Playground and connect to your agent.
Apache-2.0