Current section
Files
Jump to
Current section
Files
CHANGELOG.md
# Changelog
## v0.13.0
Requires a new migration calling `LangEx.Migration.up()` (V4) for the Postgres
checkpointer. See "Deleting a thread no longer orphans its subgraph
checkpoints" below for what changes and what old data keeps.
### Parallel fan-out branches interrupt independently
- Two `%LangEx.Send{}` entries targeting the same node derived the same
interrupt ID, so a fan-out where every branch pauses for approval collapsed
into one pending interrupt — answering it resumed every branch with the same
value, and branches with identical payloads were silently dropped on resume.
Each Send now carries a stamped ID that scopes its interrupts
(`"worker#Ab3f:0"`), so branches are individually addressable and can be
answered separately or left pending. Plain node interrupt IDs are unchanged,
so IDs recorded by earlier versions still resolve.
### Deleting a thread no longer orphans its subgraph checkpoints
- Subgraphs used to checkpoint under a derived thread ID (`"t-1/planner"`),
which `delete_thread/1` could not see: closing a conversation left its
subgraph state — including full transcripts — in the database forever.
Subgraph checkpoints now share the parent's `thread_id` and are located by a
new first-class `checkpoint_ns` field, so one thread ID addresses an entire
run tree. `delete_thread/1` removes all of it.
- Requires migration V4. Subgraph checkpoints written before the upgrade keep
their old derived thread IDs; delete those threads directly if they must go.
- `get_state/2` and `get_state_history/2` accept `:checkpoint_ns` in `:config`
to inspect a subgraph's own history.
- Subgraph checkpoints record the enclosing namespace's checkpoint ID in
`metadata.parents`, so lineage stays reconstructable across graph boundaries.
### Per-task durability
- A crash midway through a parallel super-step discarded every node that had
already succeeded, because durability was whole-step. Each unit of work is
now journaled the moment it completes, and recovering the run replays those
results instead of re-running the nodes — a crash costs only the work that
was genuinely in flight. Applies to `durability: :sync` and `:async`.
- A fresh run records an `:input` checkpoint before any node executes, so
step 0 has something to recover against and the run's input is visible in
the thread history.
- Backends opt in via the optional `put_writes/4`, `load_writes/2`, and
`discard_writes/2` callbacks; Memory, Postgres, and Redis implement them.
### Checkpoint provenance and history pagination
- Checkpoints carry a `source`: `:input`, `:step`, `:update`, or `:fork`.
`update_state/3` distinguishes continuing a thread from branching it.
- `get_state_history/2` accepts `:before` (a checkpoint ID cursor, keyset
paginated in Postgres) and `:source`, so a long history can be paged and
branch points listed without loading everything.
### Thread lifecycle
- `LangEx.copy_thread/3` copies a thread's full history — every namespace — to
a new thread ID. The copy is independent, making it safe to branch a live
conversation or snapshot before a risky operation. Postgres copies rows
inside the database rather than through the application.
- `LangEx.Checkpointer.Postgres.prune/2` accepts a `:thread_id` in the config
to scope retention to one thread, and `:keep_latest` to retain the most
recent checkpoints regardless of age so trimming never leaves a live
conversation unresumable.
### Pluggable and encrypting checkpoint codecs
- `LangEx.Checkpoint.Codec` makes the checkpoint wire format a deployment
choice, selected per run (`serializer:` in the config) or application-wide.
The existing lossless tagged-JSON serializer remains the default.
- `LangEx.Checkpoint.Codec.Encrypted` encrypts state at rest with
AES-256-GCM, so database dumps, replicas, and backups hold ciphertext
rather than transcripts. Values are sealed individually with an allowlist
for keys that must stay queryable, and keyed rotation lets a new key be
introduced while existing threads still decrypt.
### Fan-in barriers can name the branches they wait for
- `defer: true` made a node wait for the whole super-step, which is wrong for a
graph where two independent merges run side by side: each was held up by
branches it had nothing to do with. `defer:` now also accepts a list of node
names, so a merge waits for its own sources and nothing else. Naming a node
that does not exist is rejected when the graph is built.
### Ephemeral state keys
- `Graph.new/2` accepts `ephemeral:` keys that live for exactly one super-step:
readable by the next step, never written to a checkpoint, and gone after
that. Retry signals, routing hints, and per-step scratch values no longer
have to be threaded through persisted state or manually cleared. Declaring an
ephemeral key that is not in the state schema is rejected at build time.
### Graph-level default node policies
- `Graph.compile/2` accepts `node_defaults:` so a policy — `retry`, `cache`,
`timeout`, `on_error` — can be stated once for every node instead of repeated
per node and silently forgotten on the next one added. A node's own options
override the default, and options that cannot combine (an `:on_error`
fallback on a cached node) are dropped rather than applied inconsistently.
An invalid default fails at compile time.
### Per-node cache keys
- A node's `cache:` option accepts `key:` — `(state -> term())` — so a cached
node keeps its entry when unrelated state changes. Previously any state
change invalidated the cache, which made caching useless for a node that
depends on one field of a large state.
### The model call is data a middleware can redirect
- `wrap_model_call` now receives a `%LangEx.Middleware.ModelRequest{}` carrying
every input to the call — messages, tools, model, system prompt, tool choice,
provider options — and `ModelRequest.override/2` derives a changed one. A
middleware can escalate a hard turn to a stronger model, force a tool, or
swap the prompt per user without the agent needing an option for each case.
Overriding an unknown or read-only field raises instead of being ignored.
### Run-scoped and tool-scoped middleware hooks
- `:before_agent` and `:after_agent` run once per run rather than once per
turn, for setup and teardown — loading a profile, persisting what was
learned — that must not repeat on every model call.
- `:before_tools` runs after the model requests tools and before they execute,
as its own graph node. Because it is a node, it may `interrupt/1`: resuming
re-runs only the review, not the model call being reviewed.
- `:wrap_tool_call` lets a middleware intercept each tool call; wrappers from
several middleware compose, and the agent's own `:wrap_tool_call` still runs
innermost.
### Human review of tool calls
- `LangEx.Middleware.ToolApproval` pauses on the tool calls that need a human
and nowhere else. A reviewer can approve, approve with corrected arguments,
refuse with a reason, or answer the call themselves. Unguarded calls in the
same batch run untouched, and refusing one call no longer blocks the calls
beside it.
### Budgets and tool retries
- `LangEx.Middleware.CallBudget` caps model calls and token spend per run, so
an agent that would loop forever stops and says why. Tool calls left hanging
by the stop are answered, keeping the conversation valid to continue.
- `LangEx.Middleware.ToolRetry` retries a transiently failing tool inside the
call, with fixed or growing backoff, so a momentary outage does not cost a
turn or teach the model that the tool is broken.
### Tool results record whether they failed
- `%LangEx.Message.Tool{}` carries a `:status` (`:ok` / `:error`), set when a
tool raises. Failures are now recognisable without parsing prose, and the
Anthropic adapter sends `is_error` so the model is told the call failed
rather than left to infer it.
### Tool calls already answered are not re-run
- The tools step and `tools_condition/2` now consider only tool calls still
awaiting a result, and look back past trailing non-tool messages to find
them. This is what lets a reviewer answer a call in advance while the rest of
the batch runs.
### Examples
- Five runnable scripts cover the new surface, all offline: per-branch fan-out
approvals, execution policy (scoped barriers, ephemeral keys, node defaults,
cache keys), thread lifecycle (provenance, pagination, copying, namespaced
delete), the agent middleware stack (approval with argument correction,
retry, budget, request overrides, run hooks), and encrypted checkpoints with
key rotation. `06_crash_recovery.exs` now also shows per-task durability.
- The example checkpointer template keys storage on `{thread_id,
checkpoint_ns}`. A custom backend that keys on `thread_id` alone will hand a
parent graph its subgraph's checkpoints; the template shows the correct
shape, and `delete_thread/1` closing out a whole run tree depends on it.
- `IncidentResponder` gained the session lifecycle operations a durable,
Postgres-backed deployment needs: an edit audit trail, branching, close-out,
and a retention job.
## v0.12.1
### Resilient — retries keep the caller's provider opts
- `LangEx.LLM.Resilient` rebuilt only its own retry config when retrying, so
every retried call lost `:model`, `:tools`, `:max_tokens`, and streaming
callbacks and fell back to the provider's default model with no tools. A
single transient 429/5xx silently downgraded the call; once the default
model was retired it became a hard 404. Retries now reuse the original opts.
### Anthropic — default model updated
- The Anthropic default model is now `claude-sonnet-5`;
`claude-sonnet-4-20250514` was retired by Anthropic and returns
404 `not_found_error`.
## v0.12.0
### Subgraphs — replace semantics for shared keys
- A subgraph's final state now REPLACES the parent's values for every key it
carries instead of re-applying reducers. A subgraph inherits the parent's
state as input, so reducer re-application double-applied everything
inherited — an append-reducer `:messages` key gained a full duplicate of
the inherited history per subgraph invocation.
### Prebuilt agent — new middleware
- `LangEx.Middleware.Subagent` — a `task` tool that spawns ephemeral,
context-isolated child agents (own prompt/tools/model); only the child's
final report and token usage return to the parent.
- `LangEx.Middleware.Filesystem` — a state-backed virtual workspace
(`ls`/`read_file`/`write_file`/`edit_file`/`glob`/`grep`) so the agent can
offload large artifacts out of chat history; files persist via
checkpointing and survive summarization.
- `LangEx.Middleware.ModelFallback` — when the primary model call fails,
retries the same request against an ordered list of fallback models.
### Evaluation
- `LangEx.Eval.Trajectory` — extract tool-call trajectories from message
histories or checkpoints and match them against expected trajectories
(`:strict` / `:unordered` / `:subset`, partial-args matching).
- `LangEx.Eval.Judge` — LLM-as-judge scoring of trajectories/transcripts via
`ChatModel.structured/2`.
### Checkpointer — content-addressed blob dedup (Postgres)
- State values above `:blob_threshold` (default 16KB) are stored once per
`(thread_id, content_hash)` in `lang_ex_checkpoint_blobs` and referenced
from checkpoint rows — large values that never change between super-steps
(e.g. a tool catalog) are written once per thread instead of every step.
Migration V3. `blob_threshold: :infinity` restores inline storage.
### LLM — auditable thinking
- `%Message.AI{}` gains a `:thinking` field; the Anthropic adapter sets it
to the reply's extended-thinking text (never echoed back to the API).
Previously the text lived only in `usage.thinking`, where each call's
merge overwrote it.
## v0.11.3
### Run budgets — cached tokens count
- `token_budget` accounting now includes `cache_creation_input_tokens` and
`cache_read_input_tokens`. With provider prompt caching enabled,
`:input_tokens` is only the uncached tail of each request, so a budget
ignoring cache tokens effectively never triggered.
### Middleware.ContextEditing — batched, cache-aware editing
- New `:trigger_at_chars` option (default `100_000`): the conversation is
left untouched until its total content size crosses the trigger, then every
eligible tool result is cleared in one pass. Editing a little every turn
invalidated the provider prompt-cache prefix each round (a cache write
costs ~12x a cache read on Anthropic); batching makes invalidations rare
and the reclaim large. Set `trigger_at_chars: 0` for the previous
every-turn behaviour.
## v0.11.2
### Tool.Node — deep-merge parallel accumulator updates
- When several tool calls run in parallel in one round and each returns a
`%LangEx.Command{}` writing the **same map-valued state key** (e.g. a shared
cache), `LangEx.Tool.Node` now **deep-merges** those maps into a union
instead of keeping only the earliest and logging a conflict. Plain maps are
accumulators, so every call's entries survive. Diverging **scalar** keys
(e.g. two handoffs both setting `:active_agent`) keep the earliest-wins +
warning behavior, and structs are never merged.
## v0.11.1
### Checkpoint — resilient atom decoding
- `LangEx.Checkpoint.Serializer.decode/1` no longer crashes when a checkpointed
**value atom** is not loaded in the current VM. It prefers an existing atom
and falls back to creating one, so a thread resumes correctly in a fresh VM
or after a deploy (previously `binary_to_existing_atom` raised
`ArgumentError`). Module names and struct field keys stay strict — they must
already exist to rebuild the value, which still bounds atom-table growth from
structural names.
## v0.11.0
### Middleware — composable agent hooks
- `LangEx.Middleware` — a value-based hook layer for `LangEx.Prebuilt.agent/1`.
Pass `middleware: [...]` to wrap the model call with `before_model` /
`after_model` / `wrap_model_call` hooks, contribute tools, and extend the
agent's state schema — without changing the agent's shape. An `after_model`
hook can steer routing (loop, go to tools, or end) via the reserved
`LangEx.Middleware.jump_key/0`.
### ChatModel — state-derived opts
- `LangEx.LLM.ChatModel.node/1` resolves any option given as
`{:from_state, fn state -> value end}` from the node's state on each call —
e.g. an `:on_thinking` callback that needs per-run context (channel/thread)
not known when the graph was built.
### Prebuilt agent — state-derived tools
- `LangEx.Prebuilt.agent/1` accepts `tools: fn state -> [%LangEx.Tool{}] end`
in addition to a static list. The resolver runs each turn, so tools
discovered at runtime can be kept as serializable specs in state (and
materialized on demand) instead of storing executable closures in the
checkpoint. Middleware-contributed tools are appended to the resolved set.
### Built-in middleware
- `LangEx.Middleware.Summarization` — replaces older history with an
LLM-written summary once the message list passes `:max_bytes`, persisting
the summary in place (via `Message.remove_all/0`) so later turns build on it
rather than resummarising.
- `LangEx.Middleware.ContextEditing` — clears the *contents* of large, stale
tool results while keeping the message skeleton. No LLM call; idempotent.
- `LangEx.Middleware.TodoList` — a `write_todos` planning tool plus a `:todos`
state key, keeping long multi-step loops anchored to a plan.
- `LangEx.Middleware.ToolSelector` — a cheap LLM call that narrows a large
tool set to the relevant subset before the main model call (`:max_tools`,
`:always_include`); a no-op below the threshold.
- `LangEx.Middleware.Rubric` — an exit gate on the tool loop: scores the
final answer against a `:rubric` and bounces it back with feedback for
another pass, up to `:max_attempts`.
### Messages — deletion in the reducer
- `LangEx.Message.remove/1` and `LangEx.Message.remove_all/0` emit
`%Message.RemoveMessage{}` instructions that `add_messages/2` applies in
sequence, so a reducer update can prune or replace history, not only append.
### LLM — structured output & completions
- `LangEx.LLM.ChatModel.structured/2` now retries on schema-validation
failures with the error fed back as feedback (`:max_retries`, default `2`),
and supports `strategy: :provider` to force the response via the provider's
native `tool_choice`.
- `LangEx.LLM.ChatModel.complete/2` — one-shot text completion outside a graph
returning the assistant message with token usage.
- `LangEx.LLM.Anthropic`, `LangEx.LLM.OpenAI`, and `LangEx.LLM.Gemini` accept
`:tool_choice` (`:auto` / `:required` / `{:tool, name}`), each translated to
the provider's native forcing mechanism.
### Anthropic — conversation prompt caching
- `LangEx.LLM.Anthropic` marks a rolling `cache_control` breakpoint on the
last conversation message (in addition to system + last tool), so a long
agent loop reuses its cached message prefix each turn. Disable with
`cache_conversation: false`.
### Context compaction
- `LangEx.ContextCompaction.compact_if_needed/2` accepts a `:summarizer`
(`fn dropped_messages -> String.t()`) to describe dropped rounds with a real
summary instead of the mechanical tool-name notice.
- Byte accounting now counts AI `tool_calls` args (and tolerates `nil`
content), so a tool-argument-heavy history triggers compaction correctly
instead of under-reporting its size.
## v0.10.0
### Embeddings
- `LangEx.Embedding.Hashing.embed/2` — a dependency-free text embedder
(hashing trick: tokens hashed into fixed-length term-frequency buckets).
Makes `LangEx.Store` semantic search usable out of the box without a
neural embedding provider:
Graph.compile(builder,
store: {LangEx.Store.ETS, index: [embed: &LangEx.Embedding.Hashing.embed/1]}
)
It captures lexical overlap, not meaning; supply a neural embedder when
semantic similarity matters.
## v0.9.0
### Engine — run budgets & managed values
- New managed value `:is_last_step` injected into node state (LangGraph's
`IsLastStep`): `true` on the final allowed super-step so a node can
produce a final answer instead of the engine raising at the recursion
limit
- `:deadline_ms` invoke option — a wall-clock budget for the whole run.
Exposes a `:remaining_ms` managed value and flips `:is_last_step` once
the deadline passes (graceful conclusion, not a raise)
- `:token_budget` invoke option — a cumulative token budget. Exposes a
`:remaining_tokens` managed value and flips `:is_last_step` when spent;
usage is read from the `:llm_usage` state key (the `ChatModel.merge_usage/2`
reducer convention)
- All managed values are stripped before checkpointing and left untouched
when the user's schema claims the key
### LLM — structured output
- `LangEx.LLM.ChatModel.structured/2` — one-shot, provider-agnostic
structured extraction outside a graph node. Forces a synthetic `respond`
tool, decodes the result (falling back to JSON content), and validates
the schema's top-level `required` keys. Returns `{:ok, map}` or
`{:error, :no_structured_output | {:missing_required, keys} | term}`
- `LangEx.LLM.ChatModel.validate_structured/2` — reusable required-key
validation for decoded structured results
### Prebuilt — reflection
- `LangEx.Prebuilt.reflect/1` (and `LangEx.Prebuilt.Reflect.create/1`) — a
generate → critique → revise loop. A critic evaluates each draft via
`ChatModel.structured/2` (validated `approved` boolean) and the graph
loops back to revise until approval or `:max_iterations`
### Store — semantic search
- `LangEx.Store.ETS` supports similarity search via a pluggable embedder
(`store: {LangEx.Store.ETS, index: [embed: &embed/1]}`). `put/4` embeds
each value; `search/3` with a `:query` returns entries ranked by cosine
similarity. Without an embedder, `:query` falls back to prefix ordering
## v0.8.0
### Engine
- Arity-2 node functions now receive `nil` when a run sets no `:context`
(previously they crashed on a context-less invoke); arity dispatch, not
context presence, decides the call shape
### LLM
- `LangEx.LLM.ChatModel.structured_node/1` — provider-agnostic structured
output. The model is given a synthetic `respond` tool whose parameters
are a JSON-schema; the decoded result is written to an `:into` state key
and a clean JSON assistant message is appended. Works with any
tool-calling provider, no per-provider configuration
### Multi-agent
- Tool functions may return a `%LangEx.Command{}` — its `:update` is
merged into graph state and its `:goto` joins the node's routing.
`LangEx.Tool.Node` guarantees a `%Message.Tool{}` reply for every call
(synthesizing one when the command omits it) and keeps returning a
plain `%{messages_key => [...]}` update when no tool returns a command
(backwards compatible)
- `LangEx.Prebuilt.Handoff.tool/2` builds a `transfer_to_<agent>` tool
that moves the conversation to another agent; with
`task_description: true` the tool also accepts a task brief passed to
the target agent
- `Swarm.create/1` and `Supervisor.create/1` validate inputs at build
time (non-empty `:agents`, unique names, valid `:default_active_agent` /
`:supervisor_name`)
- `LangEx.Prebuilt.Swarm.create/1` — peer-to-peer team where agents hand
off to one another; the active agent is tracked in `:active_agent` and
persisted across invocations via the checkpointer
- `LangEx.Prebuilt.Supervisor.create/1` — hub-and-spoke team where a
supervisor delegates to workers (with a task brief) and workers report
back. A worker runs on a task-focused view (handoff plumbing stripped)
and its output is
reported back as a user-role message attributed to that worker
(`"Response from the <name> agent: ..."`), so the supervisor can tell
specialist findings apart from its own reasoning and the conversation
stays valid for providers that reject a trailing assistant turn.
Supports `:output_mode` (`:full_history` | `:last_message`)
- `LangEx.Prebuilt.Member` — the routable team-member agent shared by
both topologies; supports a string or `(state -> string)` callable
`:system_prompt`, forwards the team's runtime `:context` into each turn,
and contributes each turn's token usage back under `:llm_usage`
(teams accumulate usage across turns)
- `:handoff_tool_prefix` on `Swarm.create/1` and `Supervisor.create/1`
(and `:prefix` on `Handoff.tool/2`) customizes generated handoff tool
names
- `Member` accepts `:pre_model_hook` (`messages -> messages`) and
`:post_model_hook` (`update -> update`) for message trimming, extra
instructions, or guardrails around the LLM call
- `Swarm.create/1` accepts `:add_agent_name` — each agent's replies are
prefixed with `"[<name>] "` so peers can attribute who said what
- Conflicting state writes from parallel tool calls in one batch keep the
earliest value and log a warning (a single super-step cannot honour two
divergent handoffs at once)
## v0.7.0
### Release
- Fix package-scoped Hex publishing pipeline and cut the first published
release (no library API changes since v0.6.0)
## v0.6.0
### Engine hardening
- Node exceptions surface as `{:error, %LangEx.NodeError{node: ..., reason: ...}}`
instead of raising out of `invoke/3`/`stream/3`; the original exception and
failing node are preserved (**breaking**: callers matching on raises must
match on the error tuple)
- Checkpoint format v2: `next_nodes` and pending-interrupt entries persist
full work items, so `%LangEx.Send{}` payloads survive crash-continue and
interrupt-resume (v1 checkpoints still load)
- Completed parallel siblings keep their routing across an interrupt: their
resolved next targets (and any deferred fan-in backlog) are recorded in the
interrupt checkpoint and scheduled on resume
- A Send target that interrupts pauses with the shared graph state (its
payload no longer overwrites the checkpointed state) and resumes with its
payload intact
- `:node_timeout` applies to single-node super-steps (previously parallel
super-steps only); timeouts raise `LangEx.NodeTimeoutError` per attempt
- `durability: :exit` writes a final checkpoint on completion and persists
the failed super-step on error, so `get_state/2` stays truthful and an
empty re-invoke can retry the failure
- Parallel super-steps emit `node_start`/`node_end` stream events (previously
single-node super-steps only)
- Dynamic resume answers survive static breakpoints: `resume_values` persist
through breakpoint checkpoints, and the resumed super-step bypasses
breakpoints that already fired
### Validation
- `add_node/4` rejects duplicate and reserved (`:__start__`/`:__end__`)
names, and validates option values; `:cache` cannot combine with `:on_error`
- `add_edge/3` rejects edges from `:__end__`; `add_conditional_edges/4`
rejects a second routing function for the same source
- `compile/2` validates `interrupt_before`/`interrupt_after` node names
- Routing to an undefined node (Command goto / Send) raises a descriptive
`ArgumentError` naming the known nodes
### Persistence
- New `LangEx.Checkpointer.Memory` — built-in ETS backend for development
and tests
- Postgres checkpointer stores `next_nodes`/`pending_interrupts` as proper
jsonb payloads (previously unusable due to an array/jsonb type mismatch)
and breaks `created_at` ordering ties by step and checkpoint id
- Subgraphs with their own checkpointer resume interrupts from their
namespaced checkpoint instead of re-running from `:__start__`
- `LangEx.Interrupt.interrupt/1` raises a clear error when called outside a
graph node process (e.g. from tool functions)
### Streaming
- Stream modes: `modes: [:updates, :values, :messages, :custom]` on `LangEx.stream/3`
- Token deltas from streaming LLM adapters surface as `{:message_delta, ...}` events
(`:on_token` callback on the Anthropic adapter)
- `LangEx.Graph.Stream.emit/1` to publish custom events from inside nodes
- `stream/3` accepts `%Command{resume: ...}` and crash-continue (`%{}`) inputs
- Interrupts are emitted as `{:interrupt, payload}` stream events
### Execution policies
- Per-node options on `Graph.add_node/4`: `retry:` (capped exponential
backoff with jitter and `retryable?` — see `LangEx.Graph.RetryPolicy`;
`backoff_ms` accepted as a legacy alias for `initial_interval_ms`),
`cache:` (ETS memoization with TTL), `defer:` (fan-in barrier),
`timeout:` (per-attempt budget, retryable), `on_error:` (fallback handler
after retries are exhausted; its return value becomes the node result)
- Node cache verifies the stored input on lookup (hash collisions miss
instead of serving wrong results), deletes expired entries on read, and is
size-bounded via the `:node_cache_max_entries` application env
- `ChatModel.node(resilient: ...)` routes calls through `LLM.Resilient`
- `:durability` invoke option: `:sync` | `:async` | `:exit` checkpoint writes
### Prebuilts
- `LangEx.Prebuilt.agent/1` — one-call tool-loop agent with system prompt,
usage accounting, and context compaction wired in
### Long-term memory
- `LangEx.Store` behaviour with ETS and Postgres backends; attach with
`Graph.compile(store: ...)`; reachable in nodes and tools via
`LangEx.Store.get/put/delete/search`
- Migration V2 (`lang_ex_store` table + checkpoint `version` column)
### Checkpointer operations
- `delete_thread/1` on the behaviour, both backends, and the facade
(`LangEx.delete_thread/2`)
- `Checkpointer.Postgres.prune/2` retention window (`older_than:`)
- Checkpoint format `version` field persisted with every checkpoint
- Redis backend surfaces errors instead of swallowing them into `[]`/`:none`
### Graphs
- Compile-time validation of conditional-edge mapping targets; warning for
unreachable nodes (`warn_unreachable: false` to silence)
- `Graph.to_mermaid/1` flowchart export
- `%Command{goto: {:parent, target}}` routes the parent graph from inside a
subgraph (bubbles one level per graph boundary)
- A schema-declared `:remaining_steps` key is no longer overwritten by the
managed value
## v0.5.0
- Durable execution: crashed runs resume from checkpointed pending nodes
- Lossless checkpoint serialization (`LangEx.Checkpoint.Serializer`)
- State APIs: `get_state/2`, `get_state_history/2`, `update_state/3`,
`parent_id` lineage, load by `checkpoint_id`
- Interrupts v2: stable IDs, multiple interrupts per node, id-addressed
resume maps, static breakpoints (`interrupt_before` / `interrupt_after`),
parallel-step interrupt safety
- Subgraph propagation: interrupts, errors, context, stream events, and
namespaced checkpoint config flow through compiled-graph nodes
- Run-tree telemetry (`run_id` / `parent_run_id`), named graphs, and an
optional OpenTelemetry bridge
- Token usage accounting in `ChatModel` (`chat_with_usage`, `merge_usage/2`)
- Bounded concurrency: `max_concurrency` / `node_timeout` invoke options and
`Tool.Node` `max_concurrency` / `timeout`
- Streaming rework: supervised runner, no inactivity halt, crash surfacing
- Send fan-out results merge through reducers and follow target edges
## v0.1.0
Initial release.
- StateGraph builder with nodes, edges, conditional routing, and `add_sequence`
- Pregel super-step execution engine with parallel node execution via `Task.Supervisor`
- State reducers (per-key merge functions)
- Command routing (combined state update + control flow)
- Checkpointing (Redis via Redix, PostgreSQL via Ecto)
- Oban-style versioned Postgres migrations (`LangEx.Migration`)
- Interrupts / human-in-the-loop (`LangEx.Interrupt`)
- Streaming (`LangEx.Stream` via `Stream.resource`)
- Runtime context injection (arity-2 node functions)
- Subgraph support (compiled graphs as nodes)
- Send fan-out for dynamic map-reduce patterns
- Managed values (`remaining_steps`)
- ChatModels registry with model-string auto-resolution
- Built-in LLM adapters: OpenAI, Anthropic
- MessagesState convenience schema
- Message types: Human, AI, System, Tool