LemonCliRunners.CodexSubagent (lemon_cli_runners v0.1.0)

View Source

High-level API for using Codex as a collaborating subagent.

This module provides a convenient interface for spawning Codex CLI sessions and interacting with them over time. Unlike one-shot LLM calls, Codex subagents maintain state across multiple prompts, enabling iterative collaboration.

Features

  • Long-lived sessions: Keep talking to the same Codex session
  • Session persistence: Resume sessions after interruptions
  • Event streaming: Process events as they happen
  • Progress tracking: Monitor commands, tool calls, file changes

Quick Start

# Start a new session
{:ok, session} = CodexSubagent.start(
  prompt: "Create a GenServer that manages a counter",
  cwd: "/path/to/project"
)

# Process events
for event <- CodexSubagent.events(session) do
  case event do
    {:started, resume_token} ->
      IO.puts("Session: #{resume_token.value}")

    {:action, action, :completed, ok: true} ->
      IO.puts("Completed: #{action.title}")

    {:completed, answer, _opts} ->
      IO.puts("Done: #{answer}")
  end
end

# Send a follow-up prompt to continue the conversation
{:ok, session2} = CodexSubagent.continue(session, "Now add decrement functionality")

Session Resume

Sessions can be resumed after the process terminates:

# Get the resume token from a completed session
token = CodexSubagent.resume_token(session)

# Later, resume the session
{:ok, session} = CodexSubagent.resume(token, "Continue from where we left off")

Event Types

Events are normalized into a simple format:

  • {:started, resume_token} - Session began
  • {:action, action, phase, opts} - Action lifecycle (phase = :started | :updated | :completed)

  • {:completed, answer, opts} - Session ended (opts may include :resume, :error, :usage)

Integration with Main Agent

Use Codex subagents when you need:

  • Autonomous code generation with review
  • Multi-step refactoring tasks
  • Complex implementations that benefit from Codex's reasoning

Example tool integration:

def codex_subagent_tool(cwd) do
  %AgentTool{
    name: "codex_subagent",
    description: "Spawn a Codex subagent to handle a complex coding task",
    parameters: %{
      "type" => "object",
      "properties" => %{
        "task" => %{"type" => "string", "description" => "The task to perform"}
      },
      "required" => ["task"]
    },
    execute: fn _id, %{"task" => task}, _signal, on_update ->
      {:ok, session} = CodexSubagent.start(prompt: task, cwd: cwd)

      # Collect answer
      answer = CodexSubagent.collect_answer(session)

      %AgentToolResult{
        content: [%LemonAi.Types.TextContent{text: answer}],
        details: %{resume_token: CodexSubagent.resume_token(session)}
      }
    end
  }
end

Summary

Types

A Codex subagent session

Normalized event from the subagent

Functions

Collect all events and return the final answer.

Continue an existing session with a follow-up prompt.

Get the event stream as an enumerable of normalized events.

Resume an existing Codex session with a new prompt.

Get the resume token from a session.

Run a Codex task synchronously and return the answer.

Start a new Codex subagent session.

Types

session()

@type session() :: %{
  pid: pid(),
  stream: LemonAgent.EventStream.t(),
  resume_token: LemonCore.ResumeToken.t() | nil,
  token_agent: pid() | nil,
  cwd: String.t()
}

A Codex subagent session

subagent_event()

@type subagent_event() ::
  {:started, LemonCore.ResumeToken.t()}
  | {:action, action :: map(), phase :: atom(), opts :: keyword()}
  | {:completed, answer :: String.t(), opts :: keyword()}
  | {:error, reason :: term()}

Normalized event from the subagent

Functions

collect_answer(session)

@spec collect_answer(session()) :: String.t()

Collect all events and return the final answer.

This is a convenience function that processes all events and returns the final answer string. Useful when you don't need to track progress.

Example

{:ok, session} = CodexSubagent.start(prompt: "What is 2+2?", cwd: ".")
answer = CodexSubagent.collect_answer(session)
IO.puts(answer)  # "4" or similar

continue(session, prompt, opts \\ [])

@spec continue(session(), String.t(), keyword()) ::
  {:ok, session()} | {:error, term()}

Continue an existing session with a follow-up prompt.

This is a convenience wrapper around resume/2 that extracts the resume token from a completed session.

Example

{:ok, session1} = CodexSubagent.start(prompt: "Create a module")
_events = CodexSubagent.events(session1) |> Enum.to_list()

{:ok, session2} = CodexSubagent.continue(session1, "Add a public function")

events(session)

@spec events(session()) :: Enumerable.t()

Get the event stream as an enumerable of normalized events.

Events are transformed from internal CLI runner events to a simpler format. The stream completes when the session ends.

Event Format

  • {:started, resume_token} - Session began, token can be used for resume
  • {:action, action, phase, opts} - Action lifecycle event
    • action is a map with :id, :kind, :title, :detail
    • phase is :started, :updated, or :completed
    • opts may include ok: boolean() for completed phase
  • {:completed, answer, opts} - Session ended
    • opts may include :resume, :error, :usage
  • {:error, reason} - Error occurred

Example

for event <- CodexSubagent.events(session) do
  case event do
    {:started, token} ->
      IO.puts("Session started: #{token.value}")

    {:action, %{kind: :command, title: cmd}, :started, _} ->
      IO.puts("Running: #{cmd}")

    {:action, %{kind: :command}, :completed, ok: false} ->
      IO.puts("Command failed!")

    {:completed, answer, opts} ->
      if opts[:error] do
        IO.puts("Error: #{opts[:error]}")
      else
        IO.puts("Answer: #{answer}")
      end

    _ -> :ok
  end
end

resume(token, opts)

@spec resume(
  LemonCore.ResumeToken.t(),
  keyword()
) :: {:ok, session()} | {:error, term()}

Resume an existing Codex session with a new prompt.

Example

token = %ResumeToken{engine: "codex", value: "thread_abc123"}

{:ok, session} = CodexSubagent.resume(token,
  prompt: "Continue implementing the delete method",
  cwd: "/home/user/project"
)

resume_token(session)

@spec resume_token(session()) :: LemonCore.ResumeToken.t() | nil

Get the resume token from a session.

The token is populated after the session starts and can be used to resume the session later. The token is updated as events are processed, so call this after processing events.

run!(opts)

@spec run!(keyword()) :: String.t()

Run a Codex task synchronously and return the answer.

This is a convenience function that starts a session, waits for completion, and returns the answer.

Options

  • :prompt - The task prompt (required)
  • :cwd - Working directory
  • :timeout - Timeout in ms
  • :on_event - Optional callback fn event -> :ok end for progress tracking

Example

answer = CodexSubagent.run!(
  prompt: "Explain what this code does: ...",
  cwd: "/path/to/project",
  on_event: fn event -> IO.inspect(event) end
)

start(opts)

@spec start(keyword()) :: {:ok, session()} | {:error, term()}

Start a new Codex subagent session.

Options

  • :prompt - The initial prompt/task (required)
  • :cwd - Working directory (default: current directory)
  • :timeout - Session timeout in ms (default: :infinity)
  • :model - Optional model override (passed to Codex CLI --model)

Returns

{:ok, session} on success, {:error, reason} on failure.

Example

{:ok, session} = CodexSubagent.start(
  prompt: "Implement a binary search tree",
  cwd: "/home/user/project"
)