Current section
Files
Jump to
Current section
Files
lib/codex/agent_runner.ex
defmodule Codex.AgentRunner do
@moduledoc """
Multi-turn runner that orchestrates agent execution over Codex threads.
"""
alias Codex.Agent
alias Codex.Exec
alias Codex.FileSearch
alias Codex.Guardrail
alias Codex.GuardrailError
alias Codex.Handoff
alias Codex.Options
alias Codex.RunConfig
alias Codex.RunResultStreaming
alias Codex.RunResultStreaming.Control, as: StreamingControl
alias Codex.Session
alias Codex.StreamEvent.{AgentUpdated, GuardrailResult, RawResponses, RunItem, ToolApproval}
alias Codex.StreamQueue
alias Codex.Thread
alias Codex.ToolGuardrail
alias Codex.Turn.Result
defmodule State do
@moduledoc false
@enforce_keys [
:thread,
:input,
:agent,
:run_config,
:guardrails,
:turn_opts,
:backoff,
:attempt,
:events,
:usage
]
defstruct [
:thread,
:input,
:agent,
:run_config,
:guardrails,
:turn_opts,
:backoff,
:attempt,
:events,
:usage,
:queue,
:control
]
@type t :: %__MODULE__{
thread: Thread.t(),
input: String.t() | [map()],
agent: Agent.t(),
run_config: RunConfig.t(),
guardrails: map(),
turn_opts: map() | keyword(),
backoff: term(),
attempt: non_neg_integer(),
events: [term()],
usage: map(),
queue: pid() | nil,
control: pid() | nil
}
end
@spec run(Thread.t(), String.t() | [map()], map() | keyword()) ::
{:ok, Result.t()} | {:error, term()}
def run(thread, input, opts \\ %{})
def run(%Thread{} = thread, input, opts)
when is_binary(input) or is_list(input) do
{agent_opts, run_config_opts, turn_opts, backoff} = normalize_opts(opts)
with {:ok, %Agent{} = agent} <- Agent.new(agent_opts),
{:ok, %RunConfig{} = run_config} <- RunConfig.new(run_config_opts),
{:ok, tuned_thread} <- apply_model_override(thread, run_config) do
tuned_thread =
tuned_thread
|> apply_tracing_metadata(run_config)
|> apply_file_search_config(run_config)
guardrails = build_guardrails(agent, run_config)
{prepared_input, session, _history} =
maybe_prepare_session(run_config, input)
with :ok <-
run_guardrails(:input, guardrails.input, prepared_input, %{
agent: agent,
run_config: run_config
}) do
result =
do_run(%State{
thread: tuned_thread,
input: prepared_input,
agent: agent,
run_config: run_config,
guardrails: guardrails,
turn_opts: turn_opts,
backoff: backoff || (&default_backoff/1),
attempt: 1,
events: [],
usage: %{}
})
maybe_store_session(
session,
prepared_input,
finalize_run_config(run_config, result),
result
)
end
end
end
def run(%Thread{}, input, _opts), do: {:error, {:invalid_input, input}}
@spec run_streamed(Thread.t(), String.t() | [map()], map() | keyword()) ::
{:ok, RunResultStreaming.t()} | {:error, term()}
def run_streamed(thread, input, opts \\ %{})
def run_streamed(%Thread{} = thread, input, opts)
when is_binary(input) or is_list(input) do
{agent_opts, run_config_opts, turn_opts, backoff} = normalize_opts(opts)
cancellation_token = fetch_cancellation_token(turn_opts)
with {:ok, %Agent{} = agent} <- Agent.new(agent_opts),
{:ok, %RunConfig{} = run_config} <- RunConfig.new(run_config_opts),
{:ok, tuned_thread} <- apply_model_override(thread, run_config),
{:ok, queue} <- StreamQueue.start_link(),
{:ok, control} <- StreamingControl.start_link() do
:ok = StreamingControl.attach_queue(control, queue)
:ok =
StreamingControl.set_cancel_handler(control, fn
:immediate -> Exec.cancel(cancellation_token)
:after_turn -> :ok
end)
tuned_thread =
tuned_thread
|> apply_tracing_metadata(run_config)
|> apply_file_search_config(run_config)
guardrails = build_guardrails(agent, run_config)
{prepared_input, session, history} = maybe_prepare_session(run_config, input)
start_fun =
fn ->
stream_run(%State{
thread: tuned_thread,
input: prepared_input,
agent: agent,
run_config: run_config,
guardrails: guardrails,
turn_opts: turn_opts,
backoff: backoff || (&default_backoff/1),
attempt: 1,
events: [],
usage: %{},
queue: queue,
control: control
})
|> maybe_store_session(prepared_input, run_config, session, history)
end
{:ok, RunResultStreaming.new(queue, control, start_fun)}
end
end
def run_streamed(%Thread{}, input, _opts), do: {:error, {:invalid_input, input}}
defp stream_run(%State{} = state) do
emit_agent_update(state.queue, state.agent, state.run_config)
guardrail_hook = %{on_result: &emit_guardrail_event(state.queue, &1, &2, &3, &4)}
case run_guardrails(
:input,
state.guardrails.input,
state.input,
%{agent: state.agent, run_config: state.run_config},
guardrail_hook
) do
:ok ->
do_run_streamed(state)
{:error, reason} ->
close_queue(state.queue, {:error, reason})
end
end
defp maybe_store_session(result, _input, _run_config, nil, _history), do: result
defp maybe_store_session(result, input, %RunConfig{} = run_config, session, _history) do
maybe_store_session(session, input, finalize_run_config(run_config, result), result)
end
defp do_run(%State{} = state) do
state = %{state | thread: annotate_conversation(state.thread, state.run_config)}
with {:ok, %Result{} = turn_result} <-
Thread.run_turn(state.thread, state.input, state.turn_opts),
{:ok, %Result{} = processed} <- handle_tools(turn_result, state) do
state = update_after_turn(state, processed)
tool_results = tool_results_from_raw(processed.raw)
case check_tool_use_behavior(state.agent, state.run_config, tool_results) do
{:final, final_output} -> finalize_tool_stop(state, processed, final_output)
:continue -> continue_run(state, processed, tool_results)
{:error, _} = error -> error
end
end
end
defp handle_tools(%Result{} = turn_result, %State{} = state) do
Thread.handle_tool_requests(turn_result, state.attempt, %{
tool_input: state.guardrails.tool_input,
tool_output: state.guardrails.tool_output
})
end
defp update_after_turn(%State{} = state, %Result{} = processed) do
merged_events = state.events ++ processed.events
merged_usage = Thread.merge_usage(state.usage, processed.usage)
run_config = maybe_chain_previous_response_id(state.run_config, processed.last_response_id)
%{state | events: merged_events, usage: merged_usage, run_config: run_config}
end
defp finalize_tool_stop(%State{} = state, %Result{} = processed, final_output) do
with :ok <-
run_guardrails(:output, state.guardrails.output, final_output, %{agent: state.agent}) do
final_thread =
finalize_processed_thread(processed.thread, state.usage, state.run_config,
clear_continuation?: true
)
{:ok,
%Result{
processed
| events: state.events,
usage: state.usage,
thread: final_thread,
final_response: final_output,
attempts: state.attempt
}}
end
end
defp continue_run(%State{} = state, %Result{} = processed, tool_results) do
next_turn_opts = maybe_reset_tool_choice(state.agent, state.turn_opts, tool_results) || %{}
case processed.thread.continuation_token do
token when is_binary(token) and token != "" ->
continue_with_continuation(state, processed, token, next_turn_opts)
_ ->
finalize_without_continuation(state, processed)
end
end
defp continue_with_continuation(%State{} = state, %Result{} = processed, token, next_turn_opts) do
if state.attempt < state.run_config.max_turns do
safe_backoff(state.backoff, state.attempt)
next_thread = %{processed.thread | usage: state.usage}
do_run(%State{
state
| thread: next_thread,
turn_opts: next_turn_opts,
attempt: state.attempt + 1
})
else
{:error, {:max_turns_exceeded, state.run_config.max_turns, %{continuation: token}}}
end
end
defp finalize_without_continuation(%State{} = state, %Result{} = processed) do
with :ok <-
run_guardrails(:output, state.guardrails.output, processed.final_response, %{
agent: state.agent
}) do
final_thread =
processed.thread
|> Map.put(:usage, state.usage)
|> annotate_conversation(state.run_config)
{:ok,
%Result{
processed
| events: state.events,
usage: state.usage,
thread: final_thread,
attempts: state.attempt
}}
end
end
defp do_run_streamed(%State{queue: queue, control: control} = state)
when is_pid(queue) and is_pid(control) do
case StreamingControl.cancel_mode(state.control) do
:immediate -> close_queue(state.queue, {:error, :cancelled})
_ -> do_run_streamed_active(state)
end
end
defp do_run_streamed_active(%State{} = state) do
case Thread.run_turn_streamed(state.thread, state.input, state.turn_opts) do
{:ok, stream} -> do_run_streamed_with_stream(state, stream)
{:error, _reason} = error -> close_queue(state.queue, error)
end
end
defp do_run_streamed_with_stream(%State{} = state, stream) do
hooks = stream_hooks(state)
structured_output? = structured_output?(state.turn_opts)
try do
{events, status} = collect_stream_events(stream, state.queue, state.control)
{updated_thread, response, usage} =
Thread.reduce_events(state.thread, events, %{structured_output?: structured_output?})
merged_usage = Thread.merge_usage(state.usage, usage)
StreamingControl.put_usage(state.control, merged_usage)
StreamQueue.push(state.queue, %RawResponses{events: events, usage: usage})
turn_result =
streamed_turn_result(
updated_thread,
events,
response,
usage,
state.attempt,
structured_output?
)
case status do
:cancelled ->
close_queue(state.queue, {:ok, turn_result})
_ ->
handle_streamed_continuation(
state,
turn_result,
hooks
)
end
rescue
error ->
close_queue(state.queue, {:error, error})
catch
kind, reason ->
close_queue(state.queue, {:error, {kind, reason}})
end
end
defp stream_hooks(%State{} = state) do
%{
on_guardrail: &emit_guardrail_event(state.queue, &1, &2, &3, &4),
on_approval: &emit_approval_event(state.queue, &1, &2, &3)
}
end
defp structured_output?(turn_opts) do
schema =
turn_opts
|> Map.new()
|> then(fn map -> Map.get(map, :output_schema) || Map.get(map, "output_schema") end)
not is_nil(schema)
end
defp streamed_turn_result(updated_thread, events, response, usage, attempt, structured_output?) do
%Result{
thread: updated_thread,
events: events,
final_response: response,
usage: usage,
raw: %{events: events, structured_output?: structured_output?},
attempts: attempt,
last_response_id: Thread.last_response_id(events)
}
end
defp handle_streamed_continuation(%State{} = state, %Result{} = turn_result, hooks) do
case handle_tools_streamed(turn_result, state, hooks) do
{:ok, %Result{} = processed} ->
state = update_after_turn(state, processed)
StreamingControl.put_usage(state.control, state.usage)
tool_results = tool_results_from_raw(processed.raw)
case check_tool_use_behavior(state.agent, state.run_config, tool_results) do
{:final, final_output} -> finalize_streamed_tool_stop(state, processed, final_output)
:continue -> continue_streamed_run(state, processed, tool_results, hooks)
{:error, _} = error -> close_queue(state.queue, error)
end
{:error, _reason} = error ->
close_queue(state.queue, error)
end
end
defp handle_tools_streamed(%Result{} = turn_result, %State{} = state, hooks) do
Thread.handle_tool_requests(turn_result, state.attempt, %{
tool_input: state.guardrails.tool_input,
tool_output: state.guardrails.tool_output,
hooks: hooks
})
end
defp finalize_streamed_tool_stop(%State{} = state, %Result{} = processed, final_output) do
case run_guardrails(
:output,
state.guardrails.output,
final_output,
%{agent: state.agent},
%{on_result: &emit_guardrail_event(state.queue, &1, &2, &3, &4)}
) do
:ok ->
final_thread =
finalize_processed_thread(processed.thread, state.usage, state.run_config,
clear_continuation?: true
)
close_queue(
state.queue,
{:ok,
%Result{
processed
| events: state.events,
usage: state.usage,
thread: final_thread,
final_response: final_output,
attempts: state.attempt
}}
)
{:error, reason} ->
close_queue(state.queue, {:error, reason})
end
end
defp continue_streamed_run(%State{} = state, %Result{} = processed, tool_results, hooks) do
next_turn_opts = maybe_reset_tool_choice(state.agent, state.turn_opts, tool_results) || %{}
case next_stream_action(state, processed) do
:stop_after_turn ->
close_queue(
state.queue,
{:ok,
%Result{
processed
| events: state.events,
usage: state.usage,
thread: %{processed.thread | usage: state.usage},
attempts: state.attempt
}}
)
:continue ->
next_thread = %{processed.thread | usage: state.usage}
do_run_streamed(%State{
state
| thread: next_thread,
turn_opts: next_turn_opts,
attempt: state.attempt + 1
})
{:error, continuation} ->
close_queue(
state.queue,
{:error,
{:max_turns_exceeded, state.run_config.max_turns, %{continuation: continuation}}}
)
:final ->
finalize_streamed_without_continuation(state, processed, hooks)
end
end
defp next_stream_action(%State{} = state, %Result{} = processed) do
cond do
StreamingControl.cancel_mode(state.control) == :after_turn ->
:stop_after_turn
is_binary(processed.thread.continuation_token) and processed.thread.continuation_token != "" and
state.attempt < state.run_config.max_turns ->
safe_backoff(state.backoff, state.attempt)
:continue
is_binary(processed.thread.continuation_token) and processed.thread.continuation_token != "" ->
{:error, processed.thread.continuation_token}
true ->
:final
end
end
defp finalize_processed_thread(%Thread{} = thread, usage, run_config, opts) do
thread
|> maybe_clear_continuation(Keyword.get(opts, :clear_continuation?, false))
|> Map.put(:usage, usage)
|> Thread.clear_pending_tool_payloads()
|> annotate_conversation(run_config)
end
defp maybe_clear_continuation(%Thread{} = thread, true) do
%{thread | continuation_token: nil}
end
defp maybe_clear_continuation(%Thread{} = thread, false), do: thread
defp finalize_streamed_without_continuation(%State{} = state, %Result{} = processed, _hooks) do
case run_guardrails(
:output,
state.guardrails.output,
processed.final_response,
%{agent: state.agent},
%{on_result: &emit_guardrail_event(state.queue, &1, &2, &3, &4)}
) do
:ok ->
final_thread =
processed.thread
|> Map.put(:usage, state.usage)
|> annotate_conversation(state.run_config)
close_queue(
state.queue,
{:ok,
%Result{
processed
| events: state.events,
usage: state.usage,
thread: final_thread,
attempts: state.attempt
}}
)
{:error, reason} ->
close_queue(state.queue, {:error, reason})
end
end
defp close_queue(nil, result), do: result
defp close_queue(queue, {:error, reason} = error) when is_pid(queue) do
if stream_error?(reason) do
StreamQueue.close(queue, error)
else
StreamQueue.close(queue)
end
error
end
defp close_queue(queue, result) when is_pid(queue) do
StreamQueue.close(queue)
result
end
defp stream_error?(%Codex.TransportError{}), do: true
defp stream_error?(%Codex.Error{}), do: true
defp stream_error?(%Codex.ApprovalError{}), do: false
defp stream_error?(%Codex.GuardrailError{}), do: false
defp stream_error?(reason) when is_exception(reason), do: true
defp stream_error?(_reason), do: false
defp collect_stream_events(stream, queue, control) do
stream
|> Enum.reduce_while({[], :ok}, fn event, {acc, _status} ->
emit_run_item(queue, event)
case StreamingControl.cancel_mode(control) do
:immediate -> {:halt, {[event | acc], :cancelled}}
_ -> {:cont, {[event | acc], :ok}}
end
end)
|> then(fn {events, status} -> {Enum.reverse(events), status} end)
end
defp emit_agent_update(queue, agent, run_config) do
StreamQueue.push(queue, %AgentUpdated{agent: agent, run_config: run_config})
end
defp emit_run_item(queue, event) do
StreamQueue.push(queue, %RunItem{event: event, type: normalize_event_type(event)})
end
defp emit_guardrail_event(queue, stage, guardrail, result, message) do
guardrail_name = Map.get(guardrail, :name)
StreamQueue.push(queue, %GuardrailResult{
stage: stage,
guardrail: guardrail_name,
result: result,
message: message
})
end
defp emit_approval_event(queue, event, decision, reason) do
StreamQueue.push(queue, %ToolApproval{
tool_name: Map.get(event, :tool_name),
call_id: Map.get(event, :call_id),
decision: decision,
reason: reason
})
end
defp normalize_event_type(%Codex.Events.ThreadStarted{}), do: :thread_started
defp normalize_event_type(%Codex.Events.TurnStarted{}), do: :turn_started
defp normalize_event_type(%Codex.Events.TurnContinuation{}), do: :turn_continuation
defp normalize_event_type(%Codex.Events.TurnCompleted{}), do: :turn_completed
defp normalize_event_type(%Codex.Events.ItemCompleted{}), do: :item_completed
defp normalize_event_type(%Codex.Events.ItemStarted{}), do: :item_started
defp normalize_event_type(%Codex.Events.ItemUpdated{}), do: :item_updated
defp normalize_event_type(%Codex.Events.ItemAgentMessageDelta{}), do: :item_delta
defp normalize_event_type(%Codex.Events.ItemInputTextDelta{}), do: :item_delta
defp normalize_event_type(%Codex.Events.ToolCallRequested{}), do: :tool_call
defp normalize_event_type(%Codex.Events.ToolCallCompleted{}), do: :tool_call_completed
defp normalize_event_type(%Codex.Events.TurnDiffUpdated{}), do: :turn_diff
defp normalize_event_type(%Codex.Events.TurnCompaction{}), do: :turn_compaction
defp normalize_event_type(%Codex.Events.ThreadTokenUsageUpdated{}), do: :usage
defp normalize_event_type(_), do: :event
defp normalize_opts(opts) when is_list(opts), do: opts |> Map.new() |> normalize_opts()
defp normalize_opts(%RunConfig{} = config), do: {%{}, config, %{}, nil}
defp normalize_opts(%Agent{} = agent), do: {agent, %{}, %{}, nil}
defp normalize_opts(opts) when is_map(opts) do
agent_opts = Map.get(opts, :agent, Map.get(opts, "agent", %{}))
max_turns =
Map.get(opts, :max_turns) ||
Map.get(opts, "max_turns")
run_config_opts =
Map.get(opts, :run_config, Map.get(opts, "run_config", %{}))
|> case do
%RunConfig{} = config when is_nil(max_turns) -> config
%RunConfig{} = config -> %{config | max_turns: max_turns}
other when is_nil(max_turns) -> other
other -> Map.new(other) |> Map.put(:max_turns, max_turns)
end
backoff = Map.get(opts, :backoff, Map.get(opts, "backoff"))
turn_opts =
opts
|> Map.get(:turn_opts, Map.get(opts, "turn_opts"))
|> normalize_turn_opts(opts)
|> Map.delete(:agent)
|> Map.delete("agent")
|> Map.delete(:run_config)
|> Map.delete("run_config")
|> Map.delete(:max_turns)
|> Map.delete("max_turns")
|> Map.delete(:backoff)
|> Map.delete("backoff")
|> Map.delete(:turn_opts)
|> Map.delete("turn_opts")
{agent_opts, run_config_opts, turn_opts, backoff}
end
defp normalize_opts(_opts), do: {%{}, %{}, %{}, nil}
defp fetch_cancellation_token(turn_opts) when is_map(turn_opts) do
case Map.get(turn_opts, :cancellation_token, Map.get(turn_opts, "cancellation_token")) do
token when is_binary(token) and token != "" ->
token
_ ->
nil
end
end
defp normalize_turn_opts(nil, fallback),
do: Map.drop(fallback, [:agent, :run_config, :max_turns, :backoff])
defp normalize_turn_opts(opts, _fallback) when is_list(opts), do: Map.new(opts)
defp normalize_turn_opts(opts, _fallback) when is_map(opts), do: opts
defp normalize_turn_opts(_opts, fallback),
do: Map.drop(fallback, [:agent, :run_config, :max_turns, :backoff])
defp build_guardrails(agent, run_config) do
%{
input: merge_guardrails(agent.input_guardrails, run_config.input_guardrails),
output: merge_guardrails(agent.output_guardrails, run_config.output_guardrails),
tool_input: List.wrap(agent.tool_input_guardrails),
tool_output: List.wrap(agent.tool_output_guardrails)
}
end
defp merge_guardrails(left, right), do: List.wrap(left) ++ List.wrap(right)
defp run_guardrails(stage, guardrails, payload, context, hooks \\ %{})
defp run_guardrails(_stage, guardrails, _payload, _context, _hooks)
when guardrails in [nil, []],
do: :ok
defp run_guardrails(stage, guardrails, payload, context, hooks) do
{parallel, sequential} = Enum.split_with(guardrails, & &1.run_in_parallel)
case run_guardrails_sequential(stage, sequential, payload, context, hooks) do
:ok -> run_guardrails_parallel(stage, parallel, payload, context, hooks)
{:error, reason} -> {:error, reason}
end
end
defp run_guardrails_sequential(_stage, [], _payload, _context, _hooks), do: :ok
defp run_guardrails_sequential(stage, guardrails, payload, context, hooks) do
Enum.reduce_while(guardrails, :ok, fn guardrail, :ok ->
case run_guardrail(stage, guardrail, payload, context, hooks) do
:ok -> {:cont, :ok}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp run_guardrails_parallel(_stage, [], _payload, _context, _hooks), do: :ok
defp run_guardrails_parallel(stage, guardrails, payload, context, hooks) do
guardrails
|> Task.async_stream(
fn guardrail ->
run_guardrail(stage, guardrail, payload, context, hooks)
end,
ordered: true
)
|> Enum.reduce_while(:ok, fn
{:ok, :ok}, :ok ->
{:cont, :ok}
{:ok, {:error, reason}}, :ok ->
{:halt, {:error, reason}}
{:exit, reason}, :ok ->
{:halt,
{:error,
%GuardrailError{
stage: stage,
guardrail: "parallel_guardrail",
message: inspect(reason),
type: :tripwire
}}}
end)
end
defp run_guardrail(stage, guardrail, payload, context, hooks)
defp run_guardrail(stage, %Guardrail{} = guardrail, payload, context, hooks) do
case Guardrail.run(guardrail, payload, context) do
:ok ->
notify_guardrail(hooks, stage, guardrail, :ok, nil)
:ok
{:reject, message} ->
notify_guardrail(hooks, stage, guardrail, :reject, message)
{:error,
%GuardrailError{
stage: stage,
guardrail: guardrail.name,
message: message,
type: :reject
}}
{:tripwire, message} ->
notify_guardrail(hooks, stage, guardrail, :tripwire, message)
{:error,
%GuardrailError{
stage: stage,
guardrail: guardrail.name,
message: message,
type: :tripwire
}}
end
end
defp run_guardrail(_stage, %ToolGuardrail{} = guardrail, payload, context, hooks) do
tool_stage = if guardrail.stage == :output, do: :tool_output, else: :tool_input
case ToolGuardrail.run(guardrail, Map.get(context, :event), payload, context) do
:ok ->
notify_guardrail(hooks, tool_stage, guardrail, :ok, nil)
:ok
{:reject, message} ->
notify_guardrail(hooks, tool_stage, guardrail, :reject, message)
{:error,
%GuardrailError{
stage: tool_stage,
guardrail: guardrail.name,
message: message,
type: if(guardrail.behavior == :raise_exception, do: :tripwire, else: :reject)
}}
{:tripwire, message} ->
notify_guardrail(hooks, tool_stage, guardrail, :tripwire, message)
{:error,
%GuardrailError{
stage: tool_stage,
guardrail: guardrail.name,
message: message,
type: :tripwire
}}
end
end
defp run_guardrail(_stage, _guardrail, _payload, _context, _hooks), do: :ok
defp notify_guardrail(%{on_result: fun}, stage, guardrail, result, message)
when is_function(fun, 4) do
fun.(stage, guardrail, result, message)
end
defp notify_guardrail(_hooks, _stage, _guardrail, _result, _message), do: :ok
@doc """
Resolves and filters handoffs configured on the agent, returning only enabled entries.
"""
@spec get_handoffs(Agent.t(), map()) :: {:ok, [Handoff.t()]}
def get_handoffs(%Agent{} = agent, context \\ %{}) do
handoffs =
agent.handoffs
|> List.wrap()
|> Enum.map(&normalize_handoff(&1, agent))
|> Enum.reject(&is_nil/1)
|> Enum.filter(&Handoff.enabled?(&1, context, &1.agent || agent))
{:ok, handoffs}
end
defp normalize_handoff(%Handoff{} = handoff, _agent), do: handoff
defp normalize_handoff(%Agent{} = agent, _parent), do: Handoff.wrap(agent)
defp normalize_handoff(_other, _agent), do: nil
defp tool_results_from_raw(raw) do
raw
|> Map.get(:tool_outputs, Map.get(raw, "tool_outputs", []))
|> List.wrap()
|> Enum.map(&normalize_tool_result/1)
end
defp normalize_tool_result(%{} = result) do
%{
call_id: Map.get(result, :call_id) || Map.get(result, "call_id"),
tool_name: Map.get(result, :tool_name) || Map.get(result, "tool_name"),
output: Map.get(result, :output) || Map.get(result, "output")
}
end
defp normalize_tool_result(other), do: %{call_id: nil, tool_name: nil, output: other}
@doc false
@spec check_tool_use_behavior(Agent.t(), RunConfig.t(), [map()]) ::
:continue | {:final, term()} | {:error, term()}
def check_tool_use_behavior(_agent, _config, []), do: :continue
def check_tool_use_behavior(%Agent{tool_use_behavior: :run_llm_again}, _config, _results),
do: :continue
def check_tool_use_behavior(%Agent{tool_use_behavior: :stop_on_first_tool}, _config, results) do
first = Enum.find(results, &match?(%{}, &1)) || List.first(results)
{:final, first && Map.get(first, :output)}
end
def check_tool_use_behavior(
%Agent{tool_use_behavior: %{stop_at_tool_names: names}},
_config,
results
)
when is_list(names) do
names = Enum.map(names, &to_string/1)
case Enum.find(results, fn result ->
tool = result |> Map.get(:tool_name) |> to_string()
tool in names
end) do
nil -> :continue
match -> {:final, Map.get(match, :output)}
end
end
def check_tool_use_behavior(%Agent{tool_use_behavior: fun} = agent, config, results)
when is_function(fun) do
context = %{agent: agent, run_config: config}
case safe_apply(fun, context, results) do
%{is_final_output: true} = outcome ->
{:final, Map.get(outcome, :final_output)}
%{is_final_output: false} ->
:continue
%{is_final_output: other} = outcome when is_boolean(other) ->
if other, do: {:final, Map.get(outcome, :final_output)}, else: :continue
other ->
{:error, {:invalid_tool_use_behavior_result, other}}
end
end
def check_tool_use_behavior(%Agent{tool_use_behavior: other}, _config, _results),
do: {:error, {:invalid_tool_use_behavior, other}}
defp safe_apply(fun, context, results) when is_function(fun, 2), do: fun.(context, results)
defp safe_apply(fun, _context, results) when is_function(fun, 1), do: fun.(results)
defp safe_apply(fun, _context, _results) when is_function(fun, 0), do: fun.()
@doc false
@spec maybe_reset_tool_choice(Agent.t(), map() | nil, [map()]) :: map() | nil
def maybe_reset_tool_choice(%Agent{reset_tool_choice: false}, turn_opts, _tool_results),
do: turn_opts
def maybe_reset_tool_choice(_agent, turn_opts, _tool_results) when turn_opts in [%{}, nil],
do: turn_opts
def maybe_reset_tool_choice(_agent, turn_opts, []), do: turn_opts
def maybe_reset_tool_choice(%Agent{reset_tool_choice: true}, turn_opts, _tool_results)
when is_map(turn_opts) do
tool_choice = Map.get(turn_opts, :tool_choice) || Map.get(turn_opts, "tool_choice")
if is_nil(tool_choice) do
turn_opts
else
turn_opts
|> Map.put(:tool_choice, nil)
|> Map.delete("tool_choice")
end
end
def maybe_reset_tool_choice(_agent, turn_opts, _tool_results), do: turn_opts
defp maybe_prepare_session(%RunConfig{session: nil}, input), do: {input, nil, []}
defp maybe_prepare_session(
%RunConfig{session: session, session_input_callback: callback},
input
) do
case Session.load(session) do
{:ok, history} ->
prepared = apply_session_callback(callback, input, history)
{prepared, session, history}
{:error, _reason} ->
{input, session, []}
end
end
defp apply_session_callback(nil, input, _history), do: input
defp apply_session_callback(fun, input, history) when is_function(fun) do
case safe_session_callback(fun, input, history) do
{:ok, new_input} when is_binary(new_input) or is_list(new_input) -> new_input
{:ok, new_input, _ctx} when is_binary(new_input) or is_list(new_input) -> new_input
value when is_binary(value) or is_list(value) -> value
_ -> input
end
end
defp safe_session_callback(fun, input, history) when is_function(fun, 3),
do: fun.(input, history, %{})
defp safe_session_callback(fun, input, history) when is_function(fun, 2),
do: fun.(input, history)
defp safe_session_callback(fun, input, _history) when is_function(fun, 1), do: fun.(input)
defp safe_session_callback(fun, _input, _history) when is_function(fun, 0), do: fun.()
defp maybe_store_session(nil, _input, _run_config, result), do: result
defp maybe_store_session(session, input, %RunConfig{} = run_config, {:ok, %Result{} = result}) do
entry = %{
input: input,
response: result.final_response,
conversation_id: run_config.conversation_id || result.thread.thread_id,
previous_response_id: run_config.previous_response_id
}
_ = Session.save(session, entry)
{:ok, result}
end
defp maybe_store_session(_session, _input, _run_config, other), do: other
defp finalize_run_config(%RunConfig{} = run_config, {:ok, %Result{} = result}) do
maybe_chain_previous_response_id(run_config, result.last_response_id)
end
defp finalize_run_config(%RunConfig{} = run_config, _other), do: run_config
defp maybe_chain_previous_response_id(
%RunConfig{auto_previous_response_id: true} = run_config,
value
)
when is_binary(value) and value != "" do
%{run_config | previous_response_id: value}
end
defp maybe_chain_previous_response_id(%RunConfig{} = run_config, _value), do: run_config
defp apply_model_override(%Thread{codex_opts: %Options{} = opts} = thread, %RunConfig{
model: model
}) do
if is_binary(model) and model != "" do
attrs =
opts
|> Map.from_struct()
|> Map.drop([:model_payload])
|> Map.put(:model, model)
case Options.new(attrs) do
{:ok, updated_opts} -> {:ok, %{thread | codex_opts: updated_opts}}
{:error, reason} -> {:error, reason}
end
else
{:ok, thread}
end
end
defp apply_model_override(thread, _run_config), do: {:ok, thread}
defp apply_tracing_metadata(%Thread{} = thread, %RunConfig{} = run_config) do
tracing_meta =
%{}
|> maybe_put(:workflow, run_config.workflow)
|> maybe_put(:group, run_config.group)
|> maybe_put(:trace_id, run_config.trace_id)
|> maybe_put(:trace_sensitive, run_config.trace_include_sensitive_data)
|> maybe_put(:tracing_disabled, run_config.tracing_disabled)
updated_metadata =
thread.metadata
|> Map.merge(tracing_meta, fn _key, existing, updated -> updated || existing end)
Map.put(thread, :metadata, updated_metadata)
end
defp apply_file_search_config(%Thread{thread_opts: thread_opts} = thread, %RunConfig{} = config) do
merged = FileSearch.merge(thread_opts.file_search, config.file_search)
%{thread | thread_opts: %{thread_opts | file_search: merged}}
end
defp apply_file_search_config(thread, _run_config), do: thread
defp safe_backoff(fun, attempt) when is_function(fun, 1), do: fun.(attempt)
defp safe_backoff(_fun, _attempt), do: :ok
defp default_backoff(_attempt), do: :ok
defp annotate_conversation(thread, %RunConfig{} = run_config) do
conversation_meta =
%{}
|> maybe_put(:conversation_id, run_config.conversation_id)
|> maybe_put(:previous_response_id, run_config.previous_response_id)
updated_metadata =
thread.metadata
|> Map.merge(conversation_meta)
Map.put(thread, :metadata, updated_metadata)
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end