# `Sagents.Agent`

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, []}]
    })

# `t`

```elixir
@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()
}
```

# `build_default_middleware`

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`

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`

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!`

Create a new Agent, raising on error.

# `resume`

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)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
