Agent jobs (spawn_agents_on_csv) + progress UI (#10935)

## Summary
- Add agent job support: spawn a batch of sub-agents from CSV, auto-run,
auto-export, and store results in SQLite.
- Simplify workflow: remove run/resume/get-status/export tools; spawn is
deterministic and completes in one call.
- Improve exec UX: stable, single-line progress bar with ETA; suppress
sub-agent chatter in exec.

## Why
Enables map-reduce style workflows over arbitrarily large repos using
the existing Codex orchestrator. This addresses review feedback about
overly complex job controls and non-deterministic monitoring.

## Demo (progress bar)
```
./codex-rs/target/debug/codex exec \
  --enable collab \
  --enable sqlite \
  --full-auto \
  --progress-cursor \
  -c agents.max_threads=16 \
  -C /Users/daveaitel/code/codex \
  - <<'PROMPT'
Create /tmp/agent_job_progress_demo.csv with columns: path,area and 30 rows:
path = item-01..item-30, area = test.

Then call spawn_agents_on_csv with:
- csv_path: /tmp/agent_job_progress_demo.csv
- instruction: "Run `python - <<'PY'` to sleep a random 0.3–1.2s, then output JSON with keys: path, score (int). Set score = 1."
- output_csv_path: /tmp/agent_job_progress_demo_out.csv
PROMPT
```

## Review feedback addressed
- Auto-start jobs on spawn; removed run/resume/status/export tools.
- Auto-export on success.
- More descriptive tool spec + clearer prompts.
- Avoid deadlocks on spawn failure; pending/running handled safely.
- Progress bar no longer scrolls; stable single-line redraw.

## Tests
- `cd codex-rs && cargo test -p codex-exec`
- `cd codex-rs && cargo build -p codex-cli`
This commit is contained in:
daveaitel-openai
2026-02-24 16:00:19 -05:00
committed by GitHub
parent bd192b54cd
commit dcab40123f
36 changed files with 3370 additions and 50 deletions

View File

@@ -41,6 +41,7 @@ use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_oss::ensure_oss_provider_ready;
@@ -86,6 +87,7 @@ struct ThreadEventEnvelope {
thread_id: codex_protocol::ThreadId,
thread: Arc<codex_core::CodexThread>,
event: Event,
suppress_output: bool,
}
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
@@ -113,9 +115,10 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
prompt,
output_schema: output_schema_path,
config_overrides,
progress_cursor,
} = cli;
let (stdout_with_ansi, stderr_with_ansi) = match color {
let (_stdout_with_ansi, stderr_with_ansi) = match color {
cli::Color::Always => (true, true),
cli::Color::Never => (false, false),
cli::Color::Auto => (
@@ -123,6 +126,24 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
supports_color::on_cached(Stream::Stderr).is_some(),
),
};
let cursor_ansi = if progress_cursor {
true
} else {
match color {
cli::Color::Never => false,
cli::Color::Always => true,
cli::Color::Auto => {
if stderr_with_ansi || std::io::stderr().is_terminal() {
true
} else {
match std::env::var("TERM") {
Ok(term) => !term.is_empty() && term != "dumb",
Err(_) => false,
}
}
}
}
};
// Build fmt layer (existing logging) to compose with OTEL layer.
let default_level = "error";
@@ -318,7 +339,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
let mut event_processor: Box<dyn EventProcessor> = match json_mode {
true => Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone())),
_ => Box::new(EventProcessorWithHumanOutput::create_with_ansi(
stdout_with_ansi,
stderr_with_ansi,
cursor_ansi,
&config,
last_message_file.clone(),
)),
@@ -466,7 +488,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<ThreadEventEnvelope>();
let attached_threads = Arc::new(Mutex::new(HashSet::from([primary_thread_id])));
spawn_thread_listener(primary_thread_id, thread.clone(), tx.clone());
spawn_thread_listener(primary_thread_id, thread.clone(), tx.clone(), false);
{
let thread = thread.clone();
@@ -494,7 +516,14 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
match thread_manager.get_thread(thread_id).await {
Ok(thread) => {
attached_threads.lock().await.insert(thread_id);
spawn_thread_listener(thread_id, thread, tx.clone());
let suppress_output =
is_agent_job_subagent(&thread.config_snapshot().await);
spawn_thread_listener(
thread_id,
thread,
tx.clone(),
suppress_output,
);
}
Err(err) => {
warn!("failed to attach listener for thread {thread_id}: {err}")
@@ -549,7 +578,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
thread_id,
thread,
event,
suppress_output,
} = envelope;
if suppress_output && should_suppress_agent_job_event(&event.msg) {
continue;
}
if matches!(event.msg, EventMsg::Error(_)) {
error_seen = true;
}
@@ -613,6 +646,7 @@ fn spawn_thread_listener(
thread_id: codex_protocol::ThreadId,
thread: Arc<codex_core::CodexThread>,
tx: tokio::sync::mpsc::UnboundedSender<ThreadEventEnvelope>,
suppress_output: bool,
) {
tokio::spawn(async move {
loop {
@@ -625,6 +659,7 @@ fn spawn_thread_listener(
thread_id,
thread: Arc::clone(&thread),
event,
suppress_output,
}) {
error!("Error sending event: {err:?}");
break;
@@ -645,6 +680,29 @@ fn spawn_thread_listener(
});
}
fn is_agent_job_subagent(config: &codex_core::ThreadConfigSnapshot) -> bool {
match &config.session_source {
SessionSource::SubAgent(SubAgentSource::Other(source)) => source.starts_with("agent_job:"),
_ => false,
}
}
fn should_suppress_agent_job_event(msg: &EventMsg) -> bool {
!matches!(
msg,
EventMsg::ExecApprovalRequest(_)
| EventMsg::ApplyPatchApprovalRequest(_)
| EventMsg::RequestUserInput(_)
| EventMsg::DynamicToolCallRequest(_)
| EventMsg::ElicitationRequest(_)
| EventMsg::Error(_)
| EventMsg::Warning(_)
| EventMsg::DeprecationNotice(_)
| EventMsg::StreamError(_)
| EventMsg::ShutdownComplete
)
}
async fn resolve_resume_path(
config: &Config,
args: &crate::cli::ResumeArgs,