Agents
Conversations and memory
Open, continue, read and delete conversations with an agent, what each turn costs, and how facts and relationships carry from one conversation to the next.
A conversation (a session) is one thread between one end user and one agent. Your user’s device opens it with their ephemeral key, then talks on the WebSocket. Everything here is called with that token, never with an API key, and answers for the token’s person alone.
curl https://api.lumavox.ai/v1/agent/sessions \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07" }'{ "id": "0193b1f4-8e02-7c3a-a1d5-6b7e90c4f218", "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07", "end_user_id": "eu_3kQ9x7MzT2pL8vNc", "title": null, "turn_seq": 0, "resumed": false, "messages": { "data": [], "next_cursor": null }, "relation": null, "created_at": "2026-10-24T09:31:40.000Z", "updated_at": "2026-10-24T09:31:40.000Z"}Take the id to the socket: wss://api.lumavox.ai/v1/agent/stream?session_id=<id>.
Open or continue a conversation
Section titled “Open or continue a conversation”POST /v1/agent/sessions takes:
| Field | What it does |
|---|---|
agent_id |
Required. The agent to talk to. An unknown one is 404; one the token is not allowed to use is 403. |
resume |
Leave it unset to continue the person’s most recent conversation with this agent, if it is recent enough, and start a new one otherwise. false always starts a new one: send it from a “new chat” button. |
end_user_id |
Optional. If you send it, it must name the token’s own person, or the call is 403. |
title |
An optional label, up to 255 characters. |
“Recent enough” is resume_max_age_days in your organisation’s defaults, else the platform’s agents.sessions.resume_max_age_days (Limits). The response’s resumed says which happened. Branch on it: a continued conversation arrives with its latest messages already in messages and its turn_seq past zero.
Starting fresh loses nothing about the person. Facts and the relationship belong to the person, not the conversation, and carry into every new one.
Opening a session makes no model call and no speech call. A new conversation comes back with no messages, even for an agent with an opening line. The opening line is the agent’s first turn on the socket, written when the device connects.
relation is how the agent stands with this person as the conversation opens. It is null when the agent does not keep relationships. See Relationships.
Read a conversation
Section titled “Read a conversation”GET /v1/agent/sessions lists the person’s conversations, most recently used first, without messages — only conversations with agents the token may use. It is a page: limit (1 to 200, default 50) and cursor, answered as data and next_cursor (Conventions).
GET /v1/agent/sessions/{session_id} returns the conversation and a page of its most recent messages in messages, oldest first:
{ "id": "0193b1f5-0c31-7f0e-b8a2-1d4c6e9a7b30", "role": "assistant", "content": "Then let's start in Alfama. It's quiet before ten.", "mood": "cheerful", "mood_intensity": 0.6, "knowledge": null, "created_at": "2026-10-24T09:32:05.000Z"}roleisuserorassistant.moodandmood_intensityare what the agent reported for that reply, when it reports moods;nullotherwise.knowledgelists what the agent looked up in its knowledge for that reply: each lookup’s query, outcome and the entries it found, with scores. It isnullwhen nothing was looked up.
limit sets how many messages (default 50, up to 200), counting back from the newest. To read further back, pass the previous response’s messages.next_cursor as cursor. It is null once you have reached the start.
let page = await get(`/v1/agent/sessions/${id}?limit=200`);const messages = [...page.messages.data];while (page.messages.next_cursor !== null) { const cursor = encodeURIComponent(page.messages.next_cursor); page = await get(`/v1/agent/sessions/${id}?limit=200&cursor=${cursor}`); messages.unshift(...page.messages.data);}The transcript is kept exactly as it happened. Compaction shortens what the model is sent, never what you can read back.
Delete a conversation
Section titled “Delete a conversation”DELETE /v1/agent/sessions/{session_id} deletes the conversation and every message in it, and answers 204 with no body. It cannot be undone. What the agents remember about the person, and the relationship, stay.
What a turn costs
Section titled “What a turn costs”The conversation is held for you, so each turn sends only the new message. That saves bandwidth, not tokens: on every turn the model reads the conversation so far (or, once compacted, its summary and what came after) and the turn is billed for it.
- The socket’s
usageevent andturn.donereport the model and its token counts for each reply. - A spoken turn is also charged for its transcription, and a speaking agent for its speech, on separate usage rows.
- Everything a conversation causes is charged to the token that caused it, including the memory work below.
To keep long conversations affordable, compact them.
Compaction
Section titled “Compaction”Compaction summarises the conversation so far, all but its last few exchanges. Later turns start from the summary and those exchanges, word for word, instead of the full history, so the agent still has what was just said exactly: a list’s items, a number, the question it asked. How many messages stay is the platform’s agents.memory.compaction.keep_messages (Limits), never more than a quarter of the threshold, and starting at something the person said. A compaction you ask for on a conversation no longer than that summarises it whole. The transcript itself does not change.
Automatically: after a reply whose first model call read the agent’s compact_threshold_tokens, the conversation is compacted, after the reply and never inside it. What counts is what the next turn sends again: the conversation, its summary and the instruction. The inline knowledge, the turn’s looked-up passages and its tool results are left out, since no summary shrinks them. An agent that sets no threshold compacts at the platform’s agents.memory.compaction.default_threshold_tokens (Limits). An agent may set at most 200,000.
By hand:
curl https://api.lumavox.ai/v1/agent/sessions/$SESSION_ID/compact \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}'{ "session_id": "0193b1f4-8e02-7c3a-a1d5-6b7e90c4f218", "summary": "The traveller arrives Friday, likes quiet mornings and wants to see Alfama first.", "after_seq": 18, "trigger": "manual", "model": "gemini-3.1-flash-lite", "input_tokens": 2310, "output_tokens": 96, "facts_stored": 0}after_seq is the last message the summary covers. Everything after it, the kept exchanges and what comes next, is still sent to the model word for word. Every socket on the conversation is sent a compacted event, for this one and the agent’s own (WebSocket).
What it may answer instead, none of it charged:
- 402: the token has spent its credit.
- 409: another compaction of the same conversation finished first.
- 422: nothing new since the last compaction; or the model’s summary was empty, or no shorter than what it would replace.
- 502: the model provider failed or timed out.
Compaction is a model call charged to the token. The agent’s compact_instruction, compact_model, compact_thinking_level and compact_max_output_tokens shape it.
- The summary is capped: at the agent’s
compact_max_output_tokens, or the platform’sagents.memory.compaction.max_output_tokens, 4,000 tokens unless the platform changes it (Limits has today’s), the model’s thinking included, and twice that when the compaction extracts facts too. A summary no shorter than what it would replace is not kept. - A long stretch is compacted in passes, oldest first, each from the last one’s summary. A compaction you ask for answers with its first pass; the rest follow on their own, each charged to the same token.
- When the model fails, the next automatic compaction waits, longer after each failure in a row.
Facts are durable things the agents have learned about a person: “has a daughter called Ana”, “prefers mornings”. They are shared across every agent you run, because the same person talking to a different character is still the same person.
Two of an agent’s fields change that:
facts_enabled— on unless you turn it off. Off, the agent’s conversations are never read for facts and its turns recall none, so nothing is charged for either. A compaction of its conversations extracts nothing, whatever the request asks. Facts already stored stay.facts_scope—person(the default) oragent. Withagent, what the agent learns is kept to it: your other agents do not recall it, and it does not recall theirs. A fact keeps the scope it was learnt under, and says so in itsscope.
Facts are extracted after a reply, never during one:
- the platform’s
agents.memory.facts.idle_minutesafter a conversation’s last turn, and once moreagents.memory.facts.retry_minuteslater if that reading fails (Limits); - when the person opens a session, for their most recent other conversation still unread — one whose token was revoked or spent before those minutes were up, or whose readings failed — however old it is, charged to the token that opens;
- with a compaction, when the agent’s
extract_facts_on_compactis on or the request sends{"extract_facts": true}; - on demand, with
POST /v1/agent/sessions/{session_id}/extract: a compaction and an extraction in one model call.facts_storedin its response says how many were kept.
Each reading reads what was said since the last one, with the platform’s agents.memory.reading.context_messages before it for context — never the whole conversation again. A compaction that extracts reads every turn since the last compaction; when that covers everything since the last reading, it counts as the reading, and the one after the quiet minutes finds nothing new and costs nothing.
The agent’s fact_instruction says what is worth remembering. Every turn, the person’s facts are put in front of the model: as many as fit in the platform’s agents.memory.recall.budget_chars (Limits; a character is about a quarter of a token), and never more than the agent’s max_facts (1 to 100) when it sets one. They are prompt tokens on every turn, mostly served from the model’s cache.
When they do not all fit, the ones mentioned most recently go in — and, from the person’s next turn on, the ones nearest what the conversation is about: after each turn the last exchange and any facts not yet seen are embedded, and the following turn recalls a quarter of the budget by recency and the rest by relevance. That costs an embedding at cost (facts_embed usage, a fraction of a credit), only for people whose facts no longer all fit. A reading that hears a known fact again — “I’m Ada, remember?” — confirms it rather than storing it twice, and a confirmed fact moves to the front. So what a person keeps saying stays in front of the agent, and what they said once long ago is the first to give way.
Facts reach the model as what the person said about themselves, never as instructions. Whatever fact_instruction says, a reading never keeps:
- instructions addressed to the agent — how to behave, which rules to ignore;
- claims about the person’s standing with your service — that they are your staff or an administrator, or are owed a refund, a discount or access;
- secrets — passwords, PINs and codes, card, bank and account numbers, identity numbers, keys and tokens.
The built-in instruction also leaves out health, beliefs, sex life, political views, ethnicity and crime, unless the person asks to be remembered by them. If your agents need those, write your own fact_instruction.
GET /v1/agent/facts lists what is remembered about the token’s person, oldest first: 100 a page by default, up to 200 with limit. Pass next_cursor back as cursor for the next page; it is null on the last. A fact learnt while you page lands on the last page:
{ "data": [ { "id": "0193b201-77aa-7d40-9c11-3e5b8f2a6d09", "text": "Arrives in Lisbon on Friday.", "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07", "confirmed_at": "2026-10-26T18:02:40.000Z", "created_at": "2026-10-24T09:48:12.000Z", "replaced_at": null, "replaced_by": null, "scope": "person", "source": "conversation", "updated_at": "2026-10-24T09:48:12.000Z" } ], "next_cursor": null}agent_id is the agent that was being talked to when the fact was learned. confirmed_at is when a conversation last said it again, null if none has.
When a fact changes
Section titled “When a fact changes”A person who moves, changes job or ends a relationship should not be remembered both ways. A reading sees the known facts and may replace one with what is true now — “Lives in London” by “Lives in Paris” — or remove one that is no longer true, with nothing in its place. It never touches more than ten in one reading.
A replaced or removed fact is hidden from the agents and from the list at once, and deleted the platform’s agents.memory.facts.replaced_kept_days later (Limits). Until then include_replaced=true lists it too, with replaced_at set and replaced_by naming the fact that took its place (null when it was removed). If a reading got it wrong, correct the fact that replaced it; a replaced fact said again stands again.
Adding and correcting facts
Section titled “Adding and correcting facts”Your app can tell the agents what it already knows about the person, and correct what they got wrong:
curl -X POST https://api.lumavox.ai/v1/agent/facts \ -H "Authorization: Bearer $EPHEMERAL_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Prefers to be called Sam."}'POST /v1/agent/factsadds one:201with the fact, or200with the one that already said the same thing. Withagent_id, only that agent recalls it. It works before the person has ever talked to an agent.PATCH /v1/agent/facts/{fact_id}with{"text": …}rewrites one. The old words are kept as forgotten, so no reading of what was said before brings them back. A fact a later conversation replaced, or words another fact already says, are a409.
The agent forgets when the person asks. “Forget where I live” makes it call its own forget_fact tool with the line it remembers, and that fact is forgotten as DELETE /v1/agent/facts/{fact_id} would — gone from every later conversation, and not learnt again from the old words. It is offered only when the agent has facts in front of it, and shows on the socket as a platform tool call.
What you write is yours: its source is customer, and a later conversation confirms it but never replaces or removes it. A fact learnt in a conversation has source conversation. Both are free, and both refuse a secret (422). From your server, the same is /v1/end-users/{end_user_id}/facts with your API key — see End users.
Relationships
Section titled “Relationships”An agent with relation_enabled keeps a standing relationship with each person: how warm it feels, how much it trusts them and how well they know each other. It changes only when something in a conversation changes the agent’s view, which is rare, and it is kept until the person is deleted.
GET /v1/agent/relations returns one row per agent the person has talked to that keeps a relationship, most recently active first. Send agent_id for one agent’s row. It is a page: limit (1 to 200, default 50) and cursor, answered as data and next_cursor.
{ "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07", "agent_name": "Mira", "end_user_id": "eu_3kQ9x7MzT2pL8vNc", "tier": "Acquaintances", "warmth": 0.3, "trust": 0.2, "disclosure": 0.1, "turns": 14, "conversations": 2, "recorded": 1, "note": "Curious and easy to talk to.", "summary": "You have spoken a couple of times and you like them.", "first_met_at": "2026-10-20T18:02:44.000Z", "last_seen_at": "2026-10-24T09:47:51.000Z"}| Field | Meaning |
|---|---|
tier |
What the agent would call the relationship, and what it is told: Despised, Disliked, Acquaintances, Friends or Good Friends. Familiarity caps how far it can rise: a first conversation tops out at Acquaintances. Dislike is not capped. |
warmth, trust |
From -1 to 1. Zero is indifference, where every relationship starts. |
disclosure |
How much the agent has told them about itself, -1 to 1. |
turns, conversations |
Counted, not judged. |
recorded |
How many times the model recorded a change. Zero on a long history means the agent is not recording anything. |
note |
One line the model wrote about them, replaced each time; null until it writes one. |
summary |
The relationship as the model is told it. An empty string means nothing is worth saying yet. |
The same object appears as relation when a session opens and in the socket’s ready frame, and a relation event arrives on the socket on the rare turn when it changes.
Forgetting
Section titled “Forgetting”| Call | Erases | Answers |
|---|---|---|
DELETE /v1/agent/facts/{fact_id} |
One fact. | 204, no body |
DELETE /v1/agent/facts |
Every fact about the token’s person. Their conversations, their relationships and the end user remain. | how many: {"facts_deleted": 11} |
DELETE /v1/end-users/{end_user_id} |
The person, every conversation and every fact. Called from your server with an API key. See End users. | what went, counted |
None of them can be undone.
A forgotten fact stays forgotten. The conversation it came from still holds the words, but no reading of what was said before the fact was forgotten stores it again. If the person says it again afterwards, it is remembered again. To remove the words too, delete the conversation. A conversation’s own summaries keep what they summarised until it is deleted.
- The WebSocket: take turns in a conversation
- Agent reference: every field of these routes