Add spawned agent skills-message toggle

Support configuring whether spawned agents receive the skills developer message through agents.spawn.inject_skills_message and per-call spawn_agent overrides. Full-history forks continue to inherit the parent setting, and request construction now omits skills instructions when disabled.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Dylan Hurd
2026-04-17 13:25:28 -07:00
co-authored by Codex
parent 6b0ed01fed
commit 0fbef73d74
14 changed files with 158 additions and 27 deletions
+3
View File
@@ -565,6 +565,9 @@ pub struct AgentSpawnToml {
/// MCP server names to start in spawned agents. When unset, spawned agents inherit all
/// effective MCP servers. An empty list disables MCP servers for spawned agents.
pub mcp_servers: Option<Vec<String>>,
/// Whether to inject the skills developer message in spawned agents.
pub inject_skills_message: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
+4
View File
@@ -34,6 +34,10 @@
"AgentSpawnToml": {
"additionalProperties": false,
"properties": {
"inject_skills_message": {
"description": "Whether to inject the skills developer message in spawned agents.",
"type": "boolean"
},
"mcp_servers": {
"description": "MCP server names to start in spawned agents. When unset, spawned agents inherit all effective MCP servers. An empty list disables MCP servers for spawned agents.",
"items": {
+7
View File
@@ -4837,6 +4837,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
inject_skills_message: true,
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("o3".to_string()),
@@ -4989,6 +4990,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
inject_skills_message: true,
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("gpt3".to_string()),
@@ -5139,6 +5141,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
inject_skills_message: true,
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("zdr".to_string()),
@@ -5274,6 +5277,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
inject_skills_message: true,
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("gpt5".to_string()),
@@ -6529,6 +6533,7 @@ async fn agents_spawn_config_sets_child_defaults_only() -> std::io::Result<()> {
codex_home.path().join(CONFIG_TOML_FILE),
r#"[agents.spawn]
mcp_servers = ["docs", " linear ", "docs"]
inject_skills_message = false
"#,
)?;
@@ -6542,9 +6547,11 @@ mcp_servers = ["docs", " linear ", "docs"]
config.agent_spawn,
AgentSpawnConfig {
mcp_servers: Some(vec!["docs".to_string(), "linear".to_string()]),
inject_skills_message: false,
}
);
assert_eq!(config.mcp_server_allowlist, None);
assert!(config.inject_skills_message);
Ok(())
}
+17 -3
View File
@@ -560,6 +560,9 @@ pub struct Config {
/// Settings specific to the task-path-based multi-agent tool surface.
pub multi_agent_v2: MultiAgentV2Config,
/// Whether to inject the `<skills_instructions>` developer block for this session.
pub inject_skills_message: bool,
/// Centralized feature flags; source of truth for feature gating.
pub features: ManagedFeatures,
@@ -614,11 +617,15 @@ pub struct MultiAgentV2Config {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentSpawnConfig {
pub mcp_servers: Option<Vec<String>>,
pub inject_skills_message: bool,
}
impl Default for AgentSpawnConfig {
fn default() -> Self {
Self { mcp_servers: None }
Self {
mcp_servers: None,
inject_skills_message: true,
}
}
}
@@ -1427,8 +1434,9 @@ fn resolve_multi_agent_v2_config(
}
fn resolve_agent_spawn_config(spawn: Option<&AgentSpawnToml>) -> std::io::Result<AgentSpawnConfig> {
let default = AgentSpawnConfig::default();
let Some(spawn) = spawn else {
return Ok(AgentSpawnConfig { mcp_servers: None });
return Ok(default);
};
let mcp_servers = spawn
@@ -1436,7 +1444,12 @@ fn resolve_agent_spawn_config(spawn: Option<&AgentSpawnToml>) -> std::io::Result
.as_ref()
.map(|servers| normalize_mcp_server_allowlist(servers, "agents.spawn.mcp_servers"))
.transpose()?;
Ok(AgentSpawnConfig { mcp_servers })
Ok(AgentSpawnConfig {
mcp_servers,
inject_skills_message: spawn
.inject_skills_message
.unwrap_or(default.inject_skills_message),
})
}
fn normalize_mcp_server_allowlist(
@@ -2309,6 +2322,7 @@ impl Config {
background_terminal_max_timeout,
ghost_snapshot,
multi_agent_v2,
inject_skills_message: true,
features,
suppress_unstable_features_warning: cfg
.suppress_unstable_features_warning
+8 -6
View File
@@ -2411,12 +2411,14 @@ impl Session {
developer_sections.push(apps_section);
}
}
let implicit_skills = turn_context
.turn_skills
.outcome
.allowed_skills_for_implicit_invocation();
if let Some(skills_section) = render_skills_section(&implicit_skills) {
developer_sections.push(skills_section);
if turn_context.config.inject_skills_message {
let implicit_skills = turn_context
.turn_skills
.outcome
.allowed_skills_for_implicit_invocation();
if let Some(skills_section) = render_skills_section(&implicit_skills) {
developer_sections.push(skills_section);
}
}
let loaded_plugins = self
.services
@@ -66,6 +66,7 @@ impl ToolHandler for Handler {
args.model.as_deref(),
args.reasoning_effort,
args.mcp_servers.as_deref(),
args.inject_skills_message,
)?;
} else {
apply_requested_spawn_agent_model_overrides(
@@ -83,6 +84,7 @@ impl ToolHandler for Handler {
&session,
&mut config,
args.mcp_servers,
args.inject_skills_message,
)
.await?;
}
@@ -190,6 +192,7 @@ struct SpawnAgentArgs {
model: Option<String>,
reasoning_effort: Option<ReasoningEffort>,
mcp_servers: Option<Vec<String>>,
inject_skills_message: Option<bool>,
#[serde(default)]
fork_context: bool,
}
@@ -243,14 +243,16 @@ pub(crate) fn reject_full_fork_spawn_overrides(
model: Option<&str>,
reasoning_effort: Option<ReasoningEffort>,
mcp_servers: Option<&[String]>,
inject_skills_message: Option<bool>,
) -> Result<(), FunctionCallError> {
if agent_type.is_some()
|| model.is_some()
|| reasoning_effort.is_some()
|| mcp_servers.is_some()
|| inject_skills_message.is_some()
{
return Err(FunctionCallError::RespondToModel(
"Full-history forked agents inherit the parent agent type, model, reasoning effort, and MCP servers; omit agent_type, model, reasoning_effort, and mcp_servers, or spawn without fork_context/fork_turns=all.".to_string(),
"Full-history forked agents inherit the parent agent type, model, reasoning effort, MCP servers, and skills-message setting; omit agent_type, model, reasoning_effort, mcp_servers, and inject_skills_message, or spawn without fork_context/fork_turns=all.".to_string(),
));
}
Ok(())
@@ -260,6 +262,7 @@ pub(crate) async fn apply_spawn_agent_capability_overrides(
session: &Session,
config: &mut Config,
requested_mcp_servers: Option<Vec<String>>,
requested_inject_skills_message: Option<bool>,
) -> Result<(), FunctionCallError> {
let mcp_server_allowlist = match requested_mcp_servers {
Some(servers) => Some(normalize_spawn_mcp_server_allowlist(
@@ -274,6 +277,8 @@ pub(crate) async fn apply_spawn_agent_capability_overrides(
}
config.mcp_server_allowlist = mcp_server_allowlist;
config.inject_skills_message =
requested_inject_skills_message.unwrap_or(config.agent_spawn.inject_skills_message);
Ok(())
}
@@ -429,7 +429,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
}
#[tokio::test]
async fn spawn_agent_applies_per_call_mcp_settings() {
async fn spawn_agent_applies_per_call_mcp_and_skills_settings() {
#[derive(Debug, Deserialize)]
struct SpawnAgentResult {
agent_id: String,
@@ -462,7 +462,8 @@ async fn spawn_agent_applies_per_call_mcp_settings() {
"spawn_agent",
function_payload(json!({
"message": "inspect this repo",
"mcp_servers": [" docs ", "docs"]
"mcp_servers": [" docs ", "docs"],
"inject_skills_message": false
})),
))
.await
@@ -484,6 +485,7 @@ async fn spawn_agent_applies_per_call_mcp_settings() {
child_config.mcp_server_allowlist,
Some(vec!["docs".to_string()])
);
assert!(!child_config.inject_skills_message);
}
#[tokio::test]
@@ -514,6 +516,7 @@ enabled = false
[agents.spawn]
mcp_servers = ["linear"]
inject_skills_message = false
"#,
)
.await
@@ -551,7 +554,8 @@ mcp_servers = ["linear"]
function_payload(json!({
"message": "inspect this repo",
"agent_type": role_name,
"mcp_servers": ["docs"]
"mcp_servers": ["docs"],
"inject_skills_message": true
})),
))
.await
@@ -576,6 +580,7 @@ mcp_servers = ["linear"]
child_config.mcp_server_allowlist,
Some(vec!["docs".to_string()])
);
assert!(child_config.inject_skills_message);
}
#[tokio::test]
@@ -606,6 +611,7 @@ enabled = false
[agents.spawn]
mcp_servers = ["linear"]
inject_skills_message = false
"#,
)
.await
@@ -659,6 +665,7 @@ mcp_servers = ["linear"]
child_config.mcp_server_allowlist,
Some(vec!["linear".to_string()])
);
assert!(!child_config.inject_skills_message);
}
#[tokio::test]
@@ -712,7 +719,8 @@ async fn spawn_agent_fork_context_rejects_capability_overrides() {
function_payload(json!({
"message": "inspect this repo",
"fork_context": true,
"mcp_servers": []
"mcp_servers": [],
"inject_skills_message": false
})),
))
.await
@@ -721,7 +729,7 @@ async fn spawn_agent_fork_context_rejects_capability_overrides() {
assert_eq!(
err,
FunctionCallError::RespondToModel(
"Full-history forked agents inherit the parent agent type, model, reasoning effort, and MCP servers; omit agent_type, model, reasoning_effort, and mcp_servers, or spawn without fork_context/fork_turns=all.".to_string(),
"Full-history forked agents inherit the parent agent type, model, reasoning effort, MCP servers, and skills-message setting; omit agent_type, model, reasoning_effort, mcp_servers, and inject_skills_message, or spawn without fork_context/fork_turns=all.".to_string(),
)
);
}
@@ -754,7 +762,7 @@ async fn spawn_agent_fork_context_rejects_agent_type_override() {
assert_eq!(
err,
FunctionCallError::RespondToModel(
"Full-history forked agents inherit the parent agent type, model, reasoning effort, and MCP servers; omit agent_type, model, reasoning_effort, and mcp_servers, or spawn without fork_context/fork_turns=all.".to_string(),
"Full-history forked agents inherit the parent agent type, model, reasoning effort, MCP servers, and skills-message setting; omit agent_type, model, reasoning_effort, mcp_servers, and inject_skills_message, or spawn without fork_context/fork_turns=all.".to_string(),
)
);
}
@@ -788,7 +796,7 @@ async fn spawn_agent_fork_context_rejects_child_model_overrides() {
assert_eq!(
err,
FunctionCallError::RespondToModel(
"Full-history forked agents inherit the parent agent type, model, reasoning effort, and MCP servers; omit agent_type, model, reasoning_effort, and mcp_servers, or spawn without fork_context/fork_turns=all.".to_string(),
"Full-history forked agents inherit the parent agent type, model, reasoning effort, MCP servers, and skills-message setting; omit agent_type, model, reasoning_effort, mcp_servers, and inject_skills_message, or spawn without fork_context/fork_turns=all.".to_string(),
)
);
}
@@ -832,7 +840,7 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
assert_eq!(
err,
FunctionCallError::RespondToModel(
"Full-history forked agents inherit the parent agent type, model, reasoning effort, and MCP servers; omit agent_type, model, reasoning_effort, and mcp_servers, or spawn without fork_context/fork_turns=all.".to_string(),
"Full-history forked agents inherit the parent agent type, model, reasoning effort, MCP servers, and skills-message setting; omit agent_type, model, reasoning_effort, mcp_servers, and inject_skills_message, or spawn without fork_context/fork_turns=all.".to_string(),
)
);
}
@@ -873,7 +881,7 @@ async fn multi_agent_v2_spawn_fork_turns_rejects_child_model_overrides() {
assert_eq!(
err,
FunctionCallError::RespondToModel(
"Full-history forked agents inherit the parent agent type, model, reasoning effort, and MCP servers; omit agent_type, model, reasoning_effort, and mcp_servers, or spawn without fork_context/fork_turns=all.".to_string(),
"Full-history forked agents inherit the parent agent type, model, reasoning effort, MCP servers, and skills-message setting; omit agent_type, model, reasoning_effort, mcp_servers, and inject_skills_message, or spawn without fork_context/fork_turns=all.".to_string(),
)
);
}
@@ -1154,7 +1162,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
}
#[tokio::test]
async fn multi_agent_v2_spawn_applies_per_call_mcp_settings() {
async fn multi_agent_v2_spawn_applies_per_call_mcp_and_skills_settings() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
@@ -1189,7 +1197,8 @@ async fn multi_agent_v2_spawn_applies_per_call_mcp_settings() {
function_payload(json!({
"message": "inspect this repo",
"task_name": "test_process",
"mcp_servers": ["docs"]
"mcp_servers": ["docs"],
"inject_skills_message": false
})),
))
.await
@@ -1213,6 +1222,7 @@ async fn multi_agent_v2_spawn_applies_per_call_mcp_settings() {
child_config.mcp_server_allowlist,
Some(vec!["docs".to_string()])
);
assert!(!child_config.inject_skills_message);
}
#[tokio::test]
@@ -76,6 +76,7 @@ impl ToolHandler for Handler {
args.model.as_deref(),
args.reasoning_effort,
args.mcp_servers.as_deref(),
args.inject_skills_message,
)?;
} else {
apply_requested_spawn_agent_model_overrides(
@@ -93,6 +94,7 @@ impl ToolHandler for Handler {
&session,
&mut config,
args.mcp_servers,
args.inject_skills_message,
)
.await?;
}
@@ -242,6 +244,7 @@ struct SpawnAgentArgs {
model: Option<String>,
reasoning_effort: Option<ReasoningEffort>,
mcp_servers: Option<Vec<String>>,
inject_skills_message: Option<bool>,
fork_turns: Option<String>,
fork_context: Option<bool>,
}
+61
View File
@@ -1480,6 +1480,67 @@ async fn skills_append_to_developer_message() {
let _codex_home_guard = codex_home;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn inject_skills_message_false_suppresses_developer_skills_section() {
skip_if_no_network!();
let server = MockServer::start().await;
let resp_mock = mount_sse_once(
&server,
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
)
.await;
let codex_home = Arc::new(TempDir::new().unwrap());
let skill_dir = codex_home.path().join("skills/demo");
std::fs::create_dir_all(&skill_dir).expect("create skill dir");
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: demo\ndescription: build charts\n---\n\n# body\n",
)
.expect("write skill");
let codex_home_path = codex_home.path().to_path_buf();
let codex = test_codex()
.with_home(codex_home.clone())
.with_auth(CodexAuth::from_api_key("Test API Key"))
.with_config(move |config| {
config.cwd = codex_home_path.abs();
config.inject_skills_message = false;
})
.build(&server)
.await
.expect("create new conversation")
.codex;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let request = resp_mock.single_request();
let developer_messages = request.message_input_texts("developer");
let developer_text = developer_messages.join("\n\n");
assert!(
!developer_text.contains("## Skills"),
"did not expect skills section: {developer_messages:?}"
);
assert!(
!developer_text.contains("demo: build charts"),
"did not expect skill summary: {developer_messages:?}"
);
let _codex_home_guard = codex_home;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
+14
View File
@@ -548,6 +548,13 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap<St
),
),
),
(
"inject_skills_message".to_string(),
JsonSchema::boolean(Some(
"Optional override for whether the new agent receives the skills developer message."
.to_string(),
)),
),
])
}
@@ -592,6 +599,13 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
),
),
),
(
"inject_skills_message".to_string(),
JsonSchema::boolean(Some(
"Optional override for whether the new agent receives the skills developer message."
.to_string(),
)),
),
])
}
+2
View File
@@ -67,6 +67,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
assert!(properties.contains_key("message"));
assert!(properties.contains_key("fork_turns"));
assert!(properties.contains_key("mcp_servers"));
assert!(properties.contains_key("inject_skills_message"));
assert!(!properties.contains_key("items"));
assert!(!properties.contains_key("fork_context"));
assert_eq!(
@@ -107,6 +108,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
assert!(properties.contains_key("fork_context"));
assert!(properties.contains_key("mcp_servers"));
assert!(properties.contains_key("inject_skills_message"));
assert!(!properties.contains_key("fork_turns"));
}
@@ -184,6 +184,7 @@ fn test_build_specs_collab_tools_enabled() {
let (properties, _) = expect_object_schema(parameters);
assert!(properties.contains_key("fork_context"));
assert!(properties.contains_key("mcp_servers"));
assert!(properties.contains_key("inject_skills_message"));
assert!(!properties.contains_key("fork_turns"));
}
@@ -237,6 +238,7 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
assert!(properties.contains_key("message"));
assert!(properties.contains_key("fork_turns"));
assert!(properties.contains_key("mcp_servers"));
assert!(properties.contains_key("inject_skills_message"));
assert!(!properties.contains_key("items"));
assert!(!properties.contains_key("fork_context"));
assert_eq!(
+7 -6
View File
@@ -42,21 +42,22 @@ default_tools_approval_mode = "approve"
approval_mode = "prompt"
```
## Spawned agent MCP defaults
## Spawned agent defaults
Use `[agents.spawn]` to set MCP defaults for newly spawned agents. `mcp_servers`
Use `[agents.spawn]` to set defaults for newly spawned agents. `mcp_servers`
is an optional allowlist of MCP server names; omit it to inherit all effective
MCP servers, or set it to an empty list to disable MCP servers in spawned
agents.
agents. `inject_skills_message` controls whether spawned agents receive the
skills developer message.
```toml
[agents.spawn]
mcp_servers = ["docs"]
inject_skills_message = false
```
The `spawn_agent` tool can override this setting per spawned agent. Full
history forks inherit the parent session MCP settings and reject these
overrides.
The `spawn_agent` tool can override either setting per spawned agent. Full
history forks inherit the parent session settings and reject these overrides.
## Apps (Connectors)