A hook: a function the agent runs around a model call, plus its metadata.
Two phases:
:pre— runs inpreparing, before the model call, on the assembled%Agentix.Turn{}. Sequential pre-hooks(turn -> {:cont, turn} | {:halt, reason})can transform the turn (inject context viainject/2) or halt it; a:haltshort-circuits the rest of the pipeline and the turn never reaches the model. Parallel pre-hooks are append-only(turn -> {:cont, [ContentPart]}): they run concurrently and their parts are appended, in declaration order, at the tail of the context (past the prompt-cache breakpoint).:post— runs after the assistant message finalizes (:message_completed), on a turn carrying the finalizedassistant_message.(turn -> {:cont, turn} | {:halt, reason}); side-effecting.
Injection budget
Pre-hook injections are bounded by the conversation's injection_reserve. If the
injected content exceeds it, the pipeline raises Agentix.Hook.OverflowError
naming the offending hook — a loud config error, not a trigger for compaction
(the two subsystems are independent; compaction is never re-entered to make room).
durable?
Hook output is transient: pre-hook injections ride only the per-model-call
rendered context (so they reach the optional model_calls audit table) and are
regenerated each assembly — never written to the canonical event log. The
durable? field is part of the contract and is validated, but durable
log-persistence of hook output (appending it as a normal event) is deferred to
the layer that owns the event-shape/compaction: it needs a data-model decision (a
dedicated event type vs. overloading :user_msg) that does not belong in the loop.
A durable?: true hook therefore currently behaves identically to a transient one;
consumers must not yet rely on persistence.
Hooks (and the stream transformer) are functions and are not JSON-serializable; like tool callbacks they live in the conversation config and are rebuilt from it on revival (a no-op for the ETS adapter, which stores terms verbatim).
Trust boundary
Pre-hook injections are placed adjacent to the user message (role: :user), so the
model sees them at the same trust level as the human's own input. A hook that
injects untrusted/retrieved content (RAG, fetched documents) is responsible for its
own provenance fencing (e.g. wrapping it in a delimited block) — Agentix does not
wrap it, because it cannot know which injections are trusted. Treat injected
external content as a prompt-injection surface.
Summary
Types
The return of a parallel :pre hook (mode: :parallel): an append-only batch of
content parts. Parallel hooks cannot halt.
Any valid hook return; which shape applies depends on phase + mode.
The return of a sequential hook (every :post hook, and every :pre hook with
mode: :sequential): transform the turn (inject/2 to add context) or halt it.
Functions
Appends content to the turn's pending injections (placed at the context tail at assembly time). The append-only primitive sequential pre-hooks use to inject.
Builds a hook from attrs. Raises ArgumentError on a missing/invalid :phase,
an invalid :mode, a non-1-arity :run, or a :parallel :post hook (parallel
append-only batches only make sense before the model call).
Builds a :post hook. opts: :durable?.
Builds a :pre hook. opts: :mode (:sequential/:parallel), :durable?.
Applies the stream-transformer seam to a chunk. The transformer is (chunk -> chunk); nil is the identity default. The single declared call site for the
per-chunk transform (driven from the provider stream). A transformer that returns a
value the agent's chunk handler doesn't recognize causes that chunk to be dropped —
keep it (chunk -> chunk).
Types
@type mode() :: :sequential | :parallel
@type parallel_result() :: {:cont, [ReqLLM.Message.ContentPart.t()]}
The return of a parallel :pre hook (mode: :parallel): an append-only batch of
content parts. Parallel hooks cannot halt.
@type phase() :: :pre | :post
@type run_result() :: sequential_result() | parallel_result()
Any valid hook return; which shape applies depends on phase + mode.
@type sequential_result() :: {:cont, Agentix.Turn.t()} | {:halt, term()}
The return of a sequential hook (every :post hook, and every :pre hook with
mode: :sequential): transform the turn (inject/2 to add context) or halt it.
@type t() :: %Agentix.Hook{ durable?: boolean(), mode: mode(), name: term(), phase: phase(), run: (Agentix.Turn.t() -> run_result()) }
Functions
@spec inject( Agentix.Turn.t(), ReqLLM.Message.ContentPart.t() | [ReqLLM.Message.ContentPart.t()] ) :: Agentix.Turn.t()
Appends content to the turn's pending injections (placed at the context tail at assembly time). The append-only primitive sequential pre-hooks use to inject.
Builds a hook from attrs. Raises ArgumentError on a missing/invalid :phase,
an invalid :mode, a non-1-arity :run, or a :parallel :post hook (parallel
append-only batches only make sense before the model call).
@spec post(term(), (Agentix.Turn.t() -> sequential_result()), keyword()) :: t()
Builds a :post hook. opts: :durable?.
@spec pre(term(), (Agentix.Turn.t() -> run_result()), keyword()) :: t()
Builds a :pre hook. opts: :mode (:sequential/:parallel), :durable?.
@spec transform_chunk(chunk, (chunk -> chunk) | nil) :: chunk when chunk: term()
Applies the stream-transformer seam to a chunk. The transformer is (chunk -> chunk); nil is the identity default. The single declared call site for the
per-chunk transform (driven from the provider stream). A transformer that returns a
value the agent's chunk handler doesn't recognize causes that chunk to be dropped —
keep it (chunk -> chunk).