Powered by AppSignal & Oban Pro

Jidoka: Kitchen Sink

livebook/20_kitchen_sink.livemd

Jidoka: Kitchen Sink

Run in Livebook

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