mirror of
https://github.com/openai/codex.git
synced 2026-02-02 15:03:38 +00:00
Compare commits
7 Commits
patch-1
...
subagents-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75543d66f3 | ||
|
|
e36c08b36c | ||
|
|
9e96800adf | ||
|
|
a8d258f7f0 | ||
|
|
a79c71ec00 | ||
|
|
113c87e8ae | ||
|
|
bfe99e4396 |
20
codex-rs/Cargo.lock
generated
20
codex-rs/Cargo.lock
generated
@@ -726,6 +726,7 @@ dependencies = [
|
||||
"env-flags",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"include_dir",
|
||||
"landlock",
|
||||
"libc",
|
||||
"maplit",
|
||||
@@ -2402,6 +2403,25 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
|
||||
@@ -25,6 +25,7 @@ dirs = "6"
|
||||
env-flags = "0.1.1"
|
||||
eventsource-stream = "0.2.3"
|
||||
futures = "0.3"
|
||||
include_dir = "0.7"
|
||||
libc = "0.2.175"
|
||||
mcp-types = { path = "../mcp-types" }
|
||||
mime_guess = "2.0"
|
||||
|
||||
@@ -77,7 +77,6 @@ use crate::protocol::AgentReasoningRawContentEvent;
|
||||
use crate::protocol::AgentReasoningSectionBreakEvent;
|
||||
use crate::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::BackgroundEventEvent;
|
||||
use crate::protocol::ErrorEvent;
|
||||
use crate::protocol::Event;
|
||||
use crate::protocol::EventMsg;
|
||||
@@ -297,6 +296,9 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
|
||||
pub(crate) disable_response_storage: bool,
|
||||
pub(crate) tools_config: ToolsConfig,
|
||||
pub(crate) subagents_registry: crate::subagents::registry::SubagentRegistry,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
pub(crate) base_config: Arc<Config>,
|
||||
}
|
||||
|
||||
impl TurnContext {
|
||||
@@ -504,6 +506,22 @@ 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();
|
||||
|
||||
let turn_context = TurnContext {
|
||||
client,
|
||||
tools_config: ToolsConfig::new(
|
||||
@@ -514,15 +532,20 @@ impl Session {
|
||||
config.include_apply_patch_tool,
|
||||
config.tools_web_search_request,
|
||||
config.use_experimental_streamable_shell_tool,
|
||||
config.include_subagent_tool,
|
||||
),
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
shell_environment_policy: config.shell_environment_policy.clone(),
|
||||
cwd,
|
||||
cwd: cwd.clone(),
|
||||
disable_response_storage,
|
||||
subagents_registry,
|
||||
auth_manager: auth_manager.clone(),
|
||||
base_config: config.clone(),
|
||||
};
|
||||
|
||||
let sess = Arc::new(Session {
|
||||
session_id,
|
||||
tx_event: tx_event.clone(),
|
||||
@@ -839,18 +862,8 @@ impl Session {
|
||||
result
|
||||
}
|
||||
|
||||
/// Helper that emits a BackgroundEvent with the given message. This keeps
|
||||
/// the call‑sites terse so adding more diagnostics does not clutter the
|
||||
/// core agent logic.
|
||||
async fn notify_background_event(&self, sub_id: &str, message: impl Into<String>) {
|
||||
let event = Event {
|
||||
id: sub_id.to_string(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
message: message.into(),
|
||||
}),
|
||||
};
|
||||
let _ = self.tx_event.send(event).await;
|
||||
}
|
||||
/// Background events are disabled; this is a no-op to preserve call-sites.
|
||||
async fn notify_background_event(&self, _sub_id: &str, _message: impl Into<String>) {}
|
||||
|
||||
async fn notify_stream_error(&self, sub_id: &str, message: impl Into<String>) {
|
||||
let event = Event {
|
||||
@@ -1100,6 +1113,7 @@ async fn submission_loop(
|
||||
config.include_apply_patch_tool,
|
||||
config.tools_web_search_request,
|
||||
config.use_experimental_streamable_shell_tool,
|
||||
config.include_subagent_tool,
|
||||
);
|
||||
|
||||
let new_turn_context = TurnContext {
|
||||
@@ -1112,6 +1126,9 @@ async fn submission_loop(
|
||||
shell_environment_policy: prev.shell_environment_policy.clone(),
|
||||
cwd: new_cwd.clone(),
|
||||
disable_response_storage: prev.disable_response_storage,
|
||||
subagents_registry: prev.subagents_registry.clone(),
|
||||
auth_manager: prev.auth_manager.clone(),
|
||||
base_config: prev.base_config.clone(),
|
||||
};
|
||||
|
||||
// Install the new persistent context for subsequent tasks/turns.
|
||||
@@ -1180,6 +1197,7 @@ async fn submission_loop(
|
||||
config.include_apply_patch_tool,
|
||||
config.tools_web_search_request,
|
||||
config.use_experimental_streamable_shell_tool,
|
||||
config.include_subagent_tool,
|
||||
),
|
||||
user_instructions: turn_context.user_instructions.clone(),
|
||||
base_instructions: turn_context.base_instructions.clone(),
|
||||
@@ -1188,6 +1206,9 @@ async fn submission_loop(
|
||||
shell_environment_policy: turn_context.shell_environment_policy.clone(),
|
||||
cwd,
|
||||
disable_response_storage: turn_context.disable_response_storage,
|
||||
subagents_registry: turn_context.subagents_registry.clone(),
|
||||
auth_manager: turn_context.auth_manager.clone(),
|
||||
base_config: turn_context.base_config.clone(),
|
||||
};
|
||||
// TODO: record the new environment context in the conversation history
|
||||
// no current task, spawn a new one with the per‑turn context
|
||||
@@ -2096,6 +2117,131 @@ 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 agent_name = args.name.clone();
|
||||
let result = crate::subagents::runner::run(
|
||||
sess,
|
||||
turn_context,
|
||||
crate::subagents::runner::RunSubagentArgs {
|
||||
name: agent_name.clone(),
|
||||
input: args.input,
|
||||
context: args.context,
|
||||
},
|
||||
&sub_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(message) => {
|
||||
// Validate against the subagent's output schema and return
|
||||
// the JSON body unmodified.
|
||||
if let Some(def) = turn_context.subagents_registry.get(&agent_name) {
|
||||
let schema = def.output_schema();
|
||||
match serde_json::from_str::<serde_json::Value>(&message) {
|
||||
Ok(val) => {
|
||||
if let Err(err) =
|
||||
crate::subagents::runner::validate_json_against_schema(
|
||||
&val, schema,
|
||||
)
|
||||
{
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: format!(
|
||||
"subagent structured output validation failed: {err}"
|
||||
),
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
// Valid JSON: pass through as-is
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: message,
|
||||
success: Some(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(e) => ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: format!(
|
||||
"subagent must return valid JSON per its schema: {e}"
|
||||
),
|
||||
success: Some(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: format!(
|
||||
"subagent not found when validating output: {}",
|
||||
agent_name
|
||||
),
|
||||
success: Some(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: format!("subagent failed: {e}"),
|
||||
success: Some(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
"subagent_list" => {
|
||||
#[derive(serde::Serialize)]
|
||||
struct SubagentBrief<'a> {
|
||||
name: &'a str,
|
||||
description: &'a str,
|
||||
}
|
||||
let mut list = Vec::new();
|
||||
for name in turn_context.subagents_registry.all_names() {
|
||||
if let Some(def) = turn_context.subagents_registry.get(&name) {
|
||||
list.push(SubagentBrief {
|
||||
name: &def.name,
|
||||
description: &def.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
let payload = match serde_json::to_string(&list) {
|
||||
Ok(s) => s,
|
||||
Err(e) => format!("failed to serialize subagent list: {e}"),
|
||||
};
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content: payload,
|
||||
success: Some(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
EXEC_COMMAND_TOOL_NAME => {
|
||||
// TODO(mbolin): Sandbox check.
|
||||
let exec_params = match serde_json::from_str::<ExecCommandParams>(&arguments) {
|
||||
|
||||
@@ -169,6 +169,8 @@ 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,
|
||||
pub tools_web_search_request: bool,
|
||||
|
||||
/// The value for the `originator` header included with Responses API requests.
|
||||
@@ -485,6 +487,8 @@ pub struct ConfigToml {
|
||||
|
||||
/// Nested tools section for feature toggles
|
||||
pub tools: Option<ToolsToml>,
|
||||
/// Include the `subagent.run` tool allowing the model to invoke configured subagents.
|
||||
pub include_subagent_tool: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -586,6 +590,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>,
|
||||
pub tools_web_search_request: Option<bool>,
|
||||
@@ -613,6 +618,7 @@ impl Config {
|
||||
base_instructions,
|
||||
include_plan_tool,
|
||||
include_apply_patch_tool,
|
||||
include_subagent_tool,
|
||||
disable_response_storage,
|
||||
show_raw_agent_reasoning,
|
||||
tools_web_search_request: override_tools_web_search_request,
|
||||
@@ -778,6 +784,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),
|
||||
tools_web_search_request,
|
||||
responses_originator_header,
|
||||
preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT),
|
||||
@@ -1148,6 +1159,7 @@ disable_response_storage = true
|
||||
base_instructions: None,
|
||||
include_plan_tool: false,
|
||||
include_apply_patch_tool: false,
|
||||
include_subagent_tool: false,
|
||||
tools_web_search_request: false,
|
||||
responses_originator_header: "codex_cli_rs".to_string(),
|
||||
preferred_auth_method: AuthMode::ChatGPT,
|
||||
@@ -1204,6 +1216,7 @@ disable_response_storage = true
|
||||
base_instructions: None,
|
||||
include_plan_tool: false,
|
||||
include_apply_patch_tool: false,
|
||||
include_subagent_tool: false,
|
||||
tools_web_search_request: false,
|
||||
responses_originator_header: "codex_cli_rs".to_string(),
|
||||
preferred_auth_method: AuthMode::ChatGPT,
|
||||
@@ -1275,6 +1288,7 @@ disable_response_storage = true
|
||||
base_instructions: None,
|
||||
include_plan_tool: false,
|
||||
include_apply_patch_tool: false,
|
||||
include_subagent_tool: false,
|
||||
tools_web_search_request: false,
|
||||
responses_originator_header: "codex_cli_rs".to_string(),
|
||||
preferred_auth_method: AuthMode::ChatGPT,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -63,3 +63,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;
|
||||
|
||||
@@ -67,8 +67,11 @@ pub struct ToolsConfig {
|
||||
pub plan_tool: bool,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_request: bool,
|
||||
pub subagent_tool: bool,
|
||||
}
|
||||
|
||||
// TODO: have an enum for available tools
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl ToolsConfig {
|
||||
pub fn new(
|
||||
model_family: &ModelFamily,
|
||||
@@ -78,6 +81,7 @@ impl ToolsConfig {
|
||||
include_apply_patch_tool: bool,
|
||||
include_web_search_request: bool,
|
||||
use_streamable_shell_tool: bool,
|
||||
include_subagent_tool: bool,
|
||||
) -> Self {
|
||||
let mut shell_type = if use_streamable_shell_tool {
|
||||
ConfigShellToolType::StreamableShell
|
||||
@@ -109,6 +113,7 @@ impl ToolsConfig {
|
||||
plan_tool: include_plan_tool,
|
||||
apply_patch_tool_type,
|
||||
web_search_request: include_web_search_request,
|
||||
subagent_tool: include_subagent_tool,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,6 +520,12 @@ pub(crate) fn get_openai_tools(
|
||||
tools.push(PLAN_TOOL.clone());
|
||||
}
|
||||
|
||||
if config.subagent_tool {
|
||||
tracing::trace!("Adding subagent tool");
|
||||
tools.push(crate::subagents::SUBAGENT_TOOL.clone());
|
||||
tools.push(crate::subagents::SUBAGENT_LIST_TOOL.clone());
|
||||
}
|
||||
|
||||
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
|
||||
match apply_patch_tool_type {
|
||||
ApplyPatchToolType::Freeform => {
|
||||
@@ -541,6 +552,7 @@ pub(crate) fn get_openai_tools(
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("Tools: {:?}", tools);
|
||||
tools
|
||||
}
|
||||
|
||||
@@ -588,6 +600,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()));
|
||||
|
||||
@@ -605,6 +618,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()));
|
||||
|
||||
@@ -622,6 +636,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
let tools = get_openai_tools(
|
||||
&config,
|
||||
@@ -721,6 +736,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
@@ -779,6 +795,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
@@ -832,6 +849,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
@@ -888,6 +906,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
|
||||
75
codex-rs/core/src/subagents/default_agents.rs
Normal file
75
codex-rs/core/src/subagents/default_agents.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use super::definition::SubagentDefinition;
|
||||
use super::definition::SubagentSource;
|
||||
use include_dir::Dir;
|
||||
use include_dir::include_dir;
|
||||
|
||||
/// Load embedded default subagents shipped with the binary.
|
||||
///
|
||||
/// Project (~/.codex/agents) and user (.codex/agents) definitions override these.
|
||||
pub(crate) fn embedded_defs() -> Vec<SubagentDefinition> {
|
||||
static DEFAULTS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/subagents/defaults");
|
||||
|
||||
let mut defs: Vec<SubagentDefinition> = Vec::new();
|
||||
|
||||
for file in DEFAULTS_DIR.files() {
|
||||
if file
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("json"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
match file.contents_utf8() {
|
||||
Some(contents) => match SubagentDefinition::from_json_str(contents) {
|
||||
Ok(mut def) => {
|
||||
def.source = SubagentSource::EmbeddedDefault;
|
||||
defs.push(def)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"failed to parse embedded default subagent '{}': {}",
|
||||
file.path().display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"embedded defaults file is not valid UTF-8: {}",
|
||||
file.path().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_defaults_are_valid_subagents() {
|
||||
// Ensure we can load all embedded defaults and that the list is non-empty
|
||||
let defs = embedded_defs();
|
||||
assert!(
|
||||
!defs.is_empty(),
|
||||
"expected at least one default subagent in src/subagents/defaults"
|
||||
);
|
||||
|
||||
// Basic sanity checks on each definition
|
||||
for def in defs {
|
||||
assert!(
|
||||
!def.name.trim().is_empty(),
|
||||
"subagent name must not be empty"
|
||||
);
|
||||
assert!(
|
||||
!def.instructions.trim().is_empty(),
|
||||
"subagent '{}' must have instructions",
|
||||
def.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
codex-rs/core/src/subagents/defaults/hello.json
Normal file
8
codex-rs/core/src/subagents/defaults/hello.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "hello",
|
||||
"description": "Built-in test subagent that replies with a greeting",
|
||||
"instructions": "Reply with exactly this text and nothing else, as a JSON string: Hello from subagent",
|
||||
"output_schema": { "type": "string" },
|
||||
"tools": [],
|
||||
"reasoning_effort": "minimal"
|
||||
}
|
||||
65
codex-rs/core/src/subagents/definition.rs
Normal file
65
codex-rs/core/src/subagents/definition.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::openai_tools::JsonSchema;
|
||||
use codex_protocol::config_types::ReasoningEffort;
|
||||
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,
|
||||
|
||||
// TODO: add allowed tools. we inherit the parent agent's tools for now.
|
||||
// TODO: add cwd and approval policies.
|
||||
/// Structured output schema. 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.
|
||||
output_schema: JsonSchema,
|
||||
|
||||
/// Optional model override for this subagent. When not provided, inherits
|
||||
/// the parent session's configured model.
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Optional reasoning effort override for this subagent. When not provided,
|
||||
/// inherits the parent session's configured reasoning effort.
|
||||
#[serde(default)]
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
|
||||
/// 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 {
|
||||
pub fn from_json_str(s: &str) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_str::<Self>(s)
|
||||
}
|
||||
|
||||
pub fn from_file(path: &Path) -> std::io::Result<Self> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
// Surface JSON parsing error with file context
|
||||
serde_json::from_str::<Self>(&contents).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("invalid subagent JSON at {}: {e}", path.display()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn output_schema(&self) -> &JsonSchema {
|
||||
&self.output_schema
|
||||
}
|
||||
}
|
||||
8
codex-rs/core/src/subagents/mod.rs
Normal file
8
codex-rs/core/src/subagents/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod default_agents;
|
||||
pub mod definition;
|
||||
pub mod registry;
|
||||
pub mod runner;
|
||||
pub mod tool;
|
||||
|
||||
pub(crate) use tool::SUBAGENT_LIST_TOOL;
|
||||
pub(crate) use tool::SUBAGENT_TOOL;
|
||||
88
codex-rs/core/src/subagents/registry.rs
Normal file
88
codex-rs/core/src/subagents/registry.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use super::default_agents;
|
||||
use super::definition::SubagentDefinition;
|
||||
use super::definition::SubagentSource;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SubagentRegistry {
|
||||
/// Directory under the project (cwd/.codex/agents).
|
||||
project_dir: Option<PathBuf>,
|
||||
/// Directory under CODEX_HOME (~/.codex/agents).
|
||||
user_dir: Option<PathBuf>,
|
||||
/// Merged map: project definitions override user ones.
|
||||
map: HashMap<String, SubagentDefinition>,
|
||||
}
|
||||
|
||||
impl SubagentRegistry {
|
||||
pub fn new(project_dir: Option<PathBuf>, user_dir: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
project_dir,
|
||||
user_dir,
|
||||
map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads JSON files from user_dir then project_dir (project wins on conflict).
|
||||
pub fn load(&mut self) {
|
||||
let mut map: HashMap<String, SubagentDefinition> = HashMap::new();
|
||||
|
||||
// Start with embedded defaults (lowest precedence).
|
||||
for def in default_agents::embedded_defs() {
|
||||
map.insert(def.name.clone(), def);
|
||||
}
|
||||
|
||||
// Load user definitions first
|
||||
if let Some(dir) = &self.user_dir {
|
||||
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, SubagentSource::Project);
|
||||
}
|
||||
|
||||
// No ad-hoc fallback here; embedded defaults already include "hello".
|
||||
|
||||
self.map = map;
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&SubagentDefinition> {
|
||||
self.map.get(name)
|
||||
}
|
||||
|
||||
pub fn all_names(&self) -> Vec<String> {
|
||||
self.map.keys().cloned().collect()
|
||||
}
|
||||
|
||||
fn load_from_dir_into(
|
||||
dir: &Path,
|
||||
out: &mut HashMap<String, SubagentDefinition>,
|
||||
source: SubagentSource,
|
||||
) {
|
||||
let Ok(iter) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in iter.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file()
|
||||
&& path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("json"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
match SubagentDefinition::from_file(&path) {
|
||||
Ok(mut def) => {
|
||||
def.source = source;
|
||||
out.insert(def.name.clone(), def);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to load subagent from {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
codex-rs/core/src/subagents/runner.rs
Normal file
252
codex-rs/core/src/subagents/runner.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use crate::codex::Codex;
|
||||
use crate::error::Result as CodexResult;
|
||||
|
||||
use super::definition::SubagentDefinition;
|
||||
use super::definition::SubagentSource;
|
||||
use crate::openai_tools::JsonSchema;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
/// Arguments expected for the `subagent.run` tool.
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct RunSubagentArgs {
|
||||
pub name: String,
|
||||
pub input: String,
|
||||
#[serde(default)]
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
/// 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. We always augment the subagent instructions with
|
||||
/// strict JSON output requirements based on its schema.
|
||||
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 schema_json =
|
||||
serde_json::to_string_pretty(def.output_schema()).unwrap_or_else(|_| "{}".to_string());
|
||||
let child_instructions = format!(
|
||||
"{}\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{}\n",
|
||||
def.instructions, schema_json
|
||||
);
|
||||
|
||||
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,
|
||||
turn_context: &crate::codex::TurnContext,
|
||||
args: RunSubagentArgs,
|
||||
_parent_sub_id: &str,
|
||||
) -> CodexResult<String> {
|
||||
let def: &SubagentDefinition =
|
||||
turn_context
|
||||
.subagents_registry
|
||||
.get(&args.name)
|
||||
.ok_or_else(|| {
|
||||
crate::error::CodexErr::Stream(format!("unknown subagent: {}", args.name), None)
|
||||
})?;
|
||||
|
||||
let mut nested_cfg = (*turn_context.base_config).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.
|
||||
if let Some(model) = &def.model {
|
||||
nested_cfg.model = model.clone();
|
||||
}
|
||||
if let Some(re) = def.reasoning_effort {
|
||||
nested_cfg.model_reasoning_effort = re;
|
||||
}
|
||||
nested_cfg.approval_policy = turn_context.approval_policy;
|
||||
nested_cfg.sandbox_policy = turn_context.sandbox_policy.clone();
|
||||
nested_cfg.cwd = turn_context.cwd.clone();
|
||||
nested_cfg.include_subagent_tool = false;
|
||||
|
||||
let nested = Codex::spawn(nested_cfg, turn_context.auth_manager.clone(), None).await?;
|
||||
let nested_codex = nested.codex;
|
||||
|
||||
let subagent_id = uuid::Uuid::new_v4().to_string();
|
||||
forward_begin(sess, _parent_sub_id, &subagent_id, &def.name).await;
|
||||
|
||||
let text = match args.context {
|
||||
Some(ctx) if !ctx.trim().is_empty() => format!("{ctx}\n\n{input}", input = args.input),
|
||||
_ => args.input,
|
||||
};
|
||||
|
||||
nested_codex
|
||||
.submit(crate::protocol::Op::UserInput {
|
||||
items: vec![crate::protocol::InputItem::Text { text }],
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::error::CodexErr::Stream(format!("failed to submit to subagent: {e}"), None)
|
||||
})?;
|
||||
|
||||
let mut last_message: Option<String> = None;
|
||||
loop {
|
||||
let ev = nested_codex.next_event().await?;
|
||||
match ev.msg.clone() {
|
||||
crate::protocol::EventMsg::AgentMessage(m) => {
|
||||
last_message = Some(m.message);
|
||||
}
|
||||
crate::protocol::EventMsg::TaskComplete(t) => {
|
||||
let _ = nested_codex.submit(crate::protocol::Op::Shutdown).await;
|
||||
forward_forwarded(sess, _parent_sub_id, &subagent_id, &def.name, ev.msg).await;
|
||||
forward_end(
|
||||
sess,
|
||||
_parent_sub_id,
|
||||
&subagent_id,
|
||||
&def.name,
|
||||
true,
|
||||
t.last_agent_message.clone(),
|
||||
)
|
||||
.await;
|
||||
return Ok(t
|
||||
.last_agent_message
|
||||
.unwrap_or_else(|| last_message.unwrap_or_default()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
forward_forwarded(sess, _parent_sub_id, &subagent_id, &def.name, ev.msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal validator for our limited JsonSchema subset used across the codebase.
|
||||
pub(crate) fn validate_json_against_schema(
|
||||
value: &JsonValue,
|
||||
schema: &JsonSchema,
|
||||
) -> Result<(), String> {
|
||||
match schema {
|
||||
JsonSchema::Boolean { .. } => {
|
||||
if value.is_boolean() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("expected boolean".to_string())
|
||||
}
|
||||
}
|
||||
JsonSchema::String { .. } => {
|
||||
if value.is_string() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("expected string".to_string())
|
||||
}
|
||||
}
|
||||
JsonSchema::Number { .. } => {
|
||||
if value.is_number() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("expected number".to_string())
|
||||
}
|
||||
}
|
||||
JsonSchema::Array { items, .. } => {
|
||||
if let JsonValue::Array(arr) = value {
|
||||
for (i, v) in arr.iter().enumerate() {
|
||||
validate_json_against_schema(v, items)
|
||||
.map_err(|e| format!("array[{i}]: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err("expected array".to_string())
|
||||
}
|
||||
}
|
||||
JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
additional_properties,
|
||||
} => {
|
||||
let obj = match value.as_object() {
|
||||
Some(o) => o,
|
||||
None => return Err("expected object".to_string()),
|
||||
};
|
||||
// Check required
|
||||
if let Some(req) = required {
|
||||
for key in req {
|
||||
if !obj.contains_key(key) {
|
||||
return Err(format!("missing required property: {key}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate each present property
|
||||
for (k, v) in obj.iter() {
|
||||
if let Some(child_schema) = properties.get(k) {
|
||||
validate_json_against_schema(v, child_schema)
|
||||
.map_err(|e| format!("property '{k}': {e}"))?;
|
||||
} else if matches!(additional_properties, Some(false)) {
|
||||
return Err(format!("unexpected property: {k}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn forward_begin(
|
||||
sess: &crate::codex::Session,
|
||||
parent_sub_id: &str,
|
||||
subagent_id: &str,
|
||||
name: &str,
|
||||
) {
|
||||
sess.send_event(crate::protocol::Event {
|
||||
id: parent_sub_id.to_string(),
|
||||
msg: crate::protocol::EventMsg::SubagentBegin(crate::protocol::SubagentBeginEvent {
|
||||
subagent_id: subagent_id.to_string(),
|
||||
name: name.to_string(),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn forward_forwarded(
|
||||
sess: &crate::codex::Session,
|
||||
parent_sub_id: &str,
|
||||
subagent_id: &str,
|
||||
name: &str,
|
||||
msg: crate::protocol::EventMsg,
|
||||
) {
|
||||
sess.send_event(crate::protocol::Event {
|
||||
id: parent_sub_id.to_string(),
|
||||
msg: crate::protocol::EventMsg::SubagentForwarded(
|
||||
crate::protocol::SubagentForwardedEvent {
|
||||
subagent_id: subagent_id.to_string(),
|
||||
name: name.to_string(),
|
||||
event: Box::new(msg),
|
||||
},
|
||||
),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn forward_end(
|
||||
sess: &crate::codex::Session,
|
||||
parent_sub_id: &str,
|
||||
subagent_id: &str,
|
||||
name: &str,
|
||||
success: bool,
|
||||
last_agent_message: Option<String>,
|
||||
) {
|
||||
sess.send_event(crate::protocol::Event {
|
||||
id: parent_sub_id.to_string(),
|
||||
msg: crate::protocol::EventMsg::SubagentEnd(crate::protocol::SubagentEndEvent {
|
||||
subagent_id: subagent_id.to_string(),
|
||||
name: name.to_string(),
|
||||
success,
|
||||
last_agent_message,
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
56
codex-rs/core/src/subagents/tool.rs
Normal file
56
codex-rs/core/src/subagents/tool.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::openai_tools::JsonSchema;
|
||||
use crate::openai_tools::OpenAiTool;
|
||||
use crate::openai_tools::ResponsesApiTool;
|
||||
|
||||
pub(crate) static SUBAGENT_TOOL: LazyLock<OpenAiTool> = LazyLock::new(|| {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
"name".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Registered subagent name".to_string()),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"input".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Task or instruction for the subagent".to_string()),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"context".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Optional extra context to aid the task".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
OpenAiTool::Function(ResponsesApiTool {
|
||||
name: "subagent_run".to_string(),
|
||||
description: "Invoke a named subagent with isolated context and return its result"
|
||||
.to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["name".to_string(), "input".to_string()]),
|
||||
additional_properties: Some(false),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
pub(crate) static SUBAGENT_LIST_TOOL: LazyLock<OpenAiTool> = LazyLock::new(|| {
|
||||
let properties = BTreeMap::new();
|
||||
OpenAiTool::Function(ResponsesApiTool {
|
||||
name: "subagent_list".to_string(),
|
||||
description:
|
||||
"List available subagents (name and description). Call before subagent_run if unsure."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: None,
|
||||
additional_properties: Some(false),
|
||||
},
|
||||
})
|
||||
});
|
||||
@@ -169,6 +169,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}");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -148,6 +148,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
base_instructions: None,
|
||||
include_plan_tool: None,
|
||||
include_apply_patch_tool: None,
|
||||
include_subagent_tool: None,
|
||||
disable_response_storage: oss.then_some(true),
|
||||
show_raw_agent_reasoning: oss.then_some(true),
|
||||
tools_web_search_request: None,
|
||||
|
||||
@@ -736,6 +736,7 @@ fn derive_config_from_params(
|
||||
base_instructions,
|
||||
include_plan_tool,
|
||||
include_apply_patch_tool,
|
||||
include_subagent_tool: None,
|
||||
disable_response_storage: None,
|
||||
show_raw_agent_reasoning: None,
|
||||
tools_web_search_request: None,
|
||||
|
||||
@@ -161,6 +161,7 @@ impl CodexToolCallParam {
|
||||
base_instructions,
|
||||
include_plan_tool,
|
||||
include_apply_patch_tool: None,
|
||||
include_subagent_tool: None,
|
||||
disable_response_storage: None,
|
||||
show_raw_agent_reasoning: None,
|
||||
tools_web_search_request: None,
|
||||
|
||||
@@ -277,6 +277,9 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::PlanUpdate(_)
|
||||
| EventMsg::TurnAborted(_)
|
||||
| EventMsg::ConversationHistory(_)
|
||||
| EventMsg::SubagentBegin(_)
|
||||
| EventMsg::SubagentForwarded(_)
|
||||
| EventMsg::SubagentEnd(_)
|
||||
| EventMsg::ShutdownComplete => {
|
||||
// For now, we do not do anything extra for these
|
||||
// events. Note that
|
||||
|
||||
@@ -480,6 +480,13 @@ pub enum EventMsg {
|
||||
ShutdownComplete,
|
||||
|
||||
ConversationHistory(ConversationHistoryResponseEvent),
|
||||
|
||||
/// 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.
|
||||
@@ -591,6 +598,28 @@ impl fmt::Display for FinalOutput {
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AgentMessageEvent {
|
||||
pub message: String,
|
||||
|
||||
@@ -906,6 +906,25 @@ impl ChatWidget {
|
||||
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
||||
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.
|
||||
self.add_to_history(history_cell::new_log_line(format!(
|
||||
"subagent forwarded: {:?}",
|
||||
ev.event
|
||||
)));
|
||||
}
|
||||
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(ev) => {
|
||||
// Forward to App so it can process backtrack flows.
|
||||
|
||||
@@ -1152,6 +1152,12 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
||||
Line::from(invocation_spans)
|
||||
}
|
||||
|
||||
/// Simple one-line log entry (dim) to surface traces and diagnostics in the transcript.
|
||||
pub(crate) fn new_log_line(message: String) -> TranscriptOnlyHistoryCell {
|
||||
let lines: Vec<Line<'static>> = vec![Line::from(""), Line::from(message).dim()];
|
||||
TranscriptOnlyHistoryCell { lines }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -126,6 +126,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),
|
||||
|
||||
Reference in New Issue
Block a user