Current section
Files
Jump to
Current section
Files
lib/codex/thread.ex
defmodule Codex.Thread do
@moduledoc """
Represents a Codex conversation thread and exposes turn execution APIs.
"""
alias Codex.ApprovalError
alias Codex.Approvals
alias Codex.Error
alias Codex.Events
alias Codex.Items
alias Codex.Options
alias Codex.OutputSchemaFile
alias Codex.Thread.Options, as: ThreadOptions
alias Codex.Tools
alias Codex.Telemetry
alias Codex.Turn.Result
@enforce_keys [:codex_opts, :thread_opts]
defstruct thread_id: nil,
codex_opts: nil,
thread_opts: nil,
metadata: %{},
labels: %{},
continuation_token: nil,
usage: %{},
pending_tool_outputs: [],
pending_tool_failures: []
@type t :: %__MODULE__{
thread_id: String.t() | nil,
codex_opts: Options.t(),
thread_opts: ThreadOptions.t(),
metadata: map(),
labels: map(),
continuation_token: String.t() | nil,
usage: map(),
pending_tool_outputs: [map()],
pending_tool_failures: [map()]
}
@doc false
@spec build(Options.t(), ThreadOptions.t(), keyword()) :: t()
def build(%Options{} = opts, %ThreadOptions{} = thread_opts, extra \\ []) do
struct!(
__MODULE__,
Keyword.merge(
[
thread_id: nil,
codex_opts: opts,
thread_opts: thread_opts,
metadata: %{},
labels: %{},
continuation_token: nil,
usage: %{},
pending_tool_outputs: [],
pending_tool_failures: []
],
extra
)
)
end
@doc """
Executes a blocking turn against the codex engine.
"""
@spec run(t(), String.t(), map() | keyword()) ::
{:ok, Result.t()} | {:error, term()}
def run(%__MODULE__{} = thread, input, turn_opts \\ %{}) when is_binary(input) do
thread = maybe_reset_for_new(thread, input)
span_token = make_ref()
meta =
%{
thread_id: thread.thread_id,
input: input,
originator: :sdk,
span_token: span_token
}
|> maybe_put(:source, extract_source(thread.metadata))
Telemetry.emit([:codex, :thread, :start], %{system_time: System.system_time()}, meta)
started_monotonic = System.monotonic_time()
with {:ok, exec_opts, cleanup, exec_meta} <- build_exec_options(thread, turn_opts) do
structured_output? = Map.get(exec_meta, :structured_output?, false)
try do
case Codex.Exec.run(input, exec_opts) do
{:ok, %{events: events} = exec_result} ->
duration = System.monotonic_time() - started_monotonic
failure = extract_turn_failure(events)
early_exit? = early_exit?(events)
identifiers = extract_thread_context(events, thread)
progress_meta = merge_metadata(meta, identifiers)
emit_progress_events(events, progress_meta)
Telemetry.emit(
[:codex, :thread, :stop],
%{duration: duration, system_time: System.system_time()},
progress_meta
|> Map.put(:result, telemetry_result(failure, early_exit?))
|> maybe_put(:error, failure_meta(failure))
|> maybe_put(:early_exit?, early_exit?)
)
exec_result =
exec_result
|> Map.put(:structured_output?, structured_output?)
|> Map.put(:pruned?, early_exit?)
result = finalize_turn(thread, exec_result, exec_meta)
case failure do
{:error, err} -> {:error, {:turn_failed, err}}
:ok -> {:ok, result}
end
{:error, reason} ->
duration = System.monotonic_time() - started_monotonic
Telemetry.emit(
[:codex, :thread, :exception],
%{duration: duration, system_time: System.system_time()},
merge_metadata(meta, %{
thread_id: thread.thread_id,
source: extract_source(thread.metadata)
})
|> Map.put(:reason, reason)
|> Map.put(:result, :error)
)
{:error, reason}
end
after
cleanup.()
end
end
end
@doc """
Executes a turn and returns a stream of events for progressive consumption.
The stream is lazy; events will not be produced until enumerated.
"""
@spec run_streamed(t(), String.t(), map() | keyword()) ::
{:ok, Enumerable.t()} | {:error, term()}
def run_streamed(%__MODULE__{} = thread, input, turn_opts \\ %{}) when is_binary(input) do
thread = maybe_reset_for_new(thread, input)
with {:ok, exec_opts, cleanup, exec_meta} <- build_exec_options(thread, turn_opts) do
structured_output? = Map.get(exec_meta, :structured_output?, false)
progress_meta =
%{
thread_id: thread.thread_id,
originator: :sdk,
span_token: make_ref()
}
|> maybe_put(:source, extract_source(thread.metadata))
case Codex.Exec.run_stream(input, exec_opts) do
{:ok, stream} ->
wrapped =
Stream.transform(
stream,
fn -> :ok end,
fn event, acc ->
decoded = maybe_decode_stream_event(event, structured_output?)
emit_progress_event(decoded, progress_meta)
{[decoded], acc}
end,
fn _ -> cleanup.() end
)
{:ok, wrapped}
{:error, reason} ->
cleanup.()
{:error, reason}
end
end
end
@doc """
Executes an auto-run loop, retrying while a continuation token is present.
Options:
* `:max_attempts` – maximum number of attempts (default: 3)
* `:backoff` – unary function invoked with current attempt (default: exponential sleep)
* `:turn_opts` – per-turn options forwarded to each attempt
"""
@spec run_auto(t(), String.t(), keyword()) :: {:ok, Result.t()} | {:error, term()}
def run_auto(%__MODULE__{} = thread, input, opts \\ []) when is_binary(input) do
max_attempts = Keyword.get(opts, :max_attempts, 3)
backoff = Keyword.get(opts, :backoff, &default_backoff/1)
turn_opts = Keyword.get(opts, :turn_opts, %{})
do_run_auto(thread, input, turn_opts, max_attempts, backoff, 1, [], %{})
end
defp do_run_auto(
thread,
input,
turn_opts,
max_attempts,
backoff,
attempt,
acc_events,
acc_usage
) do
case run(thread, input, turn_opts) do
{:ok, %Result{} = result} ->
with {:ok, processed} <- handle_tool_requests(result, attempt) do
merged_events = acc_events ++ processed.events
merged_usage = merge_usage(acc_usage, processed.usage)
cond do
processed.thread.continuation_token && attempt < max_attempts ->
backoff.(attempt)
do_run_auto(
processed.thread,
input,
turn_opts,
max_attempts,
backoff,
attempt + 1,
merged_events,
merged_usage
)
processed.thread.continuation_token ->
{:error,
{:max_attempts_reached, max_attempts,
%{continuation: processed.thread.continuation_token}}}
true ->
final_thread = %{processed.thread | usage: merged_usage}
{:ok,
%Result{
processed
| events: merged_events,
usage: merged_usage,
thread: final_thread,
attempts: attempt
}}
end
end
{:error, _} = error ->
error
end
end
defp default_backoff(attempt) when attempt >= 1 do
exponent = attempt - 1
delay = trunc(:math.pow(2, exponent))
Process.sleep(delay * 100)
end
defp build_exec_options(thread, turn_opts) do
turn_opts_map = Map.new(turn_opts)
schema = Map.get(turn_opts_map, :output_schema, Map.get(turn_opts_map, "output_schema"))
with {:ok, schema_path, cleanup} <- OutputSchemaFile.create(schema) do
env = Map.get(turn_opts_map, :env, Map.get(turn_opts_map, "env"))
cancellation_token =
Map.get(turn_opts_map, :cancellation_token, Map.get(turn_opts_map, "cancellation_token"))
timeout_ms = Map.get(turn_opts_map, :timeout_ms, Map.get(turn_opts_map, "timeout_ms"))
filtered_turn_opts =
turn_opts_map
|> Map.delete(:output_schema)
|> Map.delete("output_schema")
|> Map.delete(:env)
|> Map.delete("env")
|> Map.delete(:cancellation_token)
|> Map.delete("cancellation_token")
|> Map.delete(:timeout_ms)
|> Map.delete("timeout_ms")
exec_opts =
%{
codex_opts: thread.codex_opts,
thread: thread,
turn_opts: filtered_turn_opts,
continuation_token: thread.continuation_token,
attachments: thread.thread_opts.attachments,
tool_outputs: thread.pending_tool_outputs,
tool_failures: thread.pending_tool_failures
}
|> maybe_put(:output_schema_path, schema_path)
|> maybe_put(:env, env)
|> maybe_put(:cancellation_token, cancellation_token)
|> maybe_put(:timeout_ms, timeout_ms)
{:ok, exec_opts, cleanup, %{structured_output?: not is_nil(schema)}}
end
end
defp finalize_turn(thread, %{events: events} = exec_result, opts) do
{updated_thread, final_response, usage} = fold_events(thread, events, opts)
updated_thread =
updated_thread
|> Map.put(:usage, usage || thread.usage)
|> Map.put(:pending_tool_outputs, [])
|> Map.put(:pending_tool_failures, [])
|> then(fn t ->
if early_exit?(events) do
reset_conversation(t)
else
t
end
end)
%Result{
thread: updated_thread,
events: events,
final_response: final_response,
usage: usage,
raw: exec_result,
attempts: 1
}
end
defp fold_events(thread, events, opts) do
structured? = Map.get(opts, :structured_output?, false)
Enum.reduce(events, {thread, nil, thread.usage, thread.continuation_token}, fn event,
{acc_thread,
response,
usage,
continuation} ->
case event do
%Events.ThreadStarted{} = started ->
labels =
case started.metadata do
%{"labels" => label_map} -> label_map
_ -> acc_thread.labels
end
updated =
acc_thread
|> maybe_put(:thread_id, started.thread_id)
|> Map.put(:metadata, started.metadata || %{})
|> Map.put(:labels, labels)
{updated, response, usage, continuation}
%Events.TurnContinuation{continuation_token: token} ->
updated = Map.put(acc_thread, :continuation_token, token)
{updated, response, usage, token}
%Events.ThreadTokenUsageUpdated{} = usage_event ->
updated_usage = apply_usage_update(usage, usage_event)
updated_thread =
acc_thread
|> maybe_put(:thread_id, usage_event.thread_id)
{updated_thread, response, updated_usage, continuation}
%Events.TurnDiffUpdated{thread_id: thread_id} ->
updated_thread = maybe_put(acc_thread, :thread_id, thread_id)
{updated_thread, response, usage, continuation}
%Events.TurnCompaction{thread_id: thread_id, compaction: compaction} ->
updated_thread = maybe_put(acc_thread, :thread_id, thread_id)
updated_usage = apply_compaction_usage_update(usage, compaction)
{updated_thread, response, updated_usage, continuation}
%Events.ItemAgentMessageDelta{item: item} ->
new_response =
case item do
%{"content" => %{"type" => "text", "text" => text}} ->
decode_agent_message(Map.get(item, "id"), text, structured?)
%{"text" => text} when is_binary(text) ->
decode_agent_message(Map.get(item, "id"), text, structured?)
_ ->
response
end
{acc_thread, new_response || response, usage, continuation}
%Events.ItemCompleted{item: %Items.AgentMessage{text: text} = item} ->
decoded_item = maybe_decode_agent_item(item, text, structured?)
{acc_thread, decoded_item, usage, continuation}
%Events.ItemCompleted{} ->
{acc_thread, response, usage, continuation}
%Events.TurnCompleted{} = completed ->
new_usage = completed.usage || usage
new_response =
completed.final_response
|> decode_final_response(structured?)
|> Kernel.||(response)
new_continuation =
if new_response do
nil
else
acc_thread.continuation_token || continuation
end
updated =
acc_thread
|> maybe_put(:thread_id, completed.thread_id)
|> Map.put(:continuation_token, new_continuation)
{updated, new_response, new_usage, new_continuation}
_other ->
{acc_thread, response, usage, continuation}
end
end)
|> then(fn {acc_thread, response, usage, continuation} ->
updated_thread =
acc_thread
|> Map.put(:continuation_token, continuation)
{updated_thread, response, usage}
end)
end
defp apply_usage_update(current_usage, %Events.ThreadTokenUsageUpdated{
usage: usage,
delta: delta
}) do
update_usage_with_maps(current_usage, usage, delta)
end
defp merge_usage(nil, nil), do: %{}
defp merge_usage(map, nil) when is_map(map), do: map
defp merge_usage(nil, map) when is_map(map), do: map
defp merge_usage(left, right) when is_map(left) and is_map(right) do
Map.merge(left, right, fn _key, l, r ->
cond do
is_number(l) and is_number(r) -> l + r
true -> r || l
end
end)
end
defp apply_compaction_usage_update(current_usage, compaction) when is_map(compaction) do
usage =
compaction
|> get_usage_map(:usage)
|> Kernel.||(get_usage_map(compaction, :token_usage))
delta =
compaction
|> get_usage_map(:usage_delta)
|> Kernel.||(get_usage_map(compaction, :usageDelta))
update_usage_with_maps(current_usage, usage, delta)
end
defp apply_compaction_usage_update(current_usage, _compaction), do: current_usage
defp update_usage_with_maps(current_usage, usage_map, delta_map) do
cond do
is_map(usage_map) and map_size(usage_map) > 0 ->
base_usage = overlay_usage(current_usage, usage_map)
merge_usage_delta(base_usage, usage_map, current_usage, delta_map)
is_map(delta_map) ->
merge_usage(current_usage || %{}, delta_map)
is_map(usage_map) ->
overlay_usage(current_usage, usage_map)
true ->
current_usage
end
end
defp overlay_usage(current_usage, usage_map) do
Map.merge(current_usage || %{}, usage_map || %{}, fn _key, _left, right -> right end)
end
defp merge_usage_delta(base_usage, usage_map, current_usage, delta_map)
when is_map(delta_map) do
Enum.reduce(delta_map, base_usage, fn {key, value}, acc ->
if Map.has_key?(usage_map || %{}, key) do
acc
else
previous = Map.get(current_usage || %{}, key)
Map.put(acc, key, add_usage(previous, value))
end
end)
end
defp merge_usage_delta(base_usage, _usage_map, _current_usage, _delta_map), do: base_usage
defp add_usage(nil, value), do: value
defp add_usage(value, nil), do: value
defp add_usage(left, right) when is_number(left) and is_number(right), do: left + right
defp add_usage(_left, right), do: right
defp get_usage_map(map, key) when is_map(map) do
fetch_map_value(map, key) || fetch_map_value(map, to_string(key))
end
defp get_usage_map(_map, _key), do: nil
defp fetch_map_value(map, key) do
case Map.fetch(map, key) do
{:ok, value} when is_map(value) -> value
_ -> nil
end
end
defp extract_source(metadata) when is_map(metadata) do
Map.get(metadata, :source) || Map.get(metadata, "source")
end
defp extract_source(_metadata), do: nil
defp decode_final_response(nil, _structured?), do: nil
defp decode_final_response(%Items.AgentMessage{} = item, _structured?), do: item
defp decode_final_response(%{"type" => "text", "text" => text}, structured?)
when is_binary(text) do
decode_agent_message(nil, text, structured?)
end
defp decode_final_response(%{type: "text", text: text}, structured?) when is_binary(text) do
decode_agent_message(nil, text, structured?)
end
defp decode_final_response(_other, _structured?), do: nil
defp decode_agent_message(id, text, structured?) do
maybe_parse_structured(id, text, structured?)
end
defp maybe_decode_agent_item(%Items.AgentMessage{id: id} = item, text, structured?) do
case maybe_parse_structured(id, text, structured?) do
%Items.AgentMessage{} = decoded -> decoded
_ -> item
end
end
defp maybe_parse_structured(id, text, true) when is_binary(text) do
with {:ok, decoded} <- Jason.decode(text) do
%Items.AgentMessage{id: id, text: text, parsed: decoded}
else
_ -> %Items.AgentMessage{id: id, text: text}
end
end
defp maybe_parse_structured(id, text, _structured?) when is_binary(text) do
%Items.AgentMessage{id: id, text: text}
end
defp maybe_parse_structured(_id, _text, _structured?), do: nil
defp maybe_decode_stream_event(
%Events.ItemCompleted{item: %Items.AgentMessage{text: text} = item} = event,
structured?
) do
decoded = maybe_decode_agent_item(item, text, structured?)
%Events.ItemCompleted{event | item: decoded}
end
defp maybe_decode_stream_event(
%Events.TurnCompleted{final_response: response} = event,
structured?
) do
decoded = decode_final_response(response, structured?) || response
%Events.TurnCompleted{event | final_response: decoded}
end
defp maybe_decode_stream_event(event, _structured?), do: event
defp extract_turn_failure(events) do
case Enum.find(events, &match?(%Events.TurnFailed{}, &1)) do
%Events.TurnFailed{error: error} ->
{:error, Error.normalize(error)}
nil ->
case Enum.find(events, fn
%Events.TurnCompleted{status: status}
when status in ["failed", :failed, "error"] ->
true
_ ->
false
end) do
%Events.TurnCompleted{final_response: response, status: status} ->
{:error, Error.normalize(turn_completed_error_payload(response, status))}
_ ->
:ok
end
end
end
defp telemetry_result(:ok, true), do: :early_exit
defp telemetry_result(:ok, false), do: :ok
defp telemetry_result({:error, _}, _), do: :error
defp emit_progress_events(events, meta) when is_list(events) do
Enum.each(events, &emit_progress_event(&1, meta))
end
defp emit_progress_events(_events, _meta), do: :ok
defp emit_progress_event(%Events.ThreadTokenUsageUpdated{} = event, meta) do
Telemetry.emit(
[:codex, :thread, :token_usage, :updated],
%{system_time: System.system_time()},
progress_metadata(meta, %{
thread_id: event.thread_id,
turn_id: event.turn_id,
usage: event.usage,
delta: event.delta
})
)
end
defp emit_progress_event(%Events.TurnDiffUpdated{} = event, meta) do
Telemetry.emit(
[:codex, :turn, :diff, :updated],
%{system_time: System.system_time()},
progress_metadata(meta, %{
thread_id: event.thread_id,
turn_id: event.turn_id,
diff: event.diff
})
)
end
defp emit_progress_event(%Events.TurnCompaction{stage: stage} = event, meta) do
stage_name = normalize_compaction_stage(stage)
measurements =
%{
system_time: System.system_time()
}
|> maybe_put(:token_savings, compaction_token_savings(event.compaction))
Telemetry.emit(
[:codex, :turn, :compaction, stage_name],
measurements,
progress_metadata(meta, %{
thread_id: event.thread_id,
turn_id: event.turn_id,
compaction: event.compaction,
stage: stage
})
)
end
defp emit_progress_event(_event, _meta), do: :ok
defp progress_metadata(meta, updates) do
meta
|> merge_metadata(updates)
end
defp merge_metadata(left, right) when is_map(left) and is_map(right) do
Map.merge(left, right, fn _key, original, updated ->
if is_nil(updated), do: original, else: updated
end)
end
defp merge_metadata(left, _right), do: left
defp extract_thread_context(events, thread) when is_list(events) do
Enum.reduce(
events,
%{thread_id: thread.thread_id, turn_id: nil, source: extract_source(thread.metadata)},
fn
%Events.ThreadStarted{thread_id: id, metadata: metadata}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:source, extract_source(metadata))
%Events.TurnStarted{thread_id: id, turn_id: turn_id}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:turn_id, turn_id)
%Events.TurnContinuation{thread_id: id, turn_id: turn_id}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:turn_id, turn_id)
%Events.TurnCompleted{thread_id: id, turn_id: turn_id, usage: usage}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:turn_id, turn_id)
|> maybe_put(:source, extract_source(usage))
%Events.ThreadTokenUsageUpdated{thread_id: id, turn_id: turn_id}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:turn_id, turn_id)
%Events.TurnDiffUpdated{thread_id: id, turn_id: turn_id}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:turn_id, turn_id)
%Events.TurnCompaction{thread_id: id, turn_id: turn_id, compaction: compaction}, acc ->
acc
|> maybe_put(:thread_id, id)
|> maybe_put(:turn_id, turn_id)
|> maybe_put(:source, extract_source(compaction))
_event, acc ->
acc
end
)
end
defp extract_thread_context(_events, thread) do
%{thread_id: thread.thread_id, turn_id: nil, source: extract_source(thread.metadata)}
end
defp compaction_token_savings(compaction) do
case compaction do
%{} ->
Map.get(compaction, :token_savings) || Map.get(compaction, "token_savings")
_ ->
nil
end
end
defp normalize_compaction_stage(stage) when is_atom(stage), do: stage
defp normalize_compaction_stage(stage) when is_binary(stage) do
stage
|> String.downcase()
|> case do
"started" -> :started
"completed" -> :completed
"failed" -> :failed
_ -> :unknown
end
end
defp normalize_compaction_stage(_stage), do: :unknown
defp failure_meta({:error, %Error{} = error}) do
%{kind: error.kind, message: error.message}
end
defp failure_meta(_), do: nil
defp early_exit?(events) do
Enum.any?(events, fn
%Events.TurnCompleted{status: status} when status in ["early_exit", :early_exit] -> true
_ -> false
end)
end
defp maybe_reset_for_new(%__MODULE__{} = thread, input) do
if new_command?(input) do
reset_conversation(thread)
else
thread
end
end
defp new_command?(input) when is_binary(input) do
String.trim(input) == "/new"
end
defp reset_conversation(%__MODULE__{} = thread) do
%__MODULE__{
thread
| thread_id: nil,
metadata: %{},
labels: %{},
continuation_token: nil,
usage: %{},
pending_tool_outputs: [],
pending_tool_failures: []
}
end
defp turn_completed_error_payload(%Items.AgentMessage{text: text}, status),
do: %{"message" => text, "type" => status}
defp turn_completed_error_payload(%{"text" => text}, status) when is_binary(text),
do: %{"message" => text, "type" => status}
defp turn_completed_error_payload(%{text: text}, status) when is_binary(text),
do: %{"message" => text, "type" => status}
defp turn_completed_error_payload(response, status) when is_map(response),
do: Map.put(response, "type", status)
defp turn_completed_error_payload(response, status),
do: %{"message" => to_string(response), "type" => status}
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp handle_tool_requests(%Result{} = result, attempt) do
tool_events = Enum.filter(result.events, &match?(%Events.ToolCallRequested{}, &1))
Enum.reduce_while(tool_events, {:ok, result}, fn event, {:ok, acc_result} ->
case maybe_invoke_tool(acc_result.thread, event, attempt) do
{:ok, output} ->
outputs = Map.get(acc_result.raw, :tool_outputs, [])
pending_outputs =
acc_result.thread.pending_tool_outputs ++ [%{call_id: event.call_id, output: output}]
updated_raw =
Map.put(
acc_result.raw,
:tool_outputs,
outputs ++ [%{call_id: event.call_id, output: output}]
)
updated_thread =
Map.put(acc_result.thread, :pending_tool_outputs, pending_outputs)
updated_result = %Result{acc_result | raw: updated_raw, thread: updated_thread}
{:cont, {:ok, updated_result}}
{:failure, reason} ->
failures =
Map.get(acc_result.raw, :tool_failures, []) ++
[%{call_id: event.call_id, reason: normalize_tool_failure(reason)}]
pending_failures =
acc_result.thread.pending_tool_failures ++
[%{call_id: event.call_id, reason: normalize_tool_failure(reason)}]
updated_raw = Map.put(acc_result.raw, :tool_failures, failures)
updated_thread =
Map.put(acc_result.thread, :pending_tool_failures, pending_failures)
updated_result = %Result{acc_result | raw: updated_raw, thread: updated_thread}
{:cont, {:ok, updated_result}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end)
end
defp handle_tool_requests(result, _attempt), do: {:ok, result}
defp maybe_invoke_tool(thread, %Events.ToolCallRequested{} = event, attempt) do
context = build_tool_context(thread, event, attempt)
# Prefer approval_hook over approval_policy
policy_or_hook = thread.thread_opts.approval_hook || thread.thread_opts.approval_policy
timeout = thread.thread_opts.approval_timeout_ms || 30_000
case Approvals.review_tool(policy_or_hook, event, context, timeout: timeout) do
:allow ->
case Tools.invoke(event.tool_name, event.arguments, context) do
{:ok, output} ->
{:ok, output}
{:error, reason} ->
{:failure,
Error.new(:tool_failure, "tool #{event.tool_name} failed", %{
tool: event.tool_name,
reason: reason
})}
end
{:deny, reason} ->
{:error, ApprovalError.new(event.tool_name, reason)}
end
end
defp build_tool_context(thread, event, attempt) do
metadata = thread.thread_opts.metadata || %{}
tool_context =
metadata[:tool_context] || metadata["tool_context"] || %{}
warnings =
Map.get(event, :sandbox_warnings) || Map.get(event, "sandbox_warnings") ||
Map.get(event, :warnings) || Map.get(event, "warnings")
capabilities = Map.get(event, :capabilities) || Map.get(event, "capabilities")
%{
thread: thread,
metadata: metadata,
context: tool_context,
event: event,
attempt: attempt,
retry?: attempt > 1
}
|> maybe_put(:capabilities, capabilities)
|> maybe_put(:sandbox_warnings, warnings)
end
defp normalize_tool_failure(%Codex.Error{} = error) do
details =
case error.details do
value when is_map(value) -> value
_ -> %{}
end
%{
message: error.message,
kind: error.kind,
details: details
}
end
defp normalize_tool_failure(value) when is_map(value), do: value
defp normalize_tool_failure(value) when is_binary(value), do: %{message: value}
defp normalize_tool_failure(value), do: %{message: inspect(value)}
end