# `LangChain.Chains.LLMChain.Mode.Steps`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L1)

Pipe-friendly building blocks for composing custom execution modes.

Every step function follows the pipeline convention:

- Input/output: `{:continue, chain}` means keep processing
- Any other tuple (`:ok`, `:pause`, `:error`) is a terminal — passed through unchanged
- Step functions take the pipeline result as first arg, optional config as second

## Example: Custom Mode

    defmodule MyApp.Modes.Simple do
      @behaviour LangChain.Chains.LLMChain.Mode
      import LangChain.Chains.LLMChain.Mode.Steps

      @impl true
      def run(chain, opts) do
        chain = ensure_mode_state(chain)

        {:continue, chain}
        |> call_llm()
        |> execute_tools()
        |> check_max_runs(opts)
        |> continue_or_done(&run/2, opts)
      end
    end

# `pipeline_result`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L36)

```elixir
@type pipeline_result() ::
  {:continue, LangChain.Chains.LLMChain.t()}
  | {:ok, LangChain.Chains.LLMChain.t()}
  | {:ok, LangChain.Chains.LLMChain.t(), term()}
  | {:pause, LangChain.Chains.LLMChain.t()}
  | {:error, LangChain.Chains.LLMChain.t(), term()}
  | {:interrupt, LangChain.Chains.LLMChain.t(), term()}
```

# `call_llm`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L51)

Call the LLM (single step). Wraps `LLMChain.execute_step/1`.

On success, increments `mode_state.run_count` in `custom_context`.

# `check_max_runs`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L199)

Check if max runs have been exceeded.

Reads `run_count` from `custom_context.mode_state` and compares against
`:max_runs` in opts (default: 25).

# `check_pause`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L223)

Check if execution should pause (e.g., node draining).

Reads `:should_pause?` from opts — a zero-arity function that returns boolean.

# `check_tool_interrupts`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L247)

Check if any tool results in the most recent tool message are interrupts.

Returns `{:interrupt, chain, interrupt_data}` if any tool results have
`is_interrupt: true`. The `interrupt_data` is extracted from the first
interrupted result (for single interrupts) or aggregated (for multiple).

This step is generic — it doesn't know *why* a tool interrupted. The
consumer (e.g., Sagents) interprets the interrupt data.

# `check_until_tool`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L297)

Check if a target tool was called in the most recent tool results.

Reads `:tool_names` from opts — a list of tool name strings.
If a matching tool result is found, returns `{:ok, chain, tool_result}`.

# `continue_or_done`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L321)

Decide whether to loop or return.

- `{:continue, chain}` with `needs_response: true` → call `run_fn.(chain, opts)` (loop)
- `{:continue, chain}` with `needs_response: false` → `{:ok, chain}` (done)
- Any terminal result → pass through as-is

# `ensure_mode_state`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L342)

Initialize mode_state in custom_context if not already present.

Call this at the top of your mode's `run/2` to set up run_count tracking.
On the first call, creates `mode_state: %{run_count: 0}`.
On recursive calls (mode_state already exists), returns chain unchanged.

# `execute_tools`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L66)

Execute pending tool calls. Wraps `LLMChain.execute_tool_calls/1`.

# `expand_tool_results`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L117)

Expand any tool results that asked to, before the model is called.

A tool result can carry a `LangChain.MessageExpansion` asking for messages to
be inserted into the conversation and for the result's own content to be
trimmed once they are. This step is what honours that request. Placing it
immediately before `call_llm/1` is what makes the guarantee the tool is
relying on true: the messages are in the conversation for the very next model
call, in the same run.

    {:continue, chain}
    |> expand_tool_results(opts)
    |> call_llm()

## What it does

Reads `chain.last_message`. If it is a `:tool` message, every result carrying
an expansion is applied in tool-call order:

1. The result's content becomes the expansion's `result_content`, or is left
   alone when that is nil.
2. The expansion is cleared from the result, so applying the step twice
   inserts once.
3. The expansion's messages are appended with `LLMChain.add_messages/2`, which
   keeps `messages`, `exchanged_messages`, `last_message` and `needs_response`
   in agreement.

Any other pipeline result passes through untouched, including a terminal. A
turn that interrupted or satisfied an until-tool contract is over, so nothing
is inserted into it.

## Where it must not go

Not after the steps that decide whether the run is over. `check_until_tool/2`,
`Sagents.Mode.Steps.check_until_tool_success/2` and the chain's telemetry all
read `chain.last_message` to answer that question, and every one of them is
entitled to find a message the model produced or the tools returned there.
Expanding before the model call keeps that true; expanding after the terminal
checks would put a synthetic message under code that cannot tell the
difference.

Inserted messages fire no callbacks, so they produce no transcript rows. A
host that mirrors a conversation from `:on_message_processed` sees them when
it next reads the whole chain, not as they are inserted.

# `get_run_count`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L369)

Get the current run count from mode_state.

# `reset_run_count`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/chains/llm_chain/mode/steps.ex#L361)

Set `mode_state.run_count` to 0, keeping any other `mode_state` keys.

`mode_state` lives in the chain's `custom_context`, which persists when the
same chain is run again after a new message is added. A mode that bounds
each run with `check_max_runs/2` calls this once on entry, before it starts
recursing, so the count covers the current run rather than the chain's
whole history.

---

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