Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
run: mix test

- name: Credo
run: mix credo --strict
run: mix credo --min-priority higher

- name: Doctor
run: mix doctor --raise
15 changes: 14 additions & 1 deletion lib/jido_codex/compatibility.ex
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,20 @@ defmodule Jido.Codex.Compatibility do
end

defp read_help(program) do
case command_module().run(program, ["--help"], timeout: @command_timeout) do
# Check both top-level and exec subcommand help, since flags like --json
# are defined on the exec subcommand, not the top-level command.
with {:ok, top_help} <- run_help(program, ["--help"]),
{:ok, exec_help} <- run_help(program, ["exec", "--help"]) do
{:ok, top_help <> "\n" <> exec_help}
else
# If exec --help fails, fall back to top-level only
{:error, _} ->
run_help(program, ["--help"])
end
end

defp run_help(program, args) do
case command_module().run(program, args, timeout: @command_timeout) do
{:ok, output} ->
{:ok, output}

Expand Down
107 changes: 105 additions & 2 deletions lib/jido_codex/mapper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Jido.Codex.Mapper do
alias Codex.Items
alias Codex.StreamEvent
alias Jido.Harness.Event
alias Jido.Harness.Event.Usage, as: UsageEvent

@doc "Maps a Codex stream event into one or more normalized events."
@spec map_event(term(), keyword()) :: {:ok, [Event.t()]} | {:error, term()}
Expand Down Expand Up @@ -92,6 +93,82 @@ defmodule Jido.Codex.Mapper do
{:ok, [build_event(:thinking_delta, event.thread_id, %{"text" => text}, event)]}
end

# Exec-transport command execution: emit both a tool_call and tool_result
# from the completed item since it contains the full command, output, and exit code.
def map_event(%Events.ItemCompleted{item: %Items.CommandExecution{} = item} = event, _opts) do
call_id = item.id || "exec-#{System.unique_integer([:positive])}"

tool_call =
build_event(
:tool_call,
event.thread_id,
%{
"name" => "exec_command",
"input" => %{"cmd" => item.command, "cwd" => item.cwd},
"call_id" => call_id
},
event
)

tool_result =
build_event(
:tool_result,
event.thread_id,
%{
"name" => "exec_command",
"output" => item.aggregated_output || "",
"call_id" => call_id,
"is_error" => item.status == :failed || (item.exit_code != nil and item.exit_code != 0)
},
event
)

{:ok, [tool_call, tool_result]}
end

# McpToolCall items from exec transport
def map_event(%Events.ItemCompleted{item: %Items.McpToolCall{} = item} = event, _opts) do
call_id = item.id || "mcp-#{System.unique_integer([:positive])}"
tool_name = item.tool || "mcp_tool"
tool_output = item.result || item.error || ""
tool_error? = item.status == :failed || not is_nil(item.error)

tool_call =
build_event(
:tool_call,
event.thread_id,
%{
"name" => tool_name,
"input" => item.arguments || %{},
"call_id" => call_id
},
event
)

tool_result =
build_event(
:tool_result,
event.thread_id,
%{
"name" => tool_name,
"output" => tool_output,
"call_id" => call_id,
"is_error" => tool_error?
},
event
)

{:ok, [tool_call, tool_result]}
end

# In-progress command execution (item.started) — skip since ItemCompleted
# will emit both tool_call and tool_result with complete data
def map_event(%Events.ItemStarted{item: %Items.CommandExecution{}}, _opts), do: {:ok, []}

# Skip other ItemStarted/ItemUpdated (noise for rendering)
def map_event(%Events.ItemStarted{}, _opts), do: {:ok, []}
def map_event(%Events.ItemUpdated{}, _opts), do: {:ok, []}

def map_event(%Events.ReasoningDelta{} = event, _opts) do
{:ok, [build_event(:thinking_delta, event.thread_id, %{"text" => event.delta}, event)]}
end
Expand Down Expand Up @@ -125,7 +202,7 @@ defmodule Jido.Codex.Mapper do

def map_event(%Events.ThreadTokenUsageUpdated{} = event, _opts) do
payload = %{"usage" => event.usage, "delta" => event.delta}
{:ok, [build_event(:usage, event.thread_id, payload, event)]}
{:ok, [build_event(:codex_token_update, event.thread_id, payload, event)]}
end

def map_event(%Events.AccountRateLimitsUpdated{} = event, _opts) do
Expand Down Expand Up @@ -184,7 +261,12 @@ defmodule Jido.Codex.Mapper do
"usage" => event.usage
}

{:ok, [build_event(:session_completed, event.thread_id, payload, event)]}
session_completed = build_event(:session_completed, event.thread_id, payload, event)

case maybe_usage_event(event.usage, event.thread_id, event) do
nil -> {:ok, [session_completed]}
usage_event -> {:ok, [usage_event, session_completed]}
end
end

def map_event(%Events.TurnFailed{} = event, _opts) do
Expand Down Expand Up @@ -247,6 +329,27 @@ defmodule Jido.Codex.Mapper do

defp detect_session_id(_), do: nil

defp maybe_usage_event(nil, _session_id, _raw), do: nil

defp maybe_usage_event(usage, session_id, raw) when is_map(usage) and is_binary(session_id) do
input = usage["input_tokens"] || usage[:input_tokens] || 0
output = usage["output_tokens"] || usage[:output_tokens] || 0
cached = usage["cached_input_tokens"] || usage[:cached_input_tokens] || 0

if input > 0 or output > 0 do
UsageEvent.build(:codex, session_id,
input_tokens: input,
output_tokens: output,
cached_input_tokens: cached,
raw: raw
)
else
nil
end
end

defp maybe_usage_event(_, _, _), do: nil

defp stringify_keys(value) when is_map(value) do
value
|> Enum.map(fn {k, v} -> {to_string(k), stringify_keys(v)} end)
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ defmodule Jido.Codex.MixProject do
{:zoi, "~> 0.17"},
{:splode, ">= 0.2.9 and < 0.4.0"},
{:jido_harness, github: "agentjido/jido_harness", branch: "main", override: true},
{:jido_shell, github: "agentjido/jido_shell", branch: "main", override: true},
{:codex_sdk, "~> 0.10"},
{:jason, "~> 1.4"}
]
Expand Down
Loading
Loading