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:
Friel
2026-04-01 04:11:55 +00:00
7 changed files with 1310 additions and 195 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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",
],
);
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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!(