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
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
@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
@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 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 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")
@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 eventactionis a map with:id,:kind,:title,:detailphaseis:started,:updated, or:completedoptsmay includeok: boolean()for completed phase
{:completed, answer, opts}- Session endedoptsmay 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
@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"
)
@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 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 callbackfn event -> :ok endfor 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 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"
)