Sagents.Agent (Sagents v0.4.2)

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 a human-in-the-loop 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(),
  middleware: term(),
  mode: term(),
  model: term(),
  name: 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})
  • :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) 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

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, decisions, opts \\ [])

Resume agent execution after a human-in-the-loop interrupt.

Takes decisions from the human reviewer and continues agent execution.

Parameters

  • agent - The agent instance
  • state - The state at the point of interruption
  • decisions - List of decision maps from human reviewer
  • opts - Options (same as execute/3, including :callbacks)

Decision Format

decisions = [
  %{type: :approve},                                    # Approve with original arguments
  %{type: :edit, arguments: %{"path" => "other.txt"}}, # Edit arguments
  %{type: :reject}                                      # Reject execution
]

Examples

# Get interrupt from execution
{:interrupt, state, interrupt_data} = Agent.execute(agent, initial_state)

# Examine the interrupt data
# interrupt_data.action_requests - List of tools needing approval
# interrupt_data.review_configs - Map of tool_name => %{allowed_decisions: [...]}

# Example: Display to user and get decisions
decisions =
  Enum.map(interrupt_data.action_requests, fn request ->
    # Show request.tool_name, request.arguments to user
    case get_user_choice(request) do
      :approve -> %{type: :approve}
      :reject -> %{type: :reject}
      {:edit, new_args} -> %{type: :edit, arguments: new_args}
    end
  end)

# Resume execution with decisions
{:ok, final_state} = Agent.resume(agent, state, decisions)

# Or handle edit decision example
decisions = [
  %{type: :approve},  # Approve first tool
  %{type: :edit, arguments: %{"path" => "/tmp/safe.txt"}},  # Edit second tool's path
  %{type: :reject}  # Reject third tool
]

{:ok, final_state} = Agent.resume(agent, state, decisions)