feat: add agent roles to collab tools (#9275)

Add `agent_type` parameter to the collab tool `spawn_agent` that
contains a preset to apply on the config when spawning this agent
This commit is contained in:
jif-oai
2026-01-15 13:33:52 +00:00
committed by GitHub
parent bad4c12b9d
commit 05b960671d
6 changed files with 142 additions and 14 deletions
+2
View File
@@ -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;
+85
View File
@@ -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<String> {
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(())
}
}
+1 -13
View File
@@ -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(),
+7 -1
View File
@@ -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<AgentRole>,
}
#[derive(Debug, Serialize)]
@@ -95,6 +97,7 @@ mod spawn {
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
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
+10
View File
@@ -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(),