Sagents.DisplayMessagePersistence behaviour (Sagents v0.8.0-rc.8)
Copy MarkdownBehaviour for persisting display messages (user-facing message representations).
Display messages are the UI-friendly representations of conversation turns: text messages, tool call cards, thinking blocks, error notifications, etc. They are separate from the agent's internal state and optimized for rendering.
Scope-first contract
Every callback takes the integrator's scope struct as its first positional argument.
When callbacks are invoked
All callbacks are invoked from within the AgentServer process, ensuring exactly-once semantics regardless of how many LiveViews are connected.
Message saving
save_message/3 is called when a new LangChain Message is processed.
A single Message can produce multiple display messages (e.g., text + tool_calls).
The implementation should return the saved records so AgentServer can
broadcast {:display_message_saved, msg} events to connected LiveViews.
Tool execution lifecycle
Tool execution status updates reflect the lifecycle of tool calls:
- Tool call identified → message saved with status "pending" (via
save_message/3) - Tool execution starts →
update_tool_status/4with status:executing - Tool execution ends →
update_tool_status/4with status:completedor:failed
Configuration
Per-agent via AgentServer start options:
supervisor_config = [
agent_id: agent_id,
conversation_id: conversation_id,
display_message_persistence: MyApp.DisplayMessagePersistence,
# ... other config
]If not configured, no display messages are persisted. The agent still broadcasts PubSub events for real-time streaming — LiveViews can render messages from events alone without persistence.
Summary
Types
Context map passed to every callback. Carries cross-cutting identifiers that every implementation needs but don't benefit from positional visibility.
Attributes for a synthetic display message produced by middleware (not by an LLM). The shape mirrors the fields a typical implementation will write to its display-message store.
Callbacks
Resolve an interrupted tool result display message with actual result content.
Save a LangChain Message as one or more display messages.
Persist a synthetic display message originated by middleware.
Update the status of a persisted tool call display message.
Types
Context map passed to every callback. Carries cross-cutting identifiers that every implementation needs but don't benefit from positional visibility.
@type synthetic_message_attrs() :: %{ :message_type => String.t(), :content_type => String.t(), :content => map(), optional(:metadata) => map() }
Attributes for a synthetic display message produced by middleware (not by an LLM). The shape mirrors the fields a typical implementation will write to its display-message store.
@type tool_status() :: :executing | :completed | :failed | :interrupted | :cancelled
Callbacks
@callback resolve_tool_result( scope :: term() | nil, tool_call_id :: String.t(), result_content :: String.t(), context :: callback_context() ) :: {:ok, term()} | {:error, :not_found | term()}
Resolve an interrupted tool result display message with actual result content.
Called after a sub-agent resumes and completes. Updates the persisted tool result display message to clear the interrupt flag and replace placeholder content with the actual result.
Optional callback — implementations that don't need this can skip it.
Parameters
scope— Integrator-defined scope struct (ornil). Use to filter DB writes.tool_call_id— The tool call ID matching the interrupted tool resultresult_content— The actual result content stringcontext— Map with:agent_idand:conversation_id
Returns
{:ok, updated_message}— Updated record, broadcast to LiveViews{:error, :not_found}— No matching interrupted tool result exists
@callback save_message( scope :: term() | nil, message :: LangChain.Message.t(), context :: callback_context() ) :: {:ok, list()} | {:error, term()}
Save a LangChain Message as one or more display messages.
A single LangChain.Message may produce multiple display messages
(e.g., an assistant message with text content + tool calls produces
a text display message and one or more tool_call display messages).
The implementation should:
- Convert the Message into display message records
- Persist them to the database (scoped via
scope) - Return the list of saved records
AgentServer broadcasts {:display_message_saved, msg} for each
returned record, so connected LiveViews can update their UI.
Parameters
scope— Integrator-defined scope struct (ornil). Use to filter DB writes.message— TheLangChain.Messagestruct to persistcontext— Map with:agent_idand:conversation_id
Returns
{:ok, [saved_messages]}— List of persisted display message records{:error, reason}— Persistence failed (logged, does not affect agent)
@callback save_synthetic_message( scope :: term() | nil, attrs :: synthetic_message_attrs(), context :: callback_context() ) :: {:ok, term()} | {:error, term()}
Persist a synthetic display message originated by middleware.
Used for transcript entries that should appear in the conversation but do
not correspond to a LangChain.Message — for example, the user's answer
to an ask_user question, or a "user cancelled" notification.
AgentServer invokes this callback in response to
Sagents.AgentServer.save_synthetic_message_from/2 and broadcasts the
saved record as {:display_message_saved, msg} so LiveViews stream it in
via the same path used for LLM-generated display messages.
Optional callback — middleware that uses this feature is responsible for ensuring the configured persistence module implements it.
Parameters
scope— Integrator-defined scope struct (ornil). Use to filter DB writes.attrs— Map with:message_type,:content_type,:content(and optionally:metadata).context— Map with:agent_idand:conversation_id.
Returns
{:ok, display_message}— Persisted record, broadcast as{:display_message_saved, msg}.{:error, reason}— Persistence failed (logged, does not affect agent).
@callback update_tool_status( scope :: term() | nil, status :: tool_status(), tool_info :: map(), context :: callback_context() ) :: {:ok, term()} | {:error, :not_found | term()}
Update the status of a persisted tool call display message.
Called at each stage of the tool execution lifecycle. The tool_info
map contains the tool call identifier and any status-specific metadata.
Parameters
scope— Integrator-defined scope struct (ornil). Use to filter DB writes.status— The new status::executing,:completed,:failed,:interrupted, or:cancelledtool_info— Map with at minimum:call_id, plus status-specific fields:Status Fields :executing%{call_id: "...", name: "...", display_text: "..."}:completed%{call_id: "...", name: "...", result: "..."}:failed%{call_id: "...", name: "...", error: "..."}:interrupted%{call_id: "...", display_text: "..."}:cancelled%{call_id: "...", name: "..."}context— Map with:agent_idand:conversation_id
Returns
{:ok, updated_message}— Updated record, broadcast to LiveViews as{:display_message_updated, msg}{:error, :not_found}— No matching tool call exists (normal if persistence wasn't configured when call was saved)