Skip to content

feat: add typed turn results and session usage totals - #171

Merged
joshrotenberg merged 2 commits into
mainfrom
feat/150-typed-turn-results
Nov 1, 2026
Merged

joshrotenberg merged 2 commits into
mainfrom
feat/150-typed-turn-results

Conversation

@joshrotenberg

@joshrotenberg joshrotenberg commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #150.

Callers can now inspect JSON command output through CodexWrapper.Turn.from_result/1 without parsing stdout themselves or requiring the final answer to be JSON. The projection exposes status, exit code, thread ID, final agent message, normalized usage, native error data, and parsed events. %Result{} is unchanged.

  • Resolve terminal events and top-level errors in stream order. An error after the final terminal event fails the turn; a later terminal supersedes an earlier error. A nonzero exit always produces a failed projection.
  • Select the last completed agent message from the last turn. Keep the five known usage counters as nonnegative integers or nil, retaining original values and unknown fields in events.
  • Expose the projection as session.last_turn and Session.last_turn/1, plus Session.total_usage/1. SessionServer provides both accessors. Session sends and history retain their existing raw Result contract.
  • Share the projection with Structured decoding and evidenced terminal selection. Structured detail includes turn; native parsed_result remains a raw terminal event independent of process status. Final evidence replacing streamed stdout no longer leaves a stale terminal event.
  • Document nonzero execution returns, typed results, missing usage, and session totals. Add explicitly synthetic CLI 0.165.0 fixtures and regressions for ordering, malformed values, session totals, bounded output, and evidence replacement.

Validation

Ran the complete local check matrix before both pushes:

Runtime Final result
Elixir 1.19.7 / OTP 27.3.4.19 in Docker 730 tests passed, 0 failed, 4 integration tests excluded
Elixir 1.20.4 / OTP 29.1.2 on macOS 730 tests passed, 0 failed, 4 integration tests excluded

Both runtimes passed mix deps.get, mix deps.unlock --check-unused, mix hex.audit, mix compile --warnings-as-errors, mix format --check-formatted, mix credo --strict, mix dialyzer, mix test, and mix docs --warnings-as-errors.

mix codex.contract passed with Node 22.23.4 for CLI 0.149.0, 0.164.0, and latest 0.166.0. The contracts do not invoke inference. The 91 targeted projection, structured, session and evidence tests also passed; git diff --check passed.

Final GitHub CI for cd602af95f5096a19504ec8d1d60227a4f7eaf73: all five jobs passed, including both runtime jobs, Docs, and both pinned CLI contracts. CI run.

Acceptance

  • Pure projection from a JSON Result; Result itself unchanged.
  • Ordered terminal/error status and nonzero exit handling.
  • Usage retains only nonnegative integers; original fields remain in raw events.
  • Session.send exposes the typed turn on the updated session; Session.total_usage sums usage across recorded turns.
  • Structured and evidenced parsed_result share the projection.
  • exec/2 and review/1 document the nonzero-exit return.
  • CLI 0.165.0 usage fixtures explicitly marked synthetic.

Compatibility and limits

Structured.decode/2 intentionally changes error precedence for existing callers. Given turn.completed followed by a top-level error with no later terminal event, it returns {:error, {:turn_failed, detail}} at both zero and nonzero process exits. Previously, a zero exit could decode successfully, while a nonzero exit returned {:error, {:process_failed, detail}}. This follows the documented event-order rule and issue #150. A later terminal event still supersedes an earlier error. The follow-up commit message also records this compatibility change.

The existing Session error and history rules are preserved: rejected nonzero exits do not update the session or contribute to recorded usage totals. A total field remains nil when no recorded turn reports a valid value; partially reported totals cover only known values. ExecutionObservation.parsed_result remains native evidence rather than a success assertion.

All new fixtures are synthetic. Live provider inference was not run; the four opt-in integration tests remain excluded as in CI. Release publication steps were not run because they publish artifacts and are not PR checks.

Review follow-up

PR #173 documents and tests the stream-wide versus last-turn field scopes and caches session usage totals during sends so reads do not reparse retained stdout. #171 was already merged when those follow-up changes were applied.

@joshrotenberg
joshrotenberg marked this pull request as ready for review November 1, 2026 13:52
@joshrotenberg
joshrotenberg merged commit 508ebca into main Nov 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: typed turn results with status, final message, thread id and usage

1 participant