Intent

Imp.react(signature, tools, opts) is Imp's tool-using loop. The model reads the task, calls tools, reads their results, and repeats until it can give the signature's outputs. It is DSPy's ReActV2: the history is structured, tools are called natively through the provider, one step may call several tools, and the answer arrives directly rather than through a separate extraction call.

Imp.Predict.ReActV2, which Imp.react/3 builds, is stable. The older trajectory-based Imp.Predict.ReAct is experimental.

Read this when a task needs the model to choose actions, when you want to know why a turn ended the way it did, or when you are moving an agent from DSPy's ReAct or ReActV2. For defining tools and importing them from MCP, see Tools and MCP.

Design decisions

1. The history is structured, and it is the prompt

Every step is recorded as one entry in an Imp.History: the inputs (on the first step), the model's thought if it wrote one, its tool calls with their IDs, and each call's result. The adapter replays that history as real messages: an assistant message with native tool calls, then one tool message per result, matched by ID. Nothing earlier is reformatted, so each request is the previous one plus the newest exchange, which is the prefix a provider's prompt cache can reuse.

The history comes back as prediction.metadata.history. Pass it as the history input of the next call to continue the conversation.

2. How a turn ends depends on the outputs

A signature with one unconstrained text output (ticket -> reply) ends the way most tool loops end: when the model stops calling tools and writes text, that text is the answer. No submit tool is offered. This costs one request fewer and matches what models are trained to do.

Every other signature (several outputs, one that is not text, or one text output with a constraint such as an enum, a pattern or an answer shape) gets a submit tool whose parameters are the outputs, as in DSPy. Calling it with valid values ends the turn; values that do not fit the signature are recorded as that call's error, and the loop goes on. The name submit is reserved.

The one-text-output rule has a consequence to design for: any step that writes text and calls no tool has answered. A model that says what it is about to do, instead of doing it, has given that sentence as its answer. A constrained output never takes that path: its allowed values are in submit's schema, and text is not an answer to it.

finish: :submit gives the one-text-output signature submit as well, so its turn ends only through submit: a step that writes text and calls no tool is a thought, not the answer. Use it when the model writes text it does not mean as the answer: a sentence about what it will do next, or a report of what it just did. The text then stays in the history as the step's thought, and only submit's argument is the answer.

After such a step the loop goes on with an ordinary step: every tool and tool_choice: "auto". The thought stays the model's own message, and one user message follows it, naming the signature's outputs:

Your message above was not sent. To reply, call `submit` with `answer`; to keep working, call a tool.

That line follows every thought-only step, in later requests and on replay too, so each request is the previous one plus the newest exchange. It is left out after a thought the turn's extracted answer follows (that thought was answered), and the forced submit's own last thought is shown on the user side instead (section 3). A step that answered in plain text, written while the program ended with finish: :auto, is an answer, not a thought, and gets no line. The other way round, a plain-text loop replaying a history written under finish: :submit follows each thought-only step with only "Your message above was not sent.", since it has no submit to name; a history written in a plain-text loop has no such step, so its requests do not change. With written tool calls the request then ends, as every such request does, on the tool listing. The model can then do what it said it would, or submit what it wrote; a thought that was only a closing note can be submitted as an empty answer. A second thought-only step in a row, or a step that says nothing, interrupts the turn (:empty_tool_calls) and the forced submit follows. Once the process's Imp.Deadline has passed, no further request is sent. Measured on real final steps from 11 model families, with that line after the thought no sample wrote text again, and replies were submitted intact, Meta's models included, whose provider cannot force a tool. Shown the thought back as a user message instead, some models took it for an echo of their own output and submitted nothing; given only "Your message above was not sent.", they asked about their status and took extra steps. Forcing at once, as DSPy does, took the announced action away and added a request whose failure lost the reply. It costs one more request whenever the model ends in prose. The default, finish: :auto, lets the signature decide as above. The option is saved with the program.

The prompt says the same. When the provider holds the tools natively, the Chat adapter's system message for Imp.react("intent -> answer", [look]) shows the input layout, no output markers to write and no output fields, because the answer is the plain text of the message that calls no tool. The answer's description, when it has one, follows the sentence that says so:

--- system
Your input fields are:
1. `intent` (string):
Inputs will be structured in the following way, with the appropriate values filled in.

[[ ## intent ## ]]
{intent}

In adhering to this structure, your objective is: 
        Given the fields `intent`, produce the fields `answer`.
        You are an Agent. Use the supplied tools to gather information and take actions.
        Call tools when more information is needed.
        When the final answer is ready, write it as plain text without calling a tool.
        The available tools are: `look`.
--- user
[[ ## intent ## ]]
What is in the box?

The tool names are listed in the text as DSPy lists them; the tools themselves go to the provider. The "You are an Agent..." line is yours to change: pass adapter_opts: [agent_line: "..."] for your own text, or agent_line: nil to leave it out. A reply that still writes [[ ## answer ## ]] sections, as replies did before, is read as before. When the model writes its tool calls as text instead (an LM without native tool calling), and for every signature with submit, the step keeps the [[ ## field ## ]] layout and its [[ ## completed ## ]] marker.

3. An interrupted turn gets one last request

A turn is interrupted when it reaches max_iters, when a step's request or parse fails, or when a step gives neither a tool call nor an answer. Rather than return nothing, Imp makes one more request:

  • With submit, the request names submit as the required tool, as DSPy does (:forced_submit). If the process's Imp.Deadline has already passed, no request is made and the turn is :incomplete (:deadline_exceeded). A provider that refuses a named tool_choice (its error names tool_choice and says what it supports, as Meta's "only auto is supported" does through OpenRouter) is asked again with "required" and submit as the only tool; if it refuses that too, or answers without submit, a typed extractor with no tools reads the history and fills the outputs (:extracted). The history then ends with those outputs, recorded as a step that called no tool.

    When the step before it wrote a thought and called nothing, that thought is the newest thing in the history, and the request would end on the model's own message. Providers refuse that: Mistral reads it as an assistant prefix it will not combine with function calling, and Gemini refuses any request that ends on a model turn. So the request shows the thought as its last user message, under its field's marker, as DSPy's ReAct shows its trajectory; the history keeps it as the model's step. DSPy's ReActV2 leaves the thought out of the forced request instead.

    An answer is not moved. So a forced request with nothing new to ask (no inputs, no last_request_note) on a history that ends with a step that answered in plain text, written under finish: :auto, still ends on that assistant message. Resume such a history with new inputs or a note.

  • With one text output, the request is an ordinary step, same tools, and its text is the answer (:last_text). Tool calls in it are not run; they are listed in unexecuted_tool_calls, unless the tool is named in last_request_tools: (below).

If the last request does not produce an answer either, the turn ends :incomplete. That is still {:ok, prediction}, carrying the history and a termination_cause, and Imp.Prediction.complete?/1 is false for it; a turn without an answer is never reported complete.

Imp adds nothing to that request (beyond the unsent line that follows each earlier thought-only step under finish: :submit, section 2) unless you pass last_request_note:, one line of your own text, sent as a user message and kept in the history.

Some programs act through a tool: they publish, send or record something by calling it, and their text only reports what they did. For them, a turn interrupted by a single provider error would lose the action it was about to take. last_request_tools: names the tools whose calls the last request does run, as any step's calls run: under the same tool policy and authorization, with the same events, and recorded in the history with their results. The turn still ends with the request's text as the answer, and calls to other tools still go unrun. DSPy's ReActV2 makes the same allowance for its terminal tool, whose call in the forced last request runs; this lets the last request take the program's own final action, and nothing further.

A request refused because the context window is full is different. Imp first retries with older episodes left out of the prompt (see below); if the request still does not fit, another would be refused the same way, so the turn ends at once, without a last request.

4. How it ended is metadata, never an output

The prediction's fields are exactly the signature's outputs, so an output can be called anything, history included. The loop's own account lives in prediction.metadata: history, termination_reason, and when the turn was interrupted, termination_cause. Imp.Prediction.complete?/1 is false exactly when the turn has no answer.

API walkthrough

The loop

The escalation program routes a ticket and finds who is on call for it:

on_call =
  Imp.tool(
    :on_call,
    "Look up the on-call engineer for a squad.",
    fn %{"team" => team} ->
      %{"atlas" => "Maya", "harbor" => "Tom", "beacon" => "Ines", "quill" => "Raj"}[team]
    end,
    schema: %{
      "type" => "object",
      "properties" => %{"team" => %{"type" => "string", "enum" => ["atlas", "harbor", "beacon", "quill"]}},
      "required" => ["team"]
    }
  )

escalation =
  Imp.signature(
    "ticket -> team: enum[atlas,harbor,beacon,quill], contact: string",
    "Find the squad that owns the ticket and its on-call engineer. " <>
      "atlas owns money, harbor the platform, beacon identity, quill the product."
  )

With gpt-5.4-mini, in six runs of six the model called on_call for atlas and then submit:

lm = Imp.req_llm("openai:gpt-5.4-mini", api_key: System.fetch_env!("OPENAI_API_KEY"))

escalate = Imp.react(escalation, [on_call], lm: lm, max_iters: 5)
{:ok, prediction} = Imp.call(escalate, %{ticket: "We were charged twice this month."})

{Imp.get(prediction, :team), Imp.get(prediction, :contact), prediction.metadata.termination_reason}
#=> {"atlas", "Maya", :submit}

Each step shows the model the task's instructions, the output fields with their types and descriptions, the tools, and the history. What the model needs to decide, such as what the squads own, can go in the instructions or in a field's description: with the squad meanings in the team field's description instead, the same model did the same in six runs of six.

A scripted model shows the loop without a provider. Each step is either tool calls or text:

script = fn steps ->
  {:ok, agent} = Agent.start_link(fn -> steps end)

  Imp.LM.Static.new(
    handler: fn _messages, _opts ->
      Agent.get_and_update(agent, fn
        [step | rest] -> {step, rest}
        [] -> {%{tool_calls: []}, []}
      end)
    end
  )
end

lm =
  script.([
    %{next_thought: "A double charge is money, so atlas.", tool_calls: [%{name: "on_call", arguments: %{"team" => "atlas"}}]},
    %{tool_calls: [%{name: "submit", arguments: %{"team" => "atlas", "contact" => "Maya"}}]}
  ])

{:ok, prediction} = Imp.call(Imp.react(escalation, [on_call], lm: lm), %{ticket: "We were charged twice this month."})

{Imp.get(prediction, :team), Imp.get(prediction, :contact), prediction.metadata.termination_reason}
#=> {"atlas", "Maya", :submit}

The history

Each entry holds the step's next_thought, its tool_calls, and its tool_call_results, each result paired with its call's ID; the final entry also holds the submitted outputs.

for step <- prediction.metadata.history.messages,
    call <- step.tool_calls.tool_calls,
    do: call.name
#=> ["on_call", "submit"]

To continue the conversation, pass the history back:

Imp.call(escalate, %{ticket: "It happened again today.", history: prediction.metadata.history})

The history keeps provider reasoning data needed to continue a turn, and tool results in full, so store it privately. Imp.History.redact/1 returns a copy safe for logs.

A text answer

A signature with one unconstrained text output asks for that field directly and ends when the model writes text without a tool call:

lm =
  script.([
    %{tool_calls: [%{name: "on_call", arguments: %{"team" => "atlas"}}]},
    %{reply: "Maya from atlas is looking at the duplicate charge.", tool_calls: []}
  ])

reply = Imp.react(Imp.signature("ticket -> reply", "Tell the customer who is handling their ticket."), [on_call], lm: lm)
{:ok, prediction} = Imp.call(reply, %{ticket: "We were charged twice this month."})

{Imp.get(prediction, :reply), prediction.metadata.termination_reason}
#=> {"Maya from atlas is looking at the duplicate charge.", :answered}

That is also how a turn ends when the model writes text instead of acting. Given "I can't log in after resetting my password." and instructions to look up who is on call before replying, gpt-5.4-mini called the tool and named Ines in five runs of six. In the sixth it wrote the tool call out as JSON text instead of making it, and that text was the answer. When an answer has to follow a tool call, give the signature a second output, so the turn ends only through submit, or end it from the tool with finish_on:.

Or keep the one output and pass finish: :submit. Text without a tool call is then a thought, not the answer. Here the model announces a lookup and forgets to make it; the next step sees what it wrote, makes the lookup, and ends the turn through submit:

lm =
  script.([
    %{next_thought: "Let me check who is on call for atlas.", tool_calls: []},
    %{tool_calls: [%{name: "on_call", arguments: %{"team" => "atlas"}}]},
    %{tool_calls: [%{name: "submit", arguments: %{"reply" => "Maya from atlas is on it."}}]}
  ])

reply = Imp.react(Imp.signature("ticket -> reply"), [on_call], lm: lm, finish: :submit)
{:ok, prediction} = Imp.call(reply, %{ticket: "We were charged twice this month."})

{Imp.get(prediction, :reply), prediction.metadata.termination_reason}
#=> {"Maya from atlas is on it.", :submit}

One more request on a history you hold

A call can give its own max_iters, last_request_note and last_request_tools beside its inputs. With a history, max_iters: 0 and a note, the call makes one request, the last one: the loop's system message, its tools and the history as the model saw them, then the note. No tool runs unless last_request_tools names it (see "Acting in the last request" below; a call for a summary gives last_request_tools: []), and the text the model writes is the answer. Codex CLI and Anthropic's SDK build a request to summarise a conversation the same way: the conversation's own messages, then one instruction.

lm = script.([%{reply: "Maya from atlas has the duplicate charge.", tool_calls: []}])
reply = Imp.react(Imp.signature("ticket -> reply"), [on_call], lm: lm)

{:ok, summary} =
  Imp.call(reply, %{
    history: prediction.metadata.history,
    max_iters: 0,
    last_request_note: "Summarise this ticket for the next shift."
  })

{Imp.get(summary, :reply), summary.metadata.termination_reason}
#=> {"Maya from atlas has the duplicate charge.", :last_text}

The call can also give its own request options as config, merged over the program's for that call only, such as an output budget for the summary: config: [max_tokens: 4_000]. A text answer's :finish_reason metadata says why the provider stopped the reply, as the LM client reports it; :length means the budget cut it short, so a host keeping the summary can tell a whole one from a cut one.

One call's own tool runners

A call can also give tools: tools the program holds, each the same in name, description and schema, whose runners that call uses instead of the program's. The model is shown exactly what the program shows it, so the request and its cacheable prefix are unchanged; only what a call to those tools does belongs to the call. A host that keeps state for one call -- what the call has done so far, whom it runs for -- builds that call's runners over it, so copies of one program value, called at once by an evaluation or Imp.parallel, never share it.

paging = %{on_call | run: fn %{"team" => team} -> "paged #{team}" end}

lm =
  script.([
    %{tool_calls: [%{name: "on_call", arguments: %{"team" => "harbor"}}]},
    %{reply: "Paged.", tool_calls: []}
  ])

reply = Imp.react(Imp.signature("ticket -> reply"), [on_call], lm: lm)
{:ok, prediction} = Imp.call(reply, %{ticket: "The deploy is stuck.", tools: [paging]})

for step <- prediction.metadata.history.messages, result <- step.tool_call_results,
    do: result.result
#=> ["paged harbor"]

A tool shown any differently, or one the program does not hold, is refused before a request with {:error, {:invalid_react_v2_tools, message}}. The program value is not changed. Updating a program's tools for good is an optimizer's path, and it still refuses a changed runner: an optimizer may rewrite what the model reads of a tool, never what the tool does.

Acting in the last request

A program whose real output is an action names the tools that take it in last_request_tools:. Here the step limit interrupts the turn, and the last request pages Maya and asks for another lookup; the page runs, the lookup does not:

page = Imp.tool(:page, "Page an engineer.", fn %{"name" => name} -> "paged #{name}" end)

lm =
  script.([
    %{tool_calls: [%{name: "on_call", arguments: %{"team" => "atlas"}}]},
    %{
      reply: "Paged Maya.",
      tool_calls: [
        %{name: "page", arguments: %{"name" => "Maya"}},
        %{name: "on_call", arguments: %{"team" => "harbor"}}
      ]
    }
  ])

pager =
  Imp.react(Imp.signature("ticket -> reply"), [on_call, page],
    lm: lm,
    max_iters: 1,
    last_request_tools: [:page]
  )

{:ok, prediction} = Imp.call(pager, %{ticket: "We were charged twice this month."})

{Imp.get(prediction, :reply), prediction.metadata.termination_reason,
 for(step <- prediction.metadata.history.messages, result <- step.tool_call_results, do: result.result),
 for(call <- prediction.metadata.unexecuted_tool_calls, do: call.name)}
#=> {"Paged Maya.", :last_text, ["Maya", "paged Maya"], ["on_call"]}

The option applies to a signature with one text output; a signature with submit ends an interrupted turn with the forced submit, and naming tools for it is refused. A listed tool also runs in a one-request call made with max_iters: 0, since that request is the last one, so a call that asks for a summary gives its own last_request_tools: [], which replaces the program's for that call, as its own max_iters and last_request_note do:

lm = script.([%{reply: "Maya was paged about the double charge.", tool_calls: [%{name: "page", arguments: %{"name" => "Maya"}}]}])

pager = Imp.react(Imp.signature("ticket -> reply"), [on_call, page], lm: lm, last_request_tools: [:page])

{:ok, summary} =
  Imp.call(pager, %{
    history: prediction.metadata.history,
    max_iters: 0,
    last_request_note: "Summarise this ticket for the next shift.",
    last_request_tools: []
  })

{Imp.get(summary, :reply), for(call <- summary.metadata.unexecuted_tool_calls, do: call.name)}
#=> {"Maya was paged about the double charge.", ["page"]}

Ending the turn from a tool

finish_on: maps a tool name to a function of the call's arguments, its result and the turn's inputs. Returning {:finish, outputs} ends the turn with those outputs, checked against the signature as a submit would be; :continue lets the loop go on.

lm = script.([%{tool_calls: [%{name: "on_call", arguments: %{"team" => "beacon"}}]}])

page_and_stop =
  Imp.react(escalation, [on_call],
    lm: lm,
    finish_on: %{
      on_call: fn %{"team" => team}, contact, _inputs ->
        {:finish, %{team: team, contact: contact}}
      end
    }
  )

{:ok, prediction} = Imp.call(page_and_stop, %{ticket: "I can't log in."})

{Imp.get(prediction, :contact), prediction.metadata.termination_reason, prediction.metadata.finished_by_tool}
#=> {"Ines", :finished_by_tool, "on_call"}

When one step calls several finishing tools, the first in call order ends the turn; the others still run and are recorded, and a submit in the same step wins.

Only a tool ends the turn: finish: :tools. Some programs act only through tools: what they say goes out through a tool, and their text is for nobody. With finish: :tools, nothing but a finish_on tool ends the turn. There is no submit, and the signature's outputs come from the finish_on functions. A step that writes text and calls no tool is a thought, as under finish: :submit: the next request is an ordinary step in which the thought stays the model's own message, followed by one line, by default "Your message above was not sent. To end the turn, call decline or send; to keep working, call a tool." Pass unsent_note: to say what ending the turn means in your program. Nothing is ever forced. A step whose request fails gets one more ordinary step, once per turn. A second thought in a row, a step that says nothing, the step limit or a second failure ends the turn :incomplete, with its termination_cause (:deadline_exceeded when the deadline has passed) and no further request, and the host decides what that means. A program built with finish: :tools needs at least one finish_on tool.

A call may give its own finish:, as it gives its own max_iters. A program with finish: :tools that needs one text answer for one call, a summary of the conversation say, calls with finish: :auto, max_iters: 0 and a note.

Earlier turns as actions: history_replay: :actions. By default a history replays every earlier step whole, as DSPy's ReActV2 does: the text each step wrote beside its calls, its reasoning, the calls and their results. With history_replay: :actions, the steps of earlier turns lose the text and reasoning written beside their calls, and a step that only wrote text is left out; earlier turns read as their inputs, the calls with their results, and the outputs. The current turn is always shown whole, and the history the prediction returns keeps every step. Use it when the model's own narration should not become something it reads back on every later turn. A step that carries the task's outputs (an answer) is shown whole. A call with new inputs or its own last_request_note starts a turn; on a call with neither (a resume mid-turn), the current turn starts at the last step that carries inputs. Some thinking APIs reject an earlier tool-call message without its reasoning content; check yours.

Why a turn ended

termination_reason says how the turn ended:

ReasonMeaning
:answeredone text output, finish: :auto: a step wrote the answer
:submitthe model called submit with valid outputs
:finished_by_toola finish_on function finished the turn; finished_by_tool names the tool
:forced_submitinterrupted; the last request, with submit required, answered
:extractedinterrupted; the provider could not require submit, and the extractor answered; the history ends with its outputs
:last_textinterrupted; one text output, and the last request's text is the answer
:incompleteinterrupted, and there is no answer

When the turn was interrupted, termination_cause says why:

CauseMeaning
:max_itersthe step budget ran out
:parse_errora step's reply could not be read
:prediction_errora step's request failed
:empty_tool_callswith submit (every signature but one unconstrained text output under finish: :auto): a step called no tool; under finish: :submit, a second such step in a row, or one that said nothing
:context_window_exceededthe prompt no longer fits, even with old episodes left out
:deadline_exceededthe process's Imp.Deadline passed

For :incomplete, termination_error holds the redacted errors of the requests that failed.

lm =
  script.([
    %{tool_calls: [%{name: "on_call", arguments: %{"team" => "atlas"}}]},
    %{tool_calls: [%{name: "submit", arguments: %{"team" => "atlas", "contact" => "Maya"}}]}
  ])

{:ok, prediction} =
  Imp.call(Imp.react(escalation, [on_call], lm: lm, max_iters: 1), %{ticket: "We were charged twice this month."})

{prediction.metadata.termination_reason, prediction.metadata.termination_cause, Imp.get(prediction, :contact)}
#=> {:forced_submit, :max_iters, "Maya"}

An enum output keeps submit, so text alone does not end the turn. A model that only ever writes text is interrupted, the forced submit gets text too, and the turn has no answer:

lm =
  script.([
    %{next_thought: "team: atlas", tool_calls: []},
    %{next_thought: "team: atlas", tool_calls: []}
  ])

router = Imp.react(Imp.signature("ticket -> team: enum[atlas,harbor,beacon,quill]"), [on_call], lm: lm)
{:ok, prediction} = Imp.call(router, %{ticket: "We were charged twice this month."})

{prediction.metadata.termination_reason, prediction.metadata.termination_cause, Imp.Prediction.complete?(prediction)}
#=> {:incomplete, :empty_tool_calls, false}

max_iters is 20 by default; a call can override it with a max_iters input.

Tools, policies and authorization

tool_policy: limits which tools the model may call; a refused call is recorded as an error and the model reads that it was not allowed. submit is subject to the policy too, so a list policy must include :submit, or the turn cannot finish with it. To ask a person or a service before each call, run the program under Imp.start_run/3 with authorize:, or serve it over ACP. See Tools and MCP.

Unknown tools, calls that fail the schema, and tools that raise all become error results in the history, and the model can respond to them on the next step.

A full context window

When the provider refuses a request because the context window is full, Imp retries with the oldest earlier episodes of the history left out of the prompt, up to eight times. The history itself keeps everything; only the request is smaller. context_projection in the metadata counts what was left out. The current turn's own tool results are never dropped. If the current turn and the instructions alone do not fit, the turn ends :incomplete with termination_cause: :context_window_exceeded.

Optimizing the loop

Imp.Optimizer.GEPA optimizes an agent as DSPy's GEPA optimizes a ReActV2: it rewrites the step predictor's instruction and leaves the tool descriptions as they are. For each example it reflects on one step of the run, drawn with the optimizer's seed, and shows the reflection model the run's finished history (metadata.history): every step's thought, tool calls and results, and the outputs, which the history holds unless finish_on or the extractor ended the turn. It also shows the tools the loop offered, by name, description and arguments, and the metric's feedback.

Coming from DSPy's ReAct and ReActV2

DSPy is replacing its trajectory-based ReAct with the structured-history ReActV2, which becomes dspy.ReAct in DSPy 3.5. Imp.react/3 is already that design. The trajectory-based loop remains as Imp.Predict.ReAct for programs that need it.

What matches DSPy's ReActV2: structured history replayed as native messages, several tool calls per step, a submit tool generated from the outputs, the forced submit when a turn is interrupted, and passing history back to continue.

What differs:

  • A signature with one text output has no submit; the model's text is the answer (:answered, and :last_text when interrupted). finish: :submit gives it submit, as DSPy does, but a thought-only step is followed by an ordinary step rather than at once by the forced submit, which DSPy sends after every step that calls no tool. A plain-text turn's last request runs no tool by default; last_request_tools: names the ones it may run, the counterpart of DSPy executing submit in its forced last request.
  • finish_on: lets a tool end the turn (:finished_by_tool).
  • When the provider cannot require submit, an extractor fills the outputs (:extracted); a turn with no answer is :incomplete, with a cause.
  • History, termination reason and cause are in prediction.metadata, not among the output fields.
  • On a full context window, old episodes are left out of the prompt; DSPy's ReActV2 does not truncate.
  • Tool policies and per-run authorization decide which calls run.