AI Agentsconversation memory for voice AI (returning callers)
Have the platform save a summary when a call ends and hand it back when the same number calls again, so the agent picks up where it left off.
memorypost-prompt
The claim
An agent forgets everything when the call ends unless something keeps it. The platform offers to: save_conversation posts a summary to your post_prompt_url at the end of the call, and conversation_id says which conversation that summary belongs to. This recipe derives the id from the caller’s number, keeps each summary in a file, and answers the platform’s request for it on the next call.
Why it holds
Three sources carry the claim.
The docs for ai.params say save_conversation will “send a summary of the conversation after the call ends”, that it requires post_prompt_url and a conversation_id, and that conversation_id is “used by check_for_input and save_conversation to identify an individual conversation”.
The inbound-call payload the platform POSTs for a SWML document carries call.from, so the per-request callback can set both params before the document renders.
The SDK’s own post-prompt route handles the return trip. In 3.0.1 it calls on_summary(summary, body). When the body’s action is fetch_conversation it returns whatever on_summary returned. Its comment says the platform “expects conversation_summary in the response” (core/mixins/web_mixin.py).
The Python surface uses that route as it is. The TypeScript SDK, 2.0.5, answers every post-prompt POST with {ok: true} and drops onSummary’s return. Its surface therefore points post_prompt_url at a small handler of its own, which is what setPostPromptUrl is for.
How it works
def configure(query_params, body_params, headers, agent):
caller = (body_params.get("call") or {}).get("from")
if caller:
agent.set_params({"save_conversation": True,
"conversation_id": conversation_id(caller)})
class FrontDesk(AgentBase):
def on_summary(self, summary, raw_data=None):
cid = raw_data.get("conversation_id")
if raw_data.get("action") == "fetch_conversation":
remembered = _load().get(cid)
return {"conversation_summary": remembered} if remembered else {}
if summary and cid:
memory = _load(); memory[cid] = summary; _save(memory)
What the document carries for a call from +1 415 555 0123:
{"ai": {"params": {"save_conversation": true, "conversation_id": "caller-14155550123"},
"post_prompt": {"text": "Summarise the call in two sentences, ..."},
"post_prompt_url": "https://your-host/front-desk/post_prompt"}}
The callback runs against an ephemeral copy of the agent on every request. One caller’s id never leaks into another caller’s document, and a request with no caller renders no id at all. The id is the caller’s digits, which keeps it inside the characters the SDK accepts for a conversation id.
A post_prompt is set because in 3.0.1 that is what makes the SDK emit post_prompt_url. The summary it asks for is also the text that gets kept.
Limitations
A caller whose number carries no digits, an anonymous call or a SIP URI without any, gets no conversation id. Nothing is saved or fetched for them. One memory shared by every anonymous caller would hand one caller’s summary to the next.
The verifier proves the document and the two POSTs, not what the model does with the summary on the next call. That is the prompt’s job, and the Memory section is where it is asked.
The exact shape of the platform’s fetch request is documented in the SDK’s route rather than in a spec. The recipe reads action and conversation_id from the body, as the route does.
What to change first
Remove "save_conversation": True from configure and run the verifier. The document still carries the id and the post prompt, and the first assertion fails. That is the point: without that flag nothing is saved, and the next call has nothing to fetch.