mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Support catalog parameter schemas for Multi-Agent V2 tools (#46505)
## What changed Add optional JSON-encoded `parameters` to catalog tool messages and apply them to all six Multi-Agent V2 tools, including plain, namespaced, and code-mode exposure. Schema selection follows the active model, including mid-turn model changes. Require an object schema supported by the existing `JsonSchema` subset and preserve bundled encryption annotations. Fall back to bundled parameters when overrides are missing, invalid, unsupported, or omit encrypted properties. Tool execution and argument handling remain unchanged. ## Testing Extend integration coverage for schema overrides, fallback behavior, encryption annotations, exposure modes, and mid-turn model changes. Add a snapshot scenario exercising `list_agents` with a catalog parameter schema. GitOrigin-RevId: 978be6d5f7e6a6865969922be5483bc697b20aca
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
//! Applies captured Multi-Agent V2 description and namespace overrides to tool specifications.
|
||||
//! Execution and parameter schemas are delegated unchanged to the underlying runtime.
|
||||
//! Applies captured Multi-Agent V2 catalog overrides and namespaces to tool specifications.
|
||||
//! Parameter schemas retain harness-owned encryption annotations; execution is unchanged.
|
||||
|
||||
use crate::session::session::Session;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolExecutor;
|
||||
@@ -12,6 +13,7 @@ use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use futures::future::BoxFuture;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
|
||||
@@ -20,14 +22,45 @@ pub(super) fn multi_agent_v2_handler(
|
||||
handler: impl CoreToolRuntime + 'static,
|
||||
namespace: Option<&str>,
|
||||
description_override: Option<&str>,
|
||||
parameters_override: Option<&str>,
|
||||
) -> Arc<dyn CoreToolRuntime> {
|
||||
if namespace.is_none() && description_override.is_none() {
|
||||
let parameters_override = parameters_override.map(|parameters| -> Result<JsonSchema, &str> {
|
||||
let parameters: Value =
|
||||
serde_json::from_str(parameters).map_err(|_| "schema is not valid JSON")?;
|
||||
if !parameters.is_object() || parameters["type"] != "object" {
|
||||
return Err("schema must declare an object type");
|
||||
}
|
||||
let mut parameters: JsonSchema = serde_json::from_value(parameters)
|
||||
.map_err(|_| "schema uses unsupported JSON Schema structures")?;
|
||||
if let ToolSpec::Function(tool) = handler.spec()
|
||||
&& let Some(properties) = tool.parameters.properties
|
||||
{
|
||||
// Argument transport requires these markers even without server encryption config.
|
||||
for (name, schema) in properties {
|
||||
if schema.encrypted == Some(true) {
|
||||
let property = parameters
|
||||
.properties
|
||||
.as_mut()
|
||||
.and_then(|properties| properties.get_mut(&name))
|
||||
.ok_or("schema omits an encrypted parameter")?;
|
||||
property.encrypted = Some(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(parameters)
|
||||
});
|
||||
if let Some(Err(reason)) = ¶meters_override {
|
||||
tracing::warn!(tool = %handler.tool_name(), reason, "Invalid catalog tool parameters; using bundled parameters");
|
||||
}
|
||||
let parameters_override = parameters_override.and_then(Result::ok);
|
||||
if namespace.is_none() && description_override.is_none() && parameters_override.is_none() {
|
||||
return Arc::new(handler);
|
||||
}
|
||||
Arc::new(MultiAgentV2ToolOverrides {
|
||||
handler: Arc::new(handler),
|
||||
namespace: namespace.map(str::to_owned),
|
||||
description_override: description_override.map(str::to_owned),
|
||||
parameters_override,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +68,7 @@ struct MultiAgentV2ToolOverrides {
|
||||
handler: Arc<dyn CoreToolRuntime>,
|
||||
namespace: Option<String>,
|
||||
description_override: Option<String>,
|
||||
parameters_override: Option<JsonSchema>,
|
||||
}
|
||||
|
||||
impl ToolExecutor<ToolInvocation> for MultiAgentV2ToolOverrides {
|
||||
@@ -48,10 +82,13 @@ impl ToolExecutor<ToolInvocation> for MultiAgentV2ToolOverrides {
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
let mut spec = self.handler.spec();
|
||||
if let ToolSpec::Function(tool) = &mut spec
|
||||
&& let Some(description) = &self.description_override
|
||||
{
|
||||
tool.description.clone_from(description);
|
||||
if let ToolSpec::Function(tool) = &mut spec {
|
||||
if let Some(description) = &self.description_override {
|
||||
tool.description.clone_from(description);
|
||||
}
|
||||
if let Some(parameters) = &self.parameters_override {
|
||||
tool.parameters.clone_from(parameters);
|
||||
}
|
||||
}
|
||||
match (&self.namespace, spec) {
|
||||
(Some(namespace), ToolSpec::Function(tool)) => {
|
||||
|
||||
@@ -1283,6 +1283,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
// Spawn composes the selected description with runtime model and usage guidance.
|
||||
/*description_override*/
|
||||
None,
|
||||
model_messages.multi_agent_tool_parameters_override("spawn_agent"),
|
||||
),
|
||||
exposure,
|
||||
);
|
||||
@@ -1291,6 +1292,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
SendMessageHandlerV2,
|
||||
tool_namespace,
|
||||
model_messages.multi_agent_tool_description_override("send_message"),
|
||||
model_messages.multi_agent_tool_parameters_override("send_message"),
|
||||
),
|
||||
exposure,
|
||||
);
|
||||
@@ -1299,6 +1301,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
FollowupTaskHandlerV2,
|
||||
tool_namespace,
|
||||
model_messages.multi_agent_tool_description_override("followup_task"),
|
||||
model_messages.multi_agent_tool_parameters_override("followup_task"),
|
||||
),
|
||||
exposure,
|
||||
);
|
||||
@@ -1308,6 +1311,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
WaitAgentHandlerV2::new(context.wait_agent_timeouts),
|
||||
tool_namespace,
|
||||
model_messages.multi_agent_tool_description_override("wait_agent"),
|
||||
model_messages.multi_agent_tool_parameters_override("wait_agent"),
|
||||
),
|
||||
exposure,
|
||||
);
|
||||
@@ -1317,6 +1321,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
InterruptAgentHandler,
|
||||
tool_namespace,
|
||||
model_messages.multi_agent_tool_description_override("interrupt_agent"),
|
||||
model_messages.multi_agent_tool_parameters_override("interrupt_agent"),
|
||||
),
|
||||
exposure,
|
||||
);
|
||||
@@ -1325,6 +1330,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
ListAgentsHandlerV2,
|
||||
tool_namespace,
|
||||
model_messages.multi_agent_tool_description_override("list_agents"),
|
||||
model_messages.multi_agent_tool_parameters_override("list_agents"),
|
||||
),
|
||||
exposure,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Verifies catalog descriptions reach V2 tools without changing their schemas or availability.
|
||||
//! Verifies V2 catalog tool messages change only their selected description or parameter schema.
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::config::AgentRoleConfig;
|
||||
@@ -24,6 +24,30 @@ const TOOL_NAMES: [&str; 6] = [
|
||||
"list_agents",
|
||||
];
|
||||
|
||||
const CATALOG_PARAMETERS: &str = r#"{
|
||||
"type": "object",
|
||||
"title": "Catalog parameters",
|
||||
"properties": {
|
||||
"catalog_limit": {"type": "integer", "description": "Catalog limit."},
|
||||
"message": {"type": "string", "description": "Catalog message.", "encrypted": false}
|
||||
},
|
||||
"required": ["catalog_limit"],
|
||||
"additionalProperties": false,
|
||||
"$defs": {"unused": {"type": "string"}}
|
||||
}"#;
|
||||
|
||||
// The existing JsonSchema subset retains supported fields and drops unknown keywords.
|
||||
const EXPECTED_CATALOG_PARAMETERS: &str = r#"{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"catalog_limit": {"type": "integer", "description": "Catalog limit."},
|
||||
"message": {"type": "string", "description": "Catalog message.", "encrypted": false}
|
||||
},
|
||||
"required": ["catalog_limit"],
|
||||
"additionalProperties": false,
|
||||
"$defs": {"unused": {"type": "string"}}
|
||||
}"#;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Exposure {
|
||||
Namespaced,
|
||||
@@ -47,24 +71,43 @@ fn all_tool_messages(message: Value) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
#[test_case(json!(null), Exposure::Namespaced; "missing_tools")]
|
||||
#[test_case(json!({}), Exposure::Namespaced; "missing_multi_agent")]
|
||||
#[test_case(json!({"multi_agent": null}), Exposure::Namespaced; "null_multi_agent")]
|
||||
#[test_case(json!({"multi_agent": {}}), Exposure::Namespaced; "missing_tools_in_family")]
|
||||
#[test_case(all_tool_messages(json!(null)), Exposure::Namespaced; "null_tool")]
|
||||
#[test_case(all_tool_messages(json!({})), Exposure::Namespaced; "missing_description")]
|
||||
#[test_case(all_tool_messages(json!({"description": null})), Exposure::Namespaced; "null_description")]
|
||||
#[test_case(all_tool_messages(json!({"description": " Catalog TOOL_NAME description.\n{{literal_placeholder}} "})), Exposure::Namespaced; "catalog_description")]
|
||||
#[test_case(all_tool_messages(json!({"description": ""})), Exposure::Namespaced; "empty_description")]
|
||||
#[test_case(json!({"multi_agent": {"send_message": {"description": "Catalog send."}}}), Exposure::Namespaced; "sparse_sibling_fallback")]
|
||||
#[test_case(all_tool_messages(json!({"description": "Catalog TOOL_NAME description."})), Exposure::Plain; "plain_tools")]
|
||||
#[test_case(all_tool_messages(json!({"description": "Catalog TOOL_NAME description."})), Exposure::CodeMode; "code_mode_declarations")]
|
||||
#[test_case(all_tool_messages(json!({"description": ""})), Exposure::CodeMode; "empty_code_mode_descriptions")]
|
||||
#[test_case(all_tool_messages(json!({"description": "Catalog TOOL_NAME description."})), Exposure::V1; "v1_unchanged")]
|
||||
#[test_case(json!(null), Exposure::Namespaced, None; "missing_tools")]
|
||||
#[test_case(json!({}), Exposure::Namespaced, None; "missing_multi_agent")]
|
||||
#[test_case(json!({"multi_agent": null}), Exposure::Namespaced, None; "null_multi_agent")]
|
||||
#[test_case(json!({"multi_agent": {}}), Exposure::Namespaced, None; "missing_tools_in_family")]
|
||||
#[test_case(all_tool_messages(json!(null)), Exposure::Namespaced, None; "null_tool")]
|
||||
#[test_case(all_tool_messages(json!({})), Exposure::Namespaced, None; "missing_description")]
|
||||
#[test_case(all_tool_messages(json!({"description": null})), Exposure::Namespaced, None; "null_description")]
|
||||
#[test_case(all_tool_messages(json!({"description": " Catalog TOOL_NAME description.\n{{literal_placeholder}} "})), Exposure::Namespaced, None; "catalog_description")]
|
||||
#[test_case(all_tool_messages(json!({"description": ""})), Exposure::Namespaced, None; "empty_description")]
|
||||
#[test_case(json!({"multi_agent": {"send_message": {"description": "Catalog send."}}}), Exposure::Namespaced, None; "sparse_sibling_fallback")]
|
||||
#[test_case(all_tool_messages(json!({"description": "Catalog TOOL_NAME description."})), Exposure::Plain, None; "plain_tools")]
|
||||
#[test_case(all_tool_messages(json!({"description": "Catalog TOOL_NAME description."})), Exposure::CodeMode, None; "code_mode_declarations")]
|
||||
#[test_case(all_tool_messages(json!({"description": ""})), Exposure::CodeMode, None; "empty_code_mode_descriptions")]
|
||||
#[test_case(all_tool_messages(json!({"description": "Catalog TOOL_NAME description."})), Exposure::V1, None; "v1_unchanged")]
|
||||
#[test_case(all_tool_messages(json!({"parameters": CATALOG_PARAMETERS})), Exposure::Namespaced, Some(EXPECTED_CATALOG_PARAMETERS); "catalog_parameters")]
|
||||
#[test_case(all_tool_messages(json!({"parameters": CATALOG_PARAMETERS})), Exposure::Plain, Some(EXPECTED_CATALOG_PARAMETERS); "plain_parameters")]
|
||||
#[test_case(all_tool_messages(json!({"parameters": CATALOG_PARAMETERS})), Exposure::CodeMode, Some(EXPECTED_CATALOG_PARAMETERS); "code_mode_parameters")]
|
||||
#[test_case(all_tool_messages(json!({"parameters": CATALOG_PARAMETERS})), Exposure::V1, None; "v1_parameters_unchanged")]
|
||||
#[test_case(json!({"multi_agent": {
|
||||
"spawn_agent": {"parameters": null},
|
||||
"send_message": {"parameters": "{"},
|
||||
"followup_task": {"parameters": r#"{"type":"object","properties":{"message":true}}"#},
|
||||
"wait_agent": {"parameters": r#"{"type":["object"]}"#},
|
||||
"interrupt_agent": {"parameters": r#"[null,"object",null,null,null,null,null,{"message":{"type":"string"}},null,null,null,null,null,null,null]"#},
|
||||
"list_agents": {"parameters": "{}"}
|
||||
}}), Exposure::Namespaced, None; "missing_or_invalid_parameters_fall_back")]
|
||||
#[test_case(json!({"multi_agent": {
|
||||
"spawn_agent": {"parameters": r#"{"type":"object"}"#},
|
||||
"send_message": {"parameters": r#"{"type":"object"}"#},
|
||||
"followup_task": {"parameters": r#"{"type":"object"}"#}
|
||||
}}), Exposure::Namespaced, None; "missing_encrypted_parameters_fall_back")]
|
||||
#[test_case(json!({"multi_agent": {"send_message": {"parameters": CATALOG_PARAMETERS}}}), Exposure::Namespaced, Some(EXPECTED_CATALOG_PARAMETERS); "sparse_parameters")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_agent_catalog_descriptions_preserve_outbound_schema(
|
||||
async fn multi_agent_catalog_messages_change_only_selected_tool_fields(
|
||||
tool_messages: Value,
|
||||
exposure: Exposure,
|
||||
expected_parameters: Option<&str>,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -201,6 +244,23 @@ async fn multi_agent_catalog_descriptions_preserve_outbound_schema(
|
||||
};
|
||||
expected_tool["description"] = json!(replacement);
|
||||
}
|
||||
if let Some(parameters) = expected_parameters
|
||||
&& tool_messages["multi_agent"][name]["parameters"].is_string()
|
||||
{
|
||||
expected_tool["parameters"] = serde_json::from_str(parameters)?;
|
||||
if matches!(name, "spawn_agent" | "send_message" | "followup_task") {
|
||||
expected_tool["parameters"]["properties"]["message"]["encrypted"] = json!(true);
|
||||
}
|
||||
if matches!(exposure, Exposure::CodeMode) {
|
||||
let description = expected_tool["description"].as_str().expect("description");
|
||||
let (prefix, signature) =
|
||||
description.rsplit_once("(args: ").expect("arguments");
|
||||
let (_, result) = signature.split_once("): Promise").expect("result type");
|
||||
expected_tool["description"] = json!(format!(
|
||||
"{prefix}(args: {{\n // Catalog limit.\n catalog_limit: number;\n // Catalog message.\n message?: string;\n}}): Promise{result}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
@@ -75,6 +75,7 @@ async fn persistent_async_message_guidance_follows_tool_availability(
|
||||
messages.tools = Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some("Catalog async message description.".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -248,6 +249,7 @@ async fn freeform_async_message_emits_an_item_without_ending_the_turn(
|
||||
.tools = Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some("Questions only.".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -344,8 +346,8 @@ async fn freeform_async_message_emits_an_item_without_ending_the_turn(
|
||||
#[test_case(None, "request_user_input_async"; "current_catalog_name")]
|
||||
#[test_case(Some(ToolMessages::default()), "send_user_message_async"; "missing_tool")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage::default()), ..Default::default() }), "send_user_message_async"; "missing_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog async message description.".to_string()) }), ..Default::default() }), "send_user_message_async"; "catalog_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()) }), ..Default::default() }), "send_user_message_async"; "empty_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog async message description.".to_string()), ..Default::default() }), ..Default::default() }), "send_user_message_async"; "catalog_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()), ..Default::default() }), ..Default::default() }), "send_user_message_async"; "empty_description")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn request_user_input_async_emits_item_and_does_not_end_the_turn(
|
||||
tool_messages: Option<ToolMessages>,
|
||||
|
||||
@@ -528,6 +528,69 @@ async fn astra_omits_disabled_executor_skills_from_model_context() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_agent_catalog_parameters() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| {
|
||||
configure_scenario_catalog(config);
|
||||
config.workspace_roots = vec![config.cwd.clone()];
|
||||
})
|
||||
.with_model_info_override("gpt-6-astra", |model| {
|
||||
model.model_messages.as_mut().expect("model messages").tools = Some(
|
||||
serde_json::from_value(json!({"multi_agent": {"list_agents": {
|
||||
"parameters": json!({
|
||||
"type": "object",
|
||||
"properties": {"path_prefix": {
|
||||
"type": "string",
|
||||
"description": "Inspect agents within this task path.",
|
||||
"minLength": 1,
|
||||
"maxLength": 128,
|
||||
}},
|
||||
"required": ["path_prefix"],
|
||||
"additionalProperties": false,
|
||||
}).to_string(),
|
||||
}}}))
|
||||
.expect("catalog tool messages"),
|
||||
);
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("agents-response"),
|
||||
ev_function_call_with_namespace(
|
||||
"agents-call",
|
||||
"collaboration",
|
||||
"list_agents",
|
||||
r#"{"path_prefix":"/root"}"#,
|
||||
),
|
||||
ev_completed("agents-response"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("final", "Only the root agent is working on this task."),
|
||||
ev_completed("final-response"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
test.submit_turn("Check which agents are working under /root before delegating more work.")
|
||||
.await?;
|
||||
insta::assert_snapshot!(
|
||||
"multi_agent_catalog_parameters",
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"Astra calls list_agents using the selected catalog parameter schema.",
|
||||
&mock.requests(),
|
||||
&ContextSnapshotOptions::default().include_request_settings(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, ignore = "the fixture uses a Unix shell command")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn astra_settings_release_check_with_direct_and_code_mode_tools() -> Result<()> {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Astra calls list_agents using the selected catalog parameter schema.\",\n&mock.requests(),\n&ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Astra calls list_agents using the selected catalog parameter schema.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "gpt-6-astra"
|
||||
parallel_tool_calls: false
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"context":"all_turns","effort":"low"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (3; hash=9d21738c014fb8eb):
|
||||
- namespace/functions; hash=e7db0a45c4ec3157
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/request_user_input
|
||||
- function/request_user_input_async
|
||||
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
|
||||
- function/sleep
|
||||
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2943c667577fdb8b
|
||||
- function/followup_task
|
||||
- function/interrupt_agent
|
||||
- function/list_agents
|
||||
- function/send_message
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
01:message/developer:
|
||||
You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803]
|
||||
|
||||
# When to ask the user for permission
|
||||
|
||||
Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388]
|
||||
|
||||
User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c]
|
||||
|
||||
<OMITTED 155 LINES; ~4895 TOKENS; hash=fd5ef18a80f62502>
|
||||
|
||||
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list.
|
||||
- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from.
|
||||
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
|
||||
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
|
||||
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
|
||||
02:message/developer[2]:
|
||||
[01] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b]
|
||||
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
|
||||
</permissions instructions>
|
||||
[02] <collaboration_mode># Collaboration Mode: Default
|
||||
|
||||
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
|
||||
|
||||
Your active mode changes only when new developer instructions with a different `<collaboration_mode>...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812]
|
||||
|
||||
## request_user_input availability
|
||||
|
||||
Use the `request_user_input` tool only when it is listed in the available tools for this turn.
|
||||
|
||||
In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions.
|
||||
|
||||
Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work.
|
||||
|
||||
If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked.
|
||||
|
||||
Never use the `request_user_input` tool for permission requests or permission-related escalations.
|
||||
|
||||
If explicit user input is required for another reason before progress can safely continue, do not use th... as a textual assistant message.</collaboration_mode> [hash=8213052bde4a62b2]
|
||||
03:message/developer:
|
||||
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
|
||||
|
||||
At the start of your turn, you are the active agent.
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
||||
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202]
|
||||
`send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers.
|
||||
<OMITTED 17 LINES; ~246 TOKENS; hash=780031e2df907944>
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
|
||||
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
|
||||
|
||||
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
|
||||
|
||||
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
|
||||
04:message/developer:
|
||||
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
|
||||
05:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="disabled"><...e="unrestricted" /></permission_profile></filesystem> [hash=d40b2f45cb028888]
|
||||
</environment_context>
|
||||
06:message/user:
|
||||
Check which agents are working under /root before delegating more work.
|
||||
-- request 2 (turn) --
|
||||
07:function_call/collaboration.list_agents:{"path_prefix":"/root"}
|
||||
08:function_call_output:{"agents":[{"agent_name":"/root","agent_status":"running"}]}
|
||||
@@ -2226,7 +2226,7 @@ async fn model_activation_uses_destination_metadata_defaults(
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
async fn tool_messages_follow_mid_turn_model_changes() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
const MULTI_AGENT_TOOLS: [&str; 6] = [
|
||||
@@ -2238,6 +2238,17 @@ async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
"list_agents",
|
||||
];
|
||||
|
||||
let parameters = |model: &str| {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {"type": "string", "description": format!("Agent on {model}.")},
|
||||
"message": {"type": "string", "encrypted": true},
|
||||
},
|
||||
"required": ["target"],
|
||||
"additionalProperties": false,
|
||||
})
|
||||
};
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = mount_sse_sequence(
|
||||
&server,
|
||||
@@ -2248,7 +2259,7 @@ async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
)
|
||||
.await;
|
||||
let test = step_settings_test()
|
||||
.with_config(|config| {
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
@@ -2263,9 +2274,10 @@ async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
model
|
||||
.experimental_supported_tools
|
||||
.push("send_user_message_async".to_string());
|
||||
let description = |name| {
|
||||
let tool_message = |name| {
|
||||
Some(ToolMessage {
|
||||
description: Some(format!("{name} description for {}.", model.slug)),
|
||||
parameters: Some(parameters(&model.slug).to_string()),
|
||||
})
|
||||
};
|
||||
model
|
||||
@@ -2273,14 +2285,17 @@ async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
.as_mut()
|
||||
.expect("model instruction metadata")
|
||||
.tools = Some(ToolMessages {
|
||||
send_user_message_async: description("Async message"),
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some(format!("Async message description for {}.", model.slug)),
|
||||
..Default::default()
|
||||
}),
|
||||
multi_agent: Some(MultiAgentToolMessages {
|
||||
spawn_agent: description("spawn_agent"),
|
||||
send_message: description("send_message"),
|
||||
followup_task: description("followup_task"),
|
||||
wait_agent: description("wait_agent"),
|
||||
interrupt_agent: description("interrupt_agent"),
|
||||
list_agents: description("list_agents"),
|
||||
spawn_agent: tool_message("spawn_agent"),
|
||||
send_message: tool_message("send_message"),
|
||||
followup_task: tool_message("followup_task"),
|
||||
wait_agent: tool_message("wait_agent"),
|
||||
interrupt_agent: tool_message("interrupt_agent"),
|
||||
list_agents: tool_message("list_agents"),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -2317,14 +2332,17 @@ async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "request_user_input_async")
|
||||
.expect("async message tool");
|
||||
let descriptions = MULTI_AGENT_TOOLS.map(|name| {
|
||||
let multi_agent_messages = MULTI_AGENT_TOOLS.map(|name| {
|
||||
let tool = namespace_child_tool(&body, "collaboration", name).expect(name);
|
||||
(name.to_string(), json!(tool["description"].as_str().expect("tool description").trim()))
|
||||
(name.to_string(), json!({
|
||||
"description": tool["description"].as_str().expect("tool description").trim(),
|
||||
"parameters": tool["parameters"],
|
||||
}))
|
||||
}).into_iter().collect::<serde_json::Map<String, Value>>();
|
||||
json!({
|
||||
"model": body["model"],
|
||||
"async_description": tool["description"],
|
||||
"multi_agent_descriptions": descriptions,
|
||||
"multi_agent_messages": multi_agent_messages,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2332,8 +2350,11 @@ async fn tool_descriptions_follow_mid_turn_model_changes() -> Result<()> {
|
||||
.map(|model| json!({
|
||||
"model": model,
|
||||
"async_description": format!("Async message description for {model}."),
|
||||
"multi_agent_descriptions": MULTI_AGENT_TOOLS
|
||||
.map(|name| (name.to_string(), json!(format!("{name} description for {model}."))))
|
||||
"multi_agent_messages": MULTI_AGENT_TOOLS
|
||||
.map(|name| (name.to_string(), json!({
|
||||
"description": format!("{name} description for {model}."),
|
||||
"parameters": parameters(model),
|
||||
})))
|
||||
.into_iter()
|
||||
.collect::<serde_json::Map<String, Value>>(),
|
||||
}))
|
||||
|
||||
@@ -86,10 +86,12 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
|
||||
tools: Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some(async_message_description.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
multi_agent: Some(MultiAgentToolMessages {
|
||||
spawn_agent: Some(ToolMessage {
|
||||
description: Some("Catalog spawn description.".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -157,10 +159,12 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
|
||||
tools: Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some(String::new()),
|
||||
..Default::default()
|
||||
}),
|
||||
multi_agent: Some(MultiAgentToolMessages {
|
||||
spawn_agent: Some(ToolMessage {
|
||||
description: Some(String::new()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use codex_protocol::openai_models::ConfirmationPolicies;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelMessages;
|
||||
use codex_protocol::openai_models::ToolMessage;
|
||||
use permissions::ResolvedApprovalMessages;
|
||||
use permissions::ResolvedPermissionMessages;
|
||||
|
||||
@@ -154,6 +155,15 @@ impl<'a> ResolvedModelMessages<'a> {
|
||||
/// Selects a V2 tool's static description by its name, independently of its runtime namespace.
|
||||
/// Missing text retains the tool's bundled description; an empty string replaces it.
|
||||
pub fn multi_agent_tool_description_override(&self, tool_name: &str) -> Option<&'a str> {
|
||||
self.multi_agent_tool(tool_name)?.description.as_deref()
|
||||
}
|
||||
|
||||
/// Selects a V2 tool's complete parameter schema; parsing belongs to the tool consumer.
|
||||
pub fn multi_agent_tool_parameters_override(&self, tool_name: &str) -> Option<&'a str> {
|
||||
self.multi_agent_tool(tool_name)?.parameters.as_deref()
|
||||
}
|
||||
|
||||
fn multi_agent_tool(self, tool_name: &str) -> Option<&'a ToolMessage> {
|
||||
let tools = self
|
||||
.catalog_messages
|
||||
.and_then(|messages| messages.tools.as_ref())
|
||||
@@ -167,7 +177,7 @@ impl<'a> ResolvedModelMessages<'a> {
|
||||
"list_agents" => &tools.list_agents,
|
||||
_ => return None,
|
||||
};
|
||||
tool.as_ref()?.description.as_deref()
|
||||
tool.as_ref()
|
||||
}
|
||||
|
||||
/// Resolves persistent-mode instructions without deciding whether the mode is active.
|
||||
|
||||
@@ -587,9 +587,17 @@ pub struct ToolMessage {
|
||||
/// text without disabling the tool. Tool-owned runtime guidance is retained.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Complete JSON Schema encoded as a string. Consumed by Multi-Agent V2 tools only.
|
||||
/// Uses the harness's supported schema subset; unrecognized keywords are ignored.
|
||||
/// Missing, null, invalid or unsupported structures, or a root without `type: "object"`
|
||||
/// retains the harness parameters. Schema semantics must remain API-compatible.
|
||||
/// Overrides must declare harness-encrypted properties so their annotations can be retained.
|
||||
/// Argument handling is unchanged.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parameters: Option<String>,
|
||||
}
|
||||
|
||||
/// Model-owned descriptions for Multi-Agent V2 tools, independent of their runtime namespace.
|
||||
/// Model-owned descriptions and parameters for Multi-Agent V2 tools, independent of their namespace.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct MultiAgentToolMessages {
|
||||
/// Replaces the static description. Missing or null uses the bundled text; an empty string
|
||||
@@ -1023,12 +1031,14 @@ mod tests {
|
||||
serde_json::json!({"tools": {"send_user_message_async": {"description": ""}}}),
|
||||
Some(Some(ToolMessage {
|
||||
description: Some(String::new()),
|
||||
..Default::default()
|
||||
})),
|
||||
),
|
||||
(
|
||||
serde_json::json!({"tools": {"send_user_message_async": {"description": "Catalog description"}}}),
|
||||
Some(Some(ToolMessage {
|
||||
description: Some("Catalog description".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
),
|
||||
] {
|
||||
@@ -1392,10 +1402,12 @@ mod tests {
|
||||
tools: Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some("Catalog description".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
multi_agent: Some(MultiAgentToolMessages {
|
||||
spawn_agent: Some(ToolMessage {
|
||||
description: Some("Catalog spawn description".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -1470,6 +1482,7 @@ mod tests {
|
||||
tools: Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
description: Some(String::new()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user