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.react agent, 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.GEPA behaves as DSPy's GEPA does; Imp's own search is execution_profile: :beam_native.
  • An Imp.Deadline reaches the work Imp starts for you: Imp.parallel/3, evaluation rows, optimizer workers and runs inherit the caller's deadline, and Imp.start_run/3 takes deadline:.
  • Imp depends on ExMCP 1.5 from Hex, unpatched. What Imp needed from the deepfates/ex_mcp fork now lives in Imp: stdio MCP servers that end with their connection, children included; a clean PATH for them inside a release; trust for authorized remote servers; the connection options public servers need; and the browser OAuth flow.
  • ReActV2 offers submit only 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.
  • ReActV2 gains finish_on for tools whose call is the answer.
  • :model_request events 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.Run and Imp.ACP refuse options they do not know, and Imp.Run.Event.kinds/0 lists 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/2 takes pool_size:, so several calls to one HTTP server run at once, and an HTTP call can take as long as its :timeout allows.
  • 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_PATH is no longer read.
  • A release that uses Imp.MCP or Imp.ACP adds erlexec: :load beside 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.GEPA defaults to DSPy's GEPA, execution_profile: :gepa_v0_1_4_merge: merge on, no evaluation cache, perfect minibatches skipped, the pinned RNG, and :generations turned into a metric budget when :max_metric_calls is 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 unless execution_profile: :beam_native is 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/3 no 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.Tool named 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's name to 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 :timeout is %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_sent with 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". An sse 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 under on_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/2 needs clauses for them.
  • Imp.Run.start/3, Imp.ACP.start_link/1, Imp.ACP.run/1 and Imp.ACP.Local.start_link/1 raise ArgumentError for an option they do not know. A transport's own options for Imp.ACP go in :transport_options, and :capabilities is spelled :agent_capabilities.
  • Imp.predict/2, Imp.chain_of_thought/2 and Imp.configure/1 raise ArgumentError for an option or setting they do not know. Request options such as :temperature go under config:; a setting of your own goes through Imp.context/2.
  • :max_errors and :retriever are no longer settings, and Imp.configure/1 and Imp.context/2 refuse them. Pass :max_errors to BootstrapFewShot, RandomSearch or COPRO (10 when not given) and a retriever to the program.
  • ReActV2 emits no :final event; :run_finished carries the prediction. Imp.Trajectory.to_atif/2's extra.outcome is extra.terminal_event, and a tool result's extra.outcome is the recorded Imp.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_calls and context_projection are in prediction.metadata. termination_reason says how the turn ended, and a turn without an answer is :incomplete with termination_cause saying why; typed extraction is :extracted (no completion_mode), and Imp.Predict.ReAct spells :parse_failure as :parse_error and :direct as :answered. Use Imp.Prediction.complete?/1 to ask whether a turn answered.
  • For a signature with one :string output, ReActV2 offers no submit 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.ReqLLM request is %Imp.LMError{} (with status, retryable and context_window_exceeded; Imp.ContextWindowExceededError is gone), and a completion that cannot be parsed is %Imp.AdapterParseError{kind: ...}, which Imp.Predict returns 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 :authorize refusal is {:tool_denied, tool, reason}; Refine and Assertions return {:error, reason}; Imp.optimize! raises Imp.Error for a failed optimization. The CHANGELOG lists every tag that changed.
  • Imp.Example and Imp.Prediction keep string keys as strings. Code that read a field of data loaded from JSON with map.field or map[:field] reads it with Imp.Example.get/2 or 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.MCP and Imp.Core.ToolCall/ToolResult are gone. Imp.MCP.connect/2 imports tools; Imp.ACP.ToolKind.derive_all/1 gives 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-tenant included) sends requests without them after loading until it is rebound with Imp.with_lm/2 or a scoped Imp.context/2.
  • Imp.save! refuses an LM whose base_url has a query, fragment or user info.
  • Prompts name types in plain words instead of Python annotations (one of: atlas, harbor where 0.4.0 wrote Literal['atlas', 'harbor']), values take their JSON spelling (null, true, false), and the structured-output schema is named outputs. A non-string answer for a string field is kept as its JSON text ("true", not "True"), and a null answer 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.Telemetry span's [:exception] event carries :kind, :reason and :stacktrace, as :telemetry.span/3 does, instead of :error as text.
  • An optimizer's compile/N is no longer documented where Imp.optimize or Imp.train runs the optimizer; call those.
  • Imp.load!/1 reading a file is Imp.read!/1; Imp.load/1 returns {:ok, program} and Imp.load!/1 takes the dumped map.
  • Imp.react/3 builds ReActV2 and Imp.react_v2 is gone. Imp.Predict.ReAct's mode: :dspy_3_2_1 is mode: :dspy.
  • Imp.Predict.Predict is Imp.Predict.
  • Imp.Retrievers.KNN is deleted; Imp.Retrieve.Memory retrieves by token overlap.
  • An LM is a struct or module whose generate/3 takes it first. The %{module:, opts:} map and a bare function are refused; so is a module that defines only generate/2. A retriever module's retrieve/3 takes itself first.
  • Imp.MCP.CallFailure has server_name, tool_name and index; an unavailable entry has server_name; :authorize returns :allow or {:deny, reason} and its context names the descriptor; :credentials is :credential_store.
  • A :tool_policy function returns :allow or {:deny, reason}, and a refused call is {:tool_denied, name, reason}.
  • max_concurrency is num_threads on evaluation, parallel, search, batch and optimizer options. Refine's max_attempts is n; RLM takes max_iterations only. Imp.Optimizer.RandomSearch and BootstrapRS are Imp.Optimizer.BootstrapFewShotWithRandomSearch.

Upgrade path

  1. Change the dependency line, run mix deps.get, and commit mix.lock.
  2. Add erlexec: :load to any release that lists ex_mcp: :load.
  3. Replace OAuth.begin/3's :flow with :client_registration if you used it.
  4. 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_failed and :imp_run_event_undelivered.
  5. Read a ReAct or ReActV2 prediction's history and termination_* from prediction.metadata, and match termination_reason against the new values.
  6. Change "type" => "sse" descriptors for Streamable HTTP servers to "http". A server that needs credentials is reached over Streamable HTTP; an sse descriptor takes none.
  7. Match LM failures on %Imp.LMError{} (or ask Imp.Errors.retryable?/1 and Imp.Errors.context_window_exceeded?/1), parse failures on %Imp.AdapterParseError{kind: ...}, and exception reasons on the struct rather than its text.
  8. Rename Imp.react_v2 to Imp.react, Imp.Predict.Predict to Imp.Predict, and Imp.load!(path) to Imp.read!(path); give custom LMs and retriever modules the generate/3 and retrieve/3 that take the client first, and wrap an LM function in a struct that implements Imp.LM.
  9. Rename max_concurrency: to num_threads: where you configure evaluation, optimizers or parallel calls; answer :authorize and :tool_policy with :allow or {:deny, reason}; match a refused tool call as {:tool_denied, name, reason}; read server_name and tool_name from MCP failures and absences. Call Imp.Signature.load!/1, Imp.History.load!/1, Imp.Optimizer.Report.load!/1 and Imp.Clients.TrainingJob.load!/2 where you called load, and Imp.Clients.TrainingJob.read!/2 where you read a checkpoint file, and the datasets' read! where you called their load(path).
  10. Rebind the LM of any loaded program that relies on custom headers, and move a base_url query, fragment or user info into configuration the host supplies at load time.
  11. Update telemetry handlers for [:exception] to read :kind, :reason and :stacktrace.
  12. 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.