mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Merge commit 'b122ecfc6b6dd9784300ebde39fe0b9edc22dd7f' into dev/friel/collab-stack
# Conflicts: # codex-rs/core/src/tools/handlers/multi_agents/spawn.rs # codex-rs/core/src/tools/spec.rs # codex-rs/core/src/tools/spec_tests.rs # codex-rs/core/subagent_watchdog_prompt.md # codex-rs/core/watchdog_agent_prompt.md # codex-rs/tui/src/chatwidget/tests.rs
This commit is contained in:
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::AuthManager;
|
||||
use crate::CodexAuth;
|
||||
use crate::ThreadManager;
|
||||
use crate::agent::WatchdogRegistration;
|
||||
use crate::built_in_model_providers;
|
||||
use crate::codex::make_session_and_context;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
@@ -30,6 +31,7 @@ use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHand
|
||||
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::WatchdogSelfCloseHandlerV2;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::AgentPath;
|
||||
@@ -89,6 +91,54 @@ fn thread_manager() -> ThreadManager {
|
||||
)
|
||||
}
|
||||
|
||||
async fn attach_watchdog_helper_for_tests(
|
||||
manager: &ThreadManager,
|
||||
agent_control: &crate::agent::AgentControl,
|
||||
config: &crate::config::Config,
|
||||
) -> ThreadId {
|
||||
let owner = manager
|
||||
.start_thread(config.clone())
|
||||
.await
|
||||
.expect("owner thread should start");
|
||||
let target = agent_control
|
||||
.spawn_agent(
|
||||
config.clone(),
|
||||
Op::UserInput {
|
||||
items: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("watchdog target thread should start");
|
||||
let helper = manager
|
||||
.start_thread(config.clone())
|
||||
.await
|
||||
.expect("helper thread should start");
|
||||
agent_control
|
||||
.register_watchdog(WatchdogRegistration {
|
||||
owner_thread_id: owner.thread_id,
|
||||
target_thread_id: target,
|
||||
child_depth: 1,
|
||||
interval_s: 30,
|
||||
prompt: "check in".to_string(),
|
||||
config: config.clone(),
|
||||
})
|
||||
.await
|
||||
.expect("watchdog registration should succeed");
|
||||
agent_control
|
||||
.set_watchdog_active_helper_for_tests(target, helper.thread_id)
|
||||
.await;
|
||||
assert_eq!(
|
||||
agent_control
|
||||
.watchdog_owner_for_active_helper(helper.thread_id)
|
||||
.await,
|
||||
Some(owner.thread_id),
|
||||
"watchdog helper should be registered for owner"
|
||||
);
|
||||
helper.thread_id
|
||||
}
|
||||
|
||||
fn history_contains_inter_agent_communication(
|
||||
history_items: &[ResponseItem],
|
||||
expected: &InterAgentCommunication,
|
||||
@@ -175,6 +225,11 @@ struct ListedAgentResult {
|
||||
last_task_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct WatchdogSelfCloseResult {
|
||||
previous_status: AgentStatus,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_rejects_non_function_payloads() {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
@@ -3001,6 +3056,116 @@ async fn close_agent_submits_shutdown_and_returns_previous_status() {
|
||||
assert_eq!(status_after, AgentStatus::NotFound);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watchdog_self_close_rejects_non_watchdog_thread() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let agent_control = manager.agent_control();
|
||||
let thread = manager
|
||||
.start_thread(turn.config.as_ref().clone())
|
||||
.await
|
||||
.expect("thread should start");
|
||||
session.services.agent_control = agent_control.clone();
|
||||
session.conversation_id = thread.thread_id;
|
||||
|
||||
let err = WatchdogSelfCloseHandler
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"watchdog_self_close",
|
||||
function_payload(json!({})),
|
||||
))
|
||||
.await
|
||||
.expect_err("non-watchdog threads should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel(
|
||||
"watchdog_self_close is only available in watchdog check-in threads.".to_string(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watchdog_self_close_closes_watchdog_helper_and_returns_previous_status() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let agent_control = manager.agent_control();
|
||||
session.services.agent_control = agent_control.clone();
|
||||
let helper_thread_id =
|
||||
attach_watchdog_helper_for_tests(&manager, &agent_control, turn.config.as_ref()).await;
|
||||
session.conversation_id = helper_thread_id;
|
||||
let status_before = agent_control.get_status(helper_thread_id).await;
|
||||
|
||||
let output = WatchdogSelfCloseHandler
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"watchdog_self_close",
|
||||
function_payload(json!({})),
|
||||
))
|
||||
.await
|
||||
.expect("watchdog helper should be allowed to self-close");
|
||||
let (content, success) = expect_text_output(output);
|
||||
let result: WatchdogSelfCloseResult =
|
||||
serde_json::from_str(&content).expect("watchdog self-close result should be json");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
WatchdogSelfCloseResult {
|
||||
previous_status: status_before,
|
||||
}
|
||||
);
|
||||
assert_eq!(success, Some(true));
|
||||
assert_eq!(
|
||||
agent_control.get_status(helper_thread_id).await,
|
||||
AgentStatus::NotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_watchdog_self_close_closes_watchdog_helper_and_returns_previous_status() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let agent_control = manager.agent_control();
|
||||
session.services.agent_control = agent_control.clone();
|
||||
let mut config = turn.config.as_ref().clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
let helper_thread_id =
|
||||
attach_watchdog_helper_for_tests(&manager, &agent_control, &config).await;
|
||||
session.conversation_id = helper_thread_id;
|
||||
turn.config = Arc::new(config);
|
||||
let status_before = agent_control.get_status(helper_thread_id).await;
|
||||
|
||||
let output = WatchdogSelfCloseHandlerV2
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"watchdog_self_close",
|
||||
function_payload(json!({})),
|
||||
))
|
||||
.await
|
||||
.expect("watchdog helper should be allowed to self-close");
|
||||
let (content, success) = expect_text_output(output);
|
||||
let result: WatchdogSelfCloseResult =
|
||||
serde_json::from_str(&content).expect("watchdog self-close result should be json");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
WatchdogSelfCloseResult {
|
||||
previous_status: status_before,
|
||||
}
|
||||
);
|
||||
assert_eq!(success, Some(true));
|
||||
assert_eq!(
|
||||
agent_control.get_status(helper_thread_id).await,
|
||||
AgentStatus::NotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed() {
|
||||
let (_session, turn) = make_session_and_context().await;
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(crate) use list_agents::Handler as ListAgentsHandler;
|
||||
pub(crate) use send_message::Handler as SendMessageHandler;
|
||||
pub(crate) use spawn::Handler as SpawnAgentHandler;
|
||||
pub(crate) use wait::Handler as WaitAgentHandler;
|
||||
pub(crate) use watchdog_self_close::Handler as WatchdogSelfCloseHandlerV2;
|
||||
|
||||
mod assign_task;
|
||||
mod close_agent;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,7 @@ use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_tools::AdditionalProperties;
|
||||
use codex_tools::CommandToolOptions;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::DiscoverablePluginInfo;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::FreeformTool;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ResponsesApiWebSearchFilters;
|
||||
use codex_tools::ResponsesApiWebSearchUserLocation;
|
||||
use codex_tools::SpawnAgentToolOptions;
|
||||
@@ -26,7 +22,6 @@ use codex_tools::WaitAgentTimeoutOptions;
|
||||
use codex_tools::create_close_agent_tool_v1;
|
||||
use codex_tools::create_close_agent_tool_v2;
|
||||
use codex_tools::create_exec_command_tool;
|
||||
use codex_tools::create_list_agents_tool;
|
||||
use codex_tools::create_request_permissions_tool;
|
||||
use codex_tools::create_request_user_input_tool;
|
||||
use codex_tools::create_resume_agent_tool;
|
||||
@@ -37,6 +32,7 @@ use codex_tools::create_spawn_agent_tool_v2;
|
||||
use codex_tools::create_view_image_tool;
|
||||
use codex_tools::create_wait_agent_tool_v1;
|
||||
use codex_tools::create_wait_agent_tool_v2;
|
||||
use codex_tools::create_watchdog_self_close_tool;
|
||||
use codex_tools::create_write_stdin_tool;
|
||||
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -174,6 +170,20 @@ fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_contains_top_level_tool_name(tools: &[ConfiguredToolSpec], expected: &str) {
|
||||
assert!(
|
||||
tools.iter().any(|tool| tool.name() == expected),
|
||||
"expected top-level tool {expected} to be present"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_lacks_top_level_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
||||
assert!(
|
||||
!tools.iter().any(|tool| tool.name() == expected_absent),
|
||||
"expected top-level tool {expected_absent} to be absent"
|
||||
);
|
||||
}
|
||||
|
||||
fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> {
|
||||
match config.shell_type {
|
||||
ConfigShellToolType::Default => Some("shell"),
|
||||
@@ -230,23 +240,36 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co
|
||||
panic!("expected tool {expected_name}")
|
||||
}
|
||||
|
||||
fn find_namespaced_tool<'a>(
|
||||
tools: &'a [ConfiguredToolSpec],
|
||||
namespace: &str,
|
||||
fn find_namespaced_tool(
|
||||
tools: &[ConfiguredToolSpec],
|
||||
namespace_name: &str,
|
||||
expected_name: &str,
|
||||
) -> &'a ResponsesApiTool {
|
||||
let namespace_tool = find_tool(tools, namespace);
|
||||
let ToolSpec::Namespace(namespace) = &namespace_tool.spec else {
|
||||
panic!("expected {namespace} namespace tool");
|
||||
};
|
||||
namespace
|
||||
) -> ConfiguredToolSpec {
|
||||
let namespace = tools
|
||||
.iter()
|
||||
.find_map(|tool| match &tool.spec {
|
||||
ToolSpec::Namespace(namespace) if namespace.name == namespace_name => Some(namespace),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("expected namespace {namespace_name}"));
|
||||
|
||||
let tool = namespace
|
||||
.tools
|
||||
.iter()
|
||||
.find_map(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) if tool.name == expected_name => Some(tool),
|
||||
codex_tools::ResponsesApiNamespaceTool::Function(tool)
|
||||
if tool.name == expected_name =>
|
||||
{
|
||||
Some(tool.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("expected tool in namespace")
|
||||
.unwrap_or_else(|| panic!("expected tool {expected_name} in {namespace_name}"));
|
||||
|
||||
ConfiguredToolSpec::new(
|
||||
ToolSpec::Function(tool),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
)
|
||||
}
|
||||
|
||||
fn strip_descriptions_schema(schema: &mut JsonSchema) {
|
||||
@@ -418,7 +441,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
create_send_message_tool(),
|
||||
create_wait_agent_tool_v2(wait_agent_timeout_options()),
|
||||
create_close_agent_tool_v2(),
|
||||
create_list_agents_tool(),
|
||||
create_list_agents_tool_v2(),
|
||||
]
|
||||
} else {
|
||||
let mut collab_specs = vec![
|
||||
@@ -428,10 +451,21 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
create_wait_agent_tool_v1(wait_agent_timeout_options()),
|
||||
create_close_agent_tool_v1(),
|
||||
];
|
||||
if config.agent_watchdog {
|
||||
collab_specs.push(create_list_agents_tool(config.agent_watchdog));
|
||||
}
|
||||
collab_specs
|
||||
};
|
||||
let spec = create_agent_tools_namespace(collab_specs.split_off(0));
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
for spec in collab_specs.split_off(0) {
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
if config.agent_watchdog {
|
||||
let spec = create_watchdog_tools_namespace(vec![
|
||||
create_compact_parent_context_tool(),
|
||||
create_watchdog_self_close_tool(),
|
||||
]);
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
|
||||
if config.exec_permission_approvals_enabled {
|
||||
let spec = create_request_permissions_tool(request_permissions_tool_description());
|
||||
@@ -476,16 +510,66 @@ fn test_build_specs_collab_tools_enabled() {
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
assert_contains_tool_names(&tools, &["agents"]);
|
||||
assert_contains_tool_names(&tools, &["tool_search"]);
|
||||
assert_contains_tool_names(
|
||||
&tools,
|
||||
&["spawn_agent", "send_input", "wait_agent", "close_agent"],
|
||||
);
|
||||
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
|
||||
assert_lacks_tool_name(&tools, "list_agents");
|
||||
assert_lacks_tool_name(&tools, "watchdog");
|
||||
}
|
||||
|
||||
let spawn_agent = find_namespaced_tool(&tools, "agents", "spawn_agent");
|
||||
let JsonSchema::Object { properties, .. } = &spawn_agent.parameters else {
|
||||
panic!("spawn_agent should use object params");
|
||||
#[test]
|
||||
fn test_build_specs_watchdog_collab_tools_include_self_close_tool() {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Collab);
|
||||
features.enable(Feature::AgentWatchdog);
|
||||
features.normalize_dependencies();
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*app_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_contains_top_level_tool_name(&tools, "watchdog");
|
||||
assert_contains_tool_names(&tools, &["tool_search", "list_agents", "close_agent"]);
|
||||
assert_lacks_top_level_tool_name(&tools, "watchdog_self_close");
|
||||
assert_lacks_top_level_tool_name(&tools, "compact_parent_context");
|
||||
|
||||
let watchdog_self_close = find_namespaced_tool(&tools, "watchdog", "watchdog_self_close");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
defer_loading: Some(deferred),
|
||||
..
|
||||
}) = &watchdog_self_close.spec
|
||||
else {
|
||||
panic!("watchdog_self_close should be a function tool");
|
||||
};
|
||||
assert!(properties.contains_key("fork_context"));
|
||||
assert!(!properties.contains_key("fork_turns"));
|
||||
assert!(*deferred);
|
||||
let compact_parent_context = find_namespaced_tool(&tools, "watchdog", "compact_parent_context");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
defer_loading: Some(deferred),
|
||||
..
|
||||
}) = &compact_parent_context.spec
|
||||
else {
|
||||
panic!("compact_parent_context should be a function tool");
|
||||
};
|
||||
assert!(*deferred);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -512,57 +596,56 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
assert_contains_tool_names(&tools, &["agents"]);
|
||||
assert_contains_tool_names(
|
||||
&tools,
|
||||
&[
|
||||
"spawn_agent",
|
||||
"send_message",
|
||||
"assign_task",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"list_agents",
|
||||
],
|
||||
);
|
||||
|
||||
let spawn_agent = find_namespaced_tool(&tools, "agents", "spawn_agent");
|
||||
let JsonSchema::Object {
|
||||
properties: _,
|
||||
required,
|
||||
let spawn_agent = find_tool(&tools, "spawn_agent");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
} = &spawn_agent.parameters
|
||||
}) = &spawn_agent.spec
|
||||
else {
|
||||
panic!("spawn_agent should be a function tool");
|
||||
};
|
||||
let output_schema = spawn_agent
|
||||
.output_schema
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = parameters
|
||||
else {
|
||||
panic!("spawn_agent should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("task_name"));
|
||||
assert_eq!(required.as_ref(), Some(&vec!["task_name".to_string()]));
|
||||
let output_schema = output_schema
|
||||
.as_ref()
|
||||
.expect("spawn_agent should define output schema");
|
||||
assert_eq!(
|
||||
required.as_ref(),
|
||||
Some(&vec!["task_name".to_string(), "items".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
output_schema["required"],
|
||||
json!(["agent_id", "task_name", "nickname"])
|
||||
);
|
||||
|
||||
let send_message = find_namespaced_tool(&tools, "agents", "send_message");
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = &send_message.parameters
|
||||
else {
|
||||
let send_message = find_tool(&tools, "send_message");
|
||||
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_message.spec else {
|
||||
panic!("send_message should be a function tool");
|
||||
};
|
||||
assert!(properties.contains_key("items"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(!properties.contains_key("interrupt"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert_eq!(
|
||||
required.as_ref(),
|
||||
Some(&vec!["target".to_string(), "items".to_string()])
|
||||
);
|
||||
|
||||
let assign_task = find_namespaced_tool(&tools, "agents", "assign_task");
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = &assign_task.parameters
|
||||
} = parameters
|
||||
else {
|
||||
panic!("assign_task should be a function tool");
|
||||
panic!("send_message should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
@@ -571,42 +654,74 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
Some(&vec!["target".to_string(), "items".to_string()])
|
||||
);
|
||||
|
||||
let wait_agent = find_namespaced_tool(&tools, "agents", "wait_agent");
|
||||
let assign_task = find_tool(&tools, "assign_task");
|
||||
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &assign_task.spec else {
|
||||
panic!("assign_task should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = &wait_agent.parameters
|
||||
} = parameters
|
||||
else {
|
||||
panic!("assign_task should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(!properties.contains_key("message"));
|
||||
assert_eq!(
|
||||
required.as_ref(),
|
||||
Some(&vec!["target".to_string(), "items".to_string()])
|
||||
);
|
||||
|
||||
let wait_agent = find_tool(&tools, "wait_agent");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = &wait_agent.spec
|
||||
else {
|
||||
panic!("wait_agent should be a function tool");
|
||||
};
|
||||
let output_schema = wait_agent
|
||||
.output_schema
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = parameters
|
||||
else {
|
||||
panic!("wait_agent should use object params");
|
||||
};
|
||||
assert!(properties.contains_key("targets"));
|
||||
assert_eq!(required.as_ref(), Some(&vec!["targets".to_string()]));
|
||||
let output_schema = output_schema
|
||||
.as_ref()
|
||||
.expect("wait_agent should define output schema");
|
||||
assert!(!properties.contains_key("targets"));
|
||||
assert!(properties.contains_key("timeout_ms"));
|
||||
assert_eq!(required, &None);
|
||||
assert_eq!(
|
||||
output_schema["properties"]["message"]["description"],
|
||||
json!("Brief wait summary without the agent's final content.")
|
||||
);
|
||||
|
||||
let list_agents = find_namespaced_tool(&tools, "agents", "list_agents");
|
||||
let list_agents = find_tool(&tools, "list_agents");
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = &list_agents.spec
|
||||
else {
|
||||
panic!("list_agents should be a function tool");
|
||||
};
|
||||
let JsonSchema::Object {
|
||||
properties,
|
||||
required,
|
||||
..
|
||||
} = &list_agents.parameters
|
||||
} = parameters
|
||||
else {
|
||||
panic!("list_agents should be a function tool");
|
||||
panic!("list_agents should use object params");
|
||||
};
|
||||
let output_schema = list_agents
|
||||
.output_schema
|
||||
.as_ref()
|
||||
.expect("list_agents should define output schema");
|
||||
assert!(properties.contains_key("path_prefix"));
|
||||
assert_eq!(required.as_ref(), None);
|
||||
let output_schema = output_schema
|
||||
.as_ref()
|
||||
.expect("list_agents should define output schema");
|
||||
assert_eq!(
|
||||
output_schema["properties"]["agents"]["items"]["required"],
|
||||
json!(["agent_name", "agent_status", "last_task_message"])
|
||||
@@ -639,7 +754,16 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
assert_contains_tool_names(&tools, &["agents", "spawn_agents_on_csv"]);
|
||||
assert_contains_tool_names(
|
||||
&tools,
|
||||
&[
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"spawn_agents_on_csv",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -748,7 +872,15 @@ fn test_build_specs_agent_job_worker_tools_enabled() {
|
||||
.build();
|
||||
assert_contains_tool_names(
|
||||
&tools,
|
||||
&["agents", "spawn_agents_on_csv", "report_agent_job_result"],
|
||||
&[
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
"spawn_agents_on_csv",
|
||||
"report_agent_job_result",
|
||||
],
|
||||
);
|
||||
assert_lacks_tool_name(&tools, "request_user_input");
|
||||
}
|
||||
@@ -1059,6 +1191,21 @@ fn image_generation_tools_require_feature_and_supported_model() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repl_freeform_grammar_blocks_common_non_js_prefixes() {
|
||||
let ToolSpec::Freeform(FreeformTool { format, .. }) = create_js_repl_tool() else {
|
||||
panic!("js_repl should use a freeform tool spec");
|
||||
};
|
||||
|
||||
assert_eq!(format.syntax, "lark");
|
||||
assert!(format.definition.contains("PRAGMA_LINE"));
|
||||
assert!(format.definition.contains("`[^`]"));
|
||||
assert!(format.definition.contains("``[^`]"));
|
||||
assert!(format.definition.contains("PLAIN_JS_SOURCE"));
|
||||
assert!(format.definition.contains("codex-js-repl:"));
|
||||
assert!(!format.definition.contains("(?!"));
|
||||
}
|
||||
|
||||
fn assert_model_tools(
|
||||
model_slug: &str,
|
||||
features: &Features,
|
||||
@@ -1368,10 +1515,15 @@ fn test_build_specs_gpt5_codex_default() {
|
||||
&[
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1387,10 +1539,15 @@ fn test_build_specs_gpt51_codex_default() {
|
||||
&[
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1408,10 +1565,15 @@ fn test_build_specs_gpt5_codex_unified_exec_web_search() {
|
||||
"write_stdin",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1429,10 +1591,15 @@ fn test_build_specs_gpt51_codex_unified_exec_web_search() {
|
||||
"write_stdin",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1448,10 +1615,15 @@ fn test_gpt_5_1_codex_max_defaults() {
|
||||
&[
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1467,10 +1639,15 @@ fn test_codex_5_1_mini_defaults() {
|
||||
&[
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1486,9 +1663,14 @@ fn test_gpt_5_defaults() {
|
||||
&[
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1504,10 +1686,15 @@ fn test_gpt_5_1_defaults() {
|
||||
&[
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"tool_search",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1528,7 +1715,11 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() {
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
"agents",
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
## Watchdog-only Guidance
|
||||
|
||||
If you are acting as a watchdog check-in agent, `compact_parent_context` may be available.
|
||||
If you are acting as a watchdog check-in agent, the deferred `watchdog` namespace may be available
|
||||
through `tool_search`.
|
||||
|
||||
- Use `compact_parent_context` only when the parent thread is idle and appears stuck.
|
||||
- `compact_parent_context` is not part of the general subagent tool surface; do not mention or rely on it unless you are explicitly operating as a watchdog check-in agent.
|
||||
- `watchdog_self_close` is also available to this watchdog thread and can be used to end the check-in when work is complete.
|
||||
- Use `watchdog.compact_parent_context` only when the parent thread is idle and appears stuck.
|
||||
- `watchdog.compact_parent_context` is not part of the general subagent tool surface; do not
|
||||
mention or rely on it unless you are explicitly operating as a watchdog check-in agent.
|
||||
- `watchdog.watchdog_self_close` is also available to this watchdog thread and can be used to end
|
||||
the check-in when work is complete.
|
||||
|
||||
@@ -54,12 +54,13 @@ Use only the multi-agent tools that exist here:
|
||||
|
||||
- `spawn_agent` (prefer `fork_context = true` when shared context matters).
|
||||
- `send_input`.
|
||||
- `compact_parent_context` (watchdog-only recovery tool; see below).
|
||||
- `watchdog_self_close` (watchdog-only immediate exit tool; see below).
|
||||
- `tool_search` to discover deferred watchdog-only tools in the `watchdog` namespace.
|
||||
- `watchdog.compact_parent_context` (watchdog-only recovery tool; see below).
|
||||
- `watchdog.watchdog_self_close` (watchdog-only immediate exit tool; see below).
|
||||
- `wait`.
|
||||
- `close_agent`.
|
||||
|
||||
There is no cancel tool. Use `watchdog_self_close` to stop this watchdog check-in thread when its job is complete; use `close_agent` to stop subagents that are done or no longer needed.
|
||||
There is no cancel tool. Use `watchdog.watchdog_self_close` to stop this watchdog check-in thread when its job is complete; use `close_agent` to stop subagents that are done or no longer needed.
|
||||
|
||||
When recommending watchdogs to the root agent, keep `agent_type` at the default.
|
||||
|
||||
@@ -73,7 +74,7 @@ For token protocols (for example `ping N` / `pong N`), treat those as literal te
|
||||
|
||||
## Parent Recovery via Context Compaction
|
||||
|
||||
`compact_parent_context` asks the system to abbreviate/compact redundant parent-thread context so the parent can recover from loops.
|
||||
`watchdog.compact_parent_context` asks the system to abbreviate/compact redundant parent-thread context so the parent can recover from loops.
|
||||
|
||||
Use it only as a last resort:
|
||||
|
||||
@@ -81,9 +82,9 @@ Use it only as a last resort:
|
||||
- The parent is taking no meaningful actions (no concrete commands/edits/tests) and making no progress.
|
||||
- You already sent at least one direct corrective instruction with `send_input`, and it was ignored.
|
||||
|
||||
`watchdog_self_close` asks the runtime to end the current watchdog check-in thread immediately. Use it only after reporting status and when the check-in has no remaining work, to avoid idle watchdog loops.
|
||||
`watchdog.watchdog_self_close` asks the runtime to end the current watchdog check-in thread immediately. Use it only after reporting status and when the check-in has no remaining work, to avoid idle watchdog loops.
|
||||
|
||||
Do not call `compact_parent_context` for routine nudges or normal delays. Prefer precise `send_input` guidance first.
|
||||
Do not call `watchdog.compact_parent_context` for routine nudges or normal delays. Prefer precise `send_input` guidance first.
|
||||
|
||||
## Style
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::JsonSchema;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::create_tools_json_for_responses_api;
|
||||
use crate::create_watchdog_self_close_tool;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
@@ -123,6 +124,32 @@ fn configured_tool_spec_name_delegates_to_tool_spec() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watchdog_self_close_tool_spec_is_deferred_and_parameterless() {
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
name,
|
||||
defer_loading,
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
}) = create_watchdog_self_close_tool()
|
||||
else {
|
||||
panic!("watchdog_self_close should be a function tool");
|
||||
};
|
||||
|
||||
assert_eq!(name, "watchdog_self_close");
|
||||
assert_eq!(defer_loading, Some(true));
|
||||
assert_eq!(
|
||||
parameters,
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: Some(AdditionalProperties::Boolean(false)),
|
||||
}
|
||||
);
|
||||
assert!(output_schema.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_config_converts_to_responses_api_types() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user