Sagents.Agent (Sagents v0.8.0-rc.5)

Copy Markdown

Main entry point for creating Agents.

A Agent is an AI agent with composable middleware that provides capabilities like TODO management, filesystem operations, and task delegation.

Basic Usage

# Create agent with default middleware
{:ok, agent} = Agent.new(%{
  agent_id: "my-agent-1",
  model: ChatAnthropic.new!(%{model: "claude-sonnet-4-6"}),
  base_system_prompt: "You are a helpful assistant."
})

# Execute with messages
state = State.new!(%{messages: [%{role: "user", content: "Hello!"}]})
{:ok, result_state} = Agent.execute(agent, state)

Middleware Composition

# Append custom middleware to defaults
{:ok, agent} = Agent.new(%{
  middleware: [MyCustomMiddleware]
})

# Customize default middleware
{:ok, agent} = Agent.new(%{
  filesystem_opts: [long_term_memory: true]
})

# Provide complete middleware stack
{:ok, agent} = Agent.new(%{
  replace_default_middleware: true,
  middleware: [{MyMiddleware, []}]
})

Summary

Functions

Build the default middleware stack.

Execute the agent with the given state.

Create a new Agent.

Create a new Agent, raising on error.

Resume agent execution after an interrupt.

Types

t()

@type t() :: %Sagents.Agent{
  agent_id: term(),
  assembled_system_prompt: term(),
  async_tool_timeout: term(),
  base_system_prompt: term(),
  before_fallback: term(),
  fallback_models: term(),
  filesystem_scope: term(),
  max_runs: term(),
  middleware: term(),
  mode: term(),
  model: term(),
  name: term(),
  scope: term(),
  tool_context: term(),
  tools: term()
}

Functions

build_default_middleware(model, agent_id, opts \\ [])

Build the default middleware stack.

This is a utility function that can be used to build the default middleware stack with custom options. Useful when you want to customize middleware configuration or when building subagents.

Parameters

  • model - The LangChain ChatModel struct
  • agent_id - The agent's unique identifier
  • opts - Keyword list of middleware options

Options

  • :todo_opts - Options for TodoList middleware
  • :filesystem_opts - Options for Filesystem middleware
  • :summarization_opts - Options for Summarization middleware
  • :subagent_opts - Options for SubAgent middleware
  • :interrupt_on - Map of tool names to interrupt configuration

Examples

middleware = Agent.build_default_middleware(
  model,
  "agent-123",
  filesystem_opts: [long_term_memory: true],
  interrupt_on: %{"write_file" => true}
)

execute(agent, state, opts \\ [])

Execute the agent with the given state.

Applies middleware hooks in order:

  1. before_model hooks (in order)
  2. LLM execution
  3. after_model hooks (in reverse order)

Options

  • :callbacks - A list of callback handler maps. Each map may contain LangChain callback keys (see LangChain.Chains.ChainCallbacks) and/or the Sagents-specific :on_after_middleware key. All maps are added to the LLMChain, and matching handlers fire in fan-out (all maps checked).

    LangChain callback keys (e.g., :on_llm_token_usage, :on_message_processed) are fired by LLMChain.run/2 during execution. The :on_after_middleware key is fired by the agent directly after before_model hooks complete, before the LLM call — it receives the prepared state as its single argument.

    When running via AgentServer, callbacks are built automatically: PubSub callbacks (for broadcasting events) are combined with middleware callbacks (from Middleware.collect_callbacks/1) into this list.

Returns

  • {:ok, state} - Normal completion
  • {:interrupt, state, interrupt_data} - Execution paused for human approval
  • {:error, reason} - Execution failed

Examples

state = State.new!(%{messages: [%{role: "user", content: "Hello"}]})

case Agent.execute(agent, state) do
  {:ok, final_state} ->
    # Normal completion
    handle_response(final_state)

  {:interrupt, interrupted_state, interrupt_data} ->
    # Human approval needed
    decisions = get_human_decisions(interrupt_data)
    {:ok, final_state} = Agent.resume(agent, interrupted_state, decisions)
    handle_response(final_state)

  {:error, err} ->
    # Handle error
    Logger.error("Agent execution failed: #{inspect(err)}")
end

# With custom callbacks
callbacks = [
  %{
    on_llm_token_usage: fn _chain, usage ->
      IO.inspect(usage, label: "tokens")
    end
  }
]

Agent.execute(agent, state, callbacks: callbacks)

new(attrs \\ %{}, opts \\ [])

Create a new Agent.

Attributes

  • :agent_id - Unique identifier for the agent (optional, auto-generated if not provided)
  • :model - LangChain ChatModel struct (required)
  • :base_system_prompt - Base system instructions
  • :tools - Additional tools beyond middleware (default: [])
  • :middleware - List of middleware modules/configs (default: [])
  • :name - Agent name for identification (default: nil)
  • :filesystem_scope - Optional scope key for referencing an independently-running filesystem (e.g., {:user, 123}, {:project, 456})
  • :scope - Integrator-defined scope struct (e.g., %MyApp.Accounts.Scope{}). Opaque to Sagents — propagated as the first positional argument to persistence callbacks (AgentPersistence, DisplayMessagePersistence, FileSystemCallbacks) and auto-merged into tool-call custom_context under the canonical :scope key. Default: nil. Note: not serialized — scope is session/runtime state belonging to the caller starting the agent, not to persisted conversations. On restore, scope comes from the fresh Coordinator invocation that starts the agent.
  • :tool_context - Map of caller-supplied data merged into LLMChain.custom_context so every tool function receives it as part of its second argument. Internal keys (:state, :parent_middleware, :parent_tools, :scope) always take precedence on collision. (default: %{})
  • :async_tool_timeout - Timeout for parallel tool execution. Integer (milliseconds) or :infinity. Overrides application-level config. See LLMChain module docs for details. (default: uses application config or :infinity)
  • :fallback_models - List of ChatModel structs to try if primary model fails (default: [])
  • :before_fallback - Optional function to modify chain before each attempt (default: nil). Signature: fn chain -> modified_chain end. Useful for provider-specific system prompts or modifications
  • :max_runs - Maximum number of LLM calls per execution (default: 50 for AgentExecution mode). Agents with many tools or complex middleware may need higher values. Can also be overridden per-invocation via execute/3 opts: execute(agent, state, max_runs: 100).

Options

  • :replace_default_middleware - If true, use only provided middleware (default: false)
  • :todo_opts - Options for TodoList middleware
  • :filesystem_opts - Options for Filesystem middleware
  • :summarization_opts - Options for Summarization middleware (e.g., [max_tokens_before_summary: 150_000, messages_to_keep: 8])
  • :subagent_opts - Options for SubAgent middleware
  • :interrupt_on - Map of tool names to interrupt configuration (default: nil)

Human-in-the-loop configuration

The :interrupt_on option enables human oversight for specific tools:

# Simple boolean configuration
interrupt_on: %{
  "write_file" => true,    # Require approval
  "delete_file" => true,
  "read_file" => false     # No approval needed
}

# Advanced configuration
interrupt_on: %{
  "write_file" => %{
    allowed_decisions: [:approve, :edit, :reject]
  }
}

Examples

# Basic agent
{:ok, agent} = Agent.new(%{
  agent_id: "basic-agent",
  model: ChatAnthropic.new!(%{model: "claude-sonnet-4-6"}),
  base_system_prompt: "You are helpful."
})

# With custom tools
{:ok, agent} = Agent.new(%{
  agent_id: "tool-agent",
  model: model,
  tools: [write_file_tool, search_tool]
})

# With human-in-the-loop for file operations
{:ok, agent} = Agent.new(
  %{
    agent_id: "hitl-agent",
    model: model,
    tools: [write_file_tool, delete_file_tool]
  },
  interrupt_on: %{
    "write_file" => true,  # Require approval for writes
    "delete_file" => %{allowed_decisions: [:approve, :reject]}  # No edit for deletes
  }
)

# Execute and handle interrupts
case Agent.execute(agent, state) do
  {:ok, final_state} ->
    IO.puts("Agent completed successfully")

  {:interrupt, interrupted_state, interrupt_data} ->
    # Present interrupt_data.action_requests to user
    # Get their decisions
    decisions = UI.get_decisions(interrupt_data)
    {:ok, final_state} = Agent.resume(agent, interrupted_state, decisions)

  {:error, reason} ->
    Logger.error("Agent failed: #{inspect(reason)}")
end

# With custom middleware configuration
{:ok, agent} = Agent.new(
  %{
    agent_id: "custom-middleware-agent",
    model: model
  },
  filesystem_opts: [long_term_memory: true]
)

# With caller-supplied context for tool functions
{:ok, agent} = Agent.new(%{
  agent_id: "context-agent",
  model: model,
  tool_context: %{user_id: 42, tenant: "acme"}
})

# Tool functions receive the context as their second argument:
# fn args, context ->
#   context.user_id  #=> 42
#   context.tenant   #=> "acme"
#   context.state    #=> %State{} (always present)
# end

new!(attrs \\ %{}, opts \\ [])

Create a new Agent, raising on error.

resume(agent, state, resume_data, opts \\ [])

Resume agent execution after an interrupt.

Cycles through the middleware stack, giving each middleware a chance to claim and handle the interrupt via handle_resume/4. The first middleware that returns {:ok, state} or {:interrupt, ...} or {:error, ...} wins. If no middleware claims the interrupt, returns an error.

Parameters

  • agent - The agent instance
  • state - The state at the point of interruption (with interrupt_data set)
  • resume_data - Data provided by the caller to resume (polymorphic per middleware)
  • opts - Options (same as execute/3, including :callbacks)

Examples

# HITL resume with decisions
{:interrupt, state, interrupt_data} = Agent.execute(agent, initial_state)
decisions = [%{type: :approve}, %{type: :reject}]
{:ok, final_state} = Agent.resume(agent, state, decisions)

# AskUserQuestion resume with response
{:interrupt, state, %{type: :ask_user_question}} = Agent.execute(agent, initial_state)
response = %{type: :answer, selected: ["PostgreSQL"]}
{:ok, final_state} = Agent.resume(agent, state, response)