mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 22:48:18 +00:00
fix(codex): send exec prompt through stdin
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user