Plain chat adapter: instructions plus field-labelled user content.
format/3 renders a signature and its inputs as a system message, one
user/assistant pair per demo, the conversation history, and a final user
message. Fields are labelled with [[ ## name ## ]] markers.
parse/3 accepts an Imp.Prediction, a map of output fields, or completion
text. Text is split on those markers and the first section for each output
field wins. An output with no section takes its declared default, or nil
when optional; a required output with no section is a parse error, and so is
a completion with no section for any output, even when every output is
optional or defaulted, so the JSON fallback or a retry runs.
One signature-declared exception: signature.metadata[:text_field] names an
output field that takes a completion carrying no marker at all. A native tool
loop asks for a thought and tool calls, and a model that answers a step in
plain text with no tool call has said something and called nothing — the
plain reading of that completion, not a format failure worth a second LM call
through Imp.Adapter.JSON. Remaining outputs take their declared defaults, so
the signature says what an unanswered field means. The exception is narrow on
purpose: the completion must carry no [[ ## field ## ]] line anywhere, no
opening tag for an output, and no JSON object with an output's key, and it
must not be blank. A completion in one of those formats tried to answer in
fields, so it is parsed by markers, and one with no output section is a
parse error that the JSON fallback reads. A signature without that metadata parses by markers
alone. A blank completion is a step that said nothing. The text a model writes beside native tool calls is
read the same way. Imp.Predict.ReActV2 sets it on its internal step
signature; see that module.
Options to format/3: :demos, :response_instruction, :guidance,
:omit_empty_request, and the renderer seams :output_renderer,
:input_section_renderer, :system_renderer, :tool_result_renderer and
:history_note_renderer,
which let another adapter reuse this message assembly with its own dialect
and let a host bound what a tool result costs in the prompt without changing
what the loop records. :history_note_renderer is the one seam for saying
something about a stored turn rather than re-rendering it: it is consulted
for every history turn, native tool turns included, after that turn's own
messages, and its text becomes one user message right after them — the next
thing the model reads. A note is data about the turn (the answer was not
delivered, the account's allowance ran out), not a rewrite of what happened,
so the record the loop keeps is unchanged. Options outside that list
are ignored; anything that is not a keyword list raises ArgumentError.
Extending the default rendering
A renderer runs for whichever adapter formats the request, which is not
always the one a program names: a Chat reply that cannot be parsed is asked
again through Imp.Adapter.JSON with the same renderers. So the options a
renderer receives carry that adapter's own default rendering, and a renderer
that adds to the default calls it rather than naming an adapter:
system_renderer = fn signature, opts ->
"Answer tersely." <> opts[:default_system].(signature, opts)
endopts[:default_system] takes (signature, opts). An :output_renderer of
arity 4 receives the options as its fourth argument, with
opts[:default_outputs] taking (signature, outputs, missing_message). The
JSON and XML adapters pass the same two, for their formats. A renderer that
writes a format of its own writes it whatever the adapter.
Summary
Functions
Formats a model-facing tool result, preserving explicit typed image content.
Renders a tool result as the text a reader should see, whether that reader is the model or a person looking at a host's tool card.
The words for a failed tool call, without the Error: that
format_tool_result/1 puts before them: the text an MCP error result carries,
or one sentence for any other reason.
Functions
@spec format_tool_content(term()) :: String.t() | [String.t() | Imp.Adapter.Types.Image.t()]
Formats a model-facing tool result, preserving explicit typed image content.
Lists containing only strings and Imp.Adapter.Types.Image values, with at
least one image, retain their ordered content. All other values use
format_tool_result/1, whose textual contract is unchanged. Custom
:tool_result_renderer functions may bound the strings while retaining the
images. Chat associates those images with the originating tool call and
places them in user messages after all tool responses in the turn.
Renders a tool result as the text a reader should see, whether that reader is the model or a person looking at a host's tool card.
Successful results format like any other value. A failed result renders as
plain text instead of an Elixir term. An MCP error result is the text its
tool wrote, after Error: unless that text already begins with "error",
since the model is given no other sign that the call failed. Every other
failure is one sentence after Error:: a JSON-RPC error is the server's
message; a call that got no answer says why in words, and unless it was
never sent, that it may have been carried out; a denied call says who
declined it; a crashed tool names itself and its message, and one that
exited says it may have been carried out; an unknown tool, a rejected
argument or a rejected submit says what is wrong with the call; an atom
reason is spelled out; and a structured :reason map reads as its reason
and limit. Only the rendering is plain: the result the loop records keeps
the whole structured error. Hosts that relay Imp tool results over a
protocol boundary should use this so the same words reach the person that
reached the model.
iex> Imp.Adapter.Chat.format_tool_result({:error, {:tool_denied, :post, :client_denied}})
"Error: post was not allowed; the person declined it."
The words for a failed tool call, without the Error: that
format_tool_result/1 puts before them: the text an MCP error result carries,
or one sentence for any other reason.
iex> Imp.Adapter.Chat.tool_error_text({:unknown_tool, "frobnicate"})
"there is no tool named frobnicate."