Current section
Files
Jump to
Current section
Files
lib/codex/handoff.ex
defmodule Codex.Handoff do
@moduledoc """
Represents a handoff from one agent to another, wrapping the downstream agent as a tool with
optional input filtering and history nesting controls.
"""
alias Codex.Agent
defmodule InputData do
@moduledoc """
Carries conversation history and run context into handoff input filters.
"""
@enforce_keys [:input_history, :pre_handoff_items, :new_items]
defstruct input_history: nil,
pre_handoff_items: [],
new_items: [],
run_context: nil
@type t :: %__MODULE__{
input_history: term(),
pre_handoff_items: list(),
new_items: list(),
run_context: term()
}
end
@enforce_keys [:tool_name, :tool_description, :agent_name, :on_invoke_handoff]
defstruct tool_name: nil,
tool_description: nil,
agent_name: nil,
agent: nil,
on_invoke_handoff: nil,
input_schema: %{},
input_filter: nil,
nest_handoff_history: nil,
strict_json_schema: true,
is_enabled: true
@type is_enabled :: boolean() | (map(), Agent.t() -> boolean() | term())
@type t :: %__MODULE__{
tool_name: String.t(),
tool_description: String.t(),
agent_name: String.t(),
agent: Agent.t() | nil,
on_invoke_handoff: (map(), term() -> Agent.t()),
input_schema: map(),
input_filter: (InputData.t() -> InputData.t()) | nil,
nest_handoff_history: boolean() | nil,
strict_json_schema: boolean(),
is_enabled: is_enabled()
}
@doc """
Wraps an agent as a handoff with optional overrides.
Options:
* `:tool_name` - override the default tool name
* `:tool_description` - override the default tool description
* `:input_filter` - function invoked to filter history passed to the downstream agent
* `:nest_handoff_history` - override history nesting behaviour
* `:input_schema` - optional JSON schema map describing expected input
* `:strict_json_schema` - whether the schema should be treated as strict (default: true)
* `:is_enabled` - boolean or function to dynamically enable the handoff
"""
@spec wrap(Agent.t(), keyword()) :: t()
def wrap(%Agent{} = agent, opts \\ []) do
tool_name = Keyword.get(opts, :tool_name, default_tool_name(agent))
tool_description = Keyword.get(opts, :tool_description, default_tool_description(agent))
on_invoke = Keyword.get(opts, :on_invoke, fn _ctx, _input -> agent end)
input_schema = Keyword.get(opts, :input_schema, %{})
%__MODULE__{
tool_name: to_string(tool_name),
tool_description: to_string(tool_description),
agent_name: agent.name || "",
agent: agent,
on_invoke_handoff: on_invoke,
input_schema: input_schema || %{},
input_filter: Keyword.get(opts, :input_filter),
nest_handoff_history: Keyword.get(opts, :nest_handoff_history),
strict_json_schema: Keyword.get(opts, :strict_json_schema, true),
is_enabled: Keyword.get(opts, :is_enabled, true)
}
end
@doc """
Default tool name derived from the downstream agent name.
"""
@spec default_tool_name(Agent.t()) :: String.t()
def default_tool_name(%Agent{name: name}) do
safe =
name
|> to_string()
|> String.trim()
|> String.replace(~r/\s+/, "_")
|> String.replace(~r/[^a-zA-Z0-9_]/, "_")
|> String.downcase()
"transfer_to_#{safe}"
end
@doc """
Default tool description referencing the downstream agent.
"""
@spec default_tool_description(Agent.t()) :: String.t()
def default_tool_description(%Agent{name: name, handoff_description: desc}) do
trimmed =
case desc do
nil -> ""
value -> String.trim(to_string(value))
end
base = "Handoff to the #{name} agent to handle the request."
if trimmed == "" do
base
else
"#{base} #{trimmed}"
end
end
@doc """
Evaluates whether a handoff is enabled for the given context/agent.
"""
@spec enabled?(t(), map(), Agent.t()) :: boolean()
def enabled?(%__MODULE__{is_enabled: flag}, _context, %Agent{} = _agent) when is_boolean(flag),
do: flag
def enabled?(%__MODULE__{is_enabled: fun}, context, %Agent{} = agent)
when is_function(fun, 2) do
fun.(context, agent) |> truthy?()
end
def enabled?(%__MODULE__{is_enabled: fun}, context, %Agent{} = _agent)
when is_function(fun, 1) do
fun.(context) |> truthy?()
end
def enabled?(_handoff, _context, _agent), do: true
defp truthy?(value) when value in [true, "true", "TRUE", "True"], do: true
defp truthy?(_), do: false
end