Current section
Files
Jump to
Current section
Files
examples/live_collaboration_modes.exs
Mix.Task.run("app.start")
Code.require_file(Path.expand("support/example_helper.exs", __DIR__))
alias CodexExamples.Support
Support.init!()
alias Codex.ExamplesSupport
alias Codex.{AppServer, Items, Models, Thread}
alias Codex.Protocol.CollaborationMode
defmodule LiveCollaborationModes do
@moduledoc false
@default_prompt """
Give a 3-step plan for testing a CSV parser.
Do not run commands, inspect files, or modify anything.
"""
@preferred_modes [:plan, :pair_programming, :code, :default, :execute, :custom]
def main(argv) do
case run(argv) do
:ok ->
:ok
{:skip, reason} ->
IO.puts("SKIPPED: #{reason}")
{:error, reason} ->
IO.puts("Failed to run collaboration mode example: #{inspect(reason)}")
System.halt(1)
end
end
defp run(argv) do
prompt = parse_prompt(argv)
with {:ok, codex_opts} <- Support.codex_options(%{}, missing_cli: :skip),
:ok <- Support.ensure_app_server_supported(codex_opts),
:ok <- Support.ensure_remote_working_directory(),
{:ok, conn, experimental_api?, fallback_reason} <-
connect_for_collaboration_modes(codex_opts) do
try do
IO.puts("collaboration_mode/list:")
if not experimental_api? do
IO.puts("""
experimentalApi initialize fallback:
#{format_connect_reason(fallback_reason)}
Reconnected without experimental app-server fields; this run will skip if the connected
build still does not advertise `collaborationMode/list`.
""")
end
with {:ok, available_modes} <- list_supported_modes(conn),
{:ok, selected_mode} <- choose_mode(available_modes) do
run_turn(codex_opts, conn, prompt, selected_mode)
end
after
:ok = AppServer.disconnect(conn)
end
end
end
defp list_supported_modes(conn) do
case AppServer.collaboration_mode_list(conn) do
{:ok, response} ->
IO.inspect(response)
{:ok, extract_mode_atoms(response)}
{:error, %{"code" => -32600, "message" => message}} when is_binary(message) ->
if unsupported_capability?(message) do
{:skip,
"collaborationMode/list requires experimental API support in this Codex CLI build"}
else
{:error, {:collaboration_mode_list_failed, message}}
end
{:error, reason} ->
{:error, {:collaboration_mode_list_failed, reason}}
end
end
defp run_turn(codex_opts, conn, prompt, selected_mode) do
{model, model_note} = resolve_selected_model(selected_mode)
{effort, effort_note} = resolve_selected_effort(selected_mode, model)
mode = %CollaborationMode{
mode: selected_mode.mode,
model: model,
reasoning_effort: effort,
developer_instructions: nil
}
IO.puts("""
Using server-advertised collaboration preset:
mode: #{mode.mode}
model: #{mode.model || "<cli default>"}#{model_note}
reasoning_effort: #{mode.reasoning_effort || "none"}#{effort_note}
developer_instructions: built-in preset instructions (`settings.developer_instructions = null`)
""")
with {:ok, thread} <-
Codex.start_thread(
codex_opts,
Support.thread_opts!(%{
transport: {:app_server, conn},
working_directory: Support.example_working_directory()
})
),
{:ok, result} <-
Thread.run(thread, prompt, %{collaboration_mode: mode, timeout_ms: 120_000}) do
IO.puts("""
Turn completed with collaboration_mode=#{mode.mode}.
model: #{mode.model || "<cli default>"}
reasoning_effort: #{mode.reasoning_effort || "none"}
developer_instructions: built-in preset instructions
final_response: #{format_final_response(result.final_response || fallback_final_response(result.events))}
""")
:ok
else
{:error, reason} -> {:error, reason}
end
end
defp choose_mode(available_modes) when is_list(available_modes) do
case Enum.find(@preferred_modes, fn preferred ->
Enum.any?(available_modes, &(&1.mode == preferred))
end) do
nil ->
{:skip, "no supported collaboration mode was advertised by this Codex build"}
mode ->
{:ok, Enum.find(available_modes, &(&1.mode == mode))}
end
end
defp choose_mode(_), do: {:skip, "unable to determine supported collaboration modes"}
defp extract_mode_atoms(%{"data" => data}) when is_list(data), do: normalize_mode_entries(data)
defp extract_mode_atoms(%{data: data}) when is_list(data), do: normalize_mode_entries(data)
defp extract_mode_atoms(_), do: []
defp normalize_mode_entries(entries) do
entries
|> Enum.map(&normalize_mode_entry/1)
|> Enum.reject(&is_nil/1)
|> Enum.uniq_by(& &1.mode)
end
defp normalize_mode_entry(entry) when is_binary(entry) do
case normalize_mode_value(entry) do
nil -> nil
mode -> %{mode: mode, model: nil, reasoning_effort: nil}
end
end
defp normalize_mode_entry(%{} = entry) do
with mode when not is_nil(mode) <- normalize_mode_from_entry(entry) do
%{
mode: mode,
model: extract_mode_model(entry),
reasoning_effort: extract_mode_effort(entry)
}
end
end
defp normalize_mode_entry(_), do: nil
defp normalize_mode_from_entry(entry) do
[
Map.get(entry, "mode"),
Map.get(entry, :mode),
Map.get(entry, "id"),
Map.get(entry, :id),
Map.get(entry, "name"),
Map.get(entry, :name),
get_in(entry, ["preset", "mode"]),
get_in(entry, [:preset, :mode])
]
|> Enum.map(&normalize_mode_value/1)
|> Enum.find(& &1)
end
defp extract_mode_model(entry) do
Map.get(entry, "model") ||
Map.get(entry, :model) ||
get_in(entry, ["settings", "model"]) ||
get_in(entry, [:settings, :model])
end
defp extract_mode_effort(entry) do
entry
|> List.wrap()
|> then(fn _ ->
Map.get(entry, "reasoning_effort") ||
Map.get(entry, :reasoning_effort) ||
Map.get(entry, "reasoningEffort") ||
Map.get(entry, :reasoningEffort) ||
get_in(entry, ["settings", "reasoning_effort"]) ||
get_in(entry, ["settings", "reasoningEffort"]) ||
get_in(entry, [:settings, :reasoning_effort]) ||
get_in(entry, [:settings, :reasoningEffort])
end)
|> normalize_effort_value()
end
defp normalize_mode_value(value) when is_atom(value),
do: normalize_mode_value(Atom.to_string(value))
defp normalize_mode_value(value) when is_binary(value) do
case String.downcase(String.trim(value)) do
"pair_programming" -> :pair_programming
"pairprogramming" -> :pair_programming
"pair-programming" -> :pair_programming
"code" -> :code
"default" -> :default
"plan" -> :plan
"execute" -> :execute
"custom" -> :custom
_ -> nil
end
end
defp normalize_mode_value(_), do: nil
defp normalize_effort_value(nil), do: nil
defp normalize_effort_value(value) when is_atom(value) do
case Models.normalize_reasoning_effort(value) do
{:ok, effort} -> effort
_ -> nil
end
end
defp normalize_effort_value(value) when is_binary(value) do
case Models.normalize_reasoning_effort(value) do
{:ok, effort} -> effort
_ -> nil
end
end
defp normalize_effort_value(_), do: nil
defp resolve_selected_model(selected_mode) do
case {ExamplesSupport.ollama_mode?(), selected_mode.model} do
{true, model} ->
{ExamplesSupport.example_model(model),
" (Ollama mode forces the selected local OSS model)"}
{false, model} when is_binary(model) and model != "" ->
{model, " (advertised by the server preset)"}
_ ->
{ExamplesSupport.example_model(Models.default_model()),
" (server omitted model; collaboration presets require an explicit model, so using the current bundled default unless CODEX_MODEL is set)"}
end
end
defp resolve_selected_effort(selected_mode, model) do
case {ExamplesSupport.ollama_mode?(), selected_mode.reasoning_effort} do
{true, _effort} ->
{nil, " (Ollama mode does not force a reasoning effort)"}
{false, effort} when not is_nil(effort) ->
note =
if effort == :low do
" (advertised by the server preset)"
else
" (advertised by the server preset; this intentionally overrides the runtime default)"
end
{effort, note}
{false, _effort} when is_nil(model) ->
{nil,
" (server omitted effort; deferring to the installed codex CLI/model default)"}
_ ->
effort = ExamplesSupport.example_reasoning(Models.default_reasoning_effort(model))
note =
" (server omitted effort; using the selected model default)"
{effort, note}
end
end
defp unsupported_capability?(message) do
lowered = String.downcase(message)
String.contains?(lowered, "experimentalapi") or
String.contains?(lowered, "requires experimentalapi capability") or
String.contains?(lowered, "method not found")
end
defp connect_for_collaboration_modes(codex_opts) do
experimental_opts = [init_timeout_ms: 30_000, experimental_api: true]
case AppServer.connect(codex_opts, experimental_opts) do
{:ok, conn} ->
{:ok, conn, true, nil}
{:error, {:init_failed, reason}} ->
if experimental_api_rejected?(reason) do
case AppServer.connect(codex_opts, init_timeout_ms: 30_000) do
{:ok, conn} ->
{:ok, conn, false, reason}
{:error, retry_reason} ->
{:error, {:experimental_api_init_failed, reason, retry_reason}}
end
else
{:error, {:init_failed, reason}}
end
{:error, reason} ->
{:error, reason}
end
end
defp experimental_api_rejected?(%{} = reason) do
message =
reason
|> Map.get("message", Map.get(reason, :message, ""))
|> to_string()
|> String.downcase()
String.contains?(message, "experimentalapi") or
String.contains?(message, "experimental api") or
(String.contains?(message, "capabilities") and
(String.contains?(message, "unknown field") or
String.contains?(message, "unexpected field") or
String.contains?(message, "invalid params")))
end
defp experimental_api_rejected?(_reason), do: false
defp format_connect_reason(%{} = reason) do
reason
|> Map.take(["code", "message"])
|> inspect()
end
defp format_connect_reason(reason), do: inspect(reason)
defp parse_prompt([prompt | _]), do: prompt
defp parse_prompt(_argv), do: @default_prompt
defp extract_text(%Items.AgentMessage{text: text}) when is_binary(text), do: text
defp extract_text(%{"type" => "text", "text" => text}) when is_binary(text), do: text
defp extract_text(%{type: "text", text: text}) when is_binary(text), do: text
defp extract_text(other) when is_binary(other), do: other
defp extract_text(other), do: inspect(other)
defp fallback_final_response(events) when is_list(events) do
events
|> Enum.reverse()
|> Enum.find_value(fn
%Codex.Events.ItemCompleted{item: %Items.AgentMessage{} = item} -> item
_other -> nil
end)
end
defp fallback_final_response(_events), do: nil
defp format_final_response(nil), do: "(no final assistant message returned)"
defp format_final_response(response), do: extract_text(response)
end
LiveCollaborationModes.main(System.argv())