Sagents.Agent (Sagents v0.4.2)
Copy MarkdownMain 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
@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 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 structagent_id- The agent's unique identifieropts- 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 the agent with the given state.
Applies middleware hooks in order:
- before_model hooks (in order)
- LLM execution
- after_model hooks (in reverse order)
Options
:callbacks- A list of callback handler maps. Each map may contain LangChain callback keys (seeLangChain.Chains.ChainCallbacks) and/or the Sagents-specific:on_after_middlewarekey. 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 byLLMChain.run/2during execution. The:on_after_middlewarekey is fired by the agent directly afterbefore_modelhooks 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 (fromMiddleware.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)
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 intoLLMChain.custom_contextso 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
Create a new Agent, raising on error.
Resume agent execution after a human-in-the-loop interrupt.
Takes decisions from the human reviewer and continues agent execution.
Parameters
agent- The agent instancestate- The state at the point of interruptiondecisions- List of decision maps from human revieweropts- Options (same asexecute/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)