Current section
Files
Jump to
Current section
Files
lib/codex/items.ex
defmodule Codex.Items do
@moduledoc """
Typed representations of thread items emitted by the Codex runtime.
This module provides helpers to convert JSON-style maps (with string keys)
into structs and back, keeping status fields normalised as atoms.
"""
alias __MODULE__.{
AgentMessage,
CommandExecution,
Error,
FileChange,
ImageView,
McpToolCall,
Reasoning,
ReviewMode,
TodoList,
UserMessage,
WebSearch
}
@type t ::
AgentMessage.t()
| Reasoning.t()
| CommandExecution.t()
| FileChange.t()
| UserMessage.t()
| ImageView.t()
| ReviewMode.t()
| McpToolCall.t()
| WebSearch.t()
| TodoList.t()
| Error.t()
defmodule AgentMessage do
@moduledoc """
Assistant-authored message item emitted by the Codex runtime, with optional parsed
payloads for structured output experiments.
"""
@enforce_keys [:text]
defstruct id: nil, type: :agent_message, text: nil, parsed: nil
@type t :: %__MODULE__{
id: String.t() | nil,
type: :agent_message,
text: String.t(),
parsed: map() | list() | nil
}
end
defmodule Reasoning do
@moduledoc """
Intermediate reasoning trace shared as part of tool or agent transparency.
"""
@enforce_keys [:text]
defstruct id: nil, type: :reasoning, text: nil, summary: [], content: []
@type t :: %__MODULE__{
id: String.t() | nil,
type: :reasoning,
text: String.t(),
summary: [String.t()],
content: [String.t()]
}
end
defmodule CommandExecution do
@moduledoc """
Captures an execution request made by the agent, including aggregated output and
status metadata.
"""
@enforce_keys [:command]
defstruct id: nil,
type: :command_execution,
command: nil,
cwd: nil,
process_id: nil,
command_actions: [],
aggregated_output: "",
exit_code: nil,
status: :in_progress,
duration_ms: nil
@type status :: :in_progress | :completed | :failed | :declined
@type t :: %__MODULE__{
id: String.t() | nil,
type: :command_execution,
command: String.t(),
cwd: String.t() | nil,
process_id: String.t() | nil,
command_actions: [map()],
aggregated_output: String.t(),
exit_code: integer() | nil,
status: status(),
duration_ms: integer() | nil
}
end
defmodule FileChange do
@moduledoc """
Represents a file diff emitted by the agent, including per-path change metadata and
completion status.
"""
@enforce_keys [:changes, :status]
defstruct id: nil,
type: :file_change,
changes: [],
status: :completed
@type change_kind :: :add | :delete | :update
@type change :: %{
required(:path) => String.t(),
required(:kind) => change_kind(),
optional(:diff) => String.t(),
optional(:move_path) => String.t() | nil
}
@type status :: :in_progress | :completed | :failed | :declined
@type t :: %__MODULE__{
id: String.t() | nil,
type: :file_change,
changes: [change()],
status: status()
}
end
defmodule McpToolCall do
@moduledoc """
Metadata describing a tool invocation routed through an MCP server.
"""
@enforce_keys [:server, :tool]
defstruct id: nil,
type: :mcp_tool_call,
server: nil,
tool: nil,
arguments: nil,
result: nil,
error: nil,
status: :in_progress,
duration_ms: nil
@type status :: :in_progress | :completed | :failed
@type t :: %__MODULE__{
id: String.t() | nil,
type: :mcp_tool_call,
server: String.t(),
tool: String.t(),
arguments: map() | list() | nil,
result: map() | nil,
error: map() | nil,
status: status(),
duration_ms: integer() | nil
}
end
defmodule UserMessage do
@moduledoc """
User-authored message item carrying a list of input blocks.
"""
@enforce_keys [:content]
defstruct id: nil, type: :user_message, content: []
@type t :: %__MODULE__{
id: String.t() | nil,
type: :user_message,
content: [map()]
}
end
defmodule ImageView do
@moduledoc """
An image view event emitted by the app-server when it renders a local image.
"""
@enforce_keys [:path]
defstruct id: nil, type: :image_view, path: nil
@type t :: %__MODULE__{
id: String.t() | nil,
type: :image_view,
path: String.t()
}
end
defmodule ReviewMode do
@moduledoc """
Indicates that review mode has been entered or exited.
"""
@enforce_keys [:review]
defstruct id: nil, type: :review_mode, entered: true, review: ""
@type t :: %__MODULE__{
id: String.t() | nil,
type: :review_mode,
entered: boolean(),
review: String.t()
}
end
defmodule WebSearch do
@moduledoc """
Records a web search request issued by the agent, preserving the original query.
"""
@enforce_keys [:query]
defstruct id: nil, type: :web_search, query: nil
@type t :: %__MODULE__{
id: String.t() | nil,
type: :web_search,
query: String.t()
}
end
defmodule TodoList do
@moduledoc """
Structured checklist shared by the agent to track outstanding follow-up items.
"""
@enforce_keys [:items]
defstruct id: nil, type: :todo_list, items: []
@type todo_item :: %{text: String.t(), completed: boolean()}
@type t :: %__MODULE__{
id: String.t() | nil,
type: :todo_list,
items: [todo_item()]
}
end
defmodule Error do
@moduledoc """
Normalised error record describing failures surfaced during a turn.
"""
@enforce_keys [:message]
defstruct id: nil, type: :error, message: nil
@type t :: %__MODULE__{
id: String.t() | nil,
type: :error,
message: String.t()
}
end
@command_status_map %{
"in_progress" => :in_progress,
"completed" => :completed,
"failed" => :failed,
"declined" => :declined
}
@file_change_status_map %{
"in_progress" => :in_progress,
"completed" => :completed,
"failed" => :failed,
"declined" => :declined
}
@file_change_kind_map %{
"add" => :add,
"delete" => :delete,
"update" => :update
}
@mcp_status_map %{
"in_progress" => :in_progress,
"completed" => :completed,
"failed" => :failed
}
@doc """
Parses a JSON-decoded map into a typed thread item struct.
"""
@spec parse!(map()) :: t()
def parse!(%{"type" => "agent_message"} = map), do: parse_agent_message(map)
def parse!(%{"type" => "reasoning"} = map), do: parse_reasoning(map)
def parse!(%{"type" => "command_execution"} = map), do: parse_command_execution(map)
def parse!(%{"type" => "file_change"} = map), do: parse_file_change(map)
def parse!(%{"type" => "user_message"} = map), do: parse_user_message(map)
def parse!(%{"type" => "image_view"} = map), do: parse_image_view(map)
def parse!(%{"type" => "review_mode"} = map), do: parse_review_mode(map)
def parse!(%{"type" => "mcp_tool_call"} = map), do: parse_mcp_tool_call(map)
def parse!(%{"type" => "web_search"} = map), do: parse_web_search(map)
def parse!(%{"type" => "todo_list"} = map), do: parse_todo_list(map)
def parse!(%{"type" => "error"} = map), do: parse_error(map)
def parse!(%{type: type} = map), do: parse!(Map.put(map, "type", type))
def parse!(%{"type" => other}) do
raise ArgumentError, "unsupported thread item type #{inspect(other)}"
end
def parse!(value) do
raise ArgumentError, "expected thread item map, got: #{inspect(value)}"
end
@doc """
Converts a typed item struct back into its JSON-serialisable map representation.
"""
@spec to_map(t()) :: map()
def to_map(%AgentMessage{} = item) do
base_item_map(item, "agent_message")
|> maybe_put("text", item.text)
end
def to_map(%Reasoning{} = item) do
base_item_map(item, "reasoning")
|> maybe_put("text", item.text)
|> maybe_put("summary", item.summary)
|> maybe_put("content", item.content)
end
def to_map(%CommandExecution{} = item) do
base_item_map(item, "command_execution")
|> maybe_put("command", item.command)
|> maybe_put("cwd", item.cwd)
|> maybe_put("process_id", item.process_id)
|> maybe_put("command_actions", item.command_actions)
|> maybe_put("aggregated_output", item.aggregated_output)
|> maybe_put("exit_code", item.exit_code)
|> maybe_put("status", status_to_string(item.status, @command_status_map))
|> maybe_put("duration_ms", item.duration_ms)
end
def to_map(%FileChange{} = item) do
base_item_map(item, "file_change")
|> maybe_put("status", status_to_string(item.status, @file_change_status_map))
|> maybe_put(
"changes",
Enum.map(item.changes, fn %{path: path, kind: kind} = change ->
%{
"path" => path,
"kind" => kind_to_string(kind, @file_change_kind_map)
}
|> maybe_put("diff", Map.get(change, :diff))
|> maybe_put("move_path", Map.get(change, :move_path))
end)
)
end
def to_map(%UserMessage{} = item) do
base_item_map(item, "user_message")
|> maybe_put("content", item.content)
end
def to_map(%ImageView{} = item) do
base_item_map(item, "image_view")
|> maybe_put("path", item.path)
end
def to_map(%ReviewMode{} = item) do
base_item_map(item, "review_mode")
|> maybe_put("entered", item.entered)
|> maybe_put("review", item.review)
end
def to_map(%McpToolCall{} = item) do
base_item_map(item, "mcp_tool_call")
|> maybe_put("server", item.server)
|> maybe_put("tool", item.tool)
|> maybe_put("arguments", item.arguments)
|> maybe_put("result", item.result)
|> maybe_put("error", item.error)
|> maybe_put("status", status_to_string(item.status, @mcp_status_map))
|> maybe_put("duration_ms", item.duration_ms)
end
def to_map(%WebSearch{} = item) do
base_item_map(item, "web_search")
|> maybe_put("query", item.query)
end
def to_map(%TodoList{} = item) do
base_item_map(item, "todo_list")
|> maybe_put(
"items",
Enum.map(item.items, fn %{text: text, completed: completed} ->
%{"text" => text, "completed" => completed}
end)
)
end
def to_map(%Error{} = item) do
base_item_map(item, "error")
|> maybe_put("message", item.message)
end
defp parse_agent_message(map) do
%AgentMessage{
id: get(map, :id),
text: get(map, :text) || "",
parsed: get(map, :parsed)
}
end
defp parse_reasoning(map) do
summary = normalize_reasoning_part(get(map, :summary))
content = normalize_reasoning_part(get(map, :content))
%Reasoning{
id: get(map, :id),
text: get(map, :text) || join_reasoning_text(summary, content) || "",
summary: summary,
content: content
}
end
defp parse_command_execution(map) do
%CommandExecution{
id: get(map, :id),
command: get(map, :command) || "",
cwd: get(map, :cwd),
process_id: get(map, :process_id),
command_actions: get(map, :command_actions) || [],
aggregated_output: get(map, :aggregated_output) || "",
exit_code: get(map, :exit_code),
status: parse_status(get(map, :status), @command_status_map, :in_progress),
duration_ms: get(map, :duration_ms)
}
end
defp parse_file_change(map) do
%FileChange{
id: get(map, :id),
status: parse_status(get(map, :status), @file_change_status_map, :completed),
changes:
map
|> get(:changes, [])
|> Enum.map(fn change ->
%{
path: get(change, :path) || "",
kind: parse_kind(get(change, :kind), @file_change_kind_map),
diff: get(change, :diff),
move_path: get(change, :move_path)
}
end)
}
end
defp parse_user_message(map) do
%UserMessage{
id: get(map, :id),
content: get(map, :content) || []
}
end
defp parse_image_view(map) do
%ImageView{
id: get(map, :id),
path: get(map, :path) || ""
}
end
defp parse_review_mode(map) do
%ReviewMode{
id: get(map, :id),
entered: !!get(map, :entered),
review: get(map, :review) || ""
}
end
defp parse_mcp_tool_call(map) do
%McpToolCall{
id: get(map, :id),
server: get(map, :server) || "",
tool: get(map, :tool) || "",
arguments: get(map, :arguments),
result: get(map, :result),
error: get(map, :error),
status: parse_status(get(map, :status), @mcp_status_map, :in_progress),
duration_ms: get(map, :duration_ms)
}
end
defp parse_web_search(map) do
%WebSearch{
id: get(map, :id),
query: get(map, :query) || ""
}
end
defp parse_todo_list(map) do
%TodoList{
id: get(map, :id),
items:
map
|> get(:items, [])
|> Enum.map(fn item ->
%{
text: get(item, :text) || "",
completed: !!get(item, :completed)
}
end)
}
end
defp parse_error(map) do
%Error{
id: get(map, :id),
message: get(map, :message) || ""
}
end
defp normalize_reasoning_part(nil), do: []
defp normalize_reasoning_part(list) when is_list(list), do: Enum.map(list, &to_string/1)
defp normalize_reasoning_part(value) when is_binary(value), do: [value]
defp normalize_reasoning_part(value), do: [to_string(value)]
defp join_reasoning_text(summary, content) do
parts =
[]
|> maybe_concat_lines(summary)
|> maybe_concat_lines(content)
if parts == [] do
nil
else
Enum.join(parts, "\n")
end
end
defp maybe_concat_lines(parts, lines) when is_list(lines) do
parts ++ Enum.map(lines, &to_string/1)
end
defp base_item_map(item, type) do
%{"type" => type}
|> maybe_put("id", item.id)
end
defp parse_status(nil, _mapping, default), do: default
defp parse_status(value, mapping, default) when is_atom(value) do
value
|> Atom.to_string()
|> parse_status(mapping, default)
end
defp parse_status(value, mapping, default) when is_binary(value) do
Map.get(mapping, value, default)
end
defp parse_status(value, _mapping, default) do
value
|> to_string()
|> parse_status(%{}, default)
end
defp parse_kind(nil, mapping), do: mapping["update"]
defp parse_kind(kind, mapping) when is_atom(kind) do
parse_kind(Atom.to_string(kind), mapping)
end
defp parse_kind(kind, mapping) when is_binary(kind) do
Map.get(mapping, kind, :update)
end
defp status_to_string(nil, _mapping), do: nil
defp status_to_string(atom, _mapping) when is_atom(atom), do: Atom.to_string(atom)
defp status_to_string(value, _mapping) when is_binary(value), do: value
defp kind_to_string(nil, _mapping), do: "update"
defp kind_to_string(atom, _mapping) when is_atom(atom), do: Atom.to_string(atom)
defp kind_to_string(value, _mapping) when is_binary(value), do: value
defp get(map, key), do: get(map, key, nil)
defp get(map, key, default) when is_atom(key) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> get(map, Atom.to_string(key), default)
end
end
defp get(map, key, default) when is_binary(key) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> fetch_atom_key(map, key, default)
end
end
defp fetch_atom_key(map, key, default) do
key
|> String.to_existing_atom()
|> then(&Map.get(map, &1, default))
rescue
ArgumentError -> default
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end