Skip to main content

CHANGELOG.md

# Changelog

## v0.9.0

Reworks the optional `:horde` distribution backend so cluster membership is
correct, dynamic, and scopable. Full write-up in
[#134](https://github.com/sagents-ai/sagents/pull/134).

### `members: :participation` — dynamic, role-scoped membership

    config :sagents, :distribution, :horde
    config :sagents, :horde, members: :participation

Membership becomes exactly the nodes that run `Sagents.Supervisor`, discovered
via an OTP `:pg` group and kept current on `:nodeup`/`:nodedown` by the new
`Sagents.Horde.MembershipManager`. Gate `Sagents.Supervisor` to your
agent-hosting role(s) and membership follows automatically — no node-name
predicate, and dead nodes are pruned for free. Prefer this over `:auto` whenever
the Erlang cluster also contains nodes that should not host agents.

### `:partition` — isolate participation into independent groups

    config :sagents, :horde,
      members: :participation,
      partition: System.get_env("FLY_REGION")

An optional per-node `:partition` (any stable, opaque grouping key) scopes
membership further so a node only clusters with same-partition nodes. The
motivating case is geographic — set it to a Fly.io `FLY_REGION` so an agent for
an Illinois user is never placed on, or routed through, a node in France — but it
works for any per-node grouping. Cross-partition request routing remains an
infra/app concern (e.g. Fly `fly-replay`). See
[`docs/clustering.md`](https://github.com/sagents-ai/sagents/blob/main/docs/clustering.md).

### Also in this release

See [#134](https://github.com/sagents-ai/sagents/pull/134) for details on each:

- **`members: :auto` is now real** — potential behaviour change. It previously
  froze to a one-time node snapshot; it now drives Horde's `NodeListener` for
  genuine dynamic membership with dead-node pruning. The undocumented static
  `:members` forms (list / `function/0` / `{m, f, a}`) are removed; the only
  values are `:auto` (default) and `:participation`.
- **Registration-timeout resilience** — `AgentsDynamicSupervisor.start_agent_sync/1`
  retries on Horde's hardcoded-5s `:via` registration timeout, via new
  `:registration_retries` / `:registration_retry_backoff` options.
- **FileSystem distribution-safety** — dropped `Process.alive?/1` checks on
  potentially-remote pids that could raise.

## v0.8.0

A large release that reworks the runtime foundations of the library: a new direct point-to-point event transport (replacing `Phoenix.PubSub`), session/factory lifecycle ownership moved into the library, interrupts that survive a process restart, a richer interrupt model (`:halt`, configurable `ask_user`), structured data extraction through the full middleware stack, cross-process caller-context propagation, and new tool-driven stop conditions.

This entry consolidates everything relevant to upgrading from the previous public release, **`v0.7.x`**. The `v0.8.0` line went through 13 release candidates; several breaking changes were introduced and then superseded *within* the RC cycle and therefore do not affect anyone moving directly from `v0.7.x` to `v0.8.0`. For the complete, blow-by-blow history of every intermediate change, see the archived [`v0.8.0-rc.13` changelog](https://github.com/sagents-ai/sagents/blob/v0.8.0-rc.13/CHANGELOG.md).

**Breaking changes** — see the Upgrading section below.

### Upgrading from v0.7.x to v0.8.0

The recommended path is to **re-run the generators on a clean, committed workspace and merge your customizations back in**, then apply a handful of host-code renames.

**1. Regenerate scaffolding.** Run `mix sagents.setup` (or the individual `mix sagents.gen.*` tasks) with the same options you used originally, accept the overwrites, and merge your customizations back with a diff tool. This absorbs the structural changes in one step: the new `Session` / `Factory` / `FactoryRouter` triad (replacing the old monolithic `coordinator.ex` + `factory.ex`), the new `agent_subscriber_session.ex` template, integer todo ids in `valid_todo_entry?/1`, the denormalized `tool_call_id` column in the persistence schema/context, and restorable-interrupt support. [#97](https://github.com/sagents-ai/sagents/pull/97) [#79](https://github.com/sagents-ai/sagents/pull/79) [#116](https://github.com/sagents-ai/sagents/pull/116) [#127](https://github.com/sagents-ai/sagents/pull/127) [#96](https://github.com/sagents-ai/sagents/pull/96)

**2. Transport: `Sagents.PubSub` is removed.** Replace any direct `Sagents.PubSub.subscribe/1` / `broadcast/2` calls with `use Sagents.Subscriber` plus the generated `subscribe/2` helper, or pass `:initial_subscribers` when starting servers to enroll the caller inside `init/1` and avoid the start/subscribe race. Existing `handle_info/2` clauses keep matching — event payload shapes (`{:agent, _}`, `{:file_system, _}`, `{:status_changed, _, _}`, `{:llm_deltas, _}`, etc.) are unchanged. [#79](https://github.com/sagents-ai/sagents/pull/79)

**3. SubAgent: `subagent_type` → `task_name`.** The `task` and `get_task_instructions` tools now take `task_name`. Rename the key in any interrupt-data pattern match (`%{type: :subagent_hitl, task_name: type, sub_agent_id: id}`) and in any `context.resume_info` maps you build for sub-agent resume. Persisted v1 state is migrated to v2 automatically by `StateSerializer`. The available-tasks listing moved into an `## Available Tasks` system-prompt section (suppressible via `:include_task_list`); update any custom prompts referencing the old wording. [#78](https://github.com/sagents-ai/sagents/pull/78)

**4. Session API rename.** `Coordinator.ensure_session_running/1` is now `ensure_agent_session_running/1` — update LiveViews, controllers, and tests. Factory helpers that were `get_model/0` / `get_middleware/0` become `build_model/1` / `build_middleware/1`, branching on a `%FactoryConfig{}` struct. Per-request data (timezone, tool_context, project records) now flows through `request_opts → FactoryRouter.resolve/3 → %FactoryConfig{} → Factory.create_agent/2` rather than being threaded as positional args. [#97](https://github.com/sagents-ai/sagents/pull/97)

**5. Debug subscriptions.** `AgentServer.subscribe_debug/1` / `unsubscribe_debug/1` are removed in favor of `AgentServer.subscribe(agent_id, :debug)` / `unsubscribe(agent_id, :debug)`. The single-arg `subscribe(agent_id)` form is unchanged. [#94](https://github.com/sagents-ai/sagents/pull/94)

**6. FileSystem: `replace_file_lines` removed.** If your config, prompts, or evals reference it, either drop it (`replace_file_text` covers the same use cases for most agents) or re-add it as a project-local tool — the previous implementation lives in the [#110](https://github.com/sagents-ai/sagents/pull/110) diff. Configs passing `tools:` / `tool_descriptions:` to `Sagents.Middleware.FileSystem` must remove the `"replace_file_lines"` entry. [#110](https://github.com/sagents-ai/sagents/pull/110)

**7. Todo ids are integers.** Host code calling `Sagents.Todo.new/1`, `State.get_todo/2`, or `State.delete_todo/2` with string ids must switch to integers (`Todo.new/1` now validates `greater_than: 0`). Code that builds todos from incoming maps should migrate to `Sagents.Todo.list_from_maps/1`, which assigns positional defaults for missing/non-numeric ids and coerces stringified integers. Persisted snapshots with legacy base64 string ids rehydrate to positional ids automatically on load. [#116](https://github.com/sagents-ai/sagents/pull/116)

**8. Opt middleware into interrupt restoration (optional).** Custom middleware that produce restorable, data-only `interrupt_data` should implement `Sagents.Middleware.restorable_interrupt?/1` returning `true` for matching shapes. The default of `false` preserves the old safe demote-on-load behaviour with no code changes. Built-in `AskUserQuestion` and `HumanInTheLoop` already opt in; `SubAgent` deliberately does not. [#96](https://github.com/sagents-ai/sagents/pull/96)

### Added

- **Direct-delivery transport** — `Sagents.Publisher` / `Sagents.Subscriber` replace `Phoenix.PubSub` with monitored point-to-point delivery, an `:initial_subscribers` start option, and a Presence-based recovery loop for crash-restart and Horde migration. [#79](https://github.com/sagents-ai/sagents/pull/79)
- **Session/Factory lifecycle in the library** — `Sagents.Session` owns the session-start lifecycle (router consult, factory invocation, state seeding, supervisor wiring, subscribers) and is idempotent on resume. `Sagents.Factory` / `Sagents.FactoryRouter` behaviours, `Sagents.Routers.Single` for one-factory apps, and a typed `%FactoryConfig{}` for per-request data. [#97](https://github.com/sagents-ai/sagents/pull/97)
- **Restorable interrupts** — an agent that shut down (inactivity timeout, deploy, crash) with a pending `ask_user` question or HITL approval now boots back into `:interrupted` status with the original `interrupt_data` intact, rather than silently demoting to an error. New optional `Sagents.Middleware.restorable_interrupt?/1` callback, `set_interrupted/3` persistence callback, and cheap pre-deserialization `interrupted?/1` read. [#96](https://github.com/sagents-ai/sagents/pull/96)
- **`:halt` terminal interrupt** via the new `Sagents.Middleware.Haltable` — tools can hard-stop a workflow (e.g. a gating validation tool) without giving the LLM a chance to continue. Includes `AgentServer.dismiss_interrupt/1` for UIs to acknowledge a halt and a `[:sagents, :agent, :halt]` telemetry event. [#115](https://github.com/sagents-ai/sagents/pull/115)
- **`Sagents.Extract`** — structured data extraction that flows through the agent's full middleware stack. The submit tool is owned by the agent and selected via the `:until_tool` / `:until_tool_success` stop condition; `run/3` returns the tool's `processed_content` when present. [#108](https://github.com/sagents-ai/sagents/pull/108) [#129](https://github.com/sagents-ai/sagents/pull/129) [#128](https://github.com/sagents-ai/sagents/pull/128)
- **`Sagents.AgentResult`** — read helpers for pulling tool results, arguments, processed content, or final text out of `Agent.execute/3` return values. [#107](https://github.com/sagents-ai/sagents/pull/107)
- **`:until_tool` and `:until_tool_success` stop conditions** on `Sagents.Agent.execute/3` and `Sagents.SubAgent` — complete a run when a target tool is called (or, for `:until_tool_success`, returns a non-error result), enabling the validate-and-retry pattern. [#128](https://github.com/sagents-ai/sagents/pull/128)
- **`Sagents.Middleware.ProcessContext`** — propagates caller-process state (OpenTelemetry trace context, Sentry context, request-scoped Logger metadata, tenant scope) across the three process boundaries an agent invocation crosses, via `:keys` and `:propagators` configuration shapes. [#82](https://github.com/sagents-ai/sagents/pull/82)
- **`Sagents.StreamingSession`** — host-agnostic streaming helpers (`handle_tool_call_identified/2`, `handle_tool_execution_update/3`) returning *changes maps* the host merges itself, with multi-tool-safe delta semantics. [#104](https://github.com/sagents-ai/sagents/pull/104)
- **TodoList `:inline` mode** — each successful `write_todos` additionally persists a `todo_snapshot` synthetic display message into the transcript. [#101](https://github.com/sagents-ai/sagents/pull/101) [#102](https://github.com/sagents-ai/sagents/pull/102)
- **`AskUserQuestion` config pinning** — optional `allow_other` / `allow_cancel` init options force those values for every question instead of leaving them to the LLM. [#124](https://github.com/sagents-ai/sagents/pull/124)
- **SubAgent `:initial_messages`** for seeding per-call messages, and **`:include_task_list`** to opt out of the auto-generated task menu. [#100](https://github.com/sagents-ai/sagents/pull/100) [#78](https://github.com/sagents-ai/sagents/pull/78)
- **`Sagents.AgentServer.save_synthetic_message_from/2`** — lets middleware persist user-facing transcript entries through the same display-message pipeline LLM messages use. `AskUserQuestion` records the user's answer this way. [#88](https://github.com/sagents-ai/sagents/pull/88) [#89](https://github.com/sagents-ai/sagents/pull/89)
- **`Sagents.State.runtime`** virtual field for process-local values that must never be persisted, with `merge_runtime/2`. [#84](https://github.com/sagents-ai/sagents/pull/84)
- **`agent_id` on tool execution context** (`context.agent_id`) so tools can publish events without reaching into `state`. [#86](https://github.com/sagents-ai/sagents/pull/86)
- Tooling hardening: Credo, Dialyzer, `sobelow`, and `mix_audit` wired into `mix precommit` and CI. [#93](https://github.com/sagents-ai/sagents/pull/93) [#90](https://github.com/sagents-ai/sagents/pull/90) [#106](https://github.com/sagents-ai/sagents/pull/106)

### Changed

- **BREAKING:** Transport, SubAgent tool arguments, session/factory API, debug subscriptions, the `FileSystem` tool set, and `Sagents.Todo` ids all changed — see the Upgrading section above. [#79](https://github.com/sagents-ai/sagents/pull/79) [#78](https://github.com/sagents-ai/sagents/pull/78) [#97](https://github.com/sagents-ai/sagents/pull/97) [#94](https://github.com/sagents-ai/sagents/pull/94) [#110](https://github.com/sagents-ai/sagents/pull/110) [#116](https://github.com/sagents-ai/sagents/pull/116)
- The generated persistence templates denormalize the tool-call linking id into a dedicated indexed `tool_call_id` column, switching the hot tool-execution queries from a JSONB `fragment(...)` to indexed equality. New generations are clean; existing host apps absorb this by regenerating as described above. [#127](https://github.com/sagents-ai/sagents/pull/127)
- `Sagents.Middleware` documents the full interrupt-data catalog (`:ask_user_question`, `:halt`, `:subagent_hitl`, HITL action-request map, `:multiple_interrupts`) and the "halt wins" policy. [#115](https://github.com/sagents-ai/sagents/pull/115)
- Upgraded to Elixir 1.20 and bumped the `langchain` dependency floor to `>= 0.8.11`. [#122](https://github.com/sagents-ai/sagents/pull/122) [#106](https://github.com/sagents-ai/sagents/pull/106)

For the per-RC `Added` / `Changed` / `Fixed` detail behind this summary — including bug fixes resolved within the RC cycle — see the archived [`v0.8.0-rc.13` changelog](https://github.com/sagents-ai/sagents/blob/v0.8.0-rc.13/CHANGELOG.md).

---

Changelog entries for `v0.1.0` through `v0.7.0` have been removed to give the `v0.8.0` line a clean slate. The full detailed history remains available in git — see the [`v0.8.0-rc.13` changelog](https://github.com/sagents-ai/sagents/blob/v0.8.0-rc.13/CHANGELOG.md), which retains every entry back to the initial release.