diff --git a/codex-rs/core/src/agent/mod.rs b/codex-rs/core/src/agent/mod.rs index d6348b38b..0387eda49 100644 --- a/codex-rs/core/src/agent/mod.rs +++ b/codex-rs/core/src/agent/mod.rs @@ -1,6 +1,8 @@ pub(crate) mod control; +pub(crate) mod role; pub(crate) mod status; pub(crate) use codex_protocol::protocol::AgentStatus; pub(crate) use control::AgentControl; +pub(crate) use role::AgentRole; pub(crate) use status::agent_status_from_event; diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs new file mode 100644 index 000000000..acb2481fc --- /dev/null +++ b/codex-rs/core/src/agent/role.rs @@ -0,0 +1,85 @@ +use crate::config::Config; +use crate::protocol::SandboxPolicy; +use serde::Deserialize; +use serde::Serialize; + +/// Base instructions for the orchestrator role. +const ORCHESTRATOR_PROMPT: &str = include_str!("../../templates/agents/orchestrator.md"); +/// Base instructions for the worker role. +const WORKER_PROMPT: &str = include_str!("../../gpt-5.2-codex_prompt.md"); +/// Default worker model override used by the worker role. +const WORKER_MODEL: &str = "gpt-5.2-codex"; + +/// Enumerated list of all supported agent roles. +const ALL_ROLES: [AgentRole; 3] = [ + AgentRole::Default, + AgentRole::Orchestrator, + AgentRole::Worker, +]; + +/// Hard-coded agent role selection used when spawning sub-agents. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentRole { + /// Inherit the parent agent's configuration unchanged. + Default, + /// Coordination-only agent that delegates to workers. + Orchestrator, + /// Task-executing agent with a fixed model override. + Worker, +} + +/// Immutable profile data that drives per-agent configuration overrides. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct AgentProfile { + /// Optional base instructions override. + pub base_instructions: Option<&'static str>, + /// Optional model override. + pub model: Option<&'static str>, + /// Whether to force a read-only sandbox policy. + pub read_only: bool, +} + +impl AgentRole { + /// Returns the string values used by JSON schema enums. + pub fn enum_values() -> Vec { + ALL_ROLES + .iter() + .filter_map(|role| serde_json::to_string(role).ok()) + .collect() + } + + /// Returns the hard-coded profile for this role. + pub fn profile(self) -> AgentProfile { + match self { + AgentRole::Default => AgentProfile::default(), + AgentRole::Orchestrator => AgentProfile { + base_instructions: Some(ORCHESTRATOR_PROMPT), + ..Default::default() + }, + AgentRole::Worker => AgentProfile { + base_instructions: Some(WORKER_PROMPT), + model: Some(WORKER_MODEL), + ..Default::default() + }, + } + } + + /// Applies this role's profile onto the provided config. + pub fn apply_to_config(self, config: &mut Config) -> Result<(), String> { + let profile = self.profile(); + if let Some(base_instructions) = profile.base_instructions { + config.base_instructions = Some(base_instructions.to_string()); + } + if let Some(model) = profile.model { + config.model = Some(model.to_string()); + } + if profile.read_only { + config + .sandbox_policy + .set(SandboxPolicy::new_read_only_policy()) + .map_err(|err| format!("sandbox_policy is invalid: {err}"))?; + } + Ok(()) + } +} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 158443ee4..f2ec33424 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -541,24 +541,12 @@ impl Session { web_search_mode: per_turn_config.web_search_mode, }); - let base_instructions = if per_turn_config.features.enabled(Feature::Collab) { - const COLLAB_INSTRUCTIONS: &str = - include_str!("../templates/collab/experimental_prompt.md"); - let base = session_configuration - .base_instructions - .as_deref() - .unwrap_or(model_info.base_instructions.as_str()); - Some(format!("{base}\n\n{COLLAB_INSTRUCTIONS}")) - } else { - session_configuration.base_instructions.clone() - }; - TurnContext { sub_id, client, cwd: session_configuration.cwd.clone(), developer_instructions: session_configuration.developer_instructions.clone(), - base_instructions, + base_instructions: session_configuration.base_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), user_instructions: session_configuration.user_instructions.clone(), approval_policy: session_configuration.approval_policy.value(), diff --git a/codex-rs/core/src/tools/handlers/collab.rs b/codex-rs/core/src/tools/handlers/collab.rs index 805af76b3..3c24cf137 100644 --- a/codex-rs/core/src/tools/handlers/collab.rs +++ b/codex-rs/core/src/tools/handlers/collab.rs @@ -76,11 +76,13 @@ impl ToolHandler for CollabHandler { mod spawn { use super::*; + use crate::agent::AgentRole; use std::sync::Arc; #[derive(Debug, Deserialize)] struct SpawnAgentArgs { message: String, + agent_type: Option, } #[derive(Debug, Serialize)] @@ -95,6 +97,7 @@ mod spawn { arguments: String, ) -> Result { let args: SpawnAgentArgs = parse_arguments(&arguments)?; + let agent_role = args.agent_type.unwrap_or(AgentRole::Default); let prompt = args.message; if prompt.trim().is_empty() { return Err(FunctionCallError::RespondToModel( @@ -112,7 +115,10 @@ mod spawn { .into(), ) .await; - let config = build_agent_spawn_config(turn.as_ref())?; + let mut config = build_agent_spawn_config(turn.as_ref())?; + agent_role + .apply_to_config(&mut config) + .map_err(FunctionCallError::RespondToModel)?; let result = session .services .agent_control diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 28fc3a170..7e14e7697 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -1,3 +1,4 @@ +use crate::agent::AgentRole; use crate::client_common::tools::ResponsesApiTool; use crate::client_common::tools::ToolSpec; use crate::features::Feature; @@ -441,6 +442,15 @@ fn create_spawn_agent_tool() -> ToolSpec { description: Some("Initial message to send to the new agent.".to_string()), }, ); + properties.insert( + "agent_type".to_string(), + JsonSchema::String { + description: Some(format!( + "Optional agent type to spawn ({}).", + AgentRole::enum_values().join(", ") + )), + }, + ); ToolSpec::Function(ResponsesApiTool { name: "spawn_agent".to_string(), diff --git a/codex-rs/core/templates/agents/orchestrator.md b/codex-rs/core/templates/agents/orchestrator.md new file mode 100644 index 000000000..dc30f8414 --- /dev/null +++ b/codex-rs/core/templates/agents/orchestrator.md @@ -0,0 +1,37 @@ +You are Codex Orchestrator, based on GPT-5. You are running as an orchestration agent in the Codex CLI on a user's computer. + +## Role + +- You do not solve the task yourself. Your job is to delegate, coordinate, and verify. +- Monitor progress, resolve conflicts, and integrate results into a single, coherent outcome. +- You should always spawn a worker to perform actual work but before this, you can discuss the problem, ask follow-up questions, discussion design etc. Workers are only here to perform the actual job. + +## Multi-agent workflow + +1. Understand the request and identify the minimum set of workers needed. +2. Spawn worker(s) with precise goals, constraints, and expected deliverables. +3. Monitor workers with `wait`, route questions via `send_input`, and keep scope boundaries clear. +4. When all workers report done, spawn a verifier agent to review the work. +5. If the verifier reports issues, assign fixes to the relevant worker(s) and repeat steps 3–5 until the verifier passes. +6. Close all agents when you don't need them anymore (i.e. when the task if fully finished). + +## Collaboration rules + +- Tell every worker they are not alone in the environment and must not revert or overwrite others' work. +- Default: workers must not spawn sub-agents unless you explicitly allow it. +- For large logs or long-running tasks (tests, builds), delegate to a worker and instruct them not to spawn additional agents. +- Use sensible `wait` timeouts and adjust for task size; do not exceed maximums. + +## Collab tools + +- `spawn_agent`: create a worker or verifier with an initial prompt (set `agent_type`). +- `send_input`: send follow-ups, clarifications, or fix requests (`interrupt` can stop the current task first). +- `wait`: poll an agent for completion or status. +- `close_agent`: close the agent when done. + +## Presenting your work and final message + +- Keep responses concise, factual, and in plain text. +- Summarize: what was delegated, key outcomes, tests/verification status, and any remaining risks. +- If verification failed, state the issues clearly and what you asked workers to change. +- Do not dump large files; reference paths with backticks.