Sagents.AgentUtils (Sagents v0.8.0-rc.5)
Copy MarkdownShared utilities for Agent and SubAgent HITL (Human-in-the-Loop) support.
Provides common functions for:
- Checking for HITL interrupts
- Building full decisions lists
- Extracting tool calls from chains
These utilities provide consistent HITL behavior across Agent and SubAgent implementations.
Summary
Functions
Compute the state transition for a single HITL approve/reject decision in a host's pending-tool list.
Build a full decisions list for ALL tool calls in a message.
Check if a chain has pending tool calls that require human approval.
Extract tool calls from the last assistant message in a chain.
Map an interrupt-data payload to the assigns/state changes a host (LiveView, GenServer, etc.) should merge to display the interrupt.
Functions
@spec advance_hitl_decisions(map(), non_neg_integer(), atom()) :: {:resume, [map()], map()} | {:more, map()}
Compute the state transition for a single HITL approve/reject decision in a host's pending-tool list.
Reads :pending_tools and :hitl_decisions from state (treating
missing keys as empty list / empty list), records decision_type for
the tool at index, and returns:
{:resume, accumulated_decisions, changes}— all pending tools have been decided. The host should callSagents.AgentServer.resume(agent_id, accumulated_decisions)and then mergechanges(which clears:pending_tools,:interrupt_data, and:hitl_decisions).{:more, changes}— tools still pending. Mergechanges(which advances:pending_toolsand:hitl_decisions).
Example
case AgentUtils.advance_hitl_decisions(socket.assigns, idx, :approve) do
{:resume, decisions, changes} ->
AgentServer.resume(agent_id, decisions)
{:noreply, assign(socket, changes)}
{:more, changes} ->
{:noreply, assign(socket, changes)}
end
Build a full decisions list for ALL tool calls in a message.
Mixes human decisions (for HITL tools) with auto-approvals (for non-HITL tools). This is needed because LLMChain.execute_tool_calls_with_decisions expects a decision for EVERY tool call, not just the ones that needed approval.
Parameters
- all_tool_calls: All tool calls from assistant message (HITL + non-HITL)
- hitl_tool_call_ids: List of tool_call_ids that needed human approval
- human_decisions: List of decisions from human (same order as action_requests)
- action_requests: List of action_requests (to map decisions to tool_call_ids)
Returns
- List of decisions matching all_tool_calls order
Example
all_tool_calls = [tc1, tc2, tc3] # 3 total tool calls
hitl_tool_call_ids = [tc1.call_id] # Only tc1 needed approval
action_requests = [%{tool_call_id: tc1.call_id, ...}]
human_decisions = [%{type: :approve}] # Human approved tc1
full_decisions = build_full_decisions(all_tool_calls, hitl_tool_call_ids, human_decisions, action_requests)
# => [%{type: :approve}, %{type: :approve}, %{type: :approve}]
# First is human decision, others are auto-approved
Check if a chain has pending tool calls that require human approval.
Parameters
- chain: LLMChain with potentially pending tool calls
- interrupt_on: Map of tool_name => true/false/config
Returns
{:interrupt, interrupt_data}- Some tools need approval:continue- No tools need approval or no tool calls present
Example
case AgentUtils.check_for_hitl_interrupt(chain, %{"write_file" => true}) do
{:interrupt, interrupt_data} ->
# Pause and request human decisions
{:interrupt, state, interrupt_data}
:continue ->
# Execute tools automatically
execute_tools(chain)
end
Extract tool calls from the last assistant message in a chain.
Parameters
- chain: LLMChain
Returns
- List of tool calls or empty list
Example
tool_calls = AgentUtils.get_tool_calls_from_last_message(chain)
# => [%ToolCall{call_id: "1", name: "write_file", ...}, ...]
Map an interrupt-data payload to the assigns/state changes a host (LiveView, GenServer, etc.) should merge to display the interrupt.
Routes the three sagents-internal interrupt variants to their UI shapes:
:ask_user_question— pull the single question to the front:multiple_interrupts— present as questions if all are questions, otherwise as a HITL tool batch:subagent_hitl— unwrap the inneraction_requests- any other map — treat as a generic HITL tool batch
Returns an empty map for nil, so callers can apply the result
unconditionally on top of base assigns.
Returned keys
When questions are present:
:pending_question, :remaining_questions, :question_responses, :pending_tools
When HITL tools are present:
:pending_tools, :pending_question
Example
base = %{loading: false, agent_status: :interrupted, interrupt_data: data}
Map.merge(base, AgentUtils.interrupt_session_changes(data))