All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
[0.5.2] - 2026-09-03
Added
config :agentix, :addressing—:local(default, unchanged behaviour) or:global. Under:global, a conversation resolves to the same agent from every node in the cluster, through Erlang's:globalregistry.This is a correctness fix for multi-node hosts, not a convenience. A conversation is meant to have exactly one agent because that agent is the single writer of its durable log. Under node-local addressing on two nodes, each node's
ensure_started/2finds nothing locally and starts its own, so two agents interleave writes to one conversation.cancel/1had the mirror problem: a turn running elsewhere was not found, and the call returned:okhaving cancelled nothing.Under
:globalthe second start loses with{:error, {:already_started, pid}}— already handled — and every entry verb reaches the live agent wherever it runs. The cost is that registration is a cluster-wide synchronous operation, so starting a conversation gets slower as nodes are added; it suits coarse conversations rather than one per request. A netsplit is out of its reach and documented as such.Agentix.Addressing—mode/0,via/1,whereis/1. The agent's internal addressing delegates to it, so theviaseam its docs always promised is real.
Documented
Conversation.cancel/1states what it already did: it returns only once the streaming task is terminated, the provider's cancel closure has run, the partial assistant message is persisted and the conversation is idle.
[0.5.1] - 2026-09-03
Fixed
featurenow lands on theagentix_conversationscolumn, not only inside the settings blob.init/1wrotetenant_keyand notfeature, so the column a host queries and joins on stayed null while the config that set it looked correct. Model-call rows were unaffected — they take the feature straight from the config.
[0.5.0] - 2026-09-03
Added
model_call_log— durable records of provider calls, replacing theaudit?boolean with three levels onAgentix.Conversation.Config(and as application config)::off(the default, unchanged),:records(one row per call carrying model, usage, latency, outcome,tenant_keyandfeature, with no prompt), and:full(the same row plusrendered_context). The middle level is the one that did not exist before: a row small enough to keep indefinitely, which is what a host needs to answer "what did this tenant spend" without also storing every prompt forever.- Calls are recorded at every outcome, not only on success. A failed call
writes a row with
status: :errorand the provider's reason; a cancelled turn writesstatus: :cancelled. The cancelled case is only reachable here: cancelling kills the streaming task, so its telemetry span emits no terminal event at all and a handler counting spend from telemetry misses it silently. feature— an optional label onAgentix.Conversation.Configfor the part of the host application a conversation serves. Stored on the conversation and mirrored onto every model-call row, indexed as(tenant_key, feature, inserted_at), so spend by feature is one query with no join. Unliketenant_keyit is a label, not an isolation boundary — nothing selects or deletes by it.tenant_keyis now mirrored onto model-call rows too, so the rows stay attributable after their conversation is deleted.pricing_versionon each row — thellm_dbcatalog version that costed the call, so a later price correction is traceable rather than a silent rewrite.latency_msis now actually populated. The column existed since the audit table was introduced and was never written.
Changed
put_model_call/2persists whatever it is given. Whether a call is recorded, and at what detail, is the agent's decision — adapters no longer consult application config themselves.gc_model_calls/2is documented as offered-but-unscheduled, with the reason: under:fullthe rows are the fastest-growing table in the schema, while under:recordsthey are usually the host's accounting record and expiring them deletes it. Agentix will not pick for you.
Breaking
agentix_model_callsgained columns andrendered_contextbecame nullable. Hosts that rancreate_agentix_tables.exsbefore 0.5.0 must applypriv/templates/migration/upgrade_agentix_model_calls.exs; a fresh install gets everything from the create migration and must not run it. The upgrade backfillstenant_keyfrom each row's surviving conversation.Config.audit?is deprecated but still accepted and still means:full, at both the conversation and application level, so conversations persisted before this release revive with the recording they were configured for. It will be removed in a later release.
0.4.0 - 2026-08-23
Added
- Per-model-call and per-tool-call telemetry. Every provider attempt emits a
[:agentix, :model_call, :start | :stop | :exception]span (token usage, latency, the rendered context, and the assembled response; one span per retry attempt), and every tool call emits[:agentix, :tool, :start | :stop | :exception](latency measured across:human/:clientsuspensions). Events fire regardless ofaudit?, so hosts can feed PostHog LLM analytics, Langfuse, or OpenTelemetry without enabling audit rows. Documented in the newguides/telemetry.md, including a worked PostHog handler. The caller'sAgentix.Scopeis deliberately never broadcast — metadata carries only a derivedsystem_call?boolean. tenant_key— optional owning tenant for multi-tenant hosts, onAgentix.Conversation.Configand as atenant_key:option onensure_started/2/send_message/4. Stored as an indexed column onagentix_conversations(and in settings, so revival keeps it) and stamped on all:model_call/:tooltelemetry metadata. Write-once: a conflicting re-key returns{:error, :tenant_key_conflict}— including when racing concurrent starts.Agentix.Scopegains an optionaltenant_keyfield: when the scope carries one and the call does not passtenant_key:explicitly, the entry verbs (send_message/4,Agentix.resolve/4) use the scope's key for the write-once check — so authenticating the tenant into the scope is enough to get tenant isolation on every call. The scope field itself is never persisted and never broadcast on telemetry.Agentix.Persistence.delete_by_tenant/1— deletes every conversation whosetenant_keymatches, cascading to events, summaries, tool calls, and audit rows; returns{:ok, count}.
Breaking
New required
Agentix.Persistencebehaviour callbackdelete_by_tenant/1(implemented by both bundled adapters). Third-party adapters must implement it.Ecto-backed installs that already ran the migration must add the new
tenant_keycolumn and index (the template only serves new installs; derived from itsadd/indexlines):alter table(:agentix_conversations) do add(:tenant_key, :text) end create(index(:agentix_conversations, [:tenant_key]))Removed the reserved
persistencefield fromAgentix.Conversation.Config(documented as unused in 0.3.0; it was never consulted).Agentix.Conversation.Config.new/1now raises on a:persistencekey — persistence is configured at the application level only (config :agentix, :persistence), and ephemeral one-shots clean up viaAgentix.Persistence.delete_conversation/1.
0.3.0 - 2026-07-07
Added
- Per-conversation
api_keyonAgentix.Conversation.Config— a string or a 0-arity resolver fun (re-evaluated on every model call) passed to the provider as a per-request option.nilkeeps ReqLLM's own key resolution. The Ecto persistence adapter's settings sanitizer drops it, so key material never lands in a durable row; a conversation revived from persisted settings alone is key-less and the host must re-pass a freshconfig:. Agentix.Conversation.stop/1— public verb to release an idle conversation's agent process without ending the conversation (persisted events remain; the nextensure_started/2revives it). Replaces hosts reaching intoAgentix.Registry/Agentix.ConversationSupervisorinternals.{:turn_failed, turn_ref, reason}live event — provider/stream failures now get their own terminal event carrying the reason. Previously they broadcast the same{:cancelled, turn_ref}as a user-initiated cancel, so consumers could not distinguish "your key/provider failed" (e.g. an auth rejection) from "you cancelled".
Changed
A per-conversation
api_keyresolver fun is now evaluated inside the monitored streaming task: a raising resolver fails the turn ({:turn_failed, …}) instead of crashing the conversation's agent process.The ETS persistence owner now starts unconditionally (previously only when ETS was the app-configured adapter), so ETS is usable in tests and tools regardless of the app default.
Config.persistenceis documented as Reserved: the agent persists through the application-level adapter only; the field was never consulted and the old doc implied otherwise.Agents now trap exits:
Conversation.stop/1and supervisor shutdown drain the in-flight callback before terminating, so an agent mid-write to durable persistence is never killed between statements (previously a stop could sever a checked-out DB connection mid-query).
Breaking
- New required
Agentix.Persistencebehaviour callbackdelete_conversation/1(implemented by both bundled adapters): removes a conversation and everything under it (events, summaries, tool calls, audit rows) — ephemeral one-shot tasks call it after reading usage so throwaway conversations never accumulate. Third-party adapters must implement it. - Turns that fail on a provider/stream error no longer emit
{:cancelled, turn_ref}— subscribe to{:turn_failed, turn_ref, reason}for that terminal.{:cancelled, …}is now exclusively user-initiated.
0.2.0 - 2026-06-25
Added
- Provider retry & backoff — a per-conversation
retrypolicy onAgentix.Conversation.Config(%{max_attempts, base_ms, max_ms}orfalse). Transient pre-stream failures (HTTP 429, 5xx, connection drops) are retried with exponential backoff + jitter, honoring aretry-afterheader (capped at 60s); 4xx and unrecognized errors fail fast. A failure after the first streamed token is never retried. Classification/backoff live in the new publicAgentix.Retry. Each retry emits a[:agentix, :turn, :retry]telemetry event. - Structured output — make the model return typed data conforming to a schema.
Pass
schema:toAgentix.Conversation.send_message/4(one-shot) or setresponse_formaton the config (default);schema: falseopts out per turn. The parsed object is surfaced viaAgentix.object/1and theAgentix.Chat:last_objectassign, and persisted in the assistant message'smetadata["object"](no migration). A schema turn is terminal (the tool loop is skipped).Agentix.Provider.ReqLLMbranches toReqLLM.stream_object/4. Agentix.Test.error/2,Agentix.Test.transport_error/1, and a:objectoption onAgentix.Test.completion/2for driving retry and structured-output scenarios with the mock provider.- New guide: Reliability & structured output.
0.1.0 - 2026-06-25
First public release.
Added
- Agent runtime — one event-sourced
:gen_statemper conversation (Agentix.Conversation), with non-blocking streaming turns, in-process tool execution, and mid-turn cancellation. Conversations are started on demand viaAgentix.Conversation.ensure_started/2and survive process death. - Provider seam —
Agentix.Providerbehaviour with a streaming + cancel + finalize contract;Agentix.Provider.ReqLLMadapts ReqLLM's canonical typed model and provider abstraction. - Tools & HITL — an explicit executor model (
:server,:human,:client,:provider) with gated approval, durable suspension intoawaiting_input, and a public, socket-independentAgentix.resolve/4(LiveView, webhook, job, or timeout). Suspended turns revive from durable state. - Hook pipeline — per-turn pre/post hooks (
Agentix.Hook), parallel append-only injections with a token reserve, halt semantics, and a per-chunk stream-transformer seam. - Compaction — reducer pipeline (tool-result stubbing, sliding window, async
cumulative summarization) gated by a token budget, behind a pluggable
Agentix.Tokenizerbehaviour (default char/4 heuristic, no extra dependency). - Persistence — the
Agentix.Persistencebehaviour with two adapters sharing one conformance suite:Agentix.Persistence.ETS(default, no database) andAgentix.Persistence.Ecto(Postgres, kill-and-resume, optional Oban-backed suspension expiry). Both LiveView and Ecto/Oban are optional dependencies. - Headless LiveView layer —
Agentix.Chat(usemacro +on_mountprojection of the live-event plane onto assigns, streamed deltas to a JS hook), optional ownableAgentix.Components, andmix agentix.gen.components/mix agentix.gen.migrationgenerators. - Live-event union — a typed event plane broadcast over
Phoenix.PubSub(Agentix.Notifierbehaviour;PubSubdefault,Noneno-op) consumable by any transport. - Test story —
Agentix.Testassertions and a scriptableAgentix.Test.MockProviderfor driving conversations deterministically with no API key. - Modern tooling: Credo, Dialyxir, Styler, ExCoveralls, MixAudit, ExDoc, and a
mix checkquality gate.