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, 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