///|
/// What the shim has learned from `codex exec --json` so far. Purely
/// structural: only the fields named below are read.
///
/// Steps are completed items other than reasoning and notices (a
/// message, a command, a file change, a tool call); cost arrives per
/// turn on `turn.completed` and is summed; the answer is the last
/// `agent_message` text,
/// parsed as JSON when a schema was requested. An `error` ITEM is a
/// notice (recorded from codex-cli 0.151: "Configured service tier … will
/// be omitted"), not a failure — the run goes on and usually completes.
/// Only `turn.failed` and a top-level `error` event end a run without a
/// report.
priv struct Codex {
mut steps : Int
mut thread_id : String?
mut last_message : String?
mut failure : String?
notices : Array[String]
/// The turns' usage, summed; `None` until a turn reported some.
mut usage : @shim.Usage?
}
///|
/// A fresh observer.
fn Codex::Codex() -> Codex {
{
steps: 0,
thread_id: None,
last_message: None,
failure: None,
notices: [],
usage: None,
}
}
///|
/// Classify one stdout line into the state.
fn Codex::observe(self : Codex, line : String) -> Unit {
let trimmed = line.trim(chars="\r\n")
if trimmed == "" {
return
}
let json = @json.parse(trimmed) catch { _ => return }
match json {
{ "type": "thread.started", "thread_id": String(id), .. } =>
self.thread_id = Some(id)
{ "type": "item.completed", "item": { "type": "reasoning", .. }, .. } => ()
{
"type": "item.completed",
"item": { "type": "error", "message": String(message), .. },
..
} => self.notices.push(message)
{ "type": "item.completed", "item": { "type": "error", .. }, .. } => ()
{ "type": "item.completed", "item": item, .. } => {
self.steps += 1
if item is { "type": "agent_message", "text": String(text), .. } {
self.last_message = Some(text)
}
}
{
"type": "turn.completed",
"usage": {
"input_tokens": Number(input, ..),
"output_tokens": Number(output, ..),
..
} as usage,
..
} => {
let cached = if usage is { "cached_input_tokens": Number(cached, ..), .. } {
@shim.integral(cached).unwrap_or(0)
} else {
0
}
if @shim.integral(input) is Some(input) &&
@shim.integral(output) is Some(output) {
let hit = cached.min(input)
let turn = @shim.Usage(
prompt_tokens=input,
completion_tokens=output,
cache_hit_tokens=hit,
cache_miss_tokens=input - hit,
)
self.usage = Some(
match self.usage {
Some(seen) => seen.add(turn)
None => turn
},
)
}
}
{ "type": "turn.failed", "error": { "message": String(message), .. }, .. }
| { "type": "error", "message": String(message), .. } =>
if self.failure is None {
self.failure = Some("codex: \{message}")
}
_ => ()
}
}
///|
/// How the run ended, given how the CLI did: a turn failure wins over the
/// exit status; a nonzero exit without a message is a failure, a clean one
/// `no_report`.
fn Codex::status(
self : Codex,
exit : @shim.CliExit,
structured~ : Bool,
) -> @shim.Status {
match exit {
Cancelled => Interrupted("the parent closed stdin")
Stopped => MaxStepsExhausted
Failed(reason) => Failed(reason)
Exited(code) =>
if self.failure is Some(error) {
Failed(error)
} else if self.report(structured~) is Some(report) {
Completed(report)
} else if code != 0 {
Failed("codex exited \{code} without a message")
} else {
NoReport
}
}
}
///|
/// The result file's content for a run that ended with `exit`.
fn Codex::result(
self : Codex,
exit : @shim.CliExit,
structured~ : Bool,
) -> @shim.ShimResult {
@shim.ShimResult(
self.status(exit, structured~),
usage?=self.usage,
steps=self.steps,
)
}
///|
/// The report, once an agent message arrived: the answer (parsed when
/// `structured`, the text otherwise), the thread id for resume, and any
/// notices the CLI raised along the way.
fn Codex::report(self : Codex, structured~ : Bool) -> Json? {
guard self.last_message is Some(text) else { return None }
let answer : Json = if structured {
@json.parse(text) catch {
_ => Json(text)
}
} else {
Json(text)
}
let fields : Map[String, Json] = { "answer": answer, "engine": "codex" }
if self.thread_id is Some(id) {
fields["thread_id"] = Json(id)
}
if !self.notices.is_empty() {
fields["notices"] = Json(self.notices)
}
Some(Json::object(fields))
}
///|
/// Feed `lines` to a fresh observer.
fn observed(lines : Array[String]) -> Codex {
let state = Codex()
for line in lines {
state.observe(line)
}
state
}
///|
/// Lines recorded from codex-cli 0.151.0 (`exec --json`, gpt-5.3-codex-spark,
/// prompt "Reply with exactly the single word: ok"), trimmed to the fields
/// that matter. The two `error` items are the service-tier notice.
test "notices are neither steps nor failures; the turn still reports" {
let state = observed([
(
#|{"type":"thread.started","thread_id":"t1"}
),
(
#|{"type":"item.completed","item":{"id":"i0","type":"error","message":"Configured service tier `priority` is not advertised as supported for model `gpt-5.3-codex-spark` and will be omitted from requests."}}
),
(
#|{"type":"turn.started"}
),
(
#|{"type":"item.completed","item":{"id":"i1","type":"error","message":"Configured service tier `priority` is not advertised as supported for model `gpt-5.3-codex-spark` and will be omitted from requests."}}
),
(
#|{"type":"item.completed","item":{"id":"i2","type":"agent_message","text":"ok"}}
),
(
#|{"type":"turn.completed","usage":{"input_tokens":13829,"cached_input_tokens":4608,"cache_write_input_tokens":0,"output_tokens":67,"reasoning_output_tokens":60}}
),
])
assert_eq(state.failure, None)
assert_eq(state.result(Exited(0), structured=false).to_json(request_id="r"), {
"version": 1,
"request_id": "r",
"status": "completed",
"output": {
"answer": "ok",
"engine": "codex",
"thread_id": "t1",
"notices": [
"Configured service tier `priority` is not advertised as supported for model `gpt-5.3-codex-spark` and will be omitted from requests.",
"Configured service tier `priority` is not advertised as supported for model `gpt-5.3-codex-spark` and will be omitted from requests.",
],
},
// Codex states no price: the usage has none.
"usage": {
"prompt_tokens": 13829,
"completion_tokens": 67,
"total_tokens": 13896,
"prompt_cache_hit_tokens": 4608,
"prompt_cache_miss_tokens": 9221,
},
"steps": 1,
})
}
///|
test "commands and messages are steps; reasoning is not; the last message wins" {
let state = observed([
(
#|{"type":"item.completed","item":{"id":"i1","type":"reasoning","text":"hmm"}}
),
(
#|{"type":"item.completed","item":{"id":"i2","type":"command_execution","command":"ls","exit_code":0}}
),
(
#|{"type":"item.completed","item":{"id":"i3","type":"agent_message","text":"draft"}}
),
(
#|{"type":"item.completed","item":{"id":"i4","type":"agent_message","text":"forty-two"}}
),
])
assert_eq(state.steps, 3)
assert_eq(
state.report(structured=false),
Some({ "answer": "forty-two", "engine": "codex" }),
)
}
///|
test "every turn's usage is summed" {
let turn =
#|{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":40,"output_tokens":7}}
let state = observed([turn, turn])
assert_eq(state.result(Exited(0), structured=false).to_json(request_id="r"), {
"version": 1,
"request_id": "r",
"status": "no_report",
"usage": {
"prompt_tokens": 200,
"completion_tokens": 14,
"total_tokens": 214,
"prompt_cache_hit_tokens": 80,
"prompt_cache_miss_tokens": 120,
},
"steps": 0,
})
}
///|
/// Verified live on codex-cli 0.151: usage arrives only on
/// `turn.completed`, which an interrupted turn never prints — so a capped
/// or cancelled run reports NO usage, never an invented zero.
test "a run stopped before its turn completed carries no usage" {
let state = observed([
(
#|{"type":"item.completed","item":{"id":"i1","type":"command_execution","command":"ls","exit_code":0}}
),
(
#|{"type":"item.completed","item":{"id":"i2","type":"command_execution","command":"pwd","exit_code":0}}
),
])
assert_eq(state.result(Stopped, structured=false).to_json(request_id="r"), {
"version": 1,
"request_id": "r",
"status": "max_steps_exhausted",
"steps": 2,
})
assert_eq(state.result(Cancelled, structured=false).to_json(request_id="r"), {
"version": 1,
"request_id": "r",
"status": "interrupted",
"reason": "the parent closed stdin",
"steps": 2,
})
assert_eq(
state.status(Exited(2), structured=false),
Failed("codex exited 2 without a message"),
)
}
///|
test "a structured answer is parsed; unparseable text stays text" {
let state = observed([
(
#|{"type":"item.completed","item":{"type":"agent_message","text":"{\"verdict\":\"real\"}"}}
),
])
assert_eq(
state.report(structured=true),
Some({ "answer": { "verdict": "real" }, "engine": "codex" }),
)
let prose = observed([
(
#|{"type":"item.completed","item":{"type":"agent_message","text":"no json here"}}
),
])
assert_eq(
prose.report(structured=true),
Some({ "answer": "no json here", "engine": "codex" }),
)
}
///|
test "turn and stream errors are failures; the first one wins" {
let state = observed([
(
#|{"type":"turn.failed","error":{"message":"quota"}}
),
(
#|{"type":"error","message":"later"}
),
])
assert_eq(state.failure, Some("codex: quota"))
assert_eq(state.status(Exited(1), structured=false), Failed("codex: quota"))
assert_eq(state.report(structured=false), None)
}
///|
// Synthetic framing cases: invalid lines must not erase the previous answer.
test "CRLF and Unicode reports survive malformed lines with no metadata" {
assert_eq(Codex().report(structured=false), None)
let state = observed([
"{\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\",\"text\":\"你好🌙\"}}\r\n",
"", "\r\n", "not json",
])
assert_eq(state.steps, 1)
assert_eq(
state.report(structured=false),
Some({ "answer": "你好🌙", "engine": "codex" }),
)
}