mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
subagents
This commit is contained in:
20
codex-rs/Cargo.lock
generated
20
codex-rs/Cargo.lock
generated
@@ -724,6 +724,7 @@ dependencies = [
|
||||
"env-flags",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"include_dir",
|
||||
"landlock",
|
||||
"libc",
|
||||
"maplit",
|
||||
@@ -2400,6 +2401,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"
|
||||
|
||||
@@ -24,6 +24,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;
|
||||
@@ -269,6 +268,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>>,
|
||||
@@ -514,19 +522,39 @@ 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,
|
||||
};
|
||||
// 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 sess = Arc::new(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),
|
||||
@@ -594,6 +622,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()
|
||||
}
|
||||
|
||||
pub async fn request_command_approval(
|
||||
&self,
|
||||
sub_id: String,
|
||||
@@ -839,18 +877,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 +1128,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 {
|
||||
@@ -1180,6 +1209,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(),
|
||||
@@ -2096,6 +2126,143 @@ 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,
|
||||
&sess.subagents_registry,
|
||||
crate::subagents::runner::RunSubagentArgs {
|
||||
name: agent_name.clone(),
|
||||
input: args.input,
|
||||
context: args.context,
|
||||
},
|
||||
&sub_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(message) => {
|
||||
// If this subagent declares an output schema, validate against it and
|
||||
// return the JSON body unmodified. Otherwise fallback to envelope for text.
|
||||
if let Some(def) = sess.subagents_registry.get(&agent_name)
|
||||
&& let Some(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 {
|
||||
// No schema: best-effort envelope
|
||||
let json_payload = match serde_json::from_str::<serde_json::Value>(&message)
|
||||
{
|
||||
Ok(val) => serde_json::json!({
|
||||
"subagent": agent_name,
|
||||
"output": val,
|
||||
}),
|
||||
Err(_) => serde_json::json!({
|
||||
"subagent": agent_name,
|
||||
"output": { "type": "text", "message": message },
|
||||
}),
|
||||
};
|
||||
let content = serde_json::to_string(&json_payload).unwrap_or(message);
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: FunctionCallOutputPayload {
|
||||
content,
|
||||
success: Some(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
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 sess.subagents_registry.all_names() {
|
||||
if let Some(def) = sess.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,6 +67,7 @@ pub struct ToolsConfig {
|
||||
pub plan_tool: bool,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_request: bool,
|
||||
pub subagent_tool: bool,
|
||||
}
|
||||
|
||||
impl ToolsConfig {
|
||||
@@ -78,6 +79,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 +111,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 +518,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 +550,7 @@ pub(crate) fn get_openai_tools(
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("Tools: {:?}", tools);
|
||||
tools
|
||||
}
|
||||
|
||||
@@ -588,6 +598,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 +616,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 +634,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
let tools = get_openai_tools(
|
||||
&config,
|
||||
@@ -721,6 +734,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
@@ -779,6 +793,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
@@ -832,6 +847,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
@@ -888,6 +904,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
/*use_experimental_streamable_shell_tool*/ false,
|
||||
/*include_subagent_tool*/ false,
|
||||
);
|
||||
|
||||
let tools = get_openai_tools(
|
||||
|
||||
71
codex-rs/core/src/subagents/default_agents.rs
Normal file
71
codex-rs/core/src/subagents/default_agents.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use super::definition::SubagentDefinition;
|
||||
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(def) => 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: Hello from subagent",
|
||||
"tools": [],
|
||||
"reasoning_effort": "minimal"
|
||||
}
|
||||
|
||||
55
codex-rs/core/src/subagents/definition.rs
Normal file
55
codex-rs/core/src/subagents/definition.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use crate::openai_tools::JsonSchema;
|
||||
use codex_protocol::config_types::ReasoningEffort;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[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<Vec<String>>, // None => inherit; Some(vec) => allow-list
|
||||
|
||||
/// 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.
|
||||
#[serde(default)]
|
||||
output_schema: Option<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>,
|
||||
}
|
||||
|
||||
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) -> Option<&JsonSchema> {
|
||||
self.output_schema.as_ref()
|
||||
}
|
||||
|
||||
}
|
||||
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;
|
||||
82
codex-rs/core/src/subagents/registry.rs
Normal file
82
codex-rs/core/src/subagents/registry.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use super::default_agents;
|
||||
use super::definition::SubagentDefinition;
|
||||
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);
|
||||
}
|
||||
// Then load project definitions which override on conflicts
|
||||
if let Some(dir) = &self.project_dir {
|
||||
Self::load_from_dir_into(dir, &mut map);
|
||||
}
|
||||
|
||||
// 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>) {
|
||||
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(def) => {
|
||||
out.insert(def.name.clone(), def);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to load subagent from {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
228
codex-rs/core/src/subagents/runner.rs
Normal file
228
codex-rs/core/src/subagents/runner.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use crate::codex::Codex;
|
||||
use crate::error::Result as CodexResult;
|
||||
|
||||
use super::definition::SubagentDefinition;
|
||||
use super::registry::SubagentRegistry;
|
||||
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>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
registry: &SubagentRegistry,
|
||||
args: RunSubagentArgs,
|
||||
_parent_sub_id: &str,
|
||||
) -> CodexResult<String> {
|
||||
let def: &SubagentDefinition = registry.get(&args.name).ok_or_else(|| {
|
||||
crate::error::CodexErr::Stream(format!("unknown subagent: {}", args.name), None)
|
||||
})?;
|
||||
|
||||
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()
|
||||
};
|
||||
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, sess.auth_manager(), 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