This commit is contained in:
Ahmed Ibrahim
2025-08-23 11:07:02 -07:00
parent d2fe780280
commit 76c209d78c
13 changed files with 255 additions and 1 deletions

View File

@@ -263,6 +263,15 @@ pub(crate) struct Session {
/// Manager for external MCP servers/tools.
mcp_connection_manager: McpConnectionManager,
/// Loaded subagent definitions from project and user scope.
subagents_registry: crate::subagents::registry::SubagentRegistry,
/// Auth manager used to spawn nested sessions (e.g., subagents).
auth_manager: Arc<AuthManager>,
/// Base configuration used to derive nested session configs.
base_config: Arc<Config>,
/// External notifier command (will be passed as args to exec()). When
/// `None` this feature is disabled.
notify: Option<Vec<String>>,
@@ -498,6 +507,31 @@ impl Session {
model_reasoning_summary,
session_id,
);
// Build subagent registry paths and load once per session
let project_agents_dir = {
let mut p = cwd.clone();
p.push(".codex");
p.push("agents");
if p.exists() { Some(p) } else { None }
};
let user_agents_dir = {
let mut p = config.codex_home.clone();
p.push("agents");
if p.exists() { Some(p) } else { None }
};
let mut subagents_registry =
crate::subagents::registry::SubagentRegistry::new(project_agents_dir, user_agents_dir);
subagents_registry.load();
// Log discovered subagents for visibility in clients (e.g., TUI).
let _ = tx_event
.send(Event {
id: INITIAL_SUBMIT_ID.to_string(),
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
message: format!("subagents discovered: {:?}", subagents_registry.all_names()),
}),
})
.await;
let turn_context = TurnContext {
client,
tools_config: ToolsConfig::new(
@@ -506,6 +540,7 @@ impl Session {
sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
config.include_subagent_tool,
),
user_instructions,
base_instructions,
@@ -519,6 +554,9 @@ impl Session {
session_id,
tx_event: tx_event.clone(),
mcp_connection_manager,
subagents_registry,
auth_manager: auth_manager.clone(),
base_config: config.clone(),
notify,
state: Mutex::new(state),
rollout: Mutex::new(rollout_recorder),
@@ -578,6 +616,16 @@ impl Session {
}
}
/// Access auth manager for nested sessions.
pub(crate) fn auth_manager(&self) -> Arc<AuthManager> {
self.auth_manager.clone()
}
/// Access base config for nested sessions.
pub(crate) fn base_config(&self) -> Arc<Config> {
self.base_config.clone()
}
/// Sends the given event to the client and swallows the send event, if
/// any, logging it as an error.
pub(crate) async fn send_event(&self, event: Event) {
@@ -1090,6 +1138,7 @@ async fn submission_loop(
new_sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
config.include_subagent_tool,
);
let new_turn_context = TurnContext {
@@ -1168,6 +1217,7 @@ async fn submission_loop(
sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
config.include_subagent_tool,
),
user_instructions: turn_context.user_instructions.clone(),
base_instructions: turn_context.base_instructions.clone(),
@@ -1555,6 +1605,26 @@ async fn run_turn(
Some(sess.mcp_connection_manager.list_all_tools()),
);
// Log tool names for visibility in the TUI/debug logs.
#[allow(clippy::match_same_arms)]
let tool_names: Vec<String> = tools
.iter()
.map(|t| match t {
crate::openai_tools::OpenAiTool::Function(f) => f.name.clone(),
crate::openai_tools::OpenAiTool::LocalShell {} => "local_shell".to_string(),
crate::openai_tools::OpenAiTool::Freeform(f) => f.name.clone(),
})
.collect();
let _ = sess
.tx_event
.send(Event {
id: sub_id.clone(),
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
message: format!("tools available: {:?}", tool_names),
}),
})
.await;
let prompt = Prompt {
input,
store: !turn_context.disable_response_storage,
@@ -2073,6 +2143,57 @@ async fn handle_function_call(
.await
}
"update_plan" => handle_update_plan(sess, arguments, sub_id, call_id).await,
"subagent.run" => {
#[derive(serde::Deserialize)]
struct Args {
name: String,
input: String,
#[serde(default)]
context: Option<String>,
}
let args = match serde_json::from_str::<Args>(&arguments) {
Ok(a) => a,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {e}"),
success: Some(false),
},
};
}
};
let result = crate::subagents::runner::run(
sess,
turn_context,
&sess.subagents_registry,
crate::subagents::runner::RunSubagentArgs {
name: args.name,
input: args.input,
context: args.context,
},
&sub_id,
)
.await;
match result {
Ok(message) => ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: message,
success: Some(true),
},
},
Err(e) => ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("subagent failed: {e}"),
success: Some(false),
},
},
}
}
_ => {
match sess.mcp_connection_manager.parse_tool_name(&name) {
Some((server, tool_name)) => {
@@ -2183,6 +2304,8 @@ fn parse_container_exec_arguments(
}
}
// (helper run_one_turn_collect removed as unused)
pub struct ExecInvokeArgs<'a> {
pub params: ExecParams,
pub sandbox_type: SandboxType,

View File

@@ -169,6 +169,9 @@ pub struct Config {
/// model family's default preference.
pub include_apply_patch_tool: bool,
/// Include the `subagent.run` tool allowing the model to invoke configured subagents.
pub include_subagent_tool: bool,
/// The value for the `originator` header included with Responses API requests.
pub responses_originator_header: String,
@@ -476,6 +479,9 @@ pub struct ConfigToml {
/// If set to `true`, the API key will be signed with the `originator` header.
pub preferred_auth_method: Option<AuthMode>,
/// Include the `subagent.run` tool allowing the model to invoke configured subagents.
pub include_subagent_tool: Option<bool>,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -570,6 +576,7 @@ pub struct ConfigOverrides {
pub base_instructions: Option<String>,
pub include_plan_tool: Option<bool>,
pub include_apply_patch_tool: Option<bool>,
pub include_subagent_tool: Option<bool>,
pub disable_response_storage: Option<bool>,
pub show_raw_agent_reasoning: Option<bool>,
}
@@ -596,6 +603,7 @@ impl Config {
base_instructions,
include_plan_tool,
include_apply_patch_tool,
include_subagent_tool,
disable_response_storage,
show_raw_agent_reasoning,
} = overrides;
@@ -756,6 +764,11 @@ impl Config {
experimental_resume,
include_plan_tool: include_plan_tool.unwrap_or(false),
include_apply_patch_tool: include_apply_patch_tool.unwrap_or(false),
include_subagent_tool: config_profile
.include_subagent_tool
.or(cfg.include_subagent_tool)
.or(include_subagent_tool)
.unwrap_or(false),
responses_originator_header,
preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT),
};
@@ -1122,6 +1135,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
include_subagent_tool: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
},
@@ -1176,6 +1190,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
include_subagent_tool: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
};
@@ -1245,6 +1260,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
include_subagent_tool: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
};

View File

@@ -21,4 +21,6 @@ pub struct ConfigProfile {
pub model_verbosity: Option<Verbosity>,
pub chatgpt_base_url: Option<String>,
pub experimental_instructions_file: Option<PathBuf>,
/// Include the `subagent.run` tool allowing the model to invoke configured subagents.
pub include_subagent_tool: Option<bool>,
}

View File

@@ -62,3 +62,4 @@ pub use codex_protocol::protocol;
// Re-export protocol config enums to ensure call sites can use the same types
// as those in the protocol crate when constructing protocol messages.
pub use codex_protocol::config_types as protocol_config_types;
pub mod subagents;

View File

@@ -63,6 +63,7 @@ pub struct ToolsConfig {
pub shell_type: ConfigShellToolType,
pub plan_tool: bool,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub subagent_tool: bool,
}
impl ToolsConfig {
@@ -72,6 +73,7 @@ impl ToolsConfig {
sandbox_policy: SandboxPolicy,
include_plan_tool: bool,
include_apply_patch_tool: bool,
include_subagent_tool: bool,
) -> Self {
let mut shell_type = if model_family.uses_local_shell_tool {
ConfigShellToolType::LocalShell
@@ -100,6 +102,7 @@ impl ToolsConfig {
shell_type,
plan_tool: include_plan_tool,
apply_patch_tool_type,
subagent_tool: include_subagent_tool,
}
}
}
@@ -509,6 +512,11 @@ pub(crate) fn get_openai_tools(
}
}
if config.subagent_tool {
tracing::trace!("Adding subagent tool");
tools.push(crate::subagents::SUBAGENT_TOOL.clone());
}
if let Some(mcp_tools) = mcp_tools {
for (name, tool) in mcp_tools {
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) {
@@ -520,6 +528,7 @@ pub(crate) fn get_openai_tools(
}
}
tracing::trace!("Tools: {tools:?}");
tools
}
@@ -564,6 +573,7 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
false,
);
let tools = get_openai_tools(&config, Some(HashMap::new()));
@@ -579,6 +589,7 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
false,
);
let tools = get_openai_tools(&config, Some(HashMap::new()));
@@ -594,6 +605,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
false,
);
let tools = get_openai_tools(
&config,
@@ -688,6 +700,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
false,
);
let tools = get_openai_tools(
@@ -744,6 +757,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
false,
);
let tools = get_openai_tools(
@@ -795,6 +809,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
false,
);
let tools = get_openai_tools(
@@ -849,6 +864,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
false,
);
let tools = get_openai_tools(

View File

@@ -168,6 +168,15 @@ impl EventProcessor for EventProcessorWithHumanOutput {
fn process_event(&mut self, event: Event) -> CodexStatus {
let Event { id: _, msg } = event;
match msg {
EventMsg::SubagentBegin(_) => {
// Ignore in human output for now.
}
EventMsg::SubagentForwarded(_) => {
// Ignore; TUI will render forwarded events.
}
EventMsg::SubagentEnd(_) => {
// Ignore in human output for now.
}
EventMsg::Error(ErrorEvent { message }) => {
let prefix = "ERROR:".style(self.red);
ts_println!(self, "{prefix} {message}");

View File

@@ -41,6 +41,12 @@ impl EventProcessor for EventProcessorWithJsonOutput {
fn process_event(&mut self, event: Event) -> CodexStatus {
match event.msg {
EventMsg::SubagentBegin(_)
| EventMsg::SubagentForwarded(_)
| EventMsg::SubagentEnd(_) => {
// Ignored for JSON output in exec for now.
CodexStatus::Running
}
EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) => {
// Suppress streaming events in JSON mode.
CodexStatus::Running

View File

@@ -216,7 +216,13 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
Ok(event) => {
debug!("Received event: {event:?}");
let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete);
let is_shutdown_complete = matches!(
event.msg,
EventMsg::ShutdownComplete
| EventMsg::SubagentBegin(_)
| EventMsg::SubagentForwarded(_)
| EventMsg::SubagentEnd(_)
);
if let Err(e) = tx.send(event) {
error!("Error sending event: {e:?}");
break;

View File

@@ -174,6 +174,11 @@ async fn run_codex_tool_session_inner(
.await;
match event.msg {
EventMsg::SubagentBegin(_)
| EventMsg::SubagentForwarded(_)
| EventMsg::SubagentEnd(_) => {
// Ignore subagent orchestration for MCP echoing.
}
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
command,
cwd,

View File

@@ -478,6 +478,14 @@ pub enum EventMsg {
ShutdownComplete,
ConversationHistory(ConversationHistoryResponseEvent),
// --- Subagent orchestration events ---
/// Emitted when a subagent starts.
SubagentBegin(SubagentBeginEvent),
/// Forwards a nested event produced by a running subagent.
SubagentForwarded(SubagentForwardedEvent),
/// Emitted when a subagent finishes.
SubagentEnd(SubagentEndEvent),
}
// Individual event payload types matching each `EventMsg` variant.
@@ -501,6 +509,28 @@ pub struct TokenUsage {
pub total_tokens: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubagentBeginEvent {
pub subagent_id: String,
pub name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubagentEndEvent {
pub subagent_id: String,
pub name: String,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_agent_message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubagentForwardedEvent {
pub subagent_id: String,
pub name: String,
pub event: Box<EventMsg>,
}
impl TokenUsage {
pub fn is_zero(&self) -> bool {
self.total_tokens == 0

View File

@@ -836,8 +836,39 @@ impl ChatWidget {
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
// Also show background logs in the transcript for visibility.
self.add_to_history(history_cell::new_log_line(message.clone()));
self.on_background_event(message)
}
EventMsg::SubagentBegin(ev) => {
let msg = format!("subagent begin: {} ({})", ev.name, ev.subagent_id);
self.add_to_history(history_cell::new_log_line(msg));
}
EventMsg::SubagentForwarded(ev) => {
// Summarize forwarded event type; include message text when it is AgentMessage.
match *ev.event {
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
let msg = format!("subagent {}: {}", ev.name, message);
self.add_to_history(history_cell::new_log_line(msg));
}
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { ref delta }) => {
let msg = format!("subagent {}: {}", ev.name, delta);
self.add_to_history(history_cell::new_log_line(msg));
}
ref other => {
let msg = format!("subagent {} forwarded: {:?}", ev.name, other);
self.add_to_history(history_cell::new_log_line(msg));
}
}
}
EventMsg::SubagentEnd(ev) => {
let summary = ev.last_agent_message.as_deref().unwrap_or("");
let msg = format!(
"subagent end: {} ({}) success={} {}",
ev.name, ev.subagent_id, ev.success, summary
);
self.add_to_history(history_cell::new_log_line(msg));
}
EventMsg::StreamError(StreamErrorEvent { message }) => self.on_stream_error(message),
EventMsg::ConversationHistory(_) => {}
}

View File

@@ -750,6 +750,14 @@ pub(crate) fn new_status_output(
PlainHistoryCell { lines }
}
/// Simple one-line log entry (dim) to surface traces and diagnostics in the transcript.
pub(crate) fn new_log_line(message: String) -> TranscriptOnlyHistoryCell {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(""));
lines.push(Line::from(message).dim());
TranscriptOnlyHistoryCell { lines }
}
/// Render a summary of configured MCP servers from the current `Config`.
pub(crate) fn empty_mcp_output() -> PlainHistoryCell {
let lines: Vec<Line<'static>> = vec![

View File

@@ -124,6 +124,7 @@ pub async fn run_main(
config_profile: cli.config_profile.clone(),
codex_linux_sandbox_exe,
base_instructions: None,
include_subagent_tool: None,
include_plan_tool: Some(true),
include_apply_patch_tool: None,
disable_response_storage: cli.oss.then_some(true),