From 113c87e8ae2bc5b8b5971a874a3c9fe5cafd661c Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sat, 23 Aug 2025 18:30:38 -0700 Subject: [PATCH] agents --- codex-rs/core/src/subagents/default_agents.rs | 6 ++- codex-rs/core/src/subagents/definition.rs | 20 ++++++-- codex-rs/core/src/subagents/registry.rs | 14 ++++-- codex-rs/core/src/subagents/runner.rs | 48 ++++++++++++++----- 4 files changed, 67 insertions(+), 21 deletions(-) diff --git a/codex-rs/core/src/subagents/default_agents.rs b/codex-rs/core/src/subagents/default_agents.rs index 3f4cf19654..602a209648 100644 --- a/codex-rs/core/src/subagents/default_agents.rs +++ b/codex-rs/core/src/subagents/default_agents.rs @@ -1,4 +1,5 @@ use super::definition::SubagentDefinition; +use super::definition::SubagentSource; use include_dir::Dir; use include_dir::include_dir; @@ -20,7 +21,10 @@ pub(crate) fn embedded_defs() -> Vec { { match file.contents_utf8() { Some(contents) => match SubagentDefinition::from_json_str(contents) { - Ok(def) => defs.push(def), + Ok(mut def) => { + def.source = SubagentSource::EmbeddedDefault; + defs.push(def) + } Err(e) => { tracing::warn!( "failed to parse embedded default subagent '{}': {}", diff --git a/codex-rs/core/src/subagents/definition.rs b/codex-rs/core/src/subagents/definition.rs index aea54f8569..74f46d9fb9 100644 --- a/codex-rs/core/src/subagents/definition.rs +++ b/codex-rs/core/src/subagents/definition.rs @@ -4,17 +4,22 @@ use serde::Deserialize; use std::fs; use std::path::Path; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SubagentSource { + #[default] + EmbeddedDefault, + User, + Project, +} + #[derive(Debug, Clone, Deserialize)] pub struct SubagentDefinition { pub name: String, pub description: String, /// Base instructions for this subagent. pub instructions: String, - /// When not set, inherits the parent agent's tool set. When set to an - /// empty list, no tools are available to the subagent. - #[serde(default)] - pub tools: Option>, // None => inherit; Some(vec) => allow-list + //TODO: add allowed tools. we inherit the parent agent's tools for now. /// Optional structured output schema. When set, the subagent must return a /// single JSON value that validates against this schema. The schema will be /// embedded into the subagent's instructions so the model can adhere to it. @@ -30,6 +35,12 @@ pub struct SubagentDefinition { /// inherits the parent session's configured reasoning effort. #[serde(default)] pub reasoning_effort: Option, + + /// Where this definition was loaded from; used for precedence rules and + /// behavior differences (e.g., instruction composition). + /// Not serialized; defaults to EmbeddedDefault. + #[serde(skip)] + pub(crate) source: SubagentSource, } impl SubagentDefinition { @@ -51,5 +62,4 @@ impl SubagentDefinition { pub(crate) fn output_schema(&self) -> Option<&JsonSchema> { self.output_schema.as_ref() } - } diff --git a/codex-rs/core/src/subagents/registry.rs b/codex-rs/core/src/subagents/registry.rs index 3c0cb9956b..e560ce2e48 100644 --- a/codex-rs/core/src/subagents/registry.rs +++ b/codex-rs/core/src/subagents/registry.rs @@ -1,5 +1,6 @@ use super::default_agents; use super::definition::SubagentDefinition; +use super::definition::SubagentSource; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -35,11 +36,11 @@ impl SubagentRegistry { // Load user definitions first if let Some(dir) = &self.user_dir { - Self::load_from_dir_into(dir, &mut map); + Self::load_from_dir_into(dir, &mut map, SubagentSource::User); } // Then load project definitions which override on conflicts if let Some(dir) = &self.project_dir { - Self::load_from_dir_into(dir, &mut map); + Self::load_from_dir_into(dir, &mut map, SubagentSource::Project); } // No ad-hoc fallback here; embedded defaults already include "hello". @@ -55,7 +56,11 @@ impl SubagentRegistry { self.map.keys().cloned().collect() } - fn load_from_dir_into(dir: &Path, out: &mut HashMap) { + fn load_from_dir_into( + dir: &Path, + out: &mut HashMap, + source: SubagentSource, + ) { let Ok(iter) = fs::read_dir(dir) else { return; }; @@ -69,7 +74,8 @@ impl SubagentRegistry { .unwrap_or(false) { match SubagentDefinition::from_file(&path) { - Ok(def) => { + Ok(mut def) => { + def.source = source; out.insert(def.name.clone(), def); } Err(e) => { diff --git a/codex-rs/core/src/subagents/runner.rs b/codex-rs/core/src/subagents/runner.rs index f9a56539c3..49721bf1d3 100644 --- a/codex-rs/core/src/subagents/runner.rs +++ b/codex-rs/core/src/subagents/runner.rs @@ -2,6 +2,7 @@ use crate::codex::Codex; use crate::error::Result as CodexResult; use super::definition::SubagentDefinition; +use super::definition::SubagentSource; use super::registry::SubagentRegistry; use crate::openai_tools::JsonSchema; use serde_json::Value as JsonValue; @@ -15,6 +16,40 @@ pub struct RunSubagentArgs { pub context: Option, } +/// Build the effective base instructions for a subagent run. +/// +/// For user- and project-scoped subagents, we append their instructions to the +/// parent session's base instructions. For embedded defaults, we use only the +/// subagent's instructions. If an output schema is present, we augment the +/// subagent instructions with strict JSON output requirements. +fn compose_base_instructions_for_subagent( + def: &SubagentDefinition, + parent_base_instructions: Option<&str>, +) -> String { + // Start with the subagent's own instructions, optionally augmented with + // structured output requirements. + let child_instructions = if let Some(schema) = def.output_schema() { + let schema_json = serde_json::to_string_pretty(schema).unwrap_or_else(|_| "{}".to_string()); + format!( + "{instructions}\n\nOutput format requirements:\n- Reply with a single JSON value that strictly matches the following JSON Schema.\n- Do not include any commentary, markdown, or extra text.\n- Do not include trailing explanations.\n\nSchema:\n{schema_json}\n", + instructions = def.instructions, + schema_json = schema_json + ) + } else { + def.instructions.clone() + }; + + match def.source { + SubagentSource::User | SubagentSource::Project => match parent_base_instructions { + Some(parent) if !parent.trim().is_empty() => { + format!("{parent}\n\n{child}", child = child_instructions) + } + _ => child_instructions, + }, + SubagentSource::EmbeddedDefault => child_instructions, + } +} + /// Run a subagent in a nested Codex session and return the final message. pub(crate) async fn run( sess: &crate::codex::Session, @@ -28,17 +63,8 @@ pub(crate) async fn run( })?; let mut nested_cfg = (*sess.base_config()).clone(); - // Compose base instructions with structured output schema instructions, if present. - let base_instructions = if let Some(schema) = def.output_schema() { - let schema_json = serde_json::to_string_pretty(schema).unwrap_or_else(|_| "{}".to_string()); - format!( - "{instructions}\n\nOutput format requirements:\n- Reply with a single JSON value that strictly matches the following JSON Schema.\n- Do not include any commentary, markdown, or extra text.\n- Do not include trailing explanations.\n\nSchema:\n{schema_json}\n", - instructions = def.instructions, - schema_json = schema_json - ) - } else { - def.instructions.clone() - }; + let base_instructions = + compose_base_instructions_for_subagent(def, turn_context.base_instructions.as_deref()); nested_cfg.base_instructions = Some(base_instructions); nested_cfg.user_instructions = None; // Apply subagent-specific overrides for model and reasoning effort.