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, workflows, subagents, handoffs, imports, 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.
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()
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
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
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
{:ok, definition} = Jidoka.inspect_agent(LivebookDemo.KitchenSink.Agent)
capability_rows = [
%{surface: "tools", names: Enum.join(definition.tool_names, ", ")},
%{surface: "plugins", names: Enum.join(LivebookDemo.KitchenSink.Agent.plugin_names(), ", ")},
%{surface: "skills", names: Enum.join(LivebookDemo.KitchenSink.Agent.skill_names(), ", ")},
%{surface: "workflows", names: Enum.join(LivebookDemo.KitchenSink.Agent.workflow_names(), ", ")},
%{surface: "subagents", names: Enum.join(LivebookDemo.KitchenSink.Agent.subagent_names(), ", ")},
%{surface: "handoffs", names: Enum.join(LivebookDemo.KitchenSink.Agent.handoff_names(), ", ")},
%{surface: "web", names: Enum.join(LivebookDemo.KitchenSink.Agent.web_tool_names(), ", ")},
%{surface: "ash", names: LivebookDemo.KitchenSink.Agent.ash_domain() |> inspect()}
]
Jidoka.Kino.table("Compiled capability surface", capability_rows)
Chapter 8: Effective Prompt And Runtime Boundary
The request transformer composes the character before the dynamic instructions. The runtime boundary applies context, hooks, skills, 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()
}
}}
)
%{
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 9: 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 10: 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 11: 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" => %{
"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]),
yaml: imported_yaml
}
Chapter 12: AgentView And Eval Checks
Jidoka.AgentView is the application boundary for UI surfaces. It is not a
Phoenix component; it projects runtime state into data a UI can render.
defmodule LivebookDemo.KitchenSink.SupportView do
use Jidoka.AgentView,
agent: LivebookDemo.KitchenSink.Agent
@impl true
def conversation_id(session) do
session
|> Map.get("conversation_id", "kitchen-sink")
|> Jidoka.AgentView.normalize_id("kitchen-sink")
end
@impl true
def agent_id(session), do: "livebook-kitchen-view-#{conversation_id(session)}"
@impl true
def runtime_context(session) do
%{
tenant: Map.get(session, "tenant", "acme"),
channel: "livebook",
session: conversation_id(session),
account_tier: Map.get(session, "account_tier", "enterprise"),
actor: %{id: Map.get(session, "actor_id", "agent-dev")}
}
end
end
view_session = %{
"conversation_id" => "kitchen-capstone",
"tenant" => "acme",
"actor_id" => "agent-dev",
"account_tier" => "enterprise"
}
{: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 13: 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: "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 Turn
This cell uses the full router through a provider-backed chat call. Without
ANTHROPIC_API_KEY, it returns a friendly missing-provider result.
Application.put_env(:jidoka, :mcp_sync_module, LivebookDemo.KitchenSink.FakeMCPSync)
{:ok, pid} =
Jidoka.Kino.start_or_reuse("livebook-kitchen-sink-agent", fn ->
LivebookDemo.KitchenSink.Agent.start_link(id: "livebook-kitchen-sink-agent")
end)
Jidoka.Kino.chat("Kitchen-sink support router", fn ->
LivebookDemo.KitchenSink.Agent.chat(
pid,
"Use the support router capabilities to triage an urgent invoice dispute for an enterprise customer.",
context: %{
tenant: "acme",
channel: "livebook",
session: "kitchen-sink-provider",
account_tier: "enterprise",
actor: %{id: "agent-dev"},
notify_pid: self()
}
)
end)
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, and guardrails around the turn
- AgentView as the UI boundary
- eval checks and pre-flight tables before provider-backed behavior