Sagents.Modes.AgentExecution (Sagents v0.16.2)
Copy MarkdownStandard sagents execution mode.
Replaces the separate execute_chain_with_state_updates and
execute_chain_with_hitl loops with a single composable pipeline.
Pipeline
- Check the
:max_runsbudget before starting another LLM call - Expand any tool result from the previous turn that asked to arrive as messages
- Call the LLM
- Check for HITL interrupts (if HumanInTheLoop middleware present)
- Execute tools
- Propagate state updates from tool results
- Check if target tool was called (if
until_toolis set) - Loop if
needs_responseis 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_messagethe model produced or the tools returned. A turn that interrupted or satisfied anuntil_toolcontract 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 (defaultfalse). Whentrue, 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 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.