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 defaultImp.Optimizer.GEPAbehaves as DSPy's GEPA does; Imp's own search isexecution_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, with
Imp.Prediction.complete?/1. Imp.RunandImp.ACPrefuse options they do not know, andImp.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: :loadbesideex_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'strusted_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, andreflection_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 withexecution_profile: :beam_native.Imp.MCP.OAuth.begin/3no longer takes:flow; a pre-registered client isclient_registration: {:pre_registered, client_id, client_secret}withclient_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_sentwithreason: :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". Anssedescriptor 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 defaulton_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 stricthandle_info/2needs clauses for them. Imp.Run.start/3,Imp.ACP.start_link/1,Imp.ACP.run/1andImp.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/1raiseArgumentErrorfor 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, andImp.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/1instead 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 inprediction.metadata.termination_reasonsays how the turn ended, and a turn without an answer is:incompletewithtermination_causesaying why; typed extraction is:extracted(nocompletion_mode), andImp.Predict.ReActspells:parse_failureas:parse_errorand:directas:answered. UseImp.Prediction.complete?/1to ask whether a turn answered. - For a signature with one
:stringoutput,ReActV2offers nosubmittool, 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.ContextWindowExceededErroris 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};RefineandAssertionsreturn{: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 as
x-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 anullanswer 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,:reasonand:stacktrace, as:telemetry.span/3does, instead of:erroras text. - An optimizer's
compile/Nis no longer documented whereImp.optimizeorImp.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; anunavailableentry 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;RLMtakesmax_iterationsonly.Imp.Optimizer.RandomSearchandBootstrapRSareImp.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_*fromprediction.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?/1andImp.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.PredicttoImp.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 implementsImp.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_namefrom MCP failures and absences. CallImp.Signature.load!/1,Imp.History.load!/1,Imp.Optimizer.Report.load!/1andImp.Clients.TrainingJob.load!/2where you calledload, andImp.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 a
base_urlquery, fragment or user info into configuration the host supplies at load time. - Update telemetry handlers for
[:exception]to read:kind,:reasonand: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.