LangChain.Chains.LLMChain.Mode.Steps (LangChain v0.14.2)

Copy Markdown View Source

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

Summary

Functions

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

Check if max runs have been exceeded.

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

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

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

Decide whether to loop or return.

Initialize mode_state in custom_context if not already present.

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

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

Get the current run count from mode_state.

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

Types

pipeline_result()

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

Functions

call_llm(terminal)

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

On success, increments mode_state.run_count in custom_context.

check_max_runs(terminal, opts)

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(terminal, opts)

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

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

check_tool_interrupts(terminal, opts)

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(terminal, opts)

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(terminal, run_fn, opts)

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(chain)

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(terminal)

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

expand_tool_results(pipeline_result, opts \\ [])

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(chain)

Get the current run count from mode_state.

reset_run_count(chain)

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.