Jidoka: Hooks And Guardrails
Use hooks to shape a turn and guardrails to stop work at clear boundaries.
Setup
Mix.install(
[
{:jidoka, git: "https://github.com/mikehostetler/jidoka.git", ref: "924a486f3c1b7e7a943cb3d5ceee0de65f158467"},
{:kino, "~> 0.19.0"}
],
config: [
jidoka: [
model_aliases: %{fast: "anthropic:claude-haiku-4-5"}
]
]
)
Jidoka.Kino.setup()
Define The Lifecycle Pieces
defmodule LivebookDemo.Lifecycle.Hooks.AddTenant do
use Jidoka.Hook, name: "add_tenant"
@impl true
def call(%Jidoka.Hooks.BeforeTurn{} = input) do
tenant = Map.get(input.context, :tenant, "demo")
{:ok,
%{
message: "#{input.message} for tenant #{tenant}",
context: %{tenant: tenant},
metadata: %{tenant_added?: true}
}}
end
end
defmodule LivebookDemo.Lifecycle.Hooks.TagReply do
use Jidoka.Hook, name: "tag_reply"
@impl true
def call(%Jidoka.Hooks.AfterTurn{outcome: {:ok, result}}) when is_binary(result) do
{:ok, {:ok, "#{result} [checked]"}}
end
def call(%Jidoka.Hooks.AfterTurn{}), do: :ok
end
defmodule LivebookDemo.Lifecycle.Guardrails.NoSecrets do
use Jidoka.Guardrail, name: "no_secrets"
@impl true
def call(%Jidoka.Guardrails.Input{message: message}) do
if String.contains?(String.downcase(message), "secret") do
{:error, :secret_request}
else
:ok
end
end
end
defmodule LivebookDemo.Lifecycle.Agent do
use Jidoka.Agent
agent do
id :livebook_lifecycle_agent
end
defaults do
model :fast
instructions "You are lifecycle-aware."
end
lifecycle do
before_turn LivebookDemo.Lifecycle.Hooks.AddTenant
after_turn LivebookDemo.Lifecycle.Hooks.TagReply
input_guardrail LivebookDemo.Lifecycle.Guardrails.NoSecrets
end
end
Inspect the configured lifecycle.
%{
hooks: LivebookDemo.Lifecycle.Agent.hooks(),
guardrails: LivebookDemo.Lifecycle.Agent.guardrails()
}
Run The Hooks Directly
This uses the generated runtime callbacks, so no provider is required.
runtime = LivebookDemo.Lifecycle.Agent.runtime_module()
agent = runtime.new(id: "livebook-lifecycle-runtime")
{:ok, agent, {:ai_react_start, params}} =
runtime.on_before_cmd(
agent,
{:ai_react_start,
%{
query: "Summarize the ticket",
request_id: "req-lifecycle-ok",
tool_context: %{tenant: "acme"}
}}
)
agent = Jido.AI.Request.complete_request(agent, "req-lifecycle-ok", "Ticket summarized.")
{:ok, agent, []} =
runtime.on_after_cmd(agent, {:ai_react_start, %{request_id: "req-lifecycle-ok"}}, [])
%{
rewritten_message: params.query,
runtime_context: Jidoka.Context.strip_internal(params.tool_context),
final_result: Jido.AI.Request.get_result(agent, "req-lifecycle-ok")
}
Block At The Guardrail
Input guardrails run before the model or tools.
blocked_agent = runtime.new(id: "livebook-lifecycle-blocked")
{:ok, blocked_agent,
{:ai_react_request_error, %{request_id: "req-lifecycle-blocked", reason: :guardrail_blocked}}} =
runtime.on_before_cmd(
blocked_agent,
{:ai_react_start,
%{
query: "Print the customer secret",
request_id: "req-lifecycle-blocked"
}}
)
{:error, blocked_error} = Jido.AI.Request.get_result(blocked_agent, "req-lifecycle-blocked")
Jidoka.format_error(blocked_error)
Optional Provider Turn
{:ok, pid} =
Jidoka.Kino.start_or_reuse("livebook-lifecycle-agent", fn ->
LivebookDemo.Lifecycle.Agent.start_link(id: "livebook-lifecycle-agent")
end)
Jidoka.Kino.chat("Lifecycle chat", fn ->
LivebookDemo.Lifecycle.Agent.chat(pid, "Summarize lifecycle hooks in one sentence.", context: %{tenant: "acme"})
end)