Imp v0.5.0
Imp is a framework for typed, optimizable language-model programs on the BEAM.
Declare a task as named inputs and outputs, call it like any other Elixir
program, measure it on examples, compile it with an optimizer, and run the
selected program under OTP.
This release puts Imp on Hex. It is 0.5.0 rather than a patch because the
install line changes, an OTP release that uses the protocol adapters lists one
more application, and ReActV2 and Imp.MCP.OAuth change shapes a program may
depend on.
Install
{:imp, "~> 0.5"}Every dependency comes from Hex. Use a path dependency only while developing
against a local checkout.
ExMCP and erlexec are declared runtime: false, so an OTP release that uses
Imp.ACP or Imp.MCP must list applications: [ex_mcp: :load, erlexec: :load]
in its release definition; see releases that use MCP or
ACP.
Ordinary Imp startup starts no protocol endpoint.
mix deps.get and mix hex.audit report two cowlib advisories
(CVE-2026-43966, CVE-2026-43969). cowlib arrives only through ExMCP's
Cowboy server, and Imp's HTTP goes through Req, Finch and Mint. The first is
fixed one layer up: Cowboy 2.16.0 and later refuse a response header
containing CR or LF, and a fresh mix deps.get resolves Cowboy 2.19.0. The second is in the encoder
for an outgoing Cookie request header, which nothing in Imp's dependency
tree calls, and no cowlib release fixes it yet.
Headline changes
- GEPA works on agents. Optimizing an
Imp.reactagent, the reflection model
reads the whole run (tool calls, tool results, the final answer) and the
agent's tools, and GEPA rewrites the agent's instruction. By default
Imp.Optimizer.GEPAbehaves as DSPy's GEPA does; Imp's own search is
execution_profile: :beam_native. - An
Imp.Deadlinereaches the work Imp starts for you:Imp.parallel/3,
evaluation rows, optimizer workers and runs inherit the caller's deadline,
andImp.start_run/3takesdeadline:. - Imp depends on ExMCP 1.5 from Hex, unpatched. What Imp needed from the
deepfates/ex_mcpfork now lives in Imp: stdio MCP servers that end with
their connection, children included; a cleanPATHfor them inside a
release; trust for authorized remote servers; the connection options public
servers need; and the browser OAuth flow. ReActV2offerssubmitonly to a signature that needs one. A task with
exactly one text output ends its turn on a step that answers in text, and an
interrupted turn makes one last request whose text is the answer instead of
failing.ReActV2gainsfinish_onfor tools whose call is the answer.:model_requestevents record the whole request, and tool definitions are
emitted once per run as:tools_sent.- An MCP tool call that got no answer says whether it was refused, had its
credential refused, was never sent, or may have run (Imp.MCP.CallFailure,
Imp.Tool.outcome/1), an error result that declares its outcome is read as
declared, and a failed tool call reaches the model as plain text. - A ReAct prediction's fields are its outputs; how the turn ended is metadata,
in one vocabulary, withImp.Prediction.complete?/1. Imp.RunandImp.ACPrefuse options they do not know, and
Imp.Run.Event.kinds/0lists every event kind.- A host names its own run pool and limit (
Imp.Run.start/3's:admission),
and a failing run event sink is reported to the run's owner. Imp.MCP.connect/2takespool_size:, so several calls to one HTTP server
run at once, and an HTTP call can take as long as its:timeoutallows.- A run no longer outlives its control process, and a cancellation that never
returns no longer holds a run.
Breaking changes from v0.4.0
- Replace
{:imp, github: "deepfates/imp", tag: "v0.4.0"}with
{:imp, "~> 0.5"}.EX_MCP_PATHis no longer read. - A release that uses
Imp.MCPorImp.ACPaddserlexec: :loadbeside
ex_mcp: :load. - Trust for an authorized remote MCP server is VM-wide. While a connection to
it is open, its exact origin (scheme://host:port) is in ExMCP's
trusted_origins, so any ExMCP client in the same VM may send credential
headers to that origin without consent. In 0.4.0 the trust belonged to the
one connection. No other origin is trusted, the origin is removed when the
last connection to it closes, and origins the host configured are left
alone. A host that runs other ExMCP clients it does not trust with those
origins should know this. Imp.Optimizer.GEPAdefaults to DSPy's GEPA,execution_profile: :gepa_v0_1_4_merge: merge on, no evaluation cache, perfect minibatches
skipped, the pinned RNG, and:generationsturned into a metric budget when
:max_metric_callsis not given. Options the DSPy profiles fix (ComBee,
:feedback_fn,:module_selector,:candidate_selection_strategy,
:proposal_concurrency,:reflection_strategy, the frontier, sampling,
selection, evaluation and acceptance policies,:max_reflection_calls, and
reflection_record_mode: :beam_native) raise unlessexecution_profile: :beam_nativeis given, which is the 0.4.0 behaviour. Resuming a checkpoint
written by a 0.4.0-default run raises under the new default; resume it with
execution_profile: :beam_native.Imp.MCP.OAuth.begin/3no longer takes:flow; a pre-registered client is
client_registration: {:pre_registered, client_id, client_secret}with
client_issuer:naming the authorization server it belongs to. A server
with no OAuth metadata at all is refused instead of given guessed endpoints.- An
Imp.Toolnamed with a string keeps the string, and tools imported from
an MCP server are named by the server's string. Code that compared an
imported tool'snameto an atom compares it to the string. - An MCP tool call that got no answer returns
{:error, %Imp.MCP.CallFailure{}}instead of
{:mcp_tool_call_failed, server, reason}or
{:mcp_connection_unavailable, server, reason}. A call that reaches its
:timeoutis%Imp.MCP.CallFailure{outcome: :unknown, reason: :timeout},
answered at the timeout while the request runs on; a call to an HTTP server
whose connections all stay busy until the timeout is:not_sentwith
reason: :no_idle_connection. "type" => "sse"is MCP's deprecated HTTP+SSE transport, and its"url"
is the event stream's. In 0.4.0 it was Streamable HTTP with a standing GET
stream; a Streamable HTTP server is now"type" => "http". Ansse
descriptor with"headers"or"auth", or with a query string in its URL,
is refused before anything is dialed (:mcp_sse_credentials_refused,
:mcp_sse_url_refused): the whole import under the default
on_failure: :refuse, only that server underon_failure: :drop.- When a run's control process ends while the run is still going, the task is
killed after its registered cancellations are called; its monitor reports
:killed. - A run's owner can receive
{:imp_run_event_sink_failed, run_id, details}
and{:imp_run_event_undelivered, run_id, event}; an owner with a strict
handle_info/2needs clauses for them. Imp.Run.start/3,Imp.ACP.start_link/1,Imp.ACP.run/1and
Imp.ACP.Local.start_link/1raiseArgumentErrorfor an option they do
not know. A transport's own options forImp.ACPgo in
:transport_options, and:capabilitiesis spelled:agent_capabilities.Imp.predict/2,Imp.chain_of_thought/2andImp.configure/1raise
ArgumentErrorfor an option or setting they do not know. Request options
such as:temperaturego underconfig:; a setting of your own goes
throughImp.context/2.:max_errorsand:retrieverare no longer settings, and
Imp.configure/1andImp.context/2refuse them. Pass:max_errorsto
BootstrapFewShot, RandomSearch or COPRO (10 when not given) and a retriever
to the program.- ReActV2 emits no
:finalevent;:run_finishedcarries the prediction.
Imp.Trajectory.to_atif/2'sextra.outcomeisextra.terminal_event, and
a tool result'sextra.outcomeis the recordedImp.Tool.outcome/1
instead of"returned"or"error". - A ReActV2 or ReAct prediction's fields are its outputs only:
history,
termination_reason,termination_cause,termination_error,
finished_by_tool,unexecuted_tool_callsandcontext_projectionare in
prediction.metadata.termination_reasonsays how the turn ended, and a
turn without an answer is:incompletewithtermination_causesaying why;
typed extraction is:extracted(nocompletion_mode), and
Imp.Predict.ReActspells:parse_failureas:parse_errorand:direct
as:answered. UseImp.Prediction.complete?/1to ask whether a turn
answered. - For a signature with one
:stringoutput,ReActV2offers nosubmit
tool, and a step answered in text with no tool call ends the turn. - Errors have one shape per tag, with the reason as a term. A failed
Imp.Clients.ReqLLMrequest is%Imp.LMError{}(withstatus,
retryableandcontext_window_exceeded;Imp.ContextWindowExceededError
is gone), and a completion that cannot be parsed is
%Imp.AdapterParseError{kind: ...}, whichImp.Predictreturns
directly instead of%{reason: {:error, _}, trace: _}. A raise inside a
client, program, tool, tool policy, retriever, optimizer or ACP callback
keeps the exception struct where 0.4.0 kept its message.
{:tool_denied, tool}is{:tool_denied, tool, :tool_policy}, and a run's
:authorizerefusal is{:tool_denied, tool, reason};Refineand
Assertionsreturn{:error, reason};
Imp.optimize!raisesImp.Errorfor a failed optimization. The CHANGELOG
lists every tag that changed. Imp.ExampleandImp.Predictionkeep string keys as strings. Code that
read a field of data loaded from JSON withmap.fieldormap[:field]
reads it withImp.Example.get/2or by its string key.Imp.MCP.Client,Imp.MCP.HTTPClient,Imp.MCP.StreamableHTTPClient,
Imp.MCP.StdioClient,Imp.MCP.Catalog,Imp.MCP.import_tools,
Imp.ACP.MCPandImp.Core.ToolCall/ToolResultare gone.
Imp.MCP.connect/2imports tools;Imp.ACP.ToolKind.derive_all/1gives an
import's ACP tool kinds.- A saved program holds no HTTP header, credential or not. An LM with custom
headers (a routing header such asx-tenantincluded) sends requests
without them after loading until it is rebound withImp.with_lm/2or a
scopedImp.context/2. Imp.save!refuses an LM whosebase_urlhas a query, fragment or user
info.- Prompts name types in plain words instead of Python annotations
(one of: atlas, harborwhere 0.4.0 wroteLiteral['atlas', 'harbor']),
values take their JSON spelling (null,true,false), and the
structured-output schema is namedoutputs. A non-string answer for a
string field is kept as its JSON text ("true", not"True"), and a
nullanswer is no value rather than the string"None". Fields, order
and parsing are unchanged, but a saved optimized program now sends
different prompt text. - An
Imp.Telemetryspan's[:exception]event carries:kind,:reason
and:stacktrace, as:telemetry.span/3does, instead of:erroras
text. - An optimizer's
compile/Nis no longer documented whereImp.optimizeor
Imp.trainruns the optimizer; call those. Imp.load!/1reading a file isImp.read!/1;Imp.load/1returns
{:ok, program}andImp.load!/1takes the dumped map.Imp.react/3builds ReActV2 andImp.react_v2is gone.
Imp.Predict.ReAct'smode: :dspy_3_2_1ismode: :dspy.Imp.Predict.PredictisImp.Predict.Imp.Retrievers.KNNis deleted;Imp.Retrieve.Memoryretrieves by token
overlap.- An LM is a struct or module whose
generate/3takes it first. The
%{module:, opts:}map and a bare function are refused; so is a module
that defines onlygenerate/2. A retriever module'sretrieve/3takes
itself first. Imp.MCP.CallFailurehasserver_name,tool_nameandindex; an
unavailableentry hasserver_name;:authorizereturns:allowor
{:deny, reason}and its context names thedescriptor;
:credentialsis:credential_store.- A
:tool_policyfunction returns:allowor{:deny, reason}, and a
refused call is{:tool_denied, name, reason}. max_concurrencyisnum_threadson evaluation, parallel, search, batch and
optimizer options.Refine'smax_attemptsisn;RLMtakes
max_iterationsonly.Imp.Optimizer.RandomSearchandBootstrapRSare
Imp.Optimizer.BootstrapFewShotWithRandomSearch.
Upgrade path
- Change the dependency line, run
mix deps.get, and commitmix.lock. - Add
erlexec: :loadto any release that listsex_mcp: :load. - Replace
OAuth.begin/3's:flowwith:client_registrationif you used it. - Match MCP call failures on
%Imp.MCP.CallFailure{outcome: ...}(a 401 is
:auth_refused), compare imported tool names as strings, and give run
owners clauses for:imp_run_event_sink_failedand
:imp_run_event_undelivered. - Read a ReAct or ReActV2 prediction's
historyandtermination_*from
prediction.metadata, and matchtermination_reasonagainst the new
values. - Change
"type" => "sse"descriptors for Streamable HTTP servers to
"http". A server that needs credentials is reached over Streamable HTTP;
anssedescriptor takes none. - Match LM failures on
%Imp.LMError{}(or askImp.Errors.retryable?/1
andImp.Errors.context_window_exceeded?/1), parse failures on
%Imp.AdapterParseError{kind: ...}, and exception reasons on the struct
rather than its text. - Rename
Imp.react_v2toImp.react,Imp.Predict.Predictto
Imp.Predict, andImp.load!(path)toImp.read!(path); give custom LMs
and retriever modules thegenerate/3andretrieve/3that take the
client first, and wrap an LM function in a struct that implements
Imp.LM. - Rename
max_concurrency:tonum_threads:where you configure
evaluation, optimizers or parallel calls; answer:authorizeand
:tool_policywith:allowor{:deny, reason}; match a refused tool
call as{:tool_denied, name, reason}; readserver_nameandtool_name
from MCP failures and absences. CallImp.Signature.load!/1,
Imp.History.load!/1,Imp.Optimizer.Report.load!/1and
Imp.Clients.TrainingJob.load!/2where you calledload, and
Imp.Clients.TrainingJob.read!/2where you read a checkpoint file, and
the datasets'read!where you called theirload(path). - Rebind the LM of any loaded program that relies on custom headers, and
move abase_urlquery, fragment or user info into configuration the
host supplies at load time. - Update telemetry handlers for
[:exception]to read:kind,:reason
and:stacktrace. - Re-evaluate saved optimized programs on held-out data, since their
prompt text changed, and run your application smoke test against the new
release; a one-text-output ReActV2 program now ends turns differently.
The CHANGELOG records every user-visible change in this
release. Generated module documentation is the complete API reference. Start
with Imp, Imp.Signature, Imp.Module, Imp.Evaluate, Imp.Optimizer,
Imp.ACP, Imp.MCP, and Imp.Telemetry.