Eai. Hub
(eai v1.0.2)
Copy Markdown
Central dispatch bus for all tool calls in eai.
Every tool invocation flows through Eai.Hub.run/3 instead of a bare
apply(mod, fun, args). This single choke-point enables:
- Pre-hooks: intercept, block, or modify arguments before execution
- Post-hooks: observe, block, or transform results after execution
- Telemetry: uniform
:eai, :tool, :hub_pre/:eai, :tool, :hub_postevents
Why this design (decision #1 + #2)?
Centralising dispatch here means the 14 tool modules are untouched. Hooks run in the same process as the caller (no PubSub, no GenServer round-trip) so they can synchronously veto a call before it executes.
Initial code is complete (decision #9)
This module ships fully wired to Eai.Hub.Pipeline from the start.
Pipeline.pre_hooks/3 and Pipeline.post_hooks/4 are no-ops when
:eai_hooks is empty (the 500ms window before Application.start fires
reload!). No "bare apply" fallback path is needed.
Graph
<<{Eai.Hub.Pipeline, required_by, Eai.Hub}. <<{Eai.Hub, required_by, Eai.PTY}. <<{Eai.Hub, required_by, Eai.PTY.Session}. <<{Eai.Hub, required_by, Eai.LLM.Direct}.
reload!
Delegates to Eai.Hub.Reloader.reload!/0. Exposed here for ergonomics:
Eai.Hub.reload!()
:persistent_term.erase(:eai_hooks); Eai.Hub.reload!()
Summary
Functions
Reload all runtime-extensible config without restarting the VM.
Dispatch a tool call through the pre→execute→post hook pipeline.
Post-only dispatch for terminal lifecycle events (e.g. PTY.Session.terminate/2).
Functions
@spec reload!() :: :ok | {:error, term()}
Reload all runtime-extensible config without restarting the VM.
Reloads three registries in one call:
- Hooks: re-compiles
config/hooks/*.exs→:persistent_term.put(:eai_hooks, ...) - Models: re-reads
config/models/*.exs→:persistent_term.put(:eai_models, ...) - Cards: re-reads
config/chara_cards/*.json→:persistent_term.put(:eai_chara_cards, ...)
All three take effect immediately — the next LLM call picks up new models/cards, the next tool invocation picks up new hooks.
IEx usage
iex> Eai.Hub.reload!()
:ok
# Force full re-scan
iex> :persistent_term.erase(:eai_hooks); Eai.Hub.reload!()
:okMix usage
mix run -e "Eai.Hub.reload!()"
Dispatch a tool call through the pre→execute→post hook pipeline.
Flow
Pipeline.pre_hooks/3— runs all interested pre-hooks in priority order.:ok→ proceed with original args{:modify, new_args}→ proceed with modified args{:block, reason}→ abort; return{:block, reason}to caller
apply(mod, fun, effective_args)— execute the actual tool.Pipeline.post_hooks/4— runs all interested post-hooks in priority order.{:ok, result}→ return result{:block, reason}→ suppress result; return{:block, reason}
Telemetry events
[:eai, :tool, :hub_pre]— fired before pre-hooks, carries{mod, fun, args}[:eai, :tool, :hub_post]— fired after post-hooks, carries{mod, fun, result, duration_ms}[:eai, :tool, :hub_blocked]— fired when any hook blocks (pre or post)
Post-only dispatch for terminal lifecycle events (e.g. PTY.Session.terminate/2).
Skips pre-hooks and execution; runs only the post-hook pipeline with
{:terminated, reason} as the result. Hook authors distinguish terminal
events by pattern-matching on the tagged tuple:
def verdict(:post, _tool, _payload, {:terminated, reason}), do: cleanup(reason)
def verdict(:post, _tool, _payload, result), do: normal(result)Semantics
:blockaborts the remaining hook chain only — it does not prevent OTP shutdown (terminate/2return is ignored by OTP regardless).- Hooks must not
GenServer.callthe dying process — deadlock. UseCache/PubSub/ETSfor side effects.
Usage
@impl true
def terminate(reason, state) do
Eai.Hub.run_post_only(__MODULE__, :terminate, [reason, state])
end