Jidoka: Handoffs
Handoffs transfer conversation ownership. Use them when another agent should continue the conversation, not merely answer a subtask.
Setup
Mix.install(
[
{:jidoka, git: "https://github.com/mikehostetler/jidoka.git", ref: "924a486f3c1b7e7a943cb3d5ceee0de65f158467"},
{:kino, "~> 0.19.0"}
],
config: [
jidoka: [
model_aliases: %{fast: "anthropic:claude-haiku-4-5"}
]
]
)
Jidoka.Kino.setup()
Define A Handoff Target
defmodule LivebookDemo.Handoffs.BillingSpecialist do
defmodule Runtime do
use Jido.Agent,
name: "livebook_billing_specialist_runtime",
schema: Zoi.object(%{})
end
def name, do: "billing_specialist"
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
defmodule LivebookDemo.Handoffs.RouterAgent do
use Jidoka.Agent
agent do
id :livebook_handoff_router
end
defaults do
model :fast
instructions "Transfer billing ownership when the user needs billing follow-up."
end
capabilities do
handoff LivebookDemo.Handoffs.BillingSpecialist,
as: "billing_specialist",
description: "Transfer the conversation to billing."
end
end
%{
handoff_names: LivebookDemo.Handoffs.RouterAgent.handoff_names(),
tool_names: LivebookDemo.Handoffs.RouterAgent.tool_names(),
handoffs: LivebookDemo.Handoffs.RouterAgent.handoffs()
}
Run A Handoff
The generated handoff tool returns {:error, {:handoff, handoff}} on purpose.
That tells the caller to stop the current turn and route the conversation to the
new owner.
conversation_id = "livebook-handoff-#{System.unique_integer([:positive])}"
request_id = "req-handoff-#{System.unique_integer([:positive])}"
handoff_tool =
Enum.find(LivebookDemo.Handoffs.RouterAgent.tools(), fn tool ->
tool.name() == "billing_specialist"
end)
context = %{
Jidoka.Handoff.context_key() => conversation_id,
Jidoka.Handoff.server_key() => self(),
Jidoka.Handoff.request_id_key() => request_id,
Jidoka.Handoff.from_agent_key() => LivebookDemo.Handoffs.RouterAgent.id(),
tenant: "acme",
secret: "not forwarded"
}
{:error, {:handoff, handoff}} =
handoff_tool.run(
%{
message: "Please continue with billing.",
summary: "Customer asked about invoice status.",
reason: "billing"
},
context
)
owner = Jidoka.handoff_owner(conversation_id)
%{
handoff: Map.take(handoff, [:conversation_id, :from_agent, :to_agent_id, :name, :message, :summary, :reason]),
owner: Map.take(owner, [:agent, :agent_id]),
forwarded_context: handoff.context
}
Reset ownership when the conversation is finished or returned to the router.
Jidoka.reset_handoff(conversation_id)
Jidoka.handoff_owner(conversation_id)
Optional Provider Turn
{:ok, pid} =
Jidoka.Kino.start_or_reuse("livebook-handoff-router", fn ->
LivebookDemo.Handoffs.RouterAgent.start_link(id: "livebook-handoff-router")
end)
Jidoka.Kino.chat("Handoff router chat", fn ->
LivebookDemo.Handoffs.RouterAgent.chat(
pid,
"The customer needs billing to take over this invoice conversation.",
conversation: "livebook-handoff-chat",
context: %{tenant: "acme"}
)
end)