智能体
github.com ·
@deepfates
Imp v0.5.0 发布:BEAM 上的类型化 LLM 程序框架上架 Hex
Imp 是运行在 BEAM 上的类型化、可优化语言模型程序框架,v0.5.0 正式发布到 Hex。主要变化包括:GEPA 优化器可作用于 Imp.react agent,通过读取完整运行轨迹与工具定义重写 agent 指令;Imp.Deadline 会传播到 Imp.parallel/3、评估行、优化器 worker 和 run;依赖 Hex 上未打补丁的 ExMCP 1.5,原先 fork 所需能力已并入 Imp;ReActV2 仅在签名需要时提供 submit,并新增 finish_on;:model_request 记录完整请求,工具定义以 :tools_sent 每次运行发送一次。发布说明还提到 mix deps.get 与 mix hex.audit 会报告两个 cowlib 安全公告(CVE-2026-43966、CVE-2026-43969),其中第一个可由 Cowboy 2.16.0+ 缓解,第二个尚无修复版本。
AI推荐理由 官方 release notes,信息密度高且具体:Imp 上架 Hex、GEPA 可优化 Imp.react agent、Deadline 传播、ReActV2 的 submit/finish_on 行为变化、依赖 ExMCP 1.5 及 cowlib 两个 CVE 说明。局限是面向 Elixir/BEAM 生态的小众框架,未提供性能数据或采用情况,安全公告也仅描述依赖层问题而非 Imp 自身漏洞。
62
正文
Official release notes (https://github.com/deepfates/imp/releases/tag/v0.5.0)
# 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
```elixir
{: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](docs/production.md#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](CHANGELOG.md) 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`.
Repository README (current repository documentation, not release changes)
# Imp
Declarative, self-improving language-model programs for Elixir.
Imp is a full port of [DSPy](https://dspy.ai) to the BEAM. You describe what
each language-model step takes and returns, choose how it thinks, and let an
optimizer improve it against examples of what good looks like. You get
signatures, modules, optimizers, agent loops and retrieval, running with the
reliability and concurrency of OTP.
DSPy makes each call to a model a declared, typed function that you can
measure and improve. On the BEAM, an agent is a process: it keeps its own
state, receives messages, and runs under a supervisor alongside the rest
of your application. With both, you can build anything from one typed call
to many long-running agents, and improve each part by measuring it.
## Declare a task
```elixir
lm = Imp.req_llm("openai:gpt-5.4-mini", api_key: System.fetch_env!("OPENAI_API_KEY"))
triage =
"issue -> kind: enum[bug,feature,question], summary"
|> Imp.signature("Triage a GitHub issue.")
|> Imp.predict(lm: lm)
{:ok, prediction} =
Imp.call(triage, %{issue: "App crashes on startup since 0.4 with ** (KeyError) key :lm not found"})
{Imp.get(prediction, :kind), Imp.get(prediction, :summary)}
#=> {"bug", "App crashes on startup since version 0.4 with a KeyError for `:lm` not found."}
```
You never write a prompt or a parser. Imp builds the prompt from the
signature, checks the reply against it, and gives you typed fields: `kind` is
always one of the three values, or the call returns an error. To make the
same task reason first, use `Imp.chain_of_thought/2`; to give it tools, use
`Imp.react/3`. The signature stays the same.
## Measure it and improve it
Give Imp labeled examples and a metric, and it scores the program and
optimizes it. You need three lists of issues you have already labeled:
`trainset`, which the optimizer learns from; `valset`, which it uses to choose
between the programs it tries; and `testset`, which you score on before and
after. `strong_lm` is a more capable model that GEPA uses to read failures and
write new instructions.
```elixir
# Each set is a list of labeled issues like this one:
example =
Imp.example(%{issue: "Please add a dark mode to the dashboard", kind: "feature"})
|> Imp.with_inputs([:issue])
metric = Imp.exact_match(:kind)
Imp.evaluate(triage, testset, metric).score
optimizer = Imp.Optimizer.GEPA.new(metric, reflection_lm: strong_lm, max_metric_calls: 300)
improved = Imp.optimize!(triage, optimizer, trainset, valset)
Imp.evaluate(improved, testset, metric).score
```
GEPA runs the program, reads where it failed, and rewrites its instructions.
Other optimizers choose worked examples (LabeledFewShot, BootstrapFewShot),
search over combinations of instructions and examples (MIPROv2), learn rules
and examples from the program's own better and worse attempts (SIMBA), or
train the model's weights (fine-tuning, GRPO). The result is a new program
whose instructions and examples you can read, save as JSON, and review as a
diff.
## Build agents
A tool is an Elixir function. `Imp.react/3` builds an agent that calls tools
until it can answer. This one reads web pages with Req. Imp depends on Req;
if your own code calls it, as this tool does, add `{:req, "~> 0.6"}` to your
dependencies:
```elixir
fetch =
Imp.tool(:fetch, "Read a web page as text.", fn %{"url" => url} -> Req.get!(url).body end,
schema: %{"type" => "object", "properties" => %{"url" => %{"type" => "string"}}, "required" => ["url"]}
)
researcher = Imp.react("question -> answer", [fetch], lm: lm)
question =
"What version does https://raw.githubusercontent.com/elixir-lang/elixir/v1.18.0/VERSION say? "
"Reply with just the version."
{:ok, prediction} = Imp.call(researcher, %{question: question})
Imp.get(prediction, :answer)
#=> "1.18.0"
```
## Run agents as processes
`Imp.call/2` runs a program in your process. `Imp.start_run/3` runs it as its
own supervised process instead, so you can watch it, stop it, and decide
which tool calls it may make:
~~~elixir
{:ok, run} =
Imp.start_run(researcher, %{question: question},
authorize: fn call ->
url = call.arguments["url"] || ""
if String.starts_with?(url, "https://raw.githubusercontent.com/"),
do: :allow,
else: {:deny, :untrusted_host}
end
)
{:ok, prediction} = Task.await(run.task, :infinity)
for event [:run_started, :tools_sent, :model_request, :model_response, :tool_call,
# :tool_result, :model_request, :model_response, :run_finished]
~~~
Imp also includes:
- **MCP:** import the tools of any MCP server you approve, and they work like
your own.
- **ACP:** serve any Imp program as an agent to Zed and other ACP clients.
- **OTP:** a run is a process you can watch, stop and limit, and a run ends
when the process that started it does. Model requests are cut to a
deadline you set. A tool call that may already have taken effect is
reported as unknown, never silently retried.
- **More shapes:** RLM for inputs far larger than a context window, CodeAct
and program of thought, which compute with small sandboxed expressions, and your own modules
composed from these.
The optimizers work on agents too. GEPA reflects on whole agent runs and
rewrites the instructions that steer them. Optimize Anything rewrites any
text or JSON you can score, such as an agent's tool descriptions.
## Install
```elixir
{:imp, "~> 0.5"}
```
Imp needs Elixir 1.19 or later and a C and C++ compiler, for the native code
in two dependencies (jaxon and erlexec). The first compile needs network
access, because erlexec's build fetches rebar3 plugins. It reaches models through
[ReqLLM](https://hex.pm/packages/req_llm), so any provider ReqLLM supports
works.
Imp 0.5 is experimental and is its first release on Hex. Its API may still
change, and its optimizers need large-scale benchmarking. Bug reports and
pull requests are welcome.
## Learn
- [Getting started](docs/getting-started/index.md) builds one program step by
step, from the first call to a supervised server, with real scores.
- [Coming from DSPy](docs/coming-from-dspy.md) maps DSPy's names to Imp's.
- [Tutorials](https://github.com/deepfates/imp/tree/v0.5.0/livebooks) are Livebook notebooks
you can run offline or with a key.
- The [cheatsheet](docs/cheatsheet.cheatmd) has the common calls on one page.
Imp is MIT licensed.