LiveKit / voice agents
Wire pgmem into a LiveKit voice agent.
pgmem is built for stateless voice workers: durable state lives in Postgres, no
compaction runs on the response path, and there's no dependency on a long-lived
process. If a worker crashes, scales out, or is replaced mid-call, resume
restores the session without losing the conversation.
Prerequisites
- pgmem integrated into a turn loop.
pool.max_size >= 2—compact_asyncholds one connection while working on another.- You've read How a turn flows (the latency rule is non-negotiable for voice).
Steps
Construct PgMem once, at worker startup
It holds the pool and is shared across calls — never per-turn.
mem = await PgMem.create(dsn, embedder=embedder, llm=llm)Resume or create per call
Persist the session.uuid on your job; resume it if the worker reconnects,
otherwise start fresh.
class SupportAgent(Agent):
async def on_turn(self, turn):
session = (
await mem.session.resume(self.session_id, self.group_id)
if self.session_id
else await mem.session.create(group_id=self.group_id, user_id=self.user_id)
)
self.session_id = session.uuidKeep only retrieval on the turn
Record the input, compile, reply, record the output — then compact off the turn. Nothing here calls an LLM except your own reply generation.
await session.add_turn(role="user", content=turn.text)
context = await session.compile_context(query=turn.text)
reply = await self.respond(context.messages) # your model call
await session.add_turn(role="assistant", content=reply)
await session.compact_async() # runs off-turn, returns immediately
return replyVerify
Kill the worker mid-call and bring it back: mem.session.resume(session_id, group_id) restores the active execution state and recent turns, so the agent
continues where it left off. A brand-new call gets a fresh session and retrieves
only the durable continuity it needs.
Gotchas
Do not await heavy work before returning the reply. add_turn is a cheap write,
but compact_async, add_episode, and session.end (promotion) must stay off the
response path — for voice, a blocked turn is a dropped beat in the conversation.
- A construction-time
injected_context_providerruns on the turn — for live state prefer the per-callinjected_context=so the fetch is concurrent. - The repo has a fuller wiring sketch at
docs/livekit-integration.md.
Related
- How a turn flows — on-turn vs off-turn
- Integrate pgmem into your agent — the base turn loop
- Use live system-of-record state — voice-safe live fetches
- Compaction off the critical path — keeping voice fast