Sagents.Modes.AgentExecution (Sagents v0.16.2)

Copy Markdown

Standard sagents execution mode.

Replaces the separate execute_chain_with_state_updates and execute_chain_with_hitl loops with a single composable pipeline.

Pipeline

  1. Check the :max_runs budget before starting another LLM call
  2. Expand any tool result from the previous turn that asked to arrive as messages
  3. Call the LLM
  4. Check for HITL interrupts (if HumanInTheLoop middleware present)
  5. Execute tools
  6. Propagate state updates from tool results
  7. Check if target tool was called (if until_tool is set)
  8. Loop if needs_response is true, or error if until_tool contract violated

LLMChain sets needs_response when a message arrives. It is true after tool results, and after an assistant message that leaves the model's turn open (LangChain.Message.continues_turn?/1): the provider reported the turn is not over, or the message is narration only. The loop in step 8 reads that flag and does not inspect the message itself.

A response the provider cut off (status :length or :content_filtered) ends the run in step 3 with a "response_truncated" or "content_filtered" error. Its tool calls may be partial, so step 5 never runs them.

The budget is checked before a call, never after one, so the tools from the final permitted response still execute. A target tool returned on that response satisfies until_tool rather than ending the run with exceeded_max_runs.

Resuming after human approval

A resume executes approved tool calls outside this pipeline, then hands the mode a chain whose last message is that tool message. Sagents.SubAgent.resume/3 does this on the chain it kept from the interrupted run. Sagents.Middleware.HumanInTheLoop does it for Sagents.Agent.resume/4, which builds a fresh chain with a fresh budget.

A chain that arrives ending in a tool message has its results pass through steps 6 and 7, plus the tool-interrupt check, before the loop starts. Their state updates reach the chain's state, a nested interrupt surfaces, and an approved target tool completes the run without another LLM call, even when the budget is spent.

Tool results that expand into messages

A tool can return material as messages rather than as tool-result content, choosing the role it arrives at, and have the model read them on its very next LLM call. A tool asks for that with LangChain.MessageExpansion.expand/3; step 2 applies it.

Step 2 comes at the top of the loop, not the bottom, and both halves of that matter:

  • Before the LLM call, which is the guarantee the tool is relying on. A tool that says "this arrives next" while the model keeps working in the same run is making a promise nothing keeps, and a model handed a description of material it does not have will write the material itself.
  • After the loop boundary, so every step that decides whether the run is over reads a last_message the model produced or the tools returned. A turn that interrupted or satisfied an until_tool contract ends without expanding anything into it.

Running at the top of the loop also covers the results a resume produces. When those results do not end the run, step 2 is the next thing to see them, so a tool gated behind human approval expands on the same terms as one that is not.

Options

  • :middleware — Agent's middleware list (for HITL checking)
  • :should_pause? — Zero-arity function for infrastructure pause
  • :max_runs — Maximum LLM calls (default: 50)
  • :until_tool — Tool name (string) or list of tool names. When set, the mode returns {:ok, chain, tool_result} once the target tool is called, or {:error, chain, %LangChainError{}} if the LLM stops without calling it.
  • :require_tool_success — Boolean (default false). When true, the mode terminates only when the target tool returns a successful (non-error) result; an error result keeps the loop running so the LLM can correct the call, bounded by :max_runs.

These two are the mode's internal representation. Callers using Sagents.Agent.execute/3 pass the friendlier mutually-exclusive :until_tool / :until_tool_success (each naming the target tool), which are collapsed into the pair above via collapse_until_tool/2.

Pausing with a cause

A pipeline step may pause the run with {:pause, chain, reason} — the 3-tuple sibling of {:pause, chain}, carrying why the step paused (a draining node, an unreachable backing store, whatever the step knows). The mode folds the reason into custom_context.pause_reason and returns the {:pause, chain} shape LLMChain.run/2 documents, so the result also passes the fallback machinery unchanged; the agent layer reads the reason back onto State.pause_reason. A step that pauses with the plain 2-tuple (like check_pause/2) keeps a nil reason.

The 3-tuple is a step-level shape only. A custom mode must not return it from run/2: LangChain.Chains.LLMChain.Mode's run_result() type does not admit it, and it corrupts into a generic error under with_fallbacks: (see Sagents.Mode.Steps.normalize_pause/1). Custom modes that want to report a pause cause should apply normalize_pause/1 to their final result, as this mode does, or set custom_context.pause_reason themselves.

Summary

Functions

Collapse the public :until_tool / :until_tool_success either-or spelling into the internal {tool_name | nil, require_success_boolean} representation.

Functions

collapse_until_tool(until_tool, until_tool_success)

@spec collapse_until_tool(term(), term()) :: {term() | nil, boolean()}

Collapse the public :until_tool / :until_tool_success either-or spelling into the internal {tool_name | nil, require_success_boolean} representation.

The friendly either-or options are mutually exclusive (validated at the public boundary in Sagents.Agent / Sagents.SubAgent.Config). If both are non-nil when this is called, the success variant wins.

Used at the two collapse points — Sagents.Agent.execute/3 (opts → mode opts) and Sagents.Middleware.SubAgent (config → sub-agent construction) — so the rest of the system carries only :until_tool + :require_tool_success.