// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Arguments for executing a Codex command.
priv struct CodexExecArgs {
/// The input prompt/message to send to Codex
input : String
// Images to send to Codex
images : Array[String]
/// Thread ID to resume, or None for a new thread
thread_id : String?
codex_options : CodexOptions
thread_options : ThreadOptions
turn_options : TurnOptions
}
// Constants for environment variable names
// TODO: Use these when implementing process spawning with env vars
///|
const INTERNAL_ORIGINATOR_ENV : String = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
///|
const MOONBIT_SDK_ORIGINATOR : String = "codex_sdk_moonbit"
///|
/// The CodexExec class handles spawning and communicating with the codex CLI.
priv struct CodexExec {
executable_path : String
}
///|
/// Create a new CodexExec instance.
///
/// # Arguments
/// * `executable_path` - Optional path to the codex executable. Default to "codex".
fn CodexExec::new(executable_path? : String = "codex") -> CodexExec {
// For simplicity, we assume "codex" is in PATH if not provided.
{ executable_path, }
}
///|
/// Run a Codex command and yield output lines as they arrive.
///
/// This spawns the codex CLI process, sends the input, and yields each line
/// of JSON output as it's received.
///
/// # Arguments
/// * `args` - The arguments for the Codex execution
///
/// # Returns
/// An iterator that yields String lines from the Codex output
async fn[T] CodexExec::run(
self : Self,
args : CodexExecArgs,
taskgroup : @async.TaskGroup[T],
) -> @generator.AsyncGenerator[String] {
// Build command arguments
let command_args : Array[String] = ["exec", "--json"]
// Add optional arguments
if args.thread_options.model is Some(model) {
command_args.push("--model")
command_args.push(model)
}
if args.thread_options.sandbox_mode is Some(sandbox_mode) {
command_args.push("--sandbox")
command_args.push(sandbox_mode.to_string())
}
if args.thread_options.working_directory is Some(working_directory) {
command_args.push("--cd")
command_args.push(working_directory)
}
if args.thread_options.additional_directories is Some(additional_directories) {
for dir in additional_directories {
command_args.push("--add-dir")
command_args.push(dir)
}
}
if args.thread_options.skip_git_repo_check is Some(true) {
command_args.push("--skip-git-repo-check")
}
if args.turn_options.output_schema is Some(schema) {
let file = create_output_schema_file(schema)
taskgroup.add_defer(() => (file.cleanup)())
command_args.push("--output-schema")
command_args.push(file.schema_path)
}
if args.thread_options.model_reasoning_effort is Some(effort) {
command_args.push("--config")
command_args.push("model_reasoning_effort=\{effort}")
}
if args.thread_options.network_access_enabled is Some(option) {
command_args.push("--config")
command_args.push("sandbox_workspace_network_access=\{option}")
}
if args.thread_options.web_search_enabled is Some(option) {
command_args.push("--config")
command_args.push("features.web_search_request=\{option}")
}
if args.thread_options.approval_policy is Some(approval_policy) {
command_args.push("--config")
command_args.push("approval_policy=\{approval_policy}")
}
for image in args.images {
command_args.push("--image")
command_args.push(image)
}
if args.thread_id is Some(thread_id) {
command_args.push("resume")
command_args.push(thread_id)
}
// Add env
let env = @sys.get_env_vars()
let extra_env = args.codex_options.env.unwrap_or({})
if env.get(INTERNAL_ORIGINATOR_ENV) is None {
extra_env.set(INTERNAL_ORIGINATOR_ENV, MOONBIT_SDK_ORIGINATOR)
}
if args.codex_options.base_url is Some(base_url) {
extra_env.set("OPENAI_BASE_URL", base_url)
}
if args.codex_options.api_key is Some(api_key) {
extra_env.set("CODEX_API_KEY", api_key)
}
// CodexExec
@generator.AsyncGenerator::new(
async fn(yield_) {
let stdin = @process.write_to_process() catch {
e => raise @error.reraise(e)
}
let stdout = @process.read_from_process() catch {
e => raise @error.reraise(e)
}
@async.with_task_group(taskgroup => {
taskgroup.spawn_bg(() => {
let (exit_code, stderr) = @process.collect_stderr(
self.executable_path,
command_args,
stdin=stdin.0,
stdout=stdout.1,
inherit_env=args.codex_options.env is None,
extra_env~,
)
if exit_code != 0 {
@error.fail("Codex CLI failed: \{stderr.text()}")
}
})
taskgroup.spawn_bg(() => {
defer stdin.1.close()
stdin.1.write(args.input)
})
let reader = stdout.0
defer reader.close()
while reader.read_until("\n") is Some(text) {
yield_(text) catch {
@generator.Return => return
e => raise @error.reraise(e)
}
}
})
},
taskgroup,
)
}