diff --git a/codex-rs/agentic.md b/codex-rs/agentic.md index 770c244591..f47f0b4660 100644 --- a/codex-rs/agentic.md +++ b/codex-rs/agentic.md @@ -13,7 +13,7 @@ The focus is on: ## 1. Mental Model -- A **Codex session** remains the top‑level unit, with one running `SessionTask` (typically `RegularTask`) at a time. +- A **Codex session** remains the top‑level unit, with one running `SessionTask` at a time. - Within a session we introduce a **Collaboration Graph**: a set of logical agents that can exchange messages and call tools, orchestrated entirely within the same session. - The **main agent** corresponds to the existing single agent Codex exposes today. It gets the user’s prompt and controls collaboration via the new `collaboration.*` tools. - **Child agents** are lightweight logical agents: @@ -624,7 +624,6 @@ Add collaboration‑related helpers on `Session`: The single‑agent path (today’s behavior) remains: -- `RegularTask::run` → `run_task` loop. - `run_task` builds `prompt` from *session* history and calls `run_turn`. For collaboration, we introduce a **CollaborationTask**: diff --git a/codex-rs/core/gpt-5.1-codex-max_prompt.md b/codex-rs/core/gpt-5.1-codex-max_prompt.md index 292e5d7d0f..bfc02776a0 100644 --- a/codex-rs/core/gpt-5.1-codex-max_prompt.md +++ b/codex-rs/core/gpt-5.1-codex-max_prompt.md @@ -18,6 +18,18 @@ You are Codex, based on GPT-5. You are running as a coding agent in the Codex CL - While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed. - **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user. +## Collaboration +You can spawn and coordinate child agents using these tools (only on this model): +- `collaboration_init_agent`: create a direct child with optional instructions/model; depth/agent limits apply. No initial message is sent at this point. +- `collaboration_send`: send a user-message to your direct children by id. +- `collaboration_wait`: run children for up to `max_duration` tokens and surface their last message/status. +- `collaboration_get_state`: see all agents, parents, statuses, and last messages. +- `collaboration_close`: close specific children (and their descendants). + +Children inherit your instructions unless overridden. Always `wait` after `send` to drive children forward; keep communication concise and include the expected output format. Use `get_state` if unsure about child ids/status. + +A `collaboration_send` is always necessary after a `collaboration_init_agent` to start the child agent working. + ## Plan tool When using the planning tool: diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d78ff894b3..1a89e067b8 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -398,10 +398,6 @@ impl SessionConfiguration { self.user_instructions.clone() } - pub(crate) fn approval_policy(&self) -> AskForApproval { - self.approval_policy - } - pub(crate) fn sandbox_policy(&self) -> SandboxPolicy { self.sandbox_policy.clone() } diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs index bb78de64fc..54bff30b68 100644 --- a/codex-rs/core/src/model_family.rs +++ b/codex-rs/core/src/model_family.rs @@ -196,10 +196,6 @@ pub fn find_family_for_model(slug: &str) -> Option { base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(), apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), experimental_supported_tools: vec![ - "grep_files".to_string(), - "list_dir".to_string(), - "read_file".to_string(), - "test_sync_tool".to_string(), "collaboration".to_string(), ], shell_type: ConfigShellToolType::ShellCommand, diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index b0c0c43381..66956cbedf 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -1,7 +1,6 @@ mod collaboration; mod compact; mod ghost_snapshot; -mod regular; mod review; mod undo; mod user_shell; @@ -30,6 +29,7 @@ use crate::state::TaskKind; use codex_protocol::user_input::UserInput; pub(crate) use collaboration::CollaborationSupervisor; +pub(crate) use collaboration::CollaborationTask; pub(crate) use compact::CompactTask; pub(crate) use ghost_snapshot::GhostSnapshotTask; pub(crate) use review::ReviewTask; diff --git a/codex-rs/core/src/tasks/regular.rs b/codex-rs/core/src/tasks/regular.rs deleted file mode 100644 index 416dba3f7f..0000000000 --- a/codex-rs/core/src/tasks/regular.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use tokio_util::sync::CancellationToken; - -use crate::codex::TurnContext; -use crate::codex::run_task; -use crate::state::TaskKind; -use codex_protocol::user_input::UserInput; - -use super::SessionTask; -use super::SessionTaskContext; - -#[derive(Clone, Copy, Default)] -pub(crate) struct RegularTask; - -#[async_trait] -impl SessionTask for RegularTask { - fn kind(&self) -> TaskKind { - TaskKind::Regular - } - - async fn run( - self: Arc, - session: Arc, - ctx: Arc, - input: Vec, - cancellation_token: CancellationToken, - ) -> Option { - let sess = session.clone_session(); - run_task(sess, ctx, input, cancellation_token).await - } -} diff --git a/codex-rs/core/src/tools/handlers/collaboration.rs b/codex-rs/core/src/tools/handlers/collaboration.rs index 173f10b609..220b2e0214 100644 --- a/codex-rs/core/src/tools/handlers/collaboration.rs +++ b/codex-rs/core/src/tools/handlers/collaboration.rs @@ -25,6 +25,7 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use serde_json::json; use std::path::PathBuf; +use tracing::info; #[derive(Debug, Default, Serialize, Deserialize)] pub(crate) struct ExtraMetadata(pub HashMap); @@ -235,7 +236,7 @@ fn build_agent_system_message( ResponseItem::Message { id: None, - role: "system".to_string(), + role: "user".to_string(), content: vec![ContentItem::InputText { text: lines.join("\n"), }], @@ -269,21 +270,6 @@ fn build_history_for_child( history } -async fn record_session_summary( - session: &crate::codex::Session, - turn: &crate::codex::TurnContext, - text: String, -) { - let summary = ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![ContentItem::InputText { text }], - }; - session - .record_conversation_items(turn, std::slice::from_ref(&summary)) - .await; -} - pub struct CollaborationHandler; impl Default for CollaborationHandler { @@ -456,29 +442,18 @@ async fn handle_init_agent( extra: ExtraMetadata(extra), }; - let content = format!( - "Initialized agent {} at depth {} in cwd {}", - assigned_id.0, - depth, - turn.cwd.display() - ); + let content = format!("Initialized agent {} at depth {}", assigned_id.0, depth); if !cfg!(test) { let supervisor = session.ensure_collaboration_supervisor().await; supervisor.kick_agent(assigned_id).await; } - record_session_summary( - session.as_ref(), - turn, - format!( - "Agent {} initialized (child of {})", - assigned_id.0, parent.id.0 - ), - ) - .await; - let output = CollaborationInitAgentOutput { content, metadata }; + info!( + "collaboration_init_agent: assigned_id={}, parent_id={}, depth={}", + assigned_id.0, parent.id.0, depth + ); serialize_function_output(&output, true) } @@ -591,19 +566,14 @@ async fn handle_send( let success = metadata.message_tool_call_success; - if success { - record_session_summary( - session.as_ref(), - turn, - format!( - "Agent {} sent to {:?}: {}", - sender_id.0, valid_recipients, input.message - ), - ) - .await; + if success && !valid_recipients.is_empty() { let supervisor = session.ensure_collaboration_supervisor().await; - for recipient in &valid_recipients { - supervisor.kick_agent(*recipient).await; + for recipient in valid_recipients + .iter() + .copied() + .filter(|recipient| recipient.0 != 0) + { + supervisor.kick_agent(recipient).await; } } @@ -710,9 +680,56 @@ async fn handle_wait( return serialize_function_output(&output, false); } - let mut agents_ran = Vec::new(); let supervisor = session.ensure_collaboration_supervisor().await; let mut rx = supervisor.subscribe(); + + let snapshot = |agent: &AgentState, delta_tokens: i32, sub_id: Option| { + json!({ + "agent_idx": agent.id.0, + "delta_tokens": delta_tokens, + "status": status_label(&agent.status), + "status_detail": status_detail(&agent.status), + "last_agent_message": last_agent_message(&agent.status), + "sub_id": sub_id, + }) + }; + + let mut agents_ran = Vec::new(); + { + // Short-circuit if all targets are already idle/terminal. + let collab = session.collaboration_state().lock().await; + if targets.iter().all(|id| { + collab + .agent(*id) + .map(|agent| !matches!(agent.status, AgentLifecycleState::Running)) + .unwrap_or(false) + }) { + let mut extra = HashMap::new(); + extra.insert( + "agents_ran".to_string(), + json!( + targets + .iter() + .filter_map(|id| collab.agent(*id)) + .map(|agent| snapshot(agent, 0, None)) + .collect::>() + ), + ); + extra.insert("token_budget_exhausted".to_string(), json!(false)); + let metadata = CollaborationWaitMetadata { + message_tool_call_success: true, + message_tool_call_error_should_penalize_model: false, + is_wait_success_msg: Some(true), + extra: ExtraMetadata(extra), + }; + let output = CollaborationWaitOutput { + content: "Finished waiting.".to_string(), + metadata, + }; + return serialize_function_output(&output, true); + } + } + supervisor .start_agents(targets.clone(), input.max_duration) .await @@ -731,29 +748,6 @@ async fn handle_wait( } remaining = remaining.saturating_sub(event.delta_tokens); - if let Some(message) = event.last_message.clone() { - record_session_summary( - session.as_ref(), - turn, - format!("Agent {} replied: {}", event.agent.0, message), - ) - .await; - } else if let AgentLifecycleState::Error { error } = &event.status { - record_session_summary( - session.as_ref(), - turn, - format!("Agent {} failed: {error}", event.agent.0), - ) - .await; - } else if !matches!(event.status, AgentLifecycleState::Error { .. }) { - record_session_summary( - session.as_ref(), - turn, - format!("Agent {} finished turn.", event.agent.0), - ) - .await; - } - let detail = status_detail(&event.status); agents_ran.push(json!({ "agent_idx": event.agent.0, @@ -949,15 +943,6 @@ async fn handle_close( ) .await; } - record_session_summary( - session.as_ref(), - turn, - format!( - "Agent {} closed agents {:?}", - caller_id.0, closed_for_summary - ), - ) - .await; serialize_function_output(&output, true) } @@ -1173,7 +1158,7 @@ mod tests { assert_eq!(items.len(), 2); assert!(matches!( items.first(), - Some(ResponseItem::Message { role, .. }) if role == "system" + Some(ResponseItem::Message { role, .. }) if role == "user" )); assert!(matches!( items.get(1), diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 9056be4ac4..f35ff06315 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::collections::HashSet; use std::sync::Arc; use std::time::Duration;