Jidoka: Kitchen Sink
Build one advanced support router that composes the main Jidoka primitives: context, dynamic instructions, characters, tools, plugins, Ash resources, MCP, web tools, skills, hooks, guardrails, memory, compaction, workflows, subagents, handoffs, imports, sessions, AgentView, eval checks, and optional provider-backed chat.
This is a capstone notebook. Start with the focused notebooks first if you are new to Jidoka.
The flow is intentionally incremental: each chapter builds one independent piece of the support agent, shows the piece in isolation, and includes a small test or inspection step. After the individual capabilities are clear, the final chapters pull them together into one agent and run an end-to-end provider-backed turn.
Setup
local_checkout? = File.exists?(Path.expand("../mix.exs", __DIR__))
jidoka_dep =
if local_checkout? do
{:jidoka, path: Path.expand("..", __DIR__)}
else
{:jidoka, git: "https://github.com/agentjido/jidoka.git", branch: "main"}
end
Mix.install(
[
jidoka_dep,
{:kino, "~> 0.19.0"}
],
config: [
jidoka: [
model_aliases: %{fast: "anthropic:claude-haiku-4-5"}
]
],
# Hosted LiveBooks read this notebook from main, but Mix.install caches git
# dependencies. Force only hosted runs so the notebook and Jidoka stay in sync.
force: !local_checkout?
)
unless Code.ensure_loaded?(Jidoka.Kino) and function_exported?(Jidoka.Kino, :setup, 0) do
raise """
This runtime has an older Jidoka.Kino module loaded.
Click "Reconnect and setup" so Livebook starts a fresh runtime.
"""
end
Jidoka.Kino.setup()
find_tool = fn agent_module, name ->
Enum.find(agent_module.tools(), fn tool ->
Code.ensure_loaded?(tool) and function_exported?(tool, :name, 0) and tool.name() == name
end)
end
Chapter 1: Scenario And Runtime Skill
The scenario is a support router. It owns context and policy, delegates bounded tasks to specialists, uses deterministic workflows for known processes, and can transfer billing conversations to a different owner.
Runtime skills are loaded from SKILL.md files. The main router uses this skill
as prompt guidance without narrowing the tool set.
skill_root = Path.join(System.tmp_dir!(), "jidoka-livebook-kitchen-skills")
skill_dir = Path.join(skill_root, "kitchen-guidelines")
File.rm_rf!(skill_root)
File.mkdir_p!(skill_dir)
File.write!(
Path.join(skill_dir, "SKILL.md"),
"""
---
name: kitchen-guidelines
description: Keeps the advanced kitchen-sink support router disciplined.
---
# Kitchen Guidelines
Use deterministic tools and workflows for known application work.
Use specialists for bounded research or editing tasks.
Keep the final answer concise and user-facing.
"""
)
skill_root
Chapter 2: Tools, Plugin, Character, And Instructions
Tools are deterministic application work. Plugins can contribute tools. A character shapes the persona, while dynamic instructions keep context-specific policy in the system prompt.
defmodule LivebookDemo.KitchenSink.Tools.AddNumbers do
use Jidoka.Tool,
name: "ks_add_numbers",
description: "Adds two integers for support calculations.",
schema: Zoi.object(%{a: Zoi.integer(), b: Zoi.integer()})
@impl true
def run(%{a: a, b: b}, context) do
tenant = Map.get(context, :tenant, Map.get(context, "tenant", "unknown"))
{:ok, %{sum: a + b, tenant: tenant}}
end
end
defmodule LivebookDemo.KitchenSink.Tools.ShowContext do
use Jidoka.Tool,
name: "ks_show_context",
description: "Summarizes public runtime context visible to tools.",
schema: Zoi.object(%{})
@impl true
def run(_params, context) do
public_context = Jidoka.Context.sanitize_for_subagent(context)
{:ok,
%{
keys: public_context |> Map.keys() |> Enum.map(&to_string/1) |> Enum.sort(),
tenant: Map.get(public_context, :tenant, Map.get(public_context, "tenant")),
channel: Map.get(public_context, :channel, Map.get(public_context, "channel"))
}}
end
end
defmodule LivebookDemo.KitchenSink.Plugins.ShowcasePlugin do
use Jidoka.Plugin,
name: "ks_showcase_plugin",
description: "Contributes support-router utility tools.",
tools: [LivebookDemo.KitchenSink.Tools.ShowContext]
end
defmodule LivebookDemo.KitchenSink.SupportCharacter do
use Jido.Character,
defaults: %{
name: "Support Router",
identity: %{role: "Advanced support triage specialist"},
voice: %{tone: :professional, style: "Concise and operational"},
instructions: ["Expose user-facing outcomes, not orchestration internals."]
}
end
defmodule LivebookDemo.KitchenSink.DynamicInstructions do
@behaviour Jidoka.Agent.SystemPrompt
@impl true
def resolve_system_prompt(%{context: context}) do
tenant = Map.get(context, :tenant, Map.get(context, "tenant", "demo"))
actor = Map.get(context, :actor, Map.get(context, "actor", %{id: "anonymous"}))
{:ok,
"""
You are the advanced Jidoka kitchen-sink support router.
Tenant: #{tenant}
Actor: #{inspect(actor)}
Prefer tools, workflows, and specialists when they apply.
Keep final replies concise and explain only the user-visible result.
"""}
end
end
LivebookDemo.KitchenSink.Tools.AddNumbers.run(%{a: 17, b: 25}, %{tenant: "acme"})
LivebookDemo.KitchenSink.Tools.ShowContext.run(%{}, %{
tenant: "acme",
channel: "livebook",
secret: "this is public in this direct call",
__internal__: "dropped"
})
Chapter 3: Hooks And Guardrails
Hooks enrich or observe a turn. Guardrails block or interrupt at explicit boundaries.
defmodule LivebookDemo.KitchenSink.Hooks.ShapeTurn do
use Jidoka.Hook, name: "ks_shape_turn"
@impl true
def call(%Jidoka.Hooks.BeforeTurn{} = input) do
tenant = Map.get(input.context, :tenant, Map.get(input.context, "tenant", "demo"))
{:ok,
%{
message: "#{input.message}\n\nUse Jidoka capabilities when helpful.",
metadata: %{tenant: tenant, shaped?: true}
}}
end
end
defmodule LivebookDemo.KitchenSink.Hooks.TagReply do
use Jidoka.Hook, name: "ks_tag_reply"
@impl true
def call(%Jidoka.Hooks.AfterTurn{outcome: {:ok, result}} = input) when is_binary(result) do
tenant = Map.get(input.context, :tenant, Map.get(input.context, "tenant", "unknown"))
{:ok, {:ok, "#{result}\n\n[kitchen_sink tenant=#{tenant}]"}}
end
def call(%Jidoka.Hooks.AfterTurn{} = input), do: {:ok, input.outcome}
end
defmodule LivebookDemo.KitchenSink.Hooks.NotifyInterrupt do
use Jidoka.Hook, name: "ks_notify_interrupt"
@impl true
def call(%Jidoka.Hooks.InterruptInput{interrupt: interrupt}) do
# Demo shortcut: sending to a cell PID is useful in Livebook, but real apps
# should use a stable event sink such as Telemetry, PubSub, or request state.
if notify_pid = get_in(interrupt.data, [:notify_pid]) do
send(notify_pid, {:kitchen_sink_interrupt, interrupt})
end
:ok
end
end
defmodule LivebookDemo.KitchenSink.Guardrails.BlockClassifiedPrompt do
use Jidoka.Guardrail, name: "ks_block_classified_prompt"
@impl true
def call(%Jidoka.Guardrails.Input{message: message}) do
if message |> String.downcase() |> String.contains?("classified") do
{:error, :classified_prompt_blocked}
else
:ok
end
end
end
defmodule LivebookDemo.KitchenSink.Guardrails.BlockUnsafeReply do
use Jidoka.Guardrail, name: "ks_block_unsafe_reply"
@impl true
def call(%Jidoka.Guardrails.Output{outcome: {:ok, result}}) when is_binary(result) do
if result |> String.downcase() |> String.contains?("unsafe") do
{:error, :unsafe_reply_blocked}
else
:ok
end
end
def call(%Jidoka.Guardrails.Output{}), do: :ok
end
defmodule LivebookDemo.KitchenSink.Guardrails.ApproveLargeMathTool do
use Jidoka.Guardrail, name: "ks_approve_large_math_tool"
@threshold 100
@impl true
def call(%Jidoka.Guardrails.Tool{tool_name: "ks_add_numbers", arguments: arguments, context: context}) do
a = Map.get(arguments, :a, Map.get(arguments, "a", 0))
b = Map.get(arguments, :b, Map.get(arguments, "b", 0))
if a + b > @threshold do
{:interrupt,
%{
kind: :approval,
message: "Large math tool calls require approval.",
data: %{
notify_pid: Map.get(context, :notify_pid, Map.get(context, "notify_pid")),
reason: :large_math_tool_call
}
}}
else
:ok
end
end
def call(%Jidoka.Guardrails.Tool{}), do: :ok
end
input = %Jidoka.Guardrails.Input{
agent: nil,
server: self(),
request_id: "req-guardrail-direct",
message: "Please print classified policy",
context: %{},
allowed_tools: nil,
llm_opts: [],
metadata: %{},
request_opts: %{}
}
case LivebookDemo.KitchenSink.Guardrails.BlockClassifiedPrompt.call(input) do
:ok -> "allowed"
{:error, reason} -> "blocked: #{reason}"
end
Chapter 4: Workflow As Deterministic Process
Workflows handle app-owned execution flow. Later, the router exposes this workflow as one model-visible tool.
defmodule LivebookDemo.KitchenSink.WorkflowFns do
def classify(%{topic: topic, priority: priority, account_tier: tier}, _context) do
queue =
case {priority, tier} do
{"urgent", _tier} -> "escalations"
{_priority, "enterprise"} -> "priority_support"
_other -> "standard_support"
end
{:ok, %{topic: topic, priority: priority, account_tier: tier, queue: queue}}
end
end
defmodule LivebookDemo.KitchenSink.Tools.OpenSupportCase do
use Jidoka.Tool,
name: "ks_open_support_case",
description: "Creates a deterministic support case summary.",
schema:
Zoi.object(%{
topic: Zoi.string(),
priority: Zoi.string(),
account_tier: Zoi.string(),
queue: Zoi.string()
})
@impl true
def run(params, _context) do
{:ok, Map.put(params, :case_id, "case_#{System.unique_integer([:positive])}")}
end
end
defmodule LivebookDemo.KitchenSink.Workflows.TriageTicket do
use Jidoka.Workflow
workflow do
id :ks_triage_ticket
description "Classifies a support topic and opens the right support case."
input Zoi.object(%{
topic: Zoi.string(),
priority: Zoi.string() |> Zoi.default("normal")
})
end
steps do
function :classify, {LivebookDemo.KitchenSink.WorkflowFns, :classify, 2},
input: %{
topic: input(:topic),
priority: input(:priority),
account_tier: context(:account_tier)
}
tool :open_case, LivebookDemo.KitchenSink.Tools.OpenSupportCase,
input: from(:classify)
end
output from(:open_case)
end
{:ok, workflow} = Jidoka.inspect_workflow(LivebookDemo.KitchenSink.Workflows.TriageTicket)
{:ok, debug} =
Jidoka.Workflow.run(
LivebookDemo.KitchenSink.Workflows.TriageTicket,
%{topic: "invoice dispute", priority: "urgent"},
context: %{account_tier: "enterprise"},
return: :debug
)
rows =
Enum.map(workflow.steps, fn step ->
%{
step: step.name,
waits_for: if(step.dependencies == [], do: "-", else: Enum.join(step.dependencies, ", ")),
output: inspect(Map.fetch!(debug.steps, step.name))
}
end)
Jidoka.Kino.table("Workflow execution", rows)
Chapter 5: Specialists And Handoff Targets
Subagents are manager-controlled specialists. Handoffs transfer conversation ownership to another agent.
defmodule LivebookDemo.KitchenSink.Specialists.Research do
defmodule Runtime do
use Jido.Agent,
name: "ks_research_runtime",
schema: Zoi.object(%{})
end
def name, do: "ks_research_agent"
def runtime_module, do: Runtime
def start_link(opts \\ []), do: Jidoka.start_agent(Runtime, opts)
def chat(_pid, message, opts \\ []) do
context = Keyword.get(opts, :context, %{})
tenant = Map.get(context, :tenant, Map.get(context, "tenant", "none"))
depth = Map.get(context, Jidoka.Subagent.depth_key(), 0)
{:ok, "research notes for #{tenant}: #{message}; depth=#{depth}"}
end
end
defmodule LivebookDemo.KitchenSink.Specialists.Editor do
defmodule Runtime do
use Jido.Agent,
name: "ks_editor_runtime",
schema: Zoi.object(%{})
end
def name, do: "ks_editor_agent"
def runtime_module, do: Runtime
def start_link(opts \\ []), do: Jidoka.start_agent(Runtime, opts)
def chat(_pid, message, _opts \\ []) do
{:ok, "edited: #{String.trim(message)}"}
end
end
defmodule LivebookDemo.KitchenSink.Specialists.Billing do
defmodule Runtime do
use Jido.Agent,
name: "ks_billing_runtime",
schema: Zoi.object(%{})
end
def name, do: "ks_billing_agent"
def runtime_module, do: Runtime
def start_link(opts \\ []), do: Jidoka.start_agent(Runtime, opts)
def chat(_pid, message, _opts \\ []), do: {:ok, "billing: #{message}"}
end
Chapter 6: Fake MCP Sync For Portable Testing
The router declares an MCP endpoint, but this notebook uses a fake sync module so the chapter stays portable and does not require an external server.
defmodule LivebookDemo.KitchenSink.FakeMCPSync do
def run(params, _context) do
send(self(), {:ks_mcp_sync_called, params})
{:ok, %{registered_count: 2, registered_tools: ["ks_fs_read_file", "ks_fs_list_files"]}}
end
end
Application.put_env(:jidoka, :mcp_sync_module, LivebookDemo.KitchenSink.FakeMCPSync)
Chapter 7: The Kitchen-Sink Agent
This agent intentionally composes more features than a normal application agent would. It is a reference surface for advanced developers.
defmodule LivebookDemo.KitchenSink.Agent do
use Jidoka.Agent
@skill_root Path.join(System.tmp_dir!(), "jidoka-livebook-kitchen-skills")
@context_fields %{
tenant: Zoi.string() |> Zoi.default("demo"),
channel: Zoi.string() |> Zoi.default("livebook"),
session: Zoi.string() |> Zoi.optional(),
account_tier: Zoi.string() |> Zoi.default("standard"),
actor: Zoi.any() |> Zoi.default(%{id: "demo-actor"}),
notify_pid: Zoi.any() |> Zoi.optional()
}
agent do
id :livebook_kitchen_sink_agent
description "Advanced support router that composes Jidoka capabilities."
schema Zoi.object(@context_fields)
end
defaults do
model :fast
character LivebookDemo.KitchenSink.SupportCharacter
instructions LivebookDemo.KitchenSink.DynamicInstructions
end
capabilities do
skill "kitchen-guidelines"
load_path @skill_root
tool LivebookDemo.KitchenSink.Tools.AddNumbers
plugin LivebookDemo.KitchenSink.Plugins.ShowcasePlugin
ash_resource Jidoka.Examples.KitchenSink.Ash.User
mcp_tools endpoint: :ks_livebook_mcp,
prefix: "ks_fs_",
transport: {:stdio, command: "echo"},
client_info: %{name: "jidoka-livebook-kitchen", version: "0.1.0"},
timeouts: %{request_ms: 15_000}
web :read_only
workflow LivebookDemo.KitchenSink.Workflows.TriageTicket,
as: :ks_triage_ticket,
description: "Classify and open a support case.",
forward_context: {:only, [:account_tier]},
result: :structured
subagent LivebookDemo.KitchenSink.Specialists.Research,
as: "ks_research_agent",
description: "Ask the research specialist for concise notes.",
forward_context: {:only, [:tenant, :channel, :session]},
result: :structured
subagent LivebookDemo.KitchenSink.Specialists.Editor,
as: "ks_editor_agent",
description: "Ask the editor specialist to polish text.",
forward_context: {:except, [:notify_pid]},
result: :text
handoff LivebookDemo.KitchenSink.Specialists.Billing,
as: "ks_billing_agent",
description: "Transfer billing conversation ownership."
end
lifecycle do
memory do
mode :conversation
namespace {:context, :session}
capture :conversation
retrieve limit: 4
inject :instructions
end
compaction do
max_messages 8
keep_last 4
max_summary_chars 1_200
prompt "Summarize the support router conversation for future turns. Preserve ticket facts, decisions, tool outcomes, handoffs, and next steps."
end
before_turn LivebookDemo.KitchenSink.Hooks.ShapeTurn
after_turn LivebookDemo.KitchenSink.Hooks.TagReply
on_interrupt LivebookDemo.KitchenSink.Hooks.NotifyInterrupt
input_guardrail LivebookDemo.KitchenSink.Guardrails.BlockClassifiedPrompt
output_guardrail LivebookDemo.KitchenSink.Guardrails.BlockUnsafeReply
tool_guardrail LivebookDemo.KitchenSink.Guardrails.ApproveLargeMathTool
end
end
Chapter 8: Agent Map And Inspection
The compiled agent is easiest to reason about in two views: a capability map for the major relationships, then an inspection summary for the exact runtime surface Jidoka generated.
{:ok, _diagram} = Jidoka.Kino.agent_diagram(LivebookDemo.KitchenSink.Agent)
:ok
{:ok, definition} = Jidoka.Kino.debug_agent(LivebookDemo.KitchenSink.Agent)
:ok
Chapter 9: Effective Prompt And Runtime Boundary
The request transformer composes the character before the dynamic instructions. The runtime boundary applies context, hooks, skills, compaction, memory, guardrails, and MCP sync before a provider call.
request = %{messages: [%{role: :user, content: "hello"}], llm_opts: [], tools: %{}}
state = Jido.AI.Reasoning.ReAct.State.new("hello", nil, request_id: "req-ks-prompt", run_id: "run-ks-prompt")
config =
Jido.AI.Reasoning.ReAct.Config.new(
model: :fast,
system_prompt: nil,
request_transformer: LivebookDemo.KitchenSink.Agent.request_transformer(),
streaming: false
)
{:ok, %{messages: [%{role: :system, content: prompt}, _user]}} =
LivebookDemo.KitchenSink.Agent.request_transformer().transform_request(
request,
state,
config,
%{tenant: "acme", actor: %{id: "agent-dev"}, account_tier: "enterprise"}
)
prompt
runtime = LivebookDemo.KitchenSink.Agent.runtime_module()
runtime_agent = runtime.new(id: "livebook-kitchen-runtime")
{:ok, runtime_agent, {:ai_react_start, params}} =
runtime.on_before_cmd(
runtime_agent,
{:ai_react_start,
%{
query: "Summarize the invoice issue",
request_id: "req-ks-runtime-1",
tool_context: %{
tenant: "acme",
channel: "livebook",
session: "ks-runtime",
account_tier: "enterprise",
actor: %{id: "agent-dev"},
notify_pid: self()
}
}}
)
Jidoka.Kino.context("Runtime context after lifecycle preparation", params.tool_context)
%{
rewritten_message: params.query,
allowed_tools: Map.get(params, :allowed_tools, :not_narrowed),
public_context: Jidoka.Context.strip_internal(params.tool_context),
mcp_sync_seen?:
receive do
{:ks_mcp_sync_called, sync_params} -> Map.take(sync_params, [:endpoint_id, :prefix])
after
100 -> false
end
}
Chapter 10: Tool, Workflow, Subagent, And Handoff Calls
Each advanced capability is still a normal generated tool module underneath. Calling them directly is the easiest way to prove behavior before involving a model.
add_tool = find_tool.(LivebookDemo.KitchenSink.Agent, "ks_add_numbers")
context_tool = find_tool.(LivebookDemo.KitchenSink.Agent, "ks_show_context")
workflow_tool = find_tool.(LivebookDemo.KitchenSink.Agent, "ks_triage_ticket")
research_tool = find_tool.(LivebookDemo.KitchenSink.Agent, "ks_research_agent")
handoff_tool = find_tool.(LivebookDemo.KitchenSink.Agent, "ks_billing_agent")
{:ok, add_result} = add_tool.run(%{a: 50, b: 25}, %{tenant: "acme"})
{:ok, context_result} = context_tool.run(%{}, %{tenant: "acme", channel: "livebook"})
{:ok, workflow_result} =
workflow_tool.run(
%{topic: "invoice dispute", priority: "urgent"},
%{account_tier: "enterprise"}
)
request_id = "req-ks-subagent-#{System.unique_integer([:positive])}"
{:ok, research_result} =
research_tool.run(
%{task: "Explain why deterministic workflows are useful."},
%{
Jidoka.Subagent.server_key() => self(),
Jidoka.Subagent.request_id_key() => request_id,
tenant: "acme",
channel: "livebook",
session: "ks-subagent"
}
)
conversation_id = "ks-handoff-#{System.unique_integer([:positive])}"
{:error, {:handoff, handoff}} =
handoff_tool.run(
%{
message: "Billing should continue this invoice conversation.",
summary: "Customer asked about an invoice.",
reason: "billing"
},
%{
Jidoka.Handoff.context_key() => conversation_id,
Jidoka.Handoff.server_key() => self(),
Jidoka.Handoff.request_id_key() => "req-ks-handoff",
Jidoka.Handoff.from_agent_key() => LivebookDemo.KitchenSink.Agent.id(),
tenant: "acme"
}
)
Jidoka.Kino.table("Generated capability calls", [
%{capability: "tool", result: inspect(add_result)},
%{capability: "plugin tool", result: inspect(context_result)},
%{capability: "workflow tool", result: inspect(workflow_result)},
%{capability: "subagent tool", result: inspect(research_result)},
%{capability: "handoff tool", result: inspect(Map.take(handoff, [:name, :conversation_id, :to_agent_id]))}
])
%{
subagent_calls: Jidoka.Subagent.request_calls(self(), request_id),
handoff_owner: Jidoka.handoff_owner(conversation_id) |> Map.take([:agent, :agent_id])
}
Jidoka.reset_handoff(conversation_id)
Chapter 11: Memory Across Turns
Memory is opt-in and conversation-first. This simulates two turns without a provider call.
memory_runtime = LivebookDemo.KitchenSink.Agent.runtime_module()
memory_agent = memory_runtime.new(id: "livebook-kitchen-memory")
session = "ks-memory-#{System.unique_integer([:positive])}"
base_context = %{
tenant: "acme",
channel: "livebook",
session: session,
account_tier: "enterprise",
actor: %{id: "agent-dev"}
}
{:ok, memory_agent, _action} =
memory_runtime.on_before_cmd(
memory_agent,
{:ai_react_start,
%{
query: "Remember that this customer prefers email updates.",
request_id: "req-ks-memory-1",
tool_context: base_context
}}
)
memory_agent =
Jido.AI.Request.complete_request(
memory_agent,
"req-ks-memory-1",
"I will remember that email updates are preferred."
)
{:ok, memory_agent, []} =
memory_runtime.on_after_cmd(memory_agent, {:ai_react_start, %{request_id: "req-ks-memory-1"}}, [])
{:ok, _memory_agent, {:ai_react_start, memory_params}} =
memory_runtime.on_before_cmd(
memory_agent,
{:ai_react_start,
%{
query: "How should we update the customer?",
request_id: "req-ks-memory-2",
tool_context: base_context
}}
)
memory_payload = memory_params.tool_context[Jidoka.Memory.context_key()]
Jidoka.Kino.table("Retrieved memory", [
%{
namespace: memory_payload.namespace,
record_count: length(memory_payload.records),
prompt_preview: memory_payload.prompt
}
])
Chapter 12: Summary Compaction
Compaction is opt-in lifecycle policy for long sessions. It summarizes older messages, stores the latest snapshot on the running agent, injects the summary into future prompts, and keeps the original thread intact for AgentView.
This cell uses a deterministic summarizer so it works without a provider key.
compaction_runtime = LivebookDemo.KitchenSink.Agent.runtime_module()
compaction_agent = compaction_runtime.new(id: "livebook-kitchen-compaction")
compaction_messages = [
{"req-ks-compact-1", :user, "The enterprise customer says invoice INV-42 was double charged."},
{"req-ks-compact-1", :assistant, "I will route this as a billing issue."},
{"req-ks-compact-2", :user, "They prefer email updates and want a status by Friday."},
{"req-ks-compact-2", :assistant, "Noted: email updates and Friday status target."},
{"req-ks-compact-3", :user, "Please prepare the next triage step."},
{"req-ks-compact-3", :assistant, "I will keep the most recent turn visible."},
{"req-ks-compact-4", :user, "Also keep the account tier as enterprise."},
{"req-ks-compact-4", :assistant, "The retained tail should include this detail."},
{"req-ks-compact-5", :user, "What should we do next?"}
]
thread_entries =
Enum.map(compaction_messages, fn {request_id, role, content} ->
%{
kind: :ai_message,
payload: %{role: role, content: content, request_id: request_id, context_ref: "default"},
refs: %{request_id: request_id}
}
end)
compaction_agent =
Jido.Thread.Agent.put(
compaction_agent,
Jido.Thread.new(id: "ks-compaction-thread") |> Jido.Thread.append(thread_entries)
)
previous_summarizer = Application.get_env(:jidoka, :compaction_summarizer)
{compaction_agent, compaction_params} =
try do
Application.put_env(:jidoka, :compaction_summarizer, fn input ->
{:ok,
"Earlier support context: #{input.source_message_count} older messages show an enterprise invoice double-charge, email-update preference, and a Friday status target."}
end)
{:ok, compacted_agent, {:ai_react_start, params}} =
compaction_runtime.on_before_cmd(
compaction_agent,
{:ai_react_start,
%{
query: "What should we do next?",
request_id: "req-ks-compact-live",
tool_context: %{
tenant: "acme",
channel: "livebook",
session: "ks-compaction",
account_tier: "enterprise",
actor: %{id: "agent-dev"}
}
}}
)
{compacted_agent, params}
after
if is_nil(previous_summarizer) do
Application.delete_env(:jidoka, :compaction_summarizer)
else
Application.put_env(:jidoka, :compaction_summarizer, previous_summarizer)
end
end
{:ok, compaction_snapshot} = Jidoka.Kino.compaction(compaction_agent)
Jidoka.Kino.context("Context after compaction", compaction_params.tool_context)
%{
status: compaction_snapshot.status,
source_messages: compaction_snapshot.source_message_count,
retained_messages: compaction_snapshot.retained_message_count,
summary_preview: compaction_snapshot.summary_preview
}
Chapter 13: Imported Agent Spec
Imported agents use explicit registries. This lets apps accept constrained JSON/YAML specs without letting the spec name arbitrary modules.
imported_spec = %{
"agent" => %{
"id" => "ks_imported_math",
"description" => "Imported math helper",
"context" => %{"tenant" => "demo"}
},
"defaults" => %{
"model" => "fast",
"instructions" => "Use available tools when they help. Keep answers short.",
"character" => "support_router"
},
"capabilities" => %{
"tools" => ["ks_add_numbers"],
"plugins" => ["ks_showcase_plugin"]
},
"lifecycle" => %{
"compaction" => %{
"mode" => "auto",
"strategy" => "summary",
"max_messages" => 20,
"keep_last" => 6,
"max_summary_chars" => 1_000,
"prompt" => "Summarize this imported support conversation."
},
"guardrails" => %{
"input" => ["ks_block_classified_prompt"]
}
}
}
{:ok, imported_agent} =
Jidoka.import_agent(imported_spec,
available_tools: [LivebookDemo.KitchenSink.Tools.AddNumbers],
available_plugins: [LivebookDemo.KitchenSink.Plugins.ShowcasePlugin],
available_guardrails: [LivebookDemo.KitchenSink.Guardrails.BlockClassifiedPrompt],
available_characters: %{"support_router" => LivebookDemo.KitchenSink.SupportCharacter}
)
{:ok, imported_definition} = Jidoka.inspect_agent(imported_agent)
{:ok, imported_yaml} = Jidoka.encode_agent(imported_agent, format: :yaml)
%{
imported_definition: Map.take(imported_definition, [:id, :description, :context, :tool_names, :compaction]),
yaml: imported_yaml
}
Chapter 14: Sessions, AgentView And Eval Checks
Jidoka.Session names the conversation, runtime agent id, and trusted context.
Jidoka.AgentView is the application boundary for UI surfaces. It is not a
Phoenix component; it projects the session’s runtime state into data a UI can
render.
defmodule LivebookDemo.KitchenSink.SupportView do
use Jidoka.AgentView
end
view_session =
Jidoka.Session.new!(
agent: LivebookDemo.KitchenSink.Agent,
id: "kitchen-capstone",
context: %{
tenant: "acme",
channel: "livebook",
account_tier: "enterprise",
actor: %{id: "agent-dev"}
}
)
{:ok, view_pid} = LivebookDemo.KitchenSink.SupportView.start_agent(view_session)
{:ok, view} = LivebookDemo.KitchenSink.SupportView.snapshot(view_pid, view_session)
pending = LivebookDemo.KitchenSink.SupportView.before_turn(view, "Please triage an invoice issue.")
Map.take(pending, [:agent_id, :conversation_id, :runtime_context, :status, :visible_messages])
Small deterministic evals make the composition safer to change.
eval_cases = [
%{
id: "tool_registry_contains_direct_tool",
pass?: "ks_add_numbers" in LivebookDemo.KitchenSink.Agent.tool_names()
},
%{
id: "workflow_capability_exposed",
pass?: "ks_triage_ticket" in LivebookDemo.KitchenSink.Agent.workflow_names()
},
%{
id: "handoff_capability_exposed",
pass?: "ks_billing_agent" in LivebookDemo.KitchenSink.Agent.handoff_names()
},
%{
id: "input_guardrail_blocks_classified",
pass?:
match?(
{:error, :classified_prompt_blocked},
LivebookDemo.KitchenSink.Guardrails.BlockClassifiedPrompt.call(%Jidoka.Guardrails.Input{
agent: nil,
server: self(),
request_id: "req-eval-classified",
message: "classified details",
context: %{},
allowed_tools: nil,
llm_opts: [],
metadata: %{},
request_opts: %{}
})
)
}
]
Jidoka.Kino.table("Deterministic eval checks", eval_cases)
Chapter 15: Production Pre-Flight
Use a checklist before allowing advanced composition into a production path.
provider_status =
case Jidoka.Kino.load_provider_env() do
{:ok, source} -> "configured via #{source}"
{:error, message} -> "missing: #{message}"
end
preflight_rows = [
%{area: "provider", status: provider_status},
%{area: "context", status: "schema defaults plus actor/account tier"},
%{area: "tools", status: "#{length(LivebookDemo.KitchenSink.Agent.tool_names())} generated tools"},
%{area: "memory", status: inspect(LivebookDemo.KitchenSink.Agent.memory())},
%{area: "compaction", status: inspect(LivebookDemo.KitchenSink.Agent.compaction())},
%{area: "guardrails", status: inspect(LivebookDemo.KitchenSink.Agent.guardrails())},
%{area: "mcp", status: "fake sync configured for notebook portability"},
%{area: "handoff", status: "owner registry can be inspected and reset"}
]
Jidoka.Kino.table("Kitchen-sink pre-flight", preflight_rows)
Optional Provider Chat And Debugging
This cell runs the full router through a provider-backed chat call. The
structured trace timeline and call graph show the runtime path across model,
tool, workflow, subagent, handoff, guardrail, memory, compaction, and MCP
events. Without ANTHROPIC_API_KEY, it returns a friendly missing-provider
result.
Application.put_env(:jidoka, :mcp_sync_module, LivebookDemo.KitchenSink.FakeMCPSync)
runtime_context = %{
tenant: "acme",
channel: "livebook",
account_tier: "enterprise",
actor: %{id: "agent-dev"},
notify_pid: self()
}
provider_session =
Jidoka.Session.new!(
agent: LivebookDemo.KitchenSink.Agent,
id: "kitchen_sink_provider",
context: runtime_context
)
{:ok, pid} =
Jidoka.Kino.start_or_reuse(provider_session.agent_id, fn ->
Jidoka.Session.start_agent(provider_session)
end)
chat_prompts = %{
workflow:
"Call the ks_triage_ticket tool exactly once with topic \"shipping delay\" and priority \"urgent\". After the tool returns, summarize the case result for the enterprise customer. Do not transfer ownership.",
subagent:
"Ask the research specialist for a concise summary of why deterministic workflows are useful in support routers.",
handoff:
"Transfer this invoice dispute to the billing specialist and include a concise handoff summary."
}
prompt = chat_prompts.workflow
result =
Jidoka.Kino.chat(
"Kitchen-sink support router",
fn ->
Jidoka.chat(provider_session, prompt)
end,
num_rows: 40
)
Jidoka.Kino.context("Provider runtime context", provider_session.context)
Jidoka.Kino.debug_agent(pid)
Jidoka.Kino.compaction(provider_session)
case Jidoka.inspect_trace(provider_session) do
{:ok, trace} ->
Jidoka.Kino.timeline(trace)
Jidoka.Kino.call_graph(trace)
Jidoka.Kino.trace_table(trace)
{:error, _reason} ->
:ok
end
result
To intentionally test ownership transfer, switch the prompt above to
chat_prompts.handoff. A successful transfer is returned as {:handoff, summary} and should no longer show as an unformatted Livebook error.
case Jidoka.handoff_owner(provider_session) do
nil ->
%{handoff_owner: :none}
owner ->
%{handoff_owner: Map.take(owner, [:agent, :agent_id])}
end
Jidoka.reset_handoff(provider_session)
What To Look For
The advanced story is not “turn everything on.” It is:
- deterministic tools and workflows for known application work
- specialists for bounded subtasks
- handoffs for ownership transfer
- skills, characters, hooks, memory, compaction, and guardrails around the turn
- AgentView as the UI boundary
- Kino helpers for trace output, context inspection, diagrams, and request debug summaries
- eval checks and pre-flight tables before provider-backed behavior