Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ User-visible changes to Imp are recorded here.

## Unreleased

## 0.17.0 — 2026-10-18

### Added

- Messages can carry a participant `name` (the OpenAI chat format's optional
Expand All @@ -14,9 +16,11 @@ User-visible changes to Imp are recorded here.
history turn names its messages with `:user_name` and `:assistant_name`
beside its fields, and the request inputs name the final user message with
`:user_name`. These keys are never rendered as fields, `Imp.Predict` does not
warn about `:user_name` as an extra input, and a signature that declares a
field of either name keeps it as an ordinary field. See "Naming messages" in
`Imp.Adapter.Chat`.
warn about `:user_name` as an extra input when its adapter builds messages
this way (Chat, JSON, XML, PlanFirst), and a signature that declares a field
of either name keeps it as an ordinary field. Many upstreams accept `name`
without showing it to the model, so identity that matters belongs in the
content too. See "Naming messages" in `Imp.Adapter.Chat`.

## 0.16.1 — 2026-10-17

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ text or JSON you can score, such as an agent's tool descriptions.
## Install

```elixir
{:imp, "~> 0.16.0"}
{:imp, "~> 0.17.0"}
```

Imp needs Elixir 1.19 or later and a C and C++ compiler, for the native code
Expand All @@ -162,7 +162,7 @@ large-scale benchmarking. Bug reports and pull requests are welcome.
- [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.16.1/livebooks) are Livebook notebooks
- [Tutorials](https://github.com/deepfates/imp/tree/v0.17.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.

Expand Down
75 changes: 44 additions & 31 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
# Imp v0.16.1
# Imp v0.17.0

Imp is a framework for typed, optimizable language-model programs on the BEAM.
This release fixes how ReActV2 replays a history turn that a host stored with
the task's inputs and its answer, and no step of the loop's own. Under
`finish: :submit` and `finish: :tools`, since 0.14.0, such a turn replayed only
its inputs: the model read the event and not the answer stored with it. It now
replays as the model's message holding that answer, as it always has under
`finish: :auto`. It adds no options and changes no public API, so under Imp's
versioning rule this is a patch release.
This release sends a chat message's participant `name` (the OpenAI chat
format's optional `name`) to the provider, where Imp used to drop it, and lets
a program name the messages of its history turns and its request. A message
without a name is sent exactly as before, so no caller that does not name
messages sees any change. The new input keys are public API a caller can rely
on, so under Imp's versioning rule this is a minor release.

## Install

```elixir
{:imp, "~> 0.16.0"}
{:imp, "~> 0.17.0"}
```

`~> 0.16.0` takes later 0.16 patch releases and nothing newer. The two-part
form, `~> 0.16`, means `>= 0.16.0 and < 1.0.0` in Mix, so `mix deps.update
`~> 0.17.0` takes later 0.17 patch releases and nothing newer. The two-part
form, `~> 0.17`, means `>= 0.17.0 and < 1.0.0` in Mix, so `mix deps.update
imp` under it takes every later 0.x release, including minor releases that
change what callers receive.

Expand All @@ -29,29 +28,43 @@ 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.

## Changes and upgrading from 0.16.0
## Changes and upgrading from 0.16.1

Under `{:imp, "~> 0.16.0"}`, run `mix deps.update imp` and commit `mix.lock`.
No code or data migration is needed.
Change the Imp dependency to `{:imp, "~> 0.17.0"}`, run `mix deps.update imp`,
and commit `mix.lock`. No code or data migration is needed. A program that
already puts `name` on a message it gives `Imp.LM`, or `:user_name` in its
inputs, now sends it.

- **Fixed: a stored answer turn replays its answer under every ending.**
ReActV2 reads a stored turn that carries the inputs and the one output of
the task, and neither `next_thought` nor `tool_calls`, as a step that
answered in text. Since 0.14.0 it did so only when the program ended in
plain text (`finish: :auto`); under `finish: :submit` and `:tools`, with
either `history_replay`, the request held the turn's inputs as a user
message and no assistant message after them. A host that writes such a
turn, for example a recap standing in for the earlier conversation, had
its content left out of every request. The turn now replays as one
assistant message holding the answer, with no "not sent" line after it,
since it carries the task's output. Turns the loop writes itself are
unchanged, and the history the caller holds is unchanged. Under
`finish: :auto`, a one-output signature whose output is constrained or not
typed string gets the same replay when the stored value is a string.
- **Added: participant names reach the provider.** `Imp.Clients.ReqLLM`
copies a message's `name` (atom or string key) onto the message it sends,
for generated, streamed and batched requests. Before, `Imp.LM.generate(lm,
[%{role: "user", content: "hi", name: "alice"}])` went out with no `name`.
A provider whose format has no such field, such as Anthropic's Messages
API, does not receive it.
- **Added: naming messages through the adapters.** With `Imp.Adapter.Chat`,
and the JSON, XML and PlanFirst adapters built on it, a history turn names
its user message with `:user_name` and its assistant message with
`:assistant_name`, beside its fields, and the request inputs name the final
user message with `:user_name`:

Rollback means restoring `0.16.0` in the lock; such turns then replay their
inputs alone again under `finish: :submit` and `:tools`. See the
[changelog](CHANGELOG.md) for every change.
```elixir
history = Imp.History.new([%{question: "Who won?", answer: "Blue.", user_name: "alice"}])
Imp.call(predict, %{question: "Rematch?", history: history, user_name: "bob"})
```

The keys are never rendered as fields, and tool results and notes are
never named. A signature that declares a field called `user_name` or
`assistant_name` keeps it as an ordinary field. `Imp.Predict` does not warn
about `:user_name` as an extra input under those adapters. ReActV2 names
the turns of a history it is given, but not its request message.
- **Not every model sees the name.** Many upstreams accept `name` and drop
it, among them Google's, DeepSeek's, Kimi's, Mistral's and Grok's, and
open-weight models served through hosts such as DeepInfra. When the model
must know who is speaking, say so in the content too.

Rollback means restoring `{:imp, "~> 0.16.0"}` and `0.16.1` in the lock;
names are then dropped again and `:user_name` in the inputs warns as an
extra input. See the [changelog](CHANGELOG.md) for every change.

## Known limits

Expand Down
4 changes: 2 additions & 2 deletions docs/coming-from-dspy.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ brought in.
## The five-minute version

```elixir
# pip install dspy -> {:imp, "~> 0.16.0"}
# pip install dspy -> {:imp, "~> 0.17.0"}
# lm = dspy.LM("openai/...") -> lm = Imp.req_llm("openai:gpt-5.4-mini", api_key: ...)
# dspy.Predict("q -> a") -> program = Imp.predict("q -> a", lm: lm)
# program(q="...") -> {:ok, pred} = Imp.call(program, %{q: "..."})
Expand Down Expand Up @@ -87,7 +87,7 @@ error row too. Each model request has a receive timeout, and
has no time limit of its own (an MCP tool call does, 30 seconds by default),
so to bound an agent, start it with `Imp.start_run/3` and cancel it when you
choose. The
[deployment example](https://github.com/deepfates/imp/blob/v0.16.1/examples/deployment/README.md)
[deployment example](https://github.com/deepfates/imp/blob/v0.17.0/examples/deployment/README.md)
is a complete OTP application.

**Optimizers see what you name.** DSPy finds predictors by walking a module's
Expand Down
2 changes: 1 addition & 1 deletion docs/diving-deeper/modules-and-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ A custom module is your code, so it is not saved as a whole. What an optimizer
chose for it is data: `Imp.ProgramParameters.values/1` reads it,
`Imp.ProgramParameters.apply_values/2` puts it on a freshly built program, and
`Imp.Optimizer.Artifact` writes it to a checksummed file. The
[deployment example](https://github.com/deepfates/imp/blob/v0.16.1/examples/deployment/README.md)
[deployment example](https://github.com/deepfates/imp/blob/v0.17.0/examples/deployment/README.md)
does this in a supervised application.

### Built-in variants
Expand Down
2 changes: 1 addition & 1 deletion docs/diving-deeper/saving-and-artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Save the whole program when it is made of Imp's modules: `Imp.predict/2`,
when the program is your own struct implementing `Imp.Module`, or when you
want the program's code to live in your release and only its tuned text to
cross the persistence boundary. The
[deployment example](https://github.com/deepfates/imp/tree/v0.16.1/examples/deployment)
[deployment example](https://github.com/deepfates/imp/tree/v0.17.0/examples/deployment)
does the second.

### 2. JSON with a checksum
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/running-in-your-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Changing models is then a configuration change, and rotating a key is a
restart.

The
[deployment example](https://github.com/deepfates/imp/blob/v0.16.1/examples/deployment/README.md)
[deployment example](https://github.com/deepfates/imp/blob/v0.17.0/examples/deployment/README.md)
is the complete version of this page: a two-stage program like `TicketTriage`,
learned parameters loaded and reloaded as an `Imp.Optimizer.Artifact`, crashed
and timed-out calls contained, and the whole thing restarted in a fresh OS
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/setting-up.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ Add Imp to a Mix project:
~~~elixir
def deps do
[
{:imp, "~> 0.16.0"}
{:imp, "~> 0.17.0"}
]
end
~~~

or, for a script or a Livebook notebook:

~~~elixir
Mix.install([{:imp, "~> 0.16.0"}])
Mix.install([{:imp, "~> 0.17.0"}])
~~~

Imp needs Elixir 1.19 and a C and C++ compiler, for the native code in two
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/where-to-go-next.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The module documentation is the reference for every function.
[Running Imp in production](../production.md) covers what the last page began:
supervision, provider failures, runs you can observe and cancel, and
telemetry. The
[deployment example](https://github.com/deepfates/imp/blob/v0.16.1/examples/deployment/README.md)
[deployment example](https://github.com/deepfates/imp/blob/v0.17.0/examples/deployment/README.md)
is a complete application to copy from.

## Try it in a notebook
Expand Down
8 changes: 4 additions & 4 deletions docs/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ where its credentials come from, how long a call may take and what it may
cost. This page covers those decisions in the order you meet them: starting
Imp, credentials, concurrency, timeouts, cost and caching, persistence and
telemetry. The
[deployment example](https://github.com/deepfates/imp/tree/v0.16.1/examples/deployment)
[deployment example](https://github.com/deepfates/imp/tree/v0.17.0/examples/deployment)
is a complete OTP application that puts them together.

## Imp in your supervision tree

Imp is an OTP application, and adding `{:imp, "~> 0.16.0"}` to your
Imp is an OTP application, and adding `{:imp, "~> 0.17.0"}` to your
dependencies starts it with yours. It supervises its settings, its response
cache and the task pools that bound its work. It opens no listener and
starts no subprocess, and the MCP and ACP runtimes start only when you use
them. In a script, `Mix.install([{:imp, "~> 0.16.0"}])` starts it too.
them. In a script, `Mix.install([{:imp, "~> 0.17.0"}])` starts it too.

## Credentials at runtime

Expand Down Expand Up @@ -263,7 +263,7 @@ standard error.

## The reference application

[`examples/deployment`](https://github.com/deepfates/imp/tree/v0.16.1/examples/deployment)
[`examples/deployment`](https://github.com/deepfates/imp/tree/v0.17.0/examples/deployment)
is a small OTP application that loads a checksummed artifact at startup, reads
its key from the environment, serves calls from bounded supervised tasks,
answers overload and timeouts with errors instead of blocking, and reloads
Expand Down
2 changes: 1 addition & 1 deletion examples/deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,4 @@ Use a whole-program artifact for a supported portable Imp shape. Use
its selected predictor parameters should cross the persistence boundary.

During source development set `IMP_PATH` to the Imp checkout. Other
applications use the Hex dependency in `mix.exs`, `{:imp, "~> 0.16.0"}`.
applications use the Hex dependency in `mix.exs`, `{:imp, "~> 0.17.0"}`.
2 changes: 1 addition & 1 deletion examples/deployment/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule ImpDeployment.MixProject do

defp deps do
case System.get_env("IMP_PATH") do
nil -> [{:imp, "~> 0.16.0"}]
nil -> [{:imp, "~> 0.17.0"}]
path -> [{:imp, path: path}]
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/imp/adapter/chat.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ defmodule Imp.Adapter.Chat do
format has no such field (Anthropic's Messages API) does not receive it, and
a message without a name goes out exactly as before.

A name that reaches the provider is not always shown to the model. Many
upstreams accept the field and drop it, among them Google's, DeepSeek's,
Kimi's, Mistral's and Grok's, and open-weight models served through hosts
such as DeepInfra, whose chat templates have no place for it. When the model
must know who is speaking, say so in the message content as well.

## Extending the default rendering

A renderer runs for whichever adapter formats the request, which is not
Expand Down
20 changes: 15 additions & 5 deletions lib/imp/predict.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ defmodule Imp.Predict do
adapter <- resolve_adapter(predict),
{:ok, inputs} <- normalize_inputs(inputs),
inputs = apply_input_defaults(predict.signature, inputs),
:ok <- validate_inputs(predict.signature, inputs),
:ok <- validate_inputs(predict.signature, inputs, adapter),
{:ok, request_signature, request_config, reasoning_fields} <-
prepare_native_reasoning(predict.signature, lm, predict.config),
{format_signature, request_config} =
Expand Down Expand Up @@ -533,10 +533,20 @@ defmodule Imp.Predict do

defp fetch_input(inputs, name), do: Imp.FieldMap.get(inputs, name)

defp validate_inputs(signature, inputs) do
# `:user_name` names the request message (see `Imp.Adapter.Chat`); it is
# message metadata, not an ignored field.
:ok = warn_extra_inputs(signature, inputs, [:user_name])
# Adapters that build their messages with `Imp.Adapter.Chat`'s assembly read
# a request's `:user_name` as the user message's participant name (see
# "Naming messages" there); any other adapter ignores it, so the warning
# stays.
@message_naming_adapters [
Imp.Adapter.Chat,
Imp.Adapter.JSON,
Imp.Adapter.XML,
Imp.Adapter.PlanFirst
]

defp validate_inputs(signature, inputs, adapter) do
except = if adapter in @message_naming_adapters, do: [:user_name], else: []
:ok = warn_extra_inputs(signature, inputs, except)
:ok = warn_type_mismatches(signature, inputs)

required =
Expand Down
2 changes: 1 addition & 1 deletion livebooks/01_real_lm_front_door.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ imp_path = System.get_env("IMP_PATH")
lockfile = imp_path && Path.join(imp_path, "mix.lock")

cond do
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.16.0"}])
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.17.0"}])
File.regular?(lockfile) -> Mix.install([{:imp, path: imp_path}], lockfile: lockfile)
true -> Mix.install([{:imp, path: imp_path}])
end
Expand Down
2 changes: 1 addition & 1 deletion livebooks/02_without_a_provider.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ imp_path = System.get_env("IMP_PATH")
lockfile = imp_path && Path.join(imp_path, "mix.lock")

cond do
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.16.0"}])
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.17.0"}])
File.regular?(lockfile) -> Mix.install([{:imp, path: imp_path}], lockfile: lockfile)
true -> Mix.install([{:imp, path: imp_path}])
end
Expand Down
2 changes: 1 addition & 1 deletion livebooks/03_evaluate_and_optimize.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ imp_path = System.get_env("IMP_PATH")
lockfile = imp_path && Path.join(imp_path, "mix.lock")

cond do
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.16.0"}])
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.17.0"}])
File.regular?(lockfile) -> Mix.install([{:imp, path: imp_path}], lockfile: lockfile)
true -> Mix.install([{:imp, path: imp_path}])
end
Expand Down
2 changes: 1 addition & 1 deletion livebooks/04_tools_agents_mcp_rlm.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ imp_path = System.get_env("IMP_PATH")
lockfile = imp_path && Path.join(imp_path, "mix.lock")

cond do
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.16.0"}])
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.17.0"}])
File.regular?(lockfile) -> Mix.install([{:imp, path: imp_path}], lockfile: lockfile)
true -> Mix.install([{:imp, path: imp_path}])
end
Expand Down
4 changes: 2 additions & 2 deletions livebooks/05_operating_imp.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ imp_path = System.get_env("IMP_PATH")
lockfile = imp_path && Path.join(imp_path, "mix.lock")

cond do
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.16.0"}])
is_nil(imp_path) -> Mix.install([{:imp, "~> 0.17.0"}])
File.regular?(lockfile) -> Mix.install([{:imp, path: imp_path}], lockfile: lockfile)
true -> Mix.install([{:imp, path: imp_path}])
end
Expand Down Expand Up @@ -103,7 +103,7 @@ Enum.each(busy, &Task.await/1)
refused
```

The [deployment example](https://github.com/deepfates/imp/tree/v0.16.1/examples/deployment)
The [deployment example](https://github.com/deepfates/imp/tree/v0.17.0/examples/deployment)
wraps the same pattern in a `ProgramServer` that also reloads parameters while
serving.

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Imp.MixProject do
use Mix.Project

@version "0.16.1"
@version "0.17.0"

def project do
[
Expand Down
2 changes: 1 addition & 1 deletion priv/public_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -10359,7 +10359,7 @@
"types": []
}
],
"package_version": "0.16.1",
"package_version": "0.17.0",
"schema_version": 3,
"scope": "Curated Imp API compiled from package-shipped sources. Exports come from non-hidden documentation entries; callbacks, types, and struct fields are category- and policy-gated."
}
Loading
Loading