Sagents.State (Sagents v0.8.0-rc.5)
Copy MarkdownAgent state structure for managing agent execution context.
The state holds the complete context for an agent execution including:
- Message history (list of
LangChain.Messagestructs) - TODO list
- Metadata
Note: Files are managed separately by FileSystemServer and are not part of
the agent's internal state. The FileSystemServer provides persistent storage with
ETS and optional backend persistence (disk, database, S3, etc.).
agent_id Management (Automatic)
The agent_id field is a runtime identifier used for process registration and
coordination. You don't need to set it when creating states—the library
automatically injects it when you call Sagents.Agent.execute/2, Sagents.Agent.resume/3, or
start an AgentServer.
Why it's automatic
The agent_id flows from the Agent struct (which is configuration) to the State
(which is data). Making you synchronize them manually could be error-prone.
For middleware developers
When middleware receives state in hooks (before_model, after_model), the
agent_id will already be set. If you create new state structs in middleware,
copy the agent_id from the incoming state:
def after_model(state, config) do
updated_state = State.new!(%{
agent_id: state.agent_id, # Copy from incoming state
messages: new_messages
})
{:ok, updated_state}
endState Merging
State merging follows specific rules:
- messages: Appends new messages to existing list
- todos: Replaces with new todos (merge handled by TodoList middleware)
- metadata: Deep merges metadata maps
- agent_id: Uses right if present, otherwise left (runtime identifier, not data)
Summary
Functions
Add a message to the state.
Add multiple messages to the state.
Demote every is_interrupt: true tool result in the message log to an
error result, signaling to the LLM that the user abandoned the pending
interrupt and that a new user message follows.
Replace stale interrupt placeholder tool results with error messages, consulting the agent's middleware to decide which interrupts can be restored from cold start.
Remove a TODO by ID.
Deserializes state data from export_state/1.
Get metadata value.
Get a TODO by ID.
Get all TODOs with a specific status.
Load an agent's persisted state via an Sagents.AgentPersistence
implementation, or return a fresh empty state if nothing is saved or the
saved data is unusable.
Merge two states together.
Create a new agent state.
Create a new agent state, raising on error.
Set metadata value.
Add or update a TODO item.
Replace a tool result in the state's messages by tool_call_id.
Reset the state to a clean slate.
Replace all messages.
Replace all TODOs.
Types
Functions
Add a message to the state.
Message must be a LangChain.Message struct.
Add multiple messages to the state.
Messages must be LangChain.Message structs.
Demote every is_interrupt: true tool result in the message log to an
error result, signaling to the LLM that the user abandoned the pending
interrupt and that a new user message follows.
Used when a user sends a free-text message instead of resuming an
interrupted tool call (e.g. ignoring an ask_user question and asking
something different). Without this demotion the trailing interrupt
placeholder would remain in the conversation, causing the next LLM call
to be malformed (Anthropic rejects with "must end with a user message").
Also clears state.interrupt_data (the virtual field).
Idempotent: a state with no interrupts is unchanged.
@spec clean_stale_interrupts(t(), [Sagents.MiddlewareEntry.t()]) :: t()
Replace stale interrupt placeholder tool results with error messages, consulting the agent's middleware to decide which interrupts can be restored from cold start.
Called after loading state from the database. For each interrupted tool result the decision is:
interrupt_data == nil(decode failed, never persisted, or written by older code) → demote to error result.interrupt_datashape is:multiple_interrupts→ demote unless every sub-interrupt is itself claimed by some middleware. Partial restore is a footgun: a resumed agent would dispatch responses that don't all have valid targets.- No middleware in
middleware_entriessaysrestorable_interrupt?for the data → demote. - Otherwise → keep
is_interrupt: trueandinterrupt_dataintact.
Demoted results carry a clear message so the LLM can see the call failed on the next turn and recover gracefully. Two messages are used to distinguish the failure modes (process-bound vs incompatible data) for debugging. Both are equivalent to the model.
An empty middleware_entries list demotes every interrupt — preserving
pre-restorable behaviour for callers that don't have an agent in scope
(e.g. from_serialized/2).
Idempotent: a state with no interrupted tool results is unchanged.
Remove a TODO by ID.
Deserializes state data from export_state/1.
This is a convenience wrapper around StateSerializer.deserialize_state/2.
Important: The agent_id is NOT serialized (it's a runtime identifier),
so you MUST provide it when deserializing. This ensures the state can properly
interact with AgentServer and middleware that rely on the agent_id.
Examples
# Load from database
{:ok, state_data} = load_from_db(conversation_id)
# Deserialize with agent_id
{:ok, state} = State.from_serialized("my-agent-123", state_data["state"])Parameters
agent_id- The agent_id to use for this state (required)data- The serialized state map (the "state" field from export_state)
Returns
{:ok, state}- Successfully deserialized with agent_id set{:error, reason}- Deserialization failed
Get metadata value.
Get a TODO by ID.
Get all TODOs with a specific status.
@spec load_or_new( module(), term() | nil, %{:agent_id => String.t(), optional(:conversation_id) => term()}, keyword() ) :: {:ok, t()}
Load an agent's persisted state via an Sagents.AgentPersistence
implementation, or return a fresh empty state if nothing is saved or the
saved data is unusable.
Always returns {:ok, state} so callers can pattern-match without
fallback branches. Failure modes (missing envelope key, malformed
serialized data) are logged at warning level and degrade gracefully to a
fresh state — restoring a partial conversation is worse than starting
fresh, since the user can re-prompt but cannot recover from a server
that won't boot.
Parameters
persistence_module— module implementingSagents.AgentPersistencescope— integrator-defined scope struct (forwarded toload_state/2)context— map with:agent_id(required) and:conversation_id(optional; useful for log lines and is included in the load context)opts— optional keyword list::fresh_state_attrs— map seeded intonew!/1only when no persisted state is found (ignored on resume).
Examples
Sagents.State.load_or_new(MyApp.AgentPersistence, scope, %{
agent_id: "conversation-123",
conversation_id: 123
})
# => {:ok, %Sagents.State{...}}
Sagents.State.load_or_new(
MyApp.AgentPersistence,
scope,
%{agent_id: "conversation-123", conversation_id: 123},
fresh_state_attrs: %{todos: seed_todos}
)
Merge two states together.
This is used when combining state updates from tools, middleware, or subagents.
Merge Rules
- messages: Concatenates lists (left + right)
- todos: Uses right if present, otherwise left
- metadata: Deep merges maps
Examples
left = State.new!(%{messages: [%{role: "user", content: "hi"}]})
right = State.new!(%{messages: [%{role: "assistant", content: "hello"}]})
merged = State.merge_states(left, right)
# merged now has both messages
Create a new agent state.
Note: The agent_id field is optional when creating a state. The library
automatically injects it when the state is passed to Agent.execute or
AgentServer. This eliminates the need for manual agent_id synchronization.
Examples
# Create state without agent_id (recommended)
state = State.new!(%{messages: [message]})
# Library injects agent_id automatically
{:ok, result_state} = Agent.execute(agent, state)
Create a new agent state, raising on error.
Set metadata value.
Add or update a TODO item.
If a TODO with the same ID exists, it will be replaced at its current position. If the TODO ID doesn't exist, it will be appended to the end of the list.
Replace a tool result in the state's messages by tool_call_id.
Delegates to LangChain.Message.replace_tool_result/3.
Reset the state to a clean slate.
Clears:
- All messages
- All TODOs
- All metadata
Note: This function only resets the Agent's state structure. File state is managed separately by FileSystemServer and must be reset through AgentServer.reset/1 which coordinates the full reset process.
Examples
state = State.new!(%{
messages: [msg1, msg2],
todos: [todo1],
metadata: %{config: "value"}
})
reset_state = State.reset(state)
# reset_state has:
# - messages: []
# - todos: []
# - metadata: %{} (cleared)
Replace all messages.
Useful for:
- Thread restoration (restoring persisted messages)
- Testing scenarios (setting sample messages)
- Bulk message updates
Parameters
state- The current State structmessages- List of Message structs
Examples
messages = [
Message.new_user!("Hello")
]
state = State.set_messages(state, messages)
Replace all TODOs.