# `Sagents.Middleware`

Behavior for DeepAgent middleware components.

Middleware provides a composable pattern for adding capabilities to agents. Each
middleware component can contribute:

- System prompt additions
- Tools (Functions)
- State schema modifications
- Pre/post processing hooks
- LLM callback handlers (token usage, tool execution, message processing)

## Middleware Lifecycle

1. **Initialization** - `init/1` is called when middleware is configured
2. **Tool Collection** - `tools/1` provides tools to add to the agent
3. **Prompt Assembly** - `system_prompt/1` contributes to the system prompt
4. **Callback Collection** - `callbacks/1` provides LLM event handlers
5. **Before Model** - `before_model/2` preprocesses state before LLM call
6. **After Model** - `after_model/2` postprocesses state after LLM response

## Example

    defmodule MyMiddleware do
      @behaviour Sagents.Middleware

      @impl true
      def init(opts) do
        config = %{enabled: Keyword.get(opts, :enabled, true)}
        {:ok, config}
      end

      @impl true
      def system_prompt(_config) do
        "You have access to custom capabilities."
      end

      @impl true
      def tools(_config) do
        [my_custom_tool()]
      end

      @impl true
      def callbacks(_config) do
        %{
          on_llm_token_usage: fn _chain, usage ->
            Logger.info("Token usage: #{inspect(usage)}")
          end
        }
      end

      @impl true
      def before_model(state, _config) do
        # Preprocess state
        {:ok, state}
      end
    end

## Middleware Configuration

Middleware can be specified as:

- Module name: `MyMiddleware`
- Tuple with options: `{MyMiddleware, [enabled: true]}`

# `config`

```elixir
@type config() :: keyword()
```

# `middleware_config`

```elixir
@type middleware_config() :: any()
```

# `middleware_result`

```elixir
@type middleware_result() ::
  {:ok, Sagents.State.t()}
  | {:interrupt, Sagents.State.t(), any()}
  | {:error, term()}
```

# `after_model`

*optional* 

```elixir
@callback after_model(Sagents.State.t(), middleware_config()) :: middleware_result()
```

Process state after receiving LLM response.

Receives the state after the LLM has responded and can modify the response,
extract information, or update state.

Defaults to `{:ok, state}` if not implemented.

## Parameters

- `state` - The current `Sagents.State` struct (with LLM response)
- `config` - The middleware configuration from `init/1`

## Returns

- `{:ok, updated_state}` - Success with potentially modified state
- `{:interrupt, state, interrupt_data}` - Pause execution for human intervention
- `{:error, reason}` - Failure, halts execution

# `before_model`

*optional* 

```elixir
@callback before_model(Sagents.State.t(), middleware_config()) :: middleware_result()
```

Process state before it's sent to the LLM.

Receives the current agent state and can modify messages, add context, or
perform validation before the LLM is invoked.

Defaults to `{:ok, state}` if not implemented.

## Parameters

- `state` - The current `Sagents.State` struct
- `config` - The middleware configuration from `init/1`

## Returns

- `{:ok, updated_state}` - Success with potentially modified state
- `{:error, reason}` - Failure, halts execution

# `callbacks`

*optional* 

```elixir
@callback callbacks(middleware_config()) :: map()
```

Provide LangChain callback handlers for this middleware.

Receives the middleware configuration from `init/1` and returns a callback
handler map compatible with `LangChain.Chains.LLMChain.add_callback/2`.
This allows middleware to observe LLM events such as token usage, tool
execution, and message processing.

When multiple middleware declare callbacks, all handlers are collected and
fire in fan-out fashion (every matching handler from every middleware fires).

Defaults to empty map (`%{}`) if not implemented. Return `%{}` for no callbacks.

## Parameters

- `config` - The middleware configuration from `init/1`

## Returns

- A map of callback keys to handler functions

## Available Callback Keys

These are the LangChain-native keys supported by `LLMChain`. Use only these
keys in your callback map (see `LangChain.Chains.ChainCallbacks` for full
type signatures):

**Model-level callbacks:**
- `:on_llm_new_delta` - Streaming token/delta received
- `:on_llm_new_message` - Complete message from LLM
- `:on_llm_ratelimit_info` - Rate limit headers from provider
- `:on_llm_token_usage` - Token usage information
- `:on_llm_response_headers` - Raw response headers

**Chain-level callbacks:**
- `:on_message_processed` - Message fully processed by chain
- `:on_message_processing_error` - Error processing a message
- `:on_error_message_created` - Error message created
- `:on_tool_call_identified` - Tool call detected during streaming
- `:on_tool_execution_started` - Tool begins executing
- `:on_tool_execution_completed` - Tool finished successfully
- `:on_tool_execution_failed` - Tool execution errored
- `:on_tool_response_created` - Tool response message created
- `:on_retries_exceeded` - Max retries exhausted

## Example

    def callbacks(_config) do
      %{
        on_llm_token_usage: fn _chain, usage ->
          Logger.info("Token usage: #{inspect(usage)}")
        end,
        on_message_processed: fn _chain, message ->
          Logger.info("Message: #{inspect(message)}")
        end
      }
    end

# `handle_message`

*optional* 

```elixir
@callback handle_message(message :: term(), Sagents.State.t(), middleware_config()) ::
  {:ok, Sagents.State.t()} | {:error, term()}
```

Handle messages sent to this middleware.

Messages are routed to a specific middleware by ID through the AgentServer's
middleware registry. Any process can send a targeted message to a middleware
using `AgentServer.notify_middleware/3`.

This enables two primary patterns:

### 1. External notifications

LiveViews, controllers, or other processes can send context updates to a
running middleware. The middleware updates state metadata, which `before_model/2`
reads on the next LLM call.

    # In a LiveView — user switched to editing a different blog post
    AgentServer.notify_middleware(agent_id, MyApp.UserContext, {:post_changed, %{
      slug: "/blog/getting-started-with-elixir",
      title: "Getting Started with Elixir"
    }})

    # In the middleware
    def handle_message({:post_changed, post_info}, state, _config) do
      {:ok, State.put_metadata(state, "current_post", post_info)}
    end

### 2. Async task results

Middleware that spawns background tasks sends results back to itself for
state updates.

    def handle_message({:title_generated, title}, state, _config) do
      {:ok, State.put_metadata(state, "conversation_title", title)}
    end

Defaults to `{:ok, state}` if not implemented.

## Parameters

- `message` - The message payload (any term — typically a tagged tuple)
- `state` - The current `Sagents.State` struct
- `config` - The middleware configuration from `init/1`

## Returns

- `{:ok, updated_state}` - Success with potentially modified state
- `{:error, reason}` - Failure (logged but does not halt agent execution)

# `init`

*optional* 

```elixir
@callback init(config()) :: {:ok, middleware_config()} | {:error, term()}
```

Initialize middleware with configuration options.

Called once when the middleware is added to an agent. Returns configuration
that will be passed to other callbacks.

## Convention

- Input: `opts` as keyword list
- Output: `config` as map for efficient runtime access

Defaults to converting opts to a map if not implemented.

## Example

    def init(opts) do
      config = %{
        enabled: Keyword.get(opts, :enabled, true),
        max_retries: Keyword.get(opts, :max_retries, 3)
      }
      {:ok, config}
    end

# `on_server_start`

*optional* 

```elixir
@callback on_server_start(Sagents.State.t(), middleware_config()) ::
  {:ok, Sagents.State.t()} | {:error, term()}
```

Called when the AgentServer starts or restarts.

This allows middleware to perform initialization actions that require
the AgentServer to be running, such as broadcasting initial state to
subscribers (e.g., TODOs for UI display).

Receives the current state and middleware config.
Returns `{:ok, state}` (state is not typically modified here but could be).

Defaults to `{:ok, state}` if not implemented.

## Parameters

- `state` - The current `Sagents.State` struct
- `config` - The middleware configuration from `init/1`

## Returns

- `{:ok, state}` - Success (state typically unchanged)
- `{:error, reason}` - Failure (logged but does not halt agent)

## Example

    def on_server_start(state, _config) do
      # Broadcast initial todos when AgentServer starts
      broadcast_todos(state.agent_id, state.todos)
      {:ok, state}
    end

# `state_schema`

*optional* 

```elixir
@callback state_schema() :: module() | nil
```

Provide the state schema module for this middleware.

If the middleware needs to add fields to the agent state, it should
return a module that defines those fields.

Defaults to `nil` if not implemented.

# `system_prompt`

*optional* 

```elixir
@callback system_prompt(middleware_config()) :: String.t() | [String.t()]
```

Provide system prompt text for this middleware.

Can return a single string or list of strings that will be joined.

Defaults to empty string if not implemented.

# `tools`

*optional* 

```elixir
@callback tools(middleware_config()) :: [LangChain.Function.t()]
```

Provide tools (Functions) that this middleware adds to the agent.

Defaults to empty list if not implemented.

# `apply_after_model`

```elixir
@spec apply_after_model(Sagents.State.t(), Sagents.MiddlewareEntry.t()) ::
  middleware_result()
```

Apply after_model hook from middleware.

## Parameters

- `state` - The current agent state (with LLM response)
- `entry` - MiddlewareEntry struct with module and config

## Returns

- `{:ok, updated_state}` - Success with potentially modified state
- `{:error, reason}` - Error from middleware

# `apply_before_model`

```elixir
@spec apply_before_model(Sagents.State.t(), Sagents.MiddlewareEntry.t()) ::
  middleware_result()
```

Apply before_model hook from middleware.

## Parameters

- `state` - The current agent state
- `entry` - MiddlewareEntry struct with module and config

## Returns

- `{:ok, updated_state}` - Success with potentially modified state
- `{:error, reason}` - Error from middleware

# `apply_handle_message`

```elixir
@spec apply_handle_message(term(), Sagents.State.t(), Sagents.MiddlewareEntry.t()) ::
  {:ok, Sagents.State.t()} | {:error, term()}
```

Apply handle_message callback from middleware.

## Parameters

- `message` - The message payload to handle
- `state` - The current agent state
- `entry` - MiddlewareEntry struct with module and config

## Returns

- `{:ok, updated_state}` - Success with potentially modified state
- `{:error, reason}` - Error from middleware

# `apply_on_server_start`

```elixir
@spec apply_on_server_start(Sagents.State.t(), Sagents.MiddlewareEntry.t()) ::
  {:ok, Sagents.State.t()} | {:error, term()}
```

Apply on_server_start callback from middleware.

Called when the AgentServer starts to allow middleware to perform
initialization actions like broadcasting initial state.

## Parameters

- `state` - The current agent state
- `entry` - MiddlewareEntry struct with module and config

## Returns

- `{:ok, state}` - Success (state typically unchanged)
- `{:error, reason}` - Error from middleware

# `collect_callbacks`

```elixir
@spec collect_callbacks([Sagents.MiddlewareEntry.t()]) :: [map()]
```

Collect callback handler maps from all middleware.

Calls `get_callbacks/1` on each middleware entry and filters out nils.
Returns a list of callback handler maps suitable for passing
to `LLMChain.add_callback/2`.

# `get_callbacks`

Get LLM callback handlers from middleware.

Returns the callback handler map from the middleware's `callbacks/1` callback,
or `nil` if the callback is not implemented.

# `get_system_prompt`

Get system prompt from middleware.

# `get_tools`

Get tools from middleware.

# `init_middleware`

Initialize a middleware module with its configuration.
Returns a MiddlewareEntry struct.

## Configuration Convention

- Input `opts` should be a keyword list
- Returned `config` should be a map for efficient runtime access

# `normalize`

Normalize middleware specification to {module, config} tuple.

Accepts:
- Module atom: `MyMiddleware` -> `{MyMiddleware, []}`
- Tuple with keyword list: `{MyMiddleware, [key: value]}` -> `{MyMiddleware, [key: value]}`

---

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