defmodule CodexAppServer do @moduledoc """ Elixir client for the [OpenAI Codex app-server](https://developers.openai.com/codex/app-server/) JSON-RPC 2.0 protocol over stdio. ## Quick start {:ok, result} = CodexAppServer.run("/path/to/workspace", "Fix the auth bug", title: "ABC-123: Fix auth", approval_policy: "never", sandbox: "workspace-write", sandbox_policy: %{"type" => "workspaceWrite"} ) ## Multi-turn sessions {:ok, session} = CodexAppServer.start_session("/path/to/workspace", approval_policy: "never", sandbox: "workspace-write" ) {:ok, turn1} = CodexAppServer.run_turn(session, "Fix the bug", title: "ABC-123: Fix auth", sandbox_policy: %{"type" => "workspaceWrite"} ) {:ok, turn2} = CodexAppServer.run_turn(session, "Now add tests", title: "ABC-123: Fix auth", sandbox_policy: %{"type" => "workspaceWrite"} ) CodexAppServer.stop_session(session) ## Dynamic tools Register client-side tools that Codex can invoke during a turn: tools = [ %{ "name" => "my_api", "description" => "Query my API", "inputSchema" => %{"type" => "object", "properties" => %{"query" => %{"type" => "string"}}} } ] CodexAppServer.run(workspace, prompt, tools: tools, tool_executor: fn "my_api", args -> %{"success" => true, "contentItems" => [...]} end ) """ alias CodexAppServer.Session @type run_opt :: {:command, String.t()} | {:title, String.t()} | {:approval_policy, String.t() | map()} | {:sandbox, String.t()} | {:sandbox_policy, map()} | {:tools, [map()]} | {:tool_executor, (String.t(), map() -> map())} | {:on_message, (map() -> any())} | {:turn_timeout_ms, pos_integer()} | {:read_timeout_ms, pos_integer()} | {:client_info, map()} @type session_opt :: {:command, String.t()} | {:approval_policy, String.t() | map()} | {:sandbox, String.t()} | {:tools, [map()]} | {:read_timeout_ms, pos_integer()} | {:client_info, map()} @type turn_opt :: {:title, String.t()} | {:approval_policy, String.t() | map()} | {:sandbox_policy, map()} | {:tools, [map()]} | {:tool_executor, (String.t(), map() -> map())} | {:on_message, (map() -> any())} | {:turn_timeout_ms, pos_integer()} @doc """ Run a single Codex turn in one call. Starts a session, runs the turn, and stops the session. Returns `{:ok, result}` on success or `{:error, reason}` on failure. """ @spec run(Path.t(), String.t(), [run_opt()]) :: {:ok, map()} | {:error, term()} def run(workspace, prompt, opts \\ []) do {session_opts, turn_opts} = split_opts(opts) with {:ok, session} <- start_session(workspace, session_opts) do try do run_turn(session, prompt, turn_opts) after stop_session(session) end end end @doc """ Start a long-lived Codex app-server session for multi-turn usage. The session spawns the Codex subprocess and completes the `initialize` / `thread/start` handshake. Use `run_turn/3` to send prompts and `stop_session/1` when done. """ @spec start_session(Path.t(), [session_opt()]) :: {:ok, Session.t()} | {:error, term()} def start_session(workspace, opts \\ []) do Session.start(workspace, opts) end @doc """ Run a turn on an existing session. Blocks until the turn completes, fails, or times out. """ @spec run_turn(Session.t(), String.t(), [turn_opt()]) :: {:ok, map()} | {:error, term()} def run_turn(session, prompt, opts \\ []) do Session.run_turn(session, prompt, opts) end @doc """ Stop a session and terminate the Codex subprocess. """ @spec stop_session(Session.t()) :: :ok def stop_session(session) do Session.stop(session) end defp split_opts(opts) do session_keys = [:command, :approval_policy, :sandbox, :tools, :read_timeout_ms, :client_info] session_opts = Keyword.take(opts, session_keys) turn_opts = Keyword.drop(opts, [:command, :sandbox, :read_timeout_ms, :client_info]) {session_opts, turn_opts} end end