Sagents.State (Sagents v0.4.2)

Copy Markdown

Agent state structure for managing agent execution context.

The state holds the complete context for an agent execution including:

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}
end

State 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.

Replace any stale interrupt placeholder tool results with error messages.

Remove a TODO by ID.

Deserializes state data from export_state/1.

Get a TODO by ID.

Get all TODOs with a specific status.

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

t()

@type t() :: %Sagents.State{
  agent_id: term(),
  interrupt_data: term(),
  messages: term(),
  metadata: term(),
  todos: term()
}

Functions

add_message(state, message)

Add a message to the state.

Message must be a LangChain.Message struct.

add_messages(state, messages)

Add multiple messages to the state.

Messages must be LangChain.Message structs.

clean_stale_interrupts(state)

@spec clean_stale_interrupts(t()) :: t()

Replace any stale interrupt placeholder tool results with error messages.

Called after loading state from the database. Interrupted tool results reference sub-agent processes that no longer exist, so they must be converted to error results before the LLM sees them.

This is idempotent — if there are no stale interrupts, it's a no-op.

delete_todo(state, todo_id)

Remove a TODO by ID.

from_serialized(agent_id, data)

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(state, key, default \\ nil)

Get metadata value.

get_todo(state, todo_id)

Get a TODO by ID.

get_todos_by_status(state, status)

Get all TODOs with a specific status.

merge_states(left, right)

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

new(attrs \\ %{})

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)

new!(attrs \\ %{})

Create a new agent state, raising on error.

put_metadata(state, key, value)

Set metadata value.

put_todo(state, todo)

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_tool_result(state, tool_call_id, new_result)

Replace a tool result in the state's messages by tool_call_id.

Delegates to LangChain.Message.replace_tool_result/3.

reset(state)

@spec reset(t()) :: t()

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)

set_messages(state, messages)

Replace all messages.

Useful for:

  • Thread restoration (restoring persisted messages)
  • Testing scenarios (setting sample messages)
  • Bulk message updates

Parameters

  • state - The current State struct
  • messages - List of Message structs

Examples

messages = [
  Message.new_user!("Hello")
]
state = State.set_messages(state, messages)

set_todos(state, todos)

Replace all TODOs.