mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
dcab40123f
## 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`
257 lines
7.8 KiB
Rust
257 lines
7.8 KiB
Rust
use anyhow::Result;
|
|
use chrono::DateTime;
|
|
use chrono::Utc;
|
|
use serde_json::Value;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum AgentJobStatus {
|
|
Pending,
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
impl AgentJobStatus {
|
|
pub const fn as_str(self) -> &'static str {
|
|
match self {
|
|
AgentJobStatus::Pending => "pending",
|
|
AgentJobStatus::Running => "running",
|
|
AgentJobStatus::Completed => "completed",
|
|
AgentJobStatus::Failed => "failed",
|
|
AgentJobStatus::Cancelled => "cancelled",
|
|
}
|
|
}
|
|
|
|
pub fn parse(value: &str) -> Result<Self> {
|
|
match value {
|
|
"pending" => Ok(Self::Pending),
|
|
"running" => Ok(Self::Running),
|
|
"completed" => Ok(Self::Completed),
|
|
"failed" => Ok(Self::Failed),
|
|
"cancelled" => Ok(Self::Cancelled),
|
|
_ => Err(anyhow::anyhow!("invalid agent job status: {value}")),
|
|
}
|
|
}
|
|
|
|
pub fn is_final(self) -> bool {
|
|
matches!(
|
|
self,
|
|
AgentJobStatus::Completed | AgentJobStatus::Failed | AgentJobStatus::Cancelled
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum AgentJobItemStatus {
|
|
Pending,
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
impl AgentJobItemStatus {
|
|
pub const fn as_str(self) -> &'static str {
|
|
match self {
|
|
AgentJobItemStatus::Pending => "pending",
|
|
AgentJobItemStatus::Running => "running",
|
|
AgentJobItemStatus::Completed => "completed",
|
|
AgentJobItemStatus::Failed => "failed",
|
|
}
|
|
}
|
|
|
|
pub fn parse(value: &str) -> Result<Self> {
|
|
match value {
|
|
"pending" => Ok(Self::Pending),
|
|
"running" => Ok(Self::Running),
|
|
"completed" => Ok(Self::Completed),
|
|
"failed" => Ok(Self::Failed),
|
|
_ => Err(anyhow::anyhow!("invalid agent job item status: {value}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct AgentJob {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub status: AgentJobStatus,
|
|
pub instruction: String,
|
|
pub auto_export: bool,
|
|
pub max_runtime_seconds: Option<u64>,
|
|
// TODO(jif-oai): Convert to JSON Schema and enforce structured outputs.
|
|
pub output_schema_json: Option<Value>,
|
|
pub input_headers: Vec<String>,
|
|
pub input_csv_path: String,
|
|
pub output_csv_path: String,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub started_at: Option<DateTime<Utc>>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
pub last_error: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct AgentJobItem {
|
|
pub job_id: String,
|
|
pub item_id: String,
|
|
pub row_index: i64,
|
|
pub source_id: Option<String>,
|
|
pub row_json: Value,
|
|
pub status: AgentJobItemStatus,
|
|
pub assigned_thread_id: Option<String>,
|
|
pub attempt_count: i64,
|
|
pub result_json: Option<Value>,
|
|
pub last_error: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
pub reported_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct AgentJobProgress {
|
|
pub total_items: usize,
|
|
pub pending_items: usize,
|
|
pub running_items: usize,
|
|
pub completed_items: usize,
|
|
pub failed_items: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct AgentJobCreateParams {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub instruction: String,
|
|
pub auto_export: bool,
|
|
pub max_runtime_seconds: Option<u64>,
|
|
pub output_schema_json: Option<Value>,
|
|
pub input_headers: Vec<String>,
|
|
pub input_csv_path: String,
|
|
pub output_csv_path: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct AgentJobItemCreateParams {
|
|
pub item_id: String,
|
|
pub row_index: i64,
|
|
pub source_id: Option<String>,
|
|
pub row_json: Value,
|
|
}
|
|
|
|
#[derive(Debug, sqlx::FromRow)]
|
|
pub(crate) struct AgentJobRow {
|
|
pub(crate) id: String,
|
|
pub(crate) name: String,
|
|
pub(crate) status: String,
|
|
pub(crate) instruction: String,
|
|
pub(crate) auto_export: i64,
|
|
pub(crate) max_runtime_seconds: Option<i64>,
|
|
pub(crate) output_schema_json: Option<String>,
|
|
pub(crate) input_headers_json: String,
|
|
pub(crate) input_csv_path: String,
|
|
pub(crate) output_csv_path: String,
|
|
pub(crate) created_at: i64,
|
|
pub(crate) updated_at: i64,
|
|
pub(crate) started_at: Option<i64>,
|
|
pub(crate) completed_at: Option<i64>,
|
|
pub(crate) last_error: Option<String>,
|
|
}
|
|
|
|
impl TryFrom<AgentJobRow> for AgentJob {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: AgentJobRow) -> Result<Self, Self::Error> {
|
|
let output_schema_json = value
|
|
.output_schema_json
|
|
.as_deref()
|
|
.map(serde_json::from_str)
|
|
.transpose()?;
|
|
let input_headers = serde_json::from_str(value.input_headers_json.as_str())?;
|
|
let max_runtime_seconds = value
|
|
.max_runtime_seconds
|
|
.map(u64::try_from)
|
|
.transpose()
|
|
.map_err(|_| anyhow::anyhow!("invalid max_runtime_seconds value"))?;
|
|
Ok(Self {
|
|
id: value.id,
|
|
name: value.name,
|
|
status: AgentJobStatus::parse(value.status.as_str())?,
|
|
instruction: value.instruction,
|
|
auto_export: value.auto_export != 0,
|
|
max_runtime_seconds,
|
|
output_schema_json,
|
|
input_headers,
|
|
input_csv_path: value.input_csv_path,
|
|
output_csv_path: value.output_csv_path,
|
|
created_at: epoch_seconds_to_datetime(value.created_at)?,
|
|
updated_at: epoch_seconds_to_datetime(value.updated_at)?,
|
|
started_at: value
|
|
.started_at
|
|
.map(epoch_seconds_to_datetime)
|
|
.transpose()?,
|
|
completed_at: value
|
|
.completed_at
|
|
.map(epoch_seconds_to_datetime)
|
|
.transpose()?,
|
|
last_error: value.last_error,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, sqlx::FromRow)]
|
|
pub(crate) struct AgentJobItemRow {
|
|
pub(crate) job_id: String,
|
|
pub(crate) item_id: String,
|
|
pub(crate) row_index: i64,
|
|
pub(crate) source_id: Option<String>,
|
|
pub(crate) row_json: String,
|
|
pub(crate) status: String,
|
|
pub(crate) assigned_thread_id: Option<String>,
|
|
pub(crate) attempt_count: i64,
|
|
pub(crate) result_json: Option<String>,
|
|
pub(crate) last_error: Option<String>,
|
|
pub(crate) created_at: i64,
|
|
pub(crate) updated_at: i64,
|
|
pub(crate) completed_at: Option<i64>,
|
|
pub(crate) reported_at: Option<i64>,
|
|
}
|
|
|
|
impl TryFrom<AgentJobItemRow> for AgentJobItem {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: AgentJobItemRow) -> Result<Self, Self::Error> {
|
|
Ok(Self {
|
|
job_id: value.job_id,
|
|
item_id: value.item_id,
|
|
row_index: value.row_index,
|
|
source_id: value.source_id,
|
|
row_json: serde_json::from_str(value.row_json.as_str())?,
|
|
status: AgentJobItemStatus::parse(value.status.as_str())?,
|
|
assigned_thread_id: value.assigned_thread_id,
|
|
attempt_count: value.attempt_count,
|
|
result_json: value
|
|
.result_json
|
|
.as_deref()
|
|
.map(serde_json::from_str)
|
|
.transpose()?,
|
|
last_error: value.last_error,
|
|
created_at: epoch_seconds_to_datetime(value.created_at)?,
|
|
updated_at: epoch_seconds_to_datetime(value.updated_at)?,
|
|
completed_at: value
|
|
.completed_at
|
|
.map(epoch_seconds_to_datetime)
|
|
.transpose()?,
|
|
reported_at: value
|
|
.reported_at
|
|
.map(epoch_seconds_to_datetime)
|
|
.transpose()?,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn epoch_seconds_to_datetime(secs: i64) -> Result<DateTime<Utc>> {
|
|
DateTime::<Utc>::from_timestamp(secs, 0)
|
|
.ok_or_else(|| anyhow::anyhow!("invalid unix timestamp: {secs}"))
|
|
}
|