Sagents.MessagePreprocessor behaviour (Sagents v0.8.0-rc.1)
Copy MarkdownBehaviour for transforming user-submitted messages into separate display and LLM representations.
When a user sends a message, the system may need to produce two different versions:
Display message — The user-facing representation persisted for UI rendering. For example,
@ProjectBriefmight render as a styled link/chip.LLM message — The message added to the model's conversation history. For example,
@ProjectBriefmight be expanded to include the full document content so the model can reason over it.
Scope-first contract
The preprocess/3 callback takes the integrator's scope struct as its first positional argument.
When this runs
Called inside AgentServer.handle_call({:add_message, ...}) before the message
is added to state or persisted as a display message. Only called for messages submitted
via AgentServer.add_message/2 — NOT for messages generated during LLM execution
(assistant responses, tool results).
Content representation
Both the display message and the LLM message are regular LangChain.Message structs.
The behaviour only constrains the return shape — not what's inside the messages.
Implementations are free to use whatever content representation makes sense:
- Plain text with HTML snippets for display rendering
- XML-tagged metadata for structured LLM context
- Structured content maps for richer UI rendering
Configuration
Per-agent via AgentServer/AgentSupervisor start options:
AgentSupervisor.start_link(
agent: agent,
conversation_id: conversation_id,
message_preprocessor: MyApp.MentionPreprocessor,
# ...
)If not configured, messages flow through unchanged to both paths.
Summary
Types
Context map passed to preprocess/3. Contains identifiers plus the caller's
tool_context (for integrator-defined tool-parameter data, no longer a transport
for scope) and the current agent state.
Callbacks
Transform a user-submitted message into separate display and LLM representations.
Types
@type preprocessor_context() :: %{ agent_id: String.t(), conversation_id: String.t() | nil, tool_context: map(), state: Sagents.State.t() }
Context map passed to preprocess/3. Contains identifiers plus the caller's
tool_context (for integrator-defined tool-parameter data, no longer a transport
for scope) and the current agent state.
Callbacks
@callback preprocess( scope :: term() | nil, message :: LangChain.Message.t(), context :: preprocessor_context() ) :: {:ok, display_message :: LangChain.Message.t(), llm_message :: LangChain.Message.t()} | {:error, term()}
Transform a user-submitted message into separate display and LLM representations.
Parameters
scope— Integrator-defined scope struct (ornil). Use for document lookups, permission checks, etc.message— TheLangChain.Messagebeing submittedcontext— Map with:agent_id,:conversation_id,:tool_context(integrator's non-scope tool-parameter data), and:state(currentSagents.State).
Returns
{:ok, display_message, llm_message}— Use different messages for each path. For passthrough, return the same message for both.{:error, reason}— Reject the message. AgentServer replies with{:error, reason}.