Packages
Graph-based agent orchestration for building stateful, multi-step LLM workflows with nodes, edges, conditional routing, state reducers, human-in-the-loop interrupts, and checkpointing. Inspired by LangGraph, built on BEAM primitives.
Current section
Files
Jump to
Current section
Files
CHANGELOG.md
# Changelog
## 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