mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(agents/cloud): add submit --wait and HUD progress; treat builtin
- Add cloud submit non-interactive path with optional --wait - Poll remote status and print final results (messages + diff) - Stream stderr progress into Agents HUD; cap very large outputs (~500KB) - Treat 'cloud' as a built-in agent (no external binary needed) - Wire core agent_run to invoke current executable with cloud submit --wait Validated via ./build-fast.sh (no warnings).
This commit is contained in:
@@ -1,9 +1,45 @@
|
||||
use clap::Parser;
|
||||
use codex_common::CliConfigOverrides;
|
||||
|
||||
#[derive(Debug, Clone, clap::Subcommand)]
|
||||
pub enum Command {
|
||||
/// Submit a new task non-interactively and print the created id
|
||||
Submit(SubmitArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Default, Clone)]
|
||||
pub struct SubmitArgs {
|
||||
/// The task prompt to submit to Codex Cloud
|
||||
#[arg(value_name = "PROMPT")]
|
||||
pub prompt: String,
|
||||
|
||||
/// Optional environment id (falls back to auto-detect when omitted)
|
||||
#[arg(long = "env", value_name = "ENV_ID")]
|
||||
pub env: Option<String>,
|
||||
|
||||
/// Best-of-N attempts for the assistant (default: 1)
|
||||
#[arg(long = "best-of", default_value_t = 1)]
|
||||
pub best_of: usize,
|
||||
|
||||
/// Enable QA/review mode when creating the task
|
||||
#[arg(long = "qa", default_value_t = false)]
|
||||
pub qa: bool,
|
||||
|
||||
/// Git ref to associate with the task (default: main)
|
||||
#[arg(long = "git-ref", default_value = "main")]
|
||||
pub git_ref: String,
|
||||
|
||||
/// Wait for completion and print final results
|
||||
#[arg(long = "wait", default_value_t = false)]
|
||||
pub wait: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Default)]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub cmd: Option<Command>,
|
||||
}
|
||||
|
||||
@@ -149,7 +149,12 @@ fn spawn_apply(
|
||||
// (no standalone patch summarizer needed – UI displays raw diffs)
|
||||
|
||||
/// Entry point for the `codex cloud` subcommand.
|
||||
pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||||
pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||||
// Non-interactive submit mode: used by the core agent runner to create a
|
||||
// cloud task and return its id on stdout.
|
||||
if let Some(crate::cli::Command::Submit(args)) = cli.cmd {
|
||||
return run_submit(args).await;
|
||||
}
|
||||
// Very minimal logging setup; mirrors other crates' pattern.
|
||||
let default_level = "error";
|
||||
let _ = tracing_subscriber::fmt()
|
||||
@@ -1489,6 +1494,159 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Lightweight non-interactive submit implementation. Accepts a prompt and
|
||||
// optional env/best-of/qa/git-ref and prints only the created task id.
|
||||
async fn run_submit(args: crate::cli::SubmitArgs) -> anyhow::Result<()> {
|
||||
set_user_agent_suffix("codex_cloud_tasks_submit");
|
||||
|
||||
let use_mock = matches!(
|
||||
std::env::var("CODEX_CLOUD_TASKS_MODE").ok().as_deref(),
|
||||
Some("mock") | Some("MOCK")
|
||||
);
|
||||
|
||||
let backend: Arc<dyn codex_cloud_tasks_client::CloudBackend> = if use_mock {
|
||||
Arc::new(codex_cloud_tasks_client::MockClient)
|
||||
} else {
|
||||
let base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
|
||||
let ua = codex_core::default_client::get_codex_user_agent(None);
|
||||
let mut http = codex_cloud_tasks_client::HttpClient::new(base_url.clone())?
|
||||
.with_user_agent(ua);
|
||||
|
||||
// Attach ChatGPT auth (required in production)
|
||||
let _token = match codex_core::config::find_codex_home()
|
||||
.ok()
|
||||
.map(|home| {
|
||||
codex_login::AuthManager::new(
|
||||
home,
|
||||
codex_login::AuthMode::ChatGPT,
|
||||
codex_core::default_client::DEFAULT_ORIGINATOR.to_string(),
|
||||
)
|
||||
})
|
||||
.and_then(|am| am.auth())
|
||||
{
|
||||
Some(auth) => match auth.get_token().await {
|
||||
Ok(t) if !t.is_empty() => {
|
||||
http = http.with_bearer_token(t.clone());
|
||||
if let Some(acc) = auth
|
||||
.get_account_id()
|
||||
.or_else(|| util::extract_chatgpt_account_id(&t))
|
||||
{
|
||||
http = http.with_chatgpt_account_id(acc);
|
||||
}
|
||||
t
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Not signed in. Run 'codex login' and retry.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
eprintln!("Not signed in. Run 'codex login' and retry.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
Arc::new(http)
|
||||
};
|
||||
|
||||
// Resolve target environment id
|
||||
let env_id = if let Some(e) = args.env.clone() { e } else {
|
||||
let base_url = util::normalize_base_url(
|
||||
&std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||||
);
|
||||
let headers = util::build_chatgpt_headers().await;
|
||||
match crate::env_detect::autodetect_environment_id(&base_url, &headers, None).await {
|
||||
Ok(sel) => sel.id,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to auto-detect environment. Provide --env <ENV_ID>.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create the task
|
||||
let created = codex_cloud_tasks_client::CloudBackend::create_task(
|
||||
&*backend,
|
||||
&env_id,
|
||||
&args.prompt,
|
||||
&args.git_ref,
|
||||
args.qa,
|
||||
args.best_of,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("create_task failed: {e}"))?;
|
||||
|
||||
// If not waiting, print id and exit quickly
|
||||
if !args.wait {
|
||||
println!("{}", created.id.0);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Poll for completion and output a friendly summary when done
|
||||
use tokio::time::{sleep, Duration};
|
||||
eprintln!("Created task {}; waiting for completion…", created.id.0);
|
||||
let task_id = created.id;
|
||||
let mut seen_msgs = 0usize;
|
||||
loop {
|
||||
let text = codex_cloud_tasks_client::CloudBackend::get_task_text(&*backend, task_id.clone())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// Emit progress hints to stderr so stdout remains the final result only
|
||||
if text.messages.len() > seen_msgs {
|
||||
let new = text.messages.len() - seen_msgs;
|
||||
seen_msgs = text.messages.len();
|
||||
eprintln!("progress: +{} message(s)", new);
|
||||
}
|
||||
|
||||
use codex_cloud_tasks_client::AttemptStatus as S;
|
||||
match text.attempt_status {
|
||||
S::Completed => {
|
||||
// Try to get a diff snapshot if available
|
||||
let diff_opt = codex_cloud_tasks_client::CloudBackend::get_task_diff(&*backend, task_id.clone())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// Build final output as plain text for the agent result
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!("Cloud task completed: {}\n\n", task_id.0));
|
||||
if let Some(p) = text.prompt.as_deref() {
|
||||
out.push_str(&format!("Prompt:\n{}\n\n", p.trim()));
|
||||
}
|
||||
if !text.messages.is_empty() {
|
||||
out.push_str("Assistant Messages:\n");
|
||||
for (i, m) in text.messages.iter().enumerate() {
|
||||
out.push_str(&format!("{}. | ||||