# moonbitlang/workflow/shim/codex

Run Codex as a workflow child. This executable speaks the
[child contract](../../docs/child-contract.md): it reads one
request line on stdin, drives `codex exec --json`, reads completed items and
turns for the steps, usage, and answer, and writes one result file. It
supports native and wasm and exposes no callable library API.

## Launch from a workflow

The machine running the shim needs the `codex` CLI on `PATH` (or a
`--command` override), configured with its normal credentials. Environment
variables are inherited. `moonx` obtains the published shim; it does not
install or authenticate Codex.

Add `moonbitlang/workflow` to your module and import
`moonbitlang/workflow` and `moonbitlang/workflow/spawn` in `moon.pkg`.
This example invokes a real agent, so documentation tests do not run it:

```moonbit nocheck
///|
async fn main {
  let wf = @workflow.Workflow(
    runner=@spawn.contract_runner(launch=_ => {
      @spawn.LaunchSpec(command="moonx", args=[
        "moonbitlang/workflow/shim/codex",
        "--result-file",
        @spawn.ResultFilePlaceholder,
      ])
    }),
    max_calls=1,
  )
  let report = wf.agent(
    prompt="Describe the repository's public API in three sentences.",
    kind="explore",
    max_steps=8,
  )
  println(report.stringify())
}
```

The runner replaces `{result_file}` (`@spawn.ResultFilePlaceholder`) with a
fresh private path per launch and reads the result from it; the shim's
stdout carries nothing. An unversioned coordinate takes the latest published
module; use
`moonbitlang/workflow/shim/codex@<version>` to pin it. Add a workflow journal
to reuse prior reports without launching Codex again.

## Options and sandbox selection

```sh
moonx moonbitlang/workflow/shim/codex --help
```

`--help` prints plain text before reading a request. An option error, or a
request that names no `request_id`, is printed on stderr and the shim exits
nonzero without a result; any other malformed request is a `failed` result.
All options, `--result-file` included, precede the passthrough separator.

| Option | Codex mapping |
| --- | --- |
| `--result-file PATH` | Required: where the result file goes. |
| `--command EXE` | CLI executable; default `codex`. |
| `--model NAME` | `-m NAME`; absent leaves selection to the CLI. |
| `--cwd DIR` | Working directory of the CLI process. |
| `--writable` | Selects the write-capable sandbox below. |
| `--schema FILE` | `--output-schema FILE`, unless the request supplies a schema. |
| `-- ARGS...` | Passed after the shim's options and before the positional prompt. |

The shim builds `exec --json --skip-git-repo-check -s read-only` by default.
With `kind="worker"` or `--writable`, it chooses `workspace-write`. These
are the flags this adapter requests; the CLI's configuration and host
permissions also apply. Passthrough flags can alter behavior, so keep the
JSON event output intact.

The prompt is the last positional argument. The CLI's stdin is closed from
the start so it cannot wait for additional input. The shim's own stdin stays
open as the parent's cancellation channel.

Kinds do not select different Codex tasks; `worker` selects the sandbox
policy. Input must be a string, `{query, hints?}`, or `{prompt}`. `wf.agent`
builds the supported query shape; an object containing only `task` does not.

## Reports and schema handling

The last completed `agent_message` supplies the answer. A `completed`
result's `output`, which the parent receives as the report, has this shape:

```json
{
  "answer": "The repository exposes ...",
  "engine": "codex",
  "thread_id": "example-thread",
  "notices": ["An optional configuration setting was ignored."]
}
```

`thread_id` is included when observed and `notices` only when nonempty.
The shim does not automatically resume that thread. Workflow journal replay
is a separate mechanism.

A request `schema` takes precedence over `--schema FILE`. The shim writes
the request's schema to a temporary JSON file and supplies it through
`--output-schema`; a command-line schema path is passed through to Codex.
Use an absolute path when also changing `--cwd`.

With a schema, the final message is parsed as JSON and placed in `answer`.
If parsing fails it remains a string. This adapter does not perform local
JSON Schema validation. The schema describes the answer, while
`wf.agent_as[T]` decodes the complete report wrapper. Use a wrapper type with
an `answer` field, or decode that field explicitly after `wf.agent`.

## Stream translation and limits

| CLI event | Adapter behavior |
| --- | --- |
| `thread.started` | Records the thread id. |
| `item.completed` with `reasoning` | Ignored for step counting. |
| `item.completed` with `error` | A notice, not a step or terminal failure. |
| Other `item.completed` | Adds a step; an `agent_message` replaces the stored answer. |
| `turn.completed` | Adds its usage to the run's when input/output counters are valid. |
| `turn.failed` or top-level `error` with a message | Records a terminal failure; the first message wins. |

Steps measure completed non-reasoning, non-notice items, including commands,
file changes, tool calls, and messages. They are not a count of model calls.
The shim counts an item before checking `steps > limits.max_steps`, so the
over-limit item is counted and the stop is reactive.

Usage arrives at turn completion and is summed over turns. Prompt tokens
are `input_tokens`, cache hits are `cached_input_tokens` capped at input
tokens, and misses are their difference. Codex states no dollar price, so
the result's `usage` has no `cost_usd`. In the recorded/tested codex-cli
0.151 behavior, interrupting a turn does not flush its usage; a capped or
cancelled run's result therefore carries no `usage` at all, and the parent
records the spend as unaccounted rather than free.

The shared [shim runtime](../README.mbt.md#run-and-classify-the-cli) sends
SIGINT on a step stop and on the parent's cancel, and observes late output
for up to 3 seconds before termination. The endings:

| How the run ended | Result `status` |
| --- | --- |
| An agent message, and a clean or nonzero exit without a recorded failure | `completed`, with the report as `output` |
| The step ceiling | `max_steps_exhausted` |
| Parent stdin EOF | `interrupted` |
| `turn.failed` or a top-level `error`, a launch failure, or a nonzero exit without a message | `failed`, with the reason |
| A clean exit without an agent message | `no_report` |

These rules follow the implementation and recorded codex-cli 0.151.0
fixtures in [`dialect.mbt`](dialect.mbt), rather than assuming that every
future CLI release has the same format.

## Development

From the repository root, inspect local help without running an agent and
exercise the recorded classifiers:

```sh
moon run shim/codex --target native -- --help
moon test shim/codex --target native
moon test shim/codex --target wasm
```

`just shims` builds both CLI adapters for both targets. [`main.mbt`](main.mbt)
wires schema files and process execution into `@shim.serve`;
[`dialect.mbt`](dialect.mbt) interprets the stream and builds the result. When a CLI format
changes, update its recorded fixtures and classifier in the same change.
