fix(codex): send exec prompt through stdin

This commit is contained in:
Abeautifulsnow
2026-06-28 19:34:48 +08:00
committed by GitHub
parent f57c41381b
commit 22a9fcf4a6
2 changed files with 58 additions and 5 deletions
+33 -1
View File
@@ -4,7 +4,7 @@ use crate::token_usage::TokenUsage;
use serde_json::Value;
use std::ffi::OsStr;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::sync::Notify;
@@ -34,6 +34,7 @@ pub enum CliAgentJsonlDialect {
pub struct CliAgentProcessSpec {
pub command: CliAgentCommandSpec,
pub env: Vec<(String, String)>,
pub stdin: Option<String>,
pub dialect: CliAgentJsonlDialect,
pub classify_spawn_error: fn(&str) -> String,
pub classify_run_error: fn(&str) -> String,
@@ -361,11 +362,18 @@ pub async fn run_cli_jsonl_agent(
let mut child = command
.args(&spec.command.args)
.envs(spec.env.iter().map(|(key, value)| (key.as_str(), value.as_str())))
.stdin(if spec.stdin.is_some() { Stdio::piped() } else { Stdio::null() })
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| (spec.classify_spawn_error)(&e.to_string()))?;
if let Some(input) = spec.stdin {
let mut stdin = child.stdin.take().ok_or_else(|| "CLI agent stdin not available".to_string())?;
stdin.write_all(input.as_bytes()).await.map_err(|e| format!("Failed to write CLI agent stdin: {e}"))?;
stdin.shutdown().await.map_err(|e| format!("Failed to close CLI agent stdin: {e}"))?;
}
let stdout = child.stdout.take().ok_or_else(|| "Failed to capture CLI agent stdout".to_string())?;
let stderr = child.stderr.take().ok_or_else(|| "Failed to capture CLI agent stderr".to_string())?;
let mut lines = BufReader::new(stdout).lines();
@@ -470,6 +478,7 @@ mod tests {
],
},
env: vec![("DBX_TEST_ENV".to_string(), "from-env".to_string())],
stdin: None,
dialect: CliAgentJsonlDialect::CodexExec,
classify_spawn_error,
classify_run_error,
@@ -480,6 +489,28 @@ mod tests {
assert_eq!(result, "from-env");
}
#[tokio::test]
async fn jsonl_agent_writes_stdin_to_child() {
let spec = CliAgentProcessSpec {
command: CliAgentCommandSpec {
program: "sh".to_string(),
args: vec![
"-c".to_string(),
"input=$(cat); printf '%s\n' \"{\\\"type\\\":\\\"item.completed\\\",\\\"item\\\":{\\\"type\\\":\\\"agent_message\\\",\\\"text\\\":\\\"$input\\\"}}\" \"{\\\"type\\\":\\\"turn.completed\\\"}\"".to_string(),
],
},
env: Vec::new(),
stdin: Some("prompt from stdin".to_string()),
dialect: CliAgentJsonlDialect::CodexExec,
classify_spawn_error,
classify_run_error,
};
let result = run_cli_jsonl_agent(spec, &Notify::new(), |_| {}).await.unwrap();
assert_eq!(result, "prompt from stdin");
}
#[tokio::test]
async fn jsonl_error_kills_and_waits_for_child() {
let pid_file = std::env::temp_dir().join(format!(
@@ -495,6 +526,7 @@ mod tests {
let spec = CliAgentProcessSpec {
command: CliAgentCommandSpec { program: "sh".to_string(), args: vec!["-c".to_string(), script] },
env: Vec::new(),
stdin: None,
dialect: CliAgentJsonlDialect::CodexExec,
classify_spawn_error,
classify_run_error,
+25 -4
View File
@@ -409,7 +409,7 @@ fn codex_mcp_config_overrides(options: &CodexRunOptions) -> Vec<String> {
overrides
}
pub fn build_codex_exec_command(config: &AiConfig, prompt: &str, options: &CodexRunOptions) -> CodexCommandSpec {
pub fn build_codex_exec_command(config: &AiConfig, _prompt: &str, options: &CodexRunOptions) -> CodexCommandSpec {
let mut args = vec![
"exec".to_string(),
"--json".to_string(),
@@ -429,7 +429,7 @@ pub fn build_codex_exec_command(config: &AiConfig, prompt: &str, options: &Codex
args.push(model.to_string());
}
args.push(prompt.to_string());
args.push("-".to_string());
CodexCommandSpec { program: codex_program(config), args }
}
@@ -519,11 +519,22 @@ fn classify_codex_spawn_error(message: &str) -> String {
if message.contains("No such file") || message.contains("not found") {
"[codexNotInstalled] Codex CLI was not found. Install Codex CLI or set the Codex CLI path in DBX AI settings."
.to_string()
} else if is_command_line_too_long_error(message) {
"[codexCommandLineTooLong] Codex CLI command line is too long. Update DBX so Codex prompts are sent through stdin instead of process arguments."
.to_string()
} else {
format!("[codexRunFailed] Failed to start Codex CLI: {message}")
}
}
fn is_command_line_too_long_error(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
message.contains("os error 206")
|| message.contains("文件名或扩展名太长")
|| lower.contains("filename or extension is too long")
|| lower.contains("the filename or extension is too long")
}
fn classify_codex_run_error(stderr: &str) -> String {
let lower = stderr.to_ascii_lowercase();
if lower.contains("not authenticated") || lower.contains("login") || lower.contains("auth") {
@@ -558,6 +569,7 @@ pub async fn run_codex_agent(
CliAgentProcessSpec {
command,
env,
stdin: Some(prompt.to_string()),
dialect: CliAgentJsonlDialect::CodexExec,
classify_spawn_error: classify_codex_spawn_error,
classify_run_error: classify_codex_run_error,
@@ -573,8 +585,8 @@ mod tests {
#[cfg(not(windows))]
use super::shell_quote;
use super::{
build_codex_exec_command, codex_cli_env, codex_enabled_tools, is_path_like_program, parse_codex_jsonl_event,
parse_codex_models, validate_codex_program, CodexRunOptions, DEFAULT_CODEX_MODELS,
build_codex_exec_command, classify_codex_spawn_error, codex_cli_env, codex_enabled_tools, is_path_like_program,
parse_codex_jsonl_event, parse_codex_models, validate_codex_program, CodexRunOptions, DEFAULT_CODEX_MODELS,
};
#[cfg(not(windows))]
use super::{codex_process_env, common_executable_dirs, merged_path_with_dir};
@@ -621,6 +633,8 @@ mod tests {
assert_eq!(spec.program, "codex");
assert!(spec.args.contains(&"--json".to_string()));
assert_eq!(spec.args.last().map(String::as_str), Some("-"));
assert!(!spec.args.contains(&"hello".to_string()));
assert!(!spec.args.contains(&"--model".to_string()));
assert!(!spec.args.contains(&"--ask-for-approval".to_string()));
assert!(spec.args.contains(&"mcp_servers.dbx.command=\"dbx-mcp-server\"".to_string()));
@@ -939,6 +953,13 @@ mod tests {
assert!(!models.iter().any(|model| model.id == "priority" || model.id == "Priority"));
}
#[test]
fn classifies_windows_command_line_too_long_spawn_error() {
let err = classify_codex_spawn_error("文件名或扩展名太长。 (os error 206)");
assert!(err.contains("[codexCommandLineTooLong]"));
}
#[test]
fn parses_codex_jsonl_events() {
assert!(parse_codex_jsonl_event(r#"{"type":"thread.started","thread_id":"t"}"#).is_none());