Save a conversation from another source
An agent can copy a conversation that happened somewhere else into a user’s Astro AI chat history. The copy appears in that user’s conversation list under its title, alongside the conversations they had in the chat, and opens like any other one.
The API takes a list of turns and nothing platform-specific. Where the transcript came from is the agent’s business: a Slack thread, an email chain, a support ticket, a transcript your own service already stores. If your agent runs on the Astro messaging sidecar, this is already available to it. There is no capability to switch on and no separate service to call.
Reading this from StreamOptions requires @astropods/adapter-core 0.10.0 or
later for Node, or astropods-adapter-core 0.7.0 or later for Python. On an
earlier version the field is absent.
Save a transcript
StreamOptions.saveConversation (save_conversation in Python) takes the turns
and returns the conversation the copy landed on.
Node.js
Python
saveConversation is optional on StreamOptions, because a caller that drives the
gRPC stream itself may not provide it. Guard on it, or assert it, before calling.
Input
Each turn:
Give every copy a title. The conversation list identifies a copy by its title,
so a save with no title arrives as an untitled conversation.
The copy must be addressed to a resolved Astro identity: userId has to start with
user_. A raw platform id, such as a Slack U…, is refused, because it would write
a conversation no session can open. Resolve the sender to an Astro user first, and
tell the user when you cannot.
Saving twice
The idempotencyKey is what makes a save repeatable. The platform derives the
conversation id from (userId, idempotencyKey), so the same key always addresses
the same copy. Key it on the source conversation, not on the message that triggered
the turn: slack:C123:1699.0001, zendesk:ticket-4417.
Re-read the whole source and send it again under the same key to keep a copy current. The refreshed copy reflects the source, including edits and deletions.
onConflict decides what happens to a copy that already exists.
The platform will not silently destroy turns it did not write. Under SKIP, a copy
the user has replied in comes back as SKIPPED_DIVERGED and is left as it was.
Choosing REPLACE there is the agent stating that intent.
Status
The status is the point of the call. Await it.
A deleted copy staying deleted is deliberate: an agent that saves on every source message would otherwise resurrect a conversation the user threw away.
Read the source thread first
An agent that mirrors a thread needs the whole thread, and the prompt only carries
the message that triggered the turn. StreamOptions.getThreadHistory
(get_thread_history) returns the source thread, hydrated from the platform so
edits and deletions are reflected.
Node.js
Python
Platform ids are opaque, and a transcript copied verbatim carries them. Slack
mention tokens such as <@U07UVWXYZ> stay in the text unless the agent rewrites
them, and they read as noise in the chat. The thread itself usually holds the
answer: each message carries its poster’s resolved username, so build a map of
id to name from the thread and substitute before saving. Prefer leaving a turn
unattributed over setting author to a raw platform id.
Why it is not automatic
Running the messaging sidecar gives your agent this API, but the sidecar will not save conversations on its own, because it cannot answer the questions the call requires. Which of the many conversations your agent sees is worth keeping. Whose history it belongs to. What identifies it, so a later save updates that copy rather than creating a second one. Whether a copy the user has since edited should be overwritten or appended to.
Only the agent can answer those, so a save happens when an agent asks for one. A user gets a conversation in their history because an agent was built to put one there.
Next steps
- Files in chat covers the other half of a rich conversation: reading a user’s uploads and returning files they can download.
- Custom adapter (Node) and
Custom adapter (Python) list every field on
StreamOptions.