mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
add deferred watchdog self-close tool
This commit is contained in:
@@ -50,6 +50,7 @@ pub(crate) use resume_agent::Handler as ResumeAgentHandler;
|
||||
pub(crate) use send_input::Handler as SendInputHandler;
|
||||
pub(crate) use spawn::Handler as SpawnAgentHandler;
|
||||
pub(crate) use wait::Handler as WaitAgentHandler;
|
||||
pub(crate) use watchdog_self_close::Handler as WatchdogSelfCloseHandler;
|
||||
|
||||
pub mod close_agent;
|
||||
mod compact_parent_context;
|
||||
@@ -58,6 +59,7 @@ mod resume_agent;
|
||||
mod send_input;
|
||||
mod spawn;
|
||||
pub(crate) mod wait;
|
||||
mod watchdog_self_close;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "multi_agents_tests.rs"]
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for Handler {
|
||||
type Output = WatchdogSelfCloseResult;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
payload,
|
||||
call_id,
|
||||
turn,
|
||||
..
|
||||
} = invocation;
|
||||
let arguments = function_arguments(payload)?;
|
||||
let _args: WatchdogSelfCloseArgs = parse_arguments(&arguments)?;
|
||||
let helper_thread_id = session.conversation_id;
|
||||
if session
|
||||
.services
|
||||
.agent_control
|
||||
.watchdog_owner_for_active_helper(helper_thread_id)
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"watchdog_self_close is only available in watchdog check-in threads.".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_metadata(helper_thread_id)
|
||||
.unwrap_or_default();
|
||||
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: helper_thread_id,
|
||||
receiver_thread_id: helper_thread_id,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let status = match session
|
||||
.services
|
||||
.agent_control
|
||||
.subscribe_status(helper_thread_id)
|
||||
.await
|
||||
{
|
||||
Ok(mut status_rx) => status_rx.borrow_and_update().clone(),
|
||||
Err(err) => {
|
||||
let status = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(helper_thread_id)
|
||||
.await;
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseEndEvent {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: helper_thread_id,
|
||||
receiver_thread_id: helper_thread_id,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
|
||||
receiver_agent_role: receiver_agent.agent_role.clone(),
|
||||
status,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
return Err(collab_agent_error(helper_thread_id, err));
|
||||
}
|
||||
};
|
||||
|
||||
let result = session
|
||||
.services
|
||||
.agent_control
|
||||
.close_agent(helper_thread_id)
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(helper_thread_id, err))
|
||||
.map(|_| ());
|
||||
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_metadata(helper_thread_id)
|
||||
.unwrap_or_default();
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseEndEvent {
|
||||
call_id,
|
||||
sender_thread_id: helper_thread_id,
|
||||
receiver_thread_id: helper_thread_id,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
status: status.clone(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
result?;
|
||||
|
||||
Ok(WatchdogSelfCloseResult {
|
||||
previous_status: status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WatchdogSelfCloseArgs {}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct WatchdogSelfCloseResult {
|
||||
previous_status: AgentStatus,
|
||||
}
|
||||
|
||||
impl ToolOutput for WatchdogSelfCloseResult {
|
||||
fn log_preview(&self) -> String {
|
||||
tool_output_json_text(self, "watchdog_self_close")
|
||||
}
|
||||
|
||||
fn success_for_logging(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
||||
tool_output_response_item(call_id, payload, self, Some(true), "watchdog_self_close")
|
||||
}
|
||||
|
||||
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
|
||||
tool_output_code_mode_result(self, "watchdog_self_close")
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,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;
|
||||
@@ -46,3 +47,4 @@ mod message_tool;
|
||||
mod send_message;
|
||||
mod spawn;
|
||||
pub(crate) mod wait;
|
||||
mod watchdog_self_close;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for Handler {
|
||||
type Output = WatchdogSelfCloseResult;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
let arguments = function_arguments(payload)?;
|
||||
let _args: WatchdogSelfCloseArgs = parse_arguments(&arguments)?;
|
||||
|
||||
let helper_thread_id = session.conversation_id;
|
||||
if session
|
||||
.services
|
||||
.agent_control
|
||||
.watchdog_owner_for_active_helper(helper_thread_id)
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"watchdog_self_close is only available in watchdog check-in threads.".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_metadata(helper_thread_id)
|
||||
.unwrap_or_default();
|
||||
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: helper_thread_id,
|
||||
receiver_thread_id: helper_thread_id,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let status = match session
|
||||
.services
|
||||
.agent_control
|
||||
.subscribe_status(helper_thread_id)
|
||||
.await
|
||||
{
|
||||
Ok(mut status_rx) => status_rx.borrow_and_update().clone(),
|
||||
Err(err) => {
|
||||
let status = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(helper_thread_id)
|
||||
.await;
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseEndEvent {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: helper_thread_id,
|
||||
receiver_thread_id: helper_thread_id,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
|
||||
receiver_agent_role: receiver_agent.agent_role.clone(),
|
||||
status,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
return Err(collab_agent_error(helper_thread_id, err));
|
||||
}
|
||||
};
|
||||
|
||||
let result = session
|
||||
.services
|
||||
.agent_control
|
||||
.close_agent(helper_thread_id)
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(helper_thread_id, err))
|
||||
.map(|_| ());
|
||||
|
||||
let receiver_agent = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_metadata(helper_thread_id)
|
||||
.unwrap_or_default();
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseEndEvent {
|
||||
call_id,
|
||||
sender_thread_id: helper_thread_id,
|
||||
receiver_thread_id: helper_thread_id,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
status: status.clone(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
result?;
|
||||
|
||||
Ok(WatchdogSelfCloseResult {
|
||||
previous_status: status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WatchdogSelfCloseArgs {}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct WatchdogSelfCloseResult {
|
||||
previous_status: AgentStatus,
|
||||
}
|
||||
|
||||
impl ToolOutput for WatchdogSelfCloseResult {
|
||||
fn log_preview(&self) -> String {
|
||||
tool_output_json_text(self, "watchdog_self_close")
|
||||
}
|
||||
|
||||
fn success_for_logging(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
|
||||
tool_output_response_item(call_id, payload, self, Some(true), "watchdog_self_close")
|
||||
}
|
||||
|
||||
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
|
||||
tool_output_code_mode_result(self, "watchdog_self_close")
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ use codex_tools::create_spawn_agents_on_csv_tool;
|
||||
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::dynamic_tool_to_responses_api_tool;
|
||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||
@@ -1129,12 +1130,14 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
use crate::tools::handlers::multi_agents::SendInputHandler;
|
||||
use crate::tools::handlers::multi_agents::SpawnAgentHandler;
|
||||
use crate::tools::handlers::multi_agents::WaitAgentHandler;
|
||||
use crate::tools::handlers::multi_agents::WatchdogSelfCloseHandler;
|
||||
use crate::tools::handlers::multi_agents_v2::AssignTaskHandler as AssignTaskHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
|
||||
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;
|
||||
let mut builder = ToolRegistryBuilder::new();
|
||||
|
||||
let shell_handler = Arc::new(ShellHandler);
|
||||
@@ -1487,6 +1490,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
];
|
||||
if config.agent_watchdog {
|
||||
agent_tools.push(create_compact_parent_context_tool());
|
||||
agent_tools.push(create_watchdog_self_close_tool());
|
||||
}
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
@@ -1504,6 +1508,13 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandlerV2));
|
||||
register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandlerV2));
|
||||
register_agent_tool_handler(&mut builder, "list_agents", Arc::new(ListAgentsHandlerV2));
|
||||
if config.agent_watchdog {
|
||||
register_agent_tool_handler(
|
||||
&mut builder,
|
||||
"watchdog_self_close",
|
||||
Arc::new(WatchdogSelfCloseHandlerV2),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let mut agent_tools = vec![
|
||||
create_spawn_agent_tool_v1(SpawnAgentToolOptions {
|
||||
@@ -1524,6 +1535,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
if config.agent_watchdog {
|
||||
agent_tools.push(create_list_agents_tool(config.agent_watchdog));
|
||||
agent_tools.push(create_compact_parent_context_tool());
|
||||
agent_tools.push(create_watchdog_self_close_tool());
|
||||
}
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
@@ -1537,11 +1549,18 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
register_agent_tool_handler(&mut builder, "wait_agent", Arc::new(WaitAgentHandler));
|
||||
register_agent_tool_handler(&mut builder, "close_agent", Arc::new(CloseAgentHandler));
|
||||
register_agent_tool_handler(&mut builder, "list_agents", Arc::new(ListAgentsHandler));
|
||||
register_agent_tool_handler(
|
||||
&mut builder,
|
||||
"compact_parent_context",
|
||||
Arc::new(CompactParentContextHandler),
|
||||
);
|
||||
if config.agent_watchdog {
|
||||
register_agent_tool_handler(
|
||||
&mut builder,
|
||||
"compact_parent_context",
|
||||
Arc::new(CompactParentContextHandler),
|
||||
);
|
||||
register_agent_tool_handler(
|
||||
&mut builder,
|
||||
"watchdog_self_close",
|
||||
Arc::new(WatchdogSelfCloseHandler),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,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;
|
||||
@@ -407,6 +408,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
if config.agent_watchdog {
|
||||
collab_specs.push(create_list_agents_tool(config.agent_watchdog));
|
||||
collab_specs.push(create_compact_parent_context_tool());
|
||||
collab_specs.push(create_watchdog_self_close_tool());
|
||||
}
|
||||
collab_specs
|
||||
};
|
||||
@@ -464,6 +466,54 @@ fn test_build_specs_collab_tools_enabled() {
|
||||
assert_lacks_tool_name(&tools, "list_agents");
|
||||
}
|
||||
|
||||
#[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_tool_names(
|
||||
&tools,
|
||||
&[
|
||||
"watchdog_self_close",
|
||||
"compact_parent_context",
|
||||
"list_agents",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
|
||||
let watchdog_self_close = find_tool(&tools, "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_eq!(*deferred, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
let config = test_config();
|
||||
|
||||
@@ -4,3 +4,4 @@ If you are acting as a watchdog check-in agent, `compact_parent_context` may be
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -55,10 +55,11 @@ Use only the multi-agent tools that exist here:
|
||||
- `spawn_agent` (prefer `spawn_mode = "fork"` 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).
|
||||
- `wait`.
|
||||
- `close_agent`.
|
||||
|
||||
There is no cancel tool. Use `close_agent` to stop agents that are done or no longer needed.
|
||||
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.
|
||||
|
||||
When recommending watchdogs to the root agent, keep `agent_type` at the default.
|
||||
|
||||
@@ -80,6 +81,8 @@ 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.
|
||||
|
||||
Do not call `compact_parent_context` for routine nudges or normal delays. Prefer precise `send_input` guidance first.
|
||||
|
||||
## Style
|
||||
|
||||
@@ -312,6 +312,23 @@ pub fn create_close_agent_tool_v2() -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_watchdog_self_close_tool() -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "watchdog_self_close".to_string(),
|
||||
description:
|
||||
"Watchdog-only: close this watchdog check-in thread and terminate immediately."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: Some(close_agent_output_schema()),
|
||||
})
|
||||
}
|
||||
|
||||
fn agent_status_output_schema() -> Value {
|
||||
json!({
|
||||
"oneOf": [
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use agent_tool::create_spawn_agent_tool_v1;
|
||||
pub use agent_tool::create_spawn_agent_tool_v2;
|
||||
pub use agent_tool::create_wait_agent_tool_v1;
|
||||
pub use agent_tool::create_wait_agent_tool_v2;
|
||||
pub use agent_tool::create_watchdog_self_close_tool;
|
||||
pub use code_mode::augment_tool_spec_for_code_mode;
|
||||
pub use code_mode::tool_spec_to_code_mode_tool_definition;
|
||||
pub use dynamic_tool::parse_dynamic_tool;
|
||||
|
||||
Reference in New Issue
Block a user