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,
McpToolCall,
Reasoning,
TodoList,
WebSearch
}
@type t ::
AgentMessage.t()
| Reasoning.t()
| CommandExecution.t()
| FileChange.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
@type t :: %__MODULE__{
id: String.t() | nil,
type: :reasoning,
text: 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,
aggregated_output: "",
exit_code: nil,
status: :in_progress
@type status :: :in_progress | :completed | :failed
@type t :: %__MODULE__{
id: String.t() | nil,
type: :command_execution,
command: String.t(),
aggregated_output: String.t(),
exit_code: integer() | nil,
status: status()
}
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 :: %{path: String.t(), kind: change_kind()}
@type status :: :completed | :failed
@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,
status: :in_progress
@type status :: :in_progress | :completed | :failed
@type t :: %__MODULE__{
id: String.t() | nil,
type: :mcp_tool_call,
server: String.t(),
tool: String.t(),
status: status()
}
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
}
@file_change_status_map %{
"completed" => :completed,
"failed" => :failed
}
@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" => "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)
end
def to_map(%CommandExecution{} = item) do
base_item_map(item, "command_execution")
|> maybe_put("command", item.command)
|> 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))
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} ->
%{
"path" => path,
"kind" => kind_to_string(kind, @file_change_kind_map)
}
end)
)
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("status", status_to_string(item.status, @mcp_status_map))
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
%Reasoning{
id: get(map, :id),
text: get(map, :text) || ""
}
end
defp parse_command_execution(map) do
%CommandExecution{
id: get(map, :id),
command: get(map, :command) || "",
aggregated_output: get(map, :aggregated_output) || "",
exit_code: get(map, :exit_code),
status: parse_status(get(map, :status), @command_status_map, :in_progress)
}
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)
}
end)
}
end
defp parse_mcp_tool_call(map) do
%McpToolCall{
id: get(map, :id),
server: get(map, :server) || "",
tool: get(map, :tool) || "",
status: parse_status(get(map, :status), @mcp_status_map, :in_progress)
}
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 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