ReAct agent with two distinct contracts, selected by :mode.
ReAct lets the LM reason about the current situation and choose an action each turn, appending observations to a growing trajectory until the task is done.
:provider_native (default) — Imp's provider-tool-calling ReAct
The LM chooses from an explicit tool catalog via provider function calls,
appends observations to history, and eventually calls the reserved submit
tool. Keep the tool policy narrow in production:
lookup = Imp.tool(:lookup, "lookup facts", fn %{"query" => query} -> query end)
program =
Imp.Predict.ReAct.new("question -> answer", [lookup],
tool_policy: [:lookup, :submit],
max_iters: 4
)This mode preserves the original Imp contract:
- unknown model-selected tools return
{:error, {:unknown_tool, name}}; - denied tools return
{:error, {:tool_denied, name, reason}}(Imp.ToolPolicy); - tool crashes return
{:error, {:tool_error, name, reason}}, wherereasonis the exception raised or{kind, value}for a throw or an exit; - tool-policy crashes return
{:error, {:tool_policy_error, name, reason}}; - final outputs that are missing or do not fit the signature return
{:error, %Imp.AdapterParseError{kind: :missing_fields | :invalid_fields}}.
:dspy — a port of DSPy 3.2.1 dspy.ReAct
This mode reproduces upstream dspy/predict/react.py, not a
provider-tool-calling loop. It builds the same reasoning signature DSPy
builds, with types and tool arguments named in Imp's neutral words rather
than Python's:
- inputs: the original inputs plus a
trajectorystring input; - outputs:
next_thought(a string),next_tool_name(one of the tool names orfinish), andnext_tool_args(an object); - instructions: DSPy's "You are an Agent..." block, listing each tool
textually (name,
<desc>, andIt takes arguments {...}), the reservedfinishtool, and the JSON-format reminder.
Each turn the model emits the three fields as ordinary chat output (no
provider tool calls). After every tool call the observation is appended to the
trajectory as text ([[ ## thought_N ## ]], [[ ## tool_name_N ## ]],
[[ ## tool_args_N ## ]], [[ ## observation_N ## ]]). The finish tool
terminates the loop; a separate dspy.ChainOfThought extraction pass then maps
(inputs + trajectory) to the original output fields.
DSPy control flow reproduced here: finish terminates, iteration exhaustion
falls through to extraction, and a reasoning-signature parse failure (an
invalid/missing action, mirroring DSPy's ValueError break) also falls
through to extraction. Tool exceptions become recoverable Execution error in <tool>: ... observations so the model can recover on a later turn (the
message text is Imp's, not Python's traceback — see the release note). Tool
policy is an Imp safety extension with no DSPy analogue: a denied or crashing
policy stays fail-fast.
Like upstream ReAct, a call may override the constructor's iteration budget
with an invocation-local :max_iters or "max_iters" input. The control
value is validated and removed before task inputs are sent to the LM.
The prediction
The prediction's fields are the signature's outputs (and, from an extraction
pass, its reasoning). Its metadata carries the redacted tool call
:history, :termination_reason, how the turn ended, and, when something
interrupted it, :termination_cause:
:submit,:finish— the model called the reserved tool.:forced_submit— a:provider_nativestep called no tool (termination_cause: :empty_tool_calls) and the forcedsubmitthat followed answered.:answered— as above, but the forcedsubmitdid not answer, so the step's own text is projected onto the outputs.:extracted— a:dspyturn that ended atmax_iters, on a step that could not be parsed, or on a step with no tool call, answered by DSPy's extraction pass;termination_causeis:max_iters,:parse_erroror:empty_tool_calls.