CodexWrapper.Structured (CodexWrapper v0.8.0)

Copy Markdown View Source

Run a command with a JSON Schema and decode the final agent message.

run/4 takes a schema as a map, owns the temporary --output-schema file for the life of the process, runs the command with --json, and decodes the final agent message. decode/2 does the decoding step for a %CodexWrapper.Result{} the caller already holds, for example from a run that set an explicit schema path. The explicit-path setters (Exec.output_schema/2 and friends) are unchanged.

schema = %{
  "type" => "object",
  "properties" => %{"name" => %{"type" => "string"}},
  "required" => ["name"],
  "additionalProperties" => false
}

{:ok, %{"name" => name}, %{result: _result, events: _events}} =
  CodexWrapper.Exec.new("Extract the name from: Hi, I'm Ada Lovelace.")
  |> CodexWrapper.Structured.run(schema, CodexWrapper.Config.new())

Schema enforcement

The schema is enforced by codex (--output-schema). This module does not re-validate the decoded value against it. It checks only that the final message is a JSON object or array; the optional :parse function is the caller-side check that the value has the expected domain shape.

Final message selection

The decoded message is the last completed agent_message item after the last turn.started event. Every parsed event is returned alongside, so earlier messages remain available. The turn outcome is the last turn.completed or turn.failed event. A top-level error event fails the run only when no terminal turn event follows it.

Outcomes

Success is {:ok, value, detail} where detail is %{result: Result.t(), events: [JsonLineEvent.t()], text: String.t()}.

Failures after the process ran carry the same detail map (plus the fields noted) so the native output is never lost:

  • {:turn_failed, detail} -- the turn failed; detail.error is the native error object or message
  • {:process_failed, detail} -- codex exited non-zero, including after a turn.completed
  • {:no_terminal, detail} -- no turn.completed was emitted
  • {:no_structured_output, detail} -- no final agent message, or its text was empty or null
  • {:invalid_json, detail} -- the final message was not JSON; detail.reason is the Jason.DecodeError
  • {:invalid_structured_output, detail} -- the decoded value was not a JSON object or array
  • {:parse_failed, reason, detail} -- :parse returned {:error, reason}
  • {:invalid_parse_result, value, detail} -- :parse returned neither {:ok, _} nor {:error, _}

Failures before spawning are {:schema_encode, reason} and {:schema_file, reason}. Runner errors such as {:timeout, ms} and {:unsupported, capability} are returned unchanged.

Summary

Functions

Decode the final agent message of a --json run the caller already made.

Run command with schema and decode its final agent message.

Types

command()

detail()

@type detail() :: %{
  :result => CodexWrapper.Result.t(),
  :events => [CodexWrapper.JsonLineEvent.t()],
  optional(atom()) => term()
}

error()

@type error() ::
  {:turn_failed, detail()}
  | {:process_failed, detail()}
  | {:no_terminal, detail()}
  | {:no_structured_output, detail()}
  | {:invalid_json, detail()}
  | {:invalid_structured_output, detail()}
  | {:parse_failed, term(), detail()}
  | {:invalid_parse_result, term(), detail()}
  | {:schema_encode, term()}
  | {:schema_file, term()}
  | term()

Functions

decode(result, opts \\ [])

@spec decode(CodexWrapper.Result.t(), keyword()) ::
  {:ok, term(), detail()} | {:error, error()}

Decode the final agent message of a --json run the caller already made.

Accepts the same :parse option as run/4 and returns the same outcomes. It never starts a process.

run(command, schema, config, opts \\ [])

@spec run(command(), map() | list(), CodexWrapper.Config.t(), keyword()) ::
  {:ok, term(), detail()} | {:error, error()}

Run command with schema and decode its final agent message.

Options:

  • :parse -- a 1-arity function mapping the decoded value to {:ok, value} or {:error, reason}. Defaults to returning the value.

Any other options are passed to the command's execute/3, so observed execution options work as they do there.

The schema file is created with mode 0600 in System.tmp_dir!/0 and removed after the process exits, whether it succeeded, failed or raised.