mirror of
https://github.com/openai/codex.git
synced 2026-10-16 16:58:46 +00:00
Add an opt-in model catalog to multi-agent context (#49560)
## Why Refreshing available model descriptions changes the `spawn_agent` tool schema. Keep model choices current while preserving tool definitions and earlier conversation input. ## What changed - Add the disabled-by-default `model_catalog_in_context` feature for multi-agent V1 and V2. When enabled and model overrides are available, move model listings from tool descriptions into developer `<model_catalog>` messages. - Include picker-visible, compatible models with descriptions, reasoning efforts, and service tiers in a listing bounded to 1,000 bytes. Point `spawn_agent` guidance to the latest catalog. - Append updated listings only when their rendered content changes, restore missing listings, and invalidate retained catalogs when they no longer apply. ## Testing Extend integration coverage for V1 and V2 with the feature enabled and disabled. Verify that catalog refreshes preserve tool definitions and prior input when enabled, append the updated listing once, and retain the existing tool-description behavior when disabled. Add a request-history snapshot for the V1 opt-in path. GitOrigin-RevId: 67b11b4ad6c35f00d0e29edf6343bb0b4804d797
This commit is contained in:
@@ -975,6 +975,9 @@
|
||||
"mentions_v2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model_catalog_in_context": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -7015,6 +7018,9 @@
|
||||
"mentions_v2": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model_catalog_in_context": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ mod environment;
|
||||
mod environments_instructions;
|
||||
mod managed_developer_instructions;
|
||||
mod model;
|
||||
mod model_catalog;
|
||||
mod multi_agent_mode;
|
||||
mod multi_agent_usage_hint;
|
||||
mod permissions;
|
||||
@@ -46,6 +47,7 @@ pub(crate) use managed_developer_instructions::ManagedDeveloperInstructions;
|
||||
pub(crate) use managed_developer_instructions::ManagedDeveloperInstructionsState;
|
||||
pub(crate) use managed_developer_instructions::validate_managed_developer_instructions;
|
||||
pub(crate) use model::ModelInstructionsState;
|
||||
pub(crate) use model_catalog::ModelCatalogState;
|
||||
pub(crate) use multi_agent_mode::MultiAgentModeState;
|
||||
pub(crate) use multi_agent_usage_hint::MultiAgentUsageHintState;
|
||||
pub(crate) use permissions::PermissionsState;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Keeps spawn model choices in append-only context instead of the tool schema.
|
||||
//! Snapshots compare the bounded text actually shown to the model; catalog refreshes
|
||||
//! never rewrite earlier messages, and missing retained context gets a fresh catalog.
|
||||
|
||||
use super::PreviousSectionState;
|
||||
use super::WorldStateHash;
|
||||
use super::WorldStateSection;
|
||||
use crate::agent::child_config::MAX_SPAWN_AGENT_MODEL_OVERRIDES;
|
||||
use crate::agent::child_config::model_supports_multi_agent_backend;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::environment_context::push_xml_escaped_text;
|
||||
use codex_protocol::models::ContentItemKind;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
|
||||
// Includes XML escaping and markers; at most 1,000 tokens even with byte fallback.
|
||||
const MAX_RENDERED_BYTES: usize = 1_000;
|
||||
const MAX_DESCRIPTION_CHARS: usize = 250;
|
||||
const OMITTED_NOTICE: &str = "Additional model choices omitted.\n";
|
||||
|
||||
/// The current model choices, or an empty catalog to invalidate a retained listing.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct ModelCatalogState {
|
||||
catalog: String,
|
||||
}
|
||||
|
||||
impl ModelCatalogState {
|
||||
pub(crate) fn new(models: &[ModelPreset], multi_agent_version: MultiAgentVersion) -> Self {
|
||||
let mut catalog = String::new();
|
||||
let mut models = models
|
||||
.iter()
|
||||
.filter(|model| model.show_in_picker)
|
||||
.filter(|model| model_supports_multi_agent_backend(model, multi_agent_version))
|
||||
.take(MAX_SPAWN_AGENT_MODEL_OVERRIDES)
|
||||
.collect::<Vec<_>>();
|
||||
// Stabilize the selected choices independently of their picker ordering.
|
||||
models.sort_by(|left, right| left.model.cmp(&right.model));
|
||||
if models.is_empty() {
|
||||
catalog.push_str("No picker-visible model overrides are currently loaded.\n");
|
||||
}
|
||||
let (open, close) = Self::type_markers();
|
||||
let body_budget = MAX_RENDERED_BYTES - open.len() - close.len() - 2;
|
||||
for model in models {
|
||||
let mut line = format!("- `{}`: ", model.model);
|
||||
line.extend(model.description.chars().take(MAX_DESCRIPTION_CHARS));
|
||||
if model.description.chars().count() > MAX_DESCRIPTION_CHARS {
|
||||
line.push('…');
|
||||
}
|
||||
if !model.supported_reasoning_efforts.is_empty() {
|
||||
line.push_str(" Reasoning efforts: ");
|
||||
for (index, preset) in model.supported_reasoning_efforts.iter().enumerate() {
|
||||
if index > 0 {
|
||||
line.push_str(", ");
|
||||
}
|
||||
line.push_str(preset.effort.as_str());
|
||||
if preset.effort == model.default_reasoning_effort {
|
||||
line.push_str(" (default)");
|
||||
}
|
||||
}
|
||||
line.push('.');
|
||||
}
|
||||
if !model.service_tiers.is_empty() {
|
||||
line.push_str(" Service tiers: ");
|
||||
for (index, tier) in model.service_tiers.iter().enumerate() {
|
||||
if index > 0 {
|
||||
line.push_str(", ");
|
||||
}
|
||||
line.push_str(tier.id.as_str());
|
||||
}
|
||||
line.push('.');
|
||||
}
|
||||
let mut escaped = String::new();
|
||||
push_xml_escaped_text(&mut escaped, &line);
|
||||
if catalog.len() + escaped.len() + 1 + OMITTED_NOTICE.len() > body_budget {
|
||||
// Keep identifiers and capability lists intact instead of advertising
|
||||
// a truncated model ID, reasoning effort, or service tier.
|
||||
catalog.push_str(OMITTED_NOTICE);
|
||||
break;
|
||||
}
|
||||
catalog.push_str(&escaped);
|
||||
catalog.push('\n');
|
||||
}
|
||||
Self { catalog }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for ModelCatalogState {
|
||||
fn content_kind(&self) -> ContentItemKind {
|
||||
ContentItemKind("multi_agent.model_catalog".to_string())
|
||||
}
|
||||
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
("<model_catalog>", "</model_catalog>")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
if self.catalog.is_empty() {
|
||||
"\nThe previous spawn_agent model catalog no longer applies.\n".to_string()
|
||||
} else {
|
||||
format!("\n{}", self.catalog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldStateSection for ModelCatalogState {
|
||||
const ID: &'static str = "model_catalog";
|
||||
type Snapshot = WorldStateHash;
|
||||
|
||||
fn snapshot(&self) -> Self::Snapshot {
|
||||
WorldStateHash::from_fragment(self)
|
||||
}
|
||||
|
||||
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
|
||||
role == "developer" && Self::matches_text(text)
|
||||
}
|
||||
|
||||
fn has_retained_fragment_matcher() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn matches_retained_fragment(role: &str, text: &str) -> bool {
|
||||
Self::matches_legacy_fragment(role, text)
|
||||
}
|
||||
|
||||
fn render_diff(
|
||||
&self,
|
||||
previous: PreviousSectionState<'_, Self::Snapshot>,
|
||||
) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
if matches!(previous, PreviousSectionState::Known(previous) if previous == &self.snapshot())
|
||||
|| self.catalog.is_empty() && matches!(previous, PreviousSectionState::Absent)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(self.clone()))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use crate::context::world_state::ContextWindowGuidanceState;
|
||||
use crate::context::world_state::EnvironmentsInstructionsState;
|
||||
use crate::context::world_state::EnvironmentsState;
|
||||
use crate::context::world_state::ManagedDeveloperInstructionsState;
|
||||
use crate::context::world_state::ModelCatalogState;
|
||||
use crate::context::world_state::ModelInstructionsState;
|
||||
use crate::context::world_state::MultiAgentModeState;
|
||||
use crate::context::world_state::MultiAgentUsageHintState;
|
||||
@@ -21,6 +22,7 @@ use crate::context::world_state::PluginsInstructionsState;
|
||||
use crate::context::world_state::RealtimeState;
|
||||
use crate::context::world_state::ToolsState;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
|
||||
use codex_connectors::AppToolPolicyEvaluator;
|
||||
use codex_extension_api::WorldStateContributionInput;
|
||||
use codex_features::Feature;
|
||||
@@ -31,6 +33,7 @@ use codex_prompts::render_model_instructions;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::BaseInstructionsProvenance;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_tools::ToolName;
|
||||
|
||||
const MAX_ENVIRONMENT_SUBAGENTS: usize = 8;
|
||||
const MAX_ENVIRONMENT_SUBAGENT_BYTES: usize = 1_024;
|
||||
@@ -324,6 +327,45 @@ impl Session {
|
||||
world_state.add_section(usage_hint);
|
||||
}
|
||||
world_state.add_section(multi_agent_mode);
|
||||
let spawn_tool = match turn_context.multi_agent_version {
|
||||
MultiAgentVersion::Disabled => None,
|
||||
MultiAgentVersion::V1 => Some(ToolName::new(
|
||||
Some(MULTI_AGENT_V1_NAMESPACE.to_string()),
|
||||
"spawn_agent",
|
||||
)),
|
||||
MultiAgentVersion::V2 => turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.expose_spawn_agent_model_overrides
|
||||
.then(|| {
|
||||
ToolName::new(
|
||||
turn_context
|
||||
.provider
|
||||
.capabilities()
|
||||
.namespace_tools
|
||||
.then(|| turn_context.config.multi_agent_v2.tool_namespace.clone())
|
||||
.flatten(),
|
||||
"spawn_agent",
|
||||
)
|
||||
}),
|
||||
};
|
||||
let model_overrides_available =
|
||||
spawn_tool.is_some_and(|name| step_context.tool_router.exposes_tool(&name));
|
||||
world_state.add_section(
|
||||
if model_overrides_available
|
||||
&& turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::ModelCatalogInContext)
|
||||
{
|
||||
ModelCatalogState::new(
|
||||
&turn_context.available_models,
|
||||
turn_context.multi_agent_version,
|
||||
)
|
||||
} else {
|
||||
ModelCatalogState::default()
|
||||
},
|
||||
);
|
||||
if !crate::guardian::is_basic_session_source(&turn_context.session_source) {
|
||||
world_state.add_section(ManagedDeveloperInstructionsState::new(
|
||||
turn_context
|
||||
|
||||
@@ -15,6 +15,8 @@ pub const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1";
|
||||
const MULTI_AGENT_V1_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
|
||||
|
||||
const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed.";
|
||||
const SPAWN_AGENT_MODEL_CATALOG_GUIDANCE: &str =
|
||||
"Pick model overrides from the latest <model_catalog> listing.";
|
||||
const SPAWN_AGENT_TYPE_OVERRIDE_DESCRIPTION_V1: &str = "Agent type override for the new agent. Omit to inherit the parent agent type with a full-history fork; otherwise, `default` is used.";
|
||||
const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str =
|
||||
"Model override for the new agent. Omit unless an explicit override is needed.";
|
||||
@@ -28,6 +30,7 @@ pub struct SpawnAgentToolOptions {
|
||||
pub hide_agent_type_model_reasoning: bool,
|
||||
pub expose_spawn_agent_model_overrides: bool,
|
||||
pub multi_agent_version: MultiAgentVersion,
|
||||
pub model_catalog_in_context: bool,
|
||||
pub usage_hint_text: Option<String>,
|
||||
}
|
||||
|
||||
@@ -40,6 +43,7 @@ impl Default for SpawnAgentToolOptions {
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: false,
|
||||
multi_agent_version: MultiAgentVersion::Disabled,
|
||||
model_catalog_in_context: false,
|
||||
usage_hint_text: None,
|
||||
}
|
||||
}
|
||||
@@ -63,9 +67,11 @@ impl Default for WaitAgentTimeoutOptions {
|
||||
}
|
||||
|
||||
pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
let available_models_description = (!options.hide_agent_type_model_reasoning).then(|| {
|
||||
spawn_agent_models_description(&options.available_models, options.multi_agent_version)
|
||||
});
|
||||
let available_models_description = (!options.model_catalog_in_context
|
||||
&& !options.hide_agent_type_model_reasoning)
|
||||
.then(|| {
|
||||
spawn_agent_models_description(&options.available_models, options.multi_agent_version)
|
||||
});
|
||||
let inherited_model_guidance =
|
||||
(!options.hide_agent_type_model_reasoning).then_some(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE);
|
||||
let return_value_description =
|
||||
@@ -85,6 +91,7 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
|
||||
name: "spawn_agent".to_string(),
|
||||
description: spawn_agent_tool_description(
|
||||
available_models_description.as_deref(),
|
||||
options.model_catalog_in_context,
|
||||
inherited_model_guidance,
|
||||
return_value_description,
|
||||
options.usage_hint_text,
|
||||
@@ -101,9 +108,11 @@ pub fn create_spawn_agent_tool_v2(
|
||||
options: SpawnAgentToolOptions,
|
||||
description_override: Option<&str>,
|
||||
) -> ToolSpec {
|
||||
let available_models_description = options.expose_spawn_agent_model_overrides.then(|| {
|
||||
spawn_agent_models_description(&options.available_models, options.multi_agent_version)
|
||||
});
|
||||
let available_models_description = (!options.model_catalog_in_context
|
||||
&& options.expose_spawn_agent_model_overrides)
|
||||
.then(|| {
|
||||
spawn_agent_models_description(&options.available_models, options.multi_agent_version)
|
||||
});
|
||||
let inherited_model_guidance = (options.expose_spawn_agent_model_overrides
|
||||
&& !options.hide_agent_type_model_reasoning)
|
||||
.then_some(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE);
|
||||
@@ -127,6 +136,10 @@ pub fn create_spawn_agent_tool_v2(
|
||||
name: "spawn_agent".to_string(),
|
||||
description: spawn_agent_tool_description_v2(
|
||||
available_models_description.as_deref(),
|
||||
options.model_catalog_in_context,
|
||||
options
|
||||
.expose_spawn_agent_model_overrides
|
||||
.then_some(SPAWN_AGENT_MODEL_CATALOG_GUIDANCE),
|
||||
inherited_model_guidance,
|
||||
options.usage_hint_text,
|
||||
description_override,
|
||||
@@ -673,18 +686,30 @@ fn hide_spawn_agent_metadata_options(properties: &mut BTreeMap<String, JsonSchem
|
||||
|
||||
fn spawn_agent_tool_description(
|
||||
available_models_description: Option<&str>,
|
||||
model_catalog_in_context: bool,
|
||||
inherited_model_guidance: Option<&str>,
|
||||
return_value_description: &str,
|
||||
usage_hint_text: Option<String>,
|
||||
) -> String {
|
||||
let agent_role_guidance = available_models_description.unwrap_or_default();
|
||||
let model_catalog_guidance = inherited_model_guidance
|
||||
.map(|_| SPAWN_AGENT_MODEL_CATALOG_GUIDANCE)
|
||||
.unwrap_or_default();
|
||||
let inherited_model_guidance = inherited_model_guidance.unwrap_or_default();
|
||||
|
||||
let tool_description = format!(
|
||||
r#"
|
||||
{agent_role_guidance}
|
||||
let tool_description = if model_catalog_in_context {
|
||||
format!(
|
||||
r#"
|
||||
Spawn a sub-agent for a well-scoped task. {return_value_description} {inherited_model_guidance}
|
||||
{model_catalog_guidance}"#
|
||||
)
|
||||
} else {
|
||||
let available_models_description = available_models_description.unwrap_or_default();
|
||||
format!(
|
||||
r#"
|
||||
{available_models_description}
|
||||
Spawn a sub-agent for a well-scoped task. {return_value_description} {inherited_model_guidance}"#
|
||||
);
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(usage_hint_text) = usage_hint_text {
|
||||
return format!(
|
||||
@@ -693,19 +718,20 @@ fn spawn_agent_tool_description(
|
||||
{usage_hint_text}"#
|
||||
);
|
||||
}
|
||||
let agent_role_usage_hint = available_models_description
|
||||
.map(|_| {
|
||||
"Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself."
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let agent_role_usage_hint = if model_catalog_in_context {
|
||||
""
|
||||
} else if available_models_description.is_some() {
|
||||
"\nAgent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself."
|
||||
} else {
|
||||
"\n"
|
||||
};
|
||||
format!(
|
||||
r#"
|
||||
{tool_description}
|
||||
This spawn_agent tool provides you access to sub-agents that inherit your current model by default. Do not set the `model` field unless the user explicitly asks for a different model. You should follow the rules and guidelines below to use this tool.
|
||||
|
||||
Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.
|
||||
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.
|
||||
{agent_role_usage_hint}
|
||||
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.{agent_role_usage_hint}
|
||||
|
||||
### When to delegate vs. do the subtask yourself
|
||||
- First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it.
|
||||
@@ -740,28 +766,37 @@ Requests for depth, thoroughness, research, investigation, or detailed codebase
|
||||
|
||||
fn spawn_agent_tool_description_v2(
|
||||
available_models_description: Option<&str>,
|
||||
model_catalog_in_context: bool,
|
||||
model_catalog_guidance: Option<&str>,
|
||||
inherited_model_guidance: Option<&str>,
|
||||
usage_hint_text: Option<String>,
|
||||
description: Option<&str>,
|
||||
) -> String {
|
||||
let agent_role_guidance = available_models_description.unwrap_or_default();
|
||||
let model_catalog_guidance = model_catalog_guidance.unwrap_or_default();
|
||||
let inherited_model_guidance = inherited_model_guidance.unwrap_or_default();
|
||||
|
||||
let (catalog_prefix, catalog_suffix) = if model_catalog_in_context {
|
||||
(String::new(), format!("\n{model_catalog_guidance}"))
|
||||
} else {
|
||||
let available_models_description = available_models_description.unwrap_or_default();
|
||||
(
|
||||
format!(" {available_models_description}\n"),
|
||||
String::new(),
|
||||
)
|
||||
};
|
||||
let tool_description = if let Some(description) = description {
|
||||
format!(
|
||||
r#"
|
||||
{agent_role_guidance}
|
||||
{description}
|
||||
{inherited_model_guidance}"#
|
||||
{catalog_prefix} {description}
|
||||
{inherited_model_guidance}{catalog_suffix}"#
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"
|
||||
{agent_role_guidance}
|
||||
Spawns an agent to work on the specified task. If your current task is `/root/task1` and you spawn_agent with task_name "task_3" the agent will have canonical task name `/root/task1/task_3`.
|
||||
{catalog_prefix} Spawns an agent to work on the specified task. If your current task is `/root/task1` and you spawn_agent with task_name "task_3" the agent will have canonical task name `/root/task1/task_3`.
|
||||
You are then able to refer to this agent as `task_3` or `/root/task1/task_3` interchangeably. However an agent `/root/task2/task_3` would only be able to communicate with this agent via its canonical name `/root/task1/task_3`.
|
||||
The spawned agent will have the same tools as you and the ability to spawn its own subagents.
|
||||
{inherited_model_guidance}
|
||||
{inherited_model_guidance}{catalog_suffix}
|
||||
It will be able to send you and other running agents messages, and its final answer will be provided to you when it finishes.
|
||||
The new agent's canonical task name will be provided to it along with the message.
|
||||
|
||||
|
||||
@@ -1,64 +1,19 @@
|
||||
use super::*;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_tools::JsonSchemaPrimitiveType;
|
||||
use codex_tools::JsonSchemaType;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: id.to_string(),
|
||||
model: format!("{id}-model"),
|
||||
display_name: format!("{id} display"),
|
||||
description: format!("{id} description"),
|
||||
model_specialty: None,
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: "Balanced".to_string(),
|
||||
}],
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: vec![ModelServiceTier {
|
||||
id: "priority".to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "1.5x speed, increased usage".to_string(),
|
||||
}],
|
||||
default_service_tier: None,
|
||||
available_access_programs: None,
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker,
|
||||
multi_agent_version: Some(MultiAgentVersion::V2),
|
||||
availability_nux: None,
|
||||
supported_in_api: true,
|
||||
input_modalities: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
let mut legacy = model_preset("legacy", /*show_in_picker*/ true);
|
||||
legacy.multi_agent_version = Some(MultiAgentVersion::V1);
|
||||
let mut disabled = model_preset("disabled", /*show_in_picker*/ true);
|
||||
disabled.multi_agent_version = Some(MultiAgentVersion::Disabled);
|
||||
fn spawn_agent_tool_v2_requires_task_name() {
|
||||
let tool = create_spawn_agent_tool_v2(
|
||||
SpawnAgentToolOptions {
|
||||
available_models: vec![
|
||||
model_preset("visible", /*show_in_picker*/ true),
|
||||
model_preset("hidden", /*show_in_picker*/ false),
|
||||
legacy,
|
||||
disabled,
|
||||
],
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
usage_hint_text: None,
|
||||
..Default::default()
|
||||
},
|
||||
/*description_override*/ None,
|
||||
);
|
||||
@@ -84,18 +39,6 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
assert!(description.contains("The spawned agent will have the same tools as you"));
|
||||
assert!(!description.contains("max_concurrent_threads_per_session"));
|
||||
assert!(description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));
|
||||
assert!(
|
||||
description
|
||||
.contains("Available model overrides (optional; inherited parent model is preferred):")
|
||||
);
|
||||
assert!(description.contains(
|
||||
"- `visible-model`: visible description Reasoning efforts: medium (default). Service tiers: priority."
|
||||
));
|
||||
assert!(description.contains(
|
||||
"- `legacy-model`: legacy description Reasoning efforts: medium (default). Service tiers: priority."
|
||||
));
|
||||
assert!(!description.contains("hidden-model"));
|
||||
assert!(!description.contains("disabled-model"));
|
||||
assert!(properties.contains_key("task_name"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert_eq!(
|
||||
@@ -133,10 +76,8 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
#[test]
|
||||
fn spawn_agent_catalog_description_preserves_generated_context() {
|
||||
let options = SpawnAgentToolOptions {
|
||||
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
|
||||
agent_type_description: "Available agent roles: explorer".to_string(),
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
usage_hint_text: Some("Local usage hint.".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -155,7 +96,6 @@ fn spawn_agent_catalog_description_preserves_generated_context() {
|
||||
.description
|
||||
.contains("Catalog spawning guidance.")
|
||||
);
|
||||
assert!(configured_tool.description.contains("`visible-model`"));
|
||||
assert!(
|
||||
configured_tool
|
||||
.description
|
||||
@@ -174,13 +114,12 @@ fn spawn_agent_catalog_description_preserves_generated_context() {
|
||||
#[test]
|
||||
fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
let tool = create_spawn_agent_tool_v1(SpawnAgentToolOptions {
|
||||
available_models: Vec::new(),
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V1,
|
||||
usage_hint_text: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let ToolSpec::Namespace(namespace) = tool else {
|
||||
@@ -224,73 +163,16 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
assert!(!properties.contains_key("service_tier"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_caps_visible_model_summaries() {
|
||||
let tool = create_spawn_agent_tool_v2(
|
||||
SpawnAgentToolOptions {
|
||||
available_models: vec![
|
||||
model_preset("first", /*show_in_picker*/ true),
|
||||
model_preset("second", /*show_in_picker*/ true),
|
||||
model_preset("third", /*show_in_picker*/ true),
|
||||
model_preset("fourth", /*show_in_picker*/ true),
|
||||
model_preset("fifth", /*show_in_picker*/ true),
|
||||
model_preset("sixth", /*show_in_picker*/ true),
|
||||
],
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: true,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
usage_hint_text: None,
|
||||
},
|
||||
/*description_override*/ None,
|
||||
);
|
||||
|
||||
let ToolSpec::Function(ResponsesApiTool { description, .. }) = tool else {
|
||||
panic!("spawn_agent should be a function tool");
|
||||
};
|
||||
|
||||
for model in ["first", "second", "third", "fourth", "fifth"] {
|
||||
assert!(
|
||||
description.contains(&format!("`{model}-model`")),
|
||||
"expected {model} model summary in spawn_agent description: {description:?}"
|
||||
);
|
||||
}
|
||||
assert!(!description.contains("`sixth-model`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_caps_reasoning_effort_value_length() {
|
||||
let mut model = model_preset("visible", /*show_in_picker*/ true);
|
||||
let custom_effort = ReasoningEffort::Custom(
|
||||
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION + 1),
|
||||
);
|
||||
model.default_reasoning_effort = custom_effort.clone();
|
||||
model.supported_reasoning_efforts = vec![ReasoningEffortPreset {
|
||||
effort: custom_effort,
|
||||
description: "Model-defined".to_string(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
spawn_agent_models_description(&[model], MultiAgentVersion::V2),
|
||||
format!(
|
||||
"Available model overrides (optional; inherited parent model is preferred):\n- `visible-model`: visible description Reasoning efforts: {} (default). Service tiers: priority.",
|
||||
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_keeps_model_controls_when_spawn_metadata_is_hidden() {
|
||||
let tool = create_spawn_agent_tool_v2(
|
||||
SpawnAgentToolOptions {
|
||||
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: false,
|
||||
hide_agent_type_model_reasoning: true,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
usage_hint_text: None,
|
||||
..Default::default()
|
||||
},
|
||||
/*description_override*/ None,
|
||||
);
|
||||
@@ -313,20 +195,18 @@ fn spawn_agent_tool_keeps_model_controls_when_spawn_metadata_is_hidden() {
|
||||
assert!(properties.contains_key("reasoning_effort"));
|
||||
assert!(!properties.contains_key("service_tier"));
|
||||
assert!(!description.contains(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE));
|
||||
assert!(description.contains("Available model overrides"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_hides_model_controls_without_override_exposure() {
|
||||
let tool = create_spawn_agent_tool_v2(
|
||||
SpawnAgentToolOptions {
|
||||
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
|
||||
agent_type_description: "role help".to_string(),
|
||||
expose_agent_type: false,
|
||||
hide_agent_type_model_reasoning: true,
|
||||
expose_spawn_agent_model_overrides: false,
|
||||
multi_agent_version: MultiAgentVersion::V2,
|
||||
usage_hint_text: None,
|
||||
..Default::default()
|
||||
},
|
||||
Some(""),
|
||||
);
|
||||
|
||||
@@ -173,7 +173,7 @@ impl ToolRouter {
|
||||
}
|
||||
|
||||
// Answers if the tool plan lets the model invoke the tool directly, through code mode, or deferred tool search.
|
||||
pub(super) fn exposes_tool(&self, name: &ToolName) -> bool {
|
||||
pub(crate) fn exposes_tool(&self, name: &ToolName) -> bool {
|
||||
let name = name.clone().with_default_namespace();
|
||||
if self
|
||||
.code_mode_tool_names
|
||||
|
||||
@@ -1320,6 +1320,11 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
SpawnAgentHandlerV2::new(
|
||||
SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
multi_agent_version: turn_context.multi_agent_version,
|
||||
model_catalog_in_context: turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::ModelCatalogInContext),
|
||||
agent_type_description,
|
||||
expose_agent_type: !turn_context.config.agent_roles.is_empty(),
|
||||
hide_agent_type_model_reasoning: hide_spawn_agent_metadata,
|
||||
@@ -1327,7 +1332,6 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.expose_spawn_agent_model_overrides,
|
||||
multi_agent_version: turn_context.multi_agent_version,
|
||||
usage_hint_text: turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
@@ -1337,7 +1341,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
spawn_agent_description.map(str::to_owned),
|
||||
),
|
||||
tool_namespace,
|
||||
// Spawn composes the selected description with runtime model and usage guidance.
|
||||
// Spawn composes the selected description with inheritance and usage guidance.
|
||||
/*description_override*/
|
||||
None,
|
||||
model_messages.multi_agent_tool_parameters_override("spawn_agent"),
|
||||
@@ -1404,11 +1408,15 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
registry.add_with_exposure(
|
||||
SpawnAgentHandler::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
multi_agent_version: turn_context.multi_agent_version,
|
||||
model_catalog_in_context: turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::ModelCatalogInContext),
|
||||
agent_type_description,
|
||||
expose_agent_type: !turn_context.config.agent_roles.is_empty(),
|
||||
hide_agent_type_model_reasoning: false,
|
||||
expose_spawn_agent_model_overrides: true,
|
||||
multi_agent_version: turn_context.multi_agent_version,
|
||||
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
|
||||
}),
|
||||
exposure,
|
||||
|
||||
@@ -330,6 +330,7 @@ fn known_segment_name(text: &str, source: TextSource<'_>) -> Option<String> {
|
||||
| "collaboration_mode"
|
||||
| "multi_agent_role"
|
||||
| "multi_agent_mode"
|
||||
| "model_catalog"
|
||||
| "apps_instructions"
|
||||
| "skills_instructions"
|
||||
| "plugins_instructions"
|
||||
|
||||
@@ -44,6 +44,8 @@ use codex_protocol::openai_models::ToolMessage;
|
||||
use codex_protocol::openai_models::ToolMode;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::TurnSettingsUpdate;
|
||||
use codex_protocol::protocol::TurnSettingsUpdateOutcome;
|
||||
@@ -1671,3 +1673,19 @@ async fn app_tool_exposure_request_history() -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn model_catalog_refresh_preserves_tools_and_history() -> Result<()> {
|
||||
let requests = super::spawn_agent_description::model_catalog_refresh_requests(
|
||||
MultiAgentVersion::V1,
|
||||
/*model_catalog_in_context*/ true,
|
||||
)
|
||||
.await?;
|
||||
insta::assert_snapshot!(context_snapshot::format_request_history_snapshot(
|
||||
"Catalog refreshes append the updated listing once while preserving tools and prior input.",
|
||||
&requests,
|
||||
&ContextSnapshotOptions::default().include_request_settings(),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Catalog refreshes append the updated listing once while preserving tools and prior input.\",\n&requests, &ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Catalog refreshes append the updated listing once while preserving tools and prior input.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
instructions: Test model instructions.
|
||||
model: "visible-model"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium","summary":"auto"}
|
||||
store: false
|
||||
stream: true
|
||||
tool_choice: "auto"
|
||||
tools (6; hash=1ea0fa9abde186d4):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[path]; hash=1908de9c0be7118a
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=94c598b3c6ca141b
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- web_search; hash=8586237e354ee5ed
|
||||
-- request 1 (turn) --
|
||||
00: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] <model_catalog>
|
||||
- `visible-model`: Fast and capable Reasoning efforts: low, medium (default), high. Service tiers: priority.
|
||||
</model_catalog>
|
||||
01:message/user:
|
||||
hello
|
||||
-- request 2 (turn) --
|
||||
02:message/developer:
|
||||
<model_catalog>
|
||||
- `visible-model`: Updated picker copy Reasoning efforts: low, medium (default), high. Service tiers: priority.
|
||||
</model_catalog>
|
||||
03:message/user:
|
||||
after catalog refresh
|
||||
-- request 3 (turn) --
|
||||
04:message/user:
|
||||
without another catalog change
|
||||
@@ -20,6 +20,7 @@ use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::openai_models::TruncationPolicyConfig;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
@@ -30,6 +31,7 @@ use core_test_support::responses::namespace_child_tool;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
@@ -40,8 +42,8 @@ const MULTI_AGENT_V1_NAMESPACE: &str = "multi_agent_v1";
|
||||
const MULTI_AGENT_V2_NAMESPACE: &str = "collaboration";
|
||||
const SPAWN_AGENT_TOOL_NAME: &str = "spawn_agent";
|
||||
|
||||
fn spawn_agent_description(body: &Value) -> Option<String> {
|
||||
namespace_child_tool(body, MULTI_AGENT_V1_NAMESPACE, SPAWN_AGENT_TOOL_NAME)
|
||||
fn spawn_agent_description(body: &Value, namespace: &str) -> Option<String> {
|
||||
namespace_child_tool(body, namespace, SPAWN_AGENT_TOOL_NAME)
|
||||
.and_then(|tool| tool.get("description"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
@@ -148,8 +150,23 @@ async fn wait_for_model_available(manager: &SharedModelsManager, slug: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(MultiAgentVersion::V1, false; "v1_default")]
|
||||
#[test_case(MultiAgentVersion::V2, false; "v2_default")]
|
||||
#[test_case(MultiAgentVersion::V2, true; "v2_context")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> Result<()> {
|
||||
async fn model_catalog_flag_controls_refresh(
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
model_catalog_in_context: bool,
|
||||
) -> Result<()> {
|
||||
// The V1 opt-in case also snapshots the full request history in scenarios.rs.
|
||||
model_catalog_refresh_requests(multi_agent_version, model_catalog_in_context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn model_catalog_refresh_requests(
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
model_catalog_in_context: bool,
|
||||
) -> Result<Vec<ResponsesRequest>> {
|
||||
let server = start_mock_server().await;
|
||||
mount_models_once(
|
||||
&server,
|
||||
@@ -206,30 +223,64 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_model("visible-model")
|
||||
.with_config(|config| {
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Collab)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.set_enabled(
|
||||
Feature::MultiAgentV2,
|
||||
multi_agent_version == MultiAgentVersion::V2,
|
||||
)
|
||||
.expect("set multi-agent version");
|
||||
if model_catalog_in_context {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ModelCatalogInContext)
|
||||
.expect("enable context catalogs");
|
||||
}
|
||||
config.multi_agent_v2.hide_spawn_agent_metadata = false;
|
||||
config.base_instructions = Some("Test model instructions.".to_string());
|
||||
config.include_environment_context = false;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
wait_for_model_available(&test.thread_manager.get_models_manager(), "visible-model").await;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
let body = resp_mock.single_request().body_json();
|
||||
let description =
|
||||
spawn_agent_description(&body).expect("spawn_agent description should be present");
|
||||
let request = resp_mock.single_request();
|
||||
let body = request.body_json();
|
||||
let namespace = match multi_agent_version {
|
||||
MultiAgentVersion::V1 => MULTI_AGENT_V1_NAMESPACE,
|
||||
MultiAgentVersion::V2 => MULTI_AGENT_V2_NAMESPACE,
|
||||
MultiAgentVersion::Disabled => unreachable!("this scenario requires spawning"),
|
||||
};
|
||||
let description = spawn_agent_description(&body, namespace)
|
||||
.expect("spawn_agent description should be present");
|
||||
let catalog = request.message_input_texts("developer").join("\n");
|
||||
let listing = if model_catalog_in_context {
|
||||
&catalog
|
||||
} else {
|
||||
&description
|
||||
};
|
||||
|
||||
assert!(
|
||||
description.contains("- `visible-model`: Fast and capable"),
|
||||
"expected visible model summary in spawn_agent description: {description:?}"
|
||||
listing.contains("- `visible-model`: Fast and capable"),
|
||||
"expected visible model summary in model catalog: {catalog:?}"
|
||||
);
|
||||
assert!(
|
||||
description
|
||||
.contains("Available model overrides (optional; inherited parent model is preferred):"),
|
||||
"expected model choices to be framed as overrides in spawn_agent description: {description:?}"
|
||||
assert_eq!(
|
||||
description.contains("Pick model overrides from the latest <model_catalog> listing."),
|
||||
model_catalog_in_context
|
||||
);
|
||||
assert_eq!(
|
||||
catalog.contains("<model_catalog>"),
|
||||
model_catalog_in_context
|
||||
);
|
||||
assert_eq!(
|
||||
description.contains("- `visible-model`: Fast and capable"),
|
||||
!model_catalog_in_context
|
||||
);
|
||||
assert!(
|
||||
description.contains(
|
||||
@@ -237,48 +288,106 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
|
||||
),
|
||||
"expected inherited-model guidance in spawn_agent description: {description:?}"
|
||||
);
|
||||
assert!(
|
||||
if multi_agent_version == MultiAgentVersion::V1 {
|
||||
assert!(
|
||||
description.contains(
|
||||
"Do not set the `model` field unless the user explicitly asks for a different model."
|
||||
),
|
||||
"expected model override usage guidance in spawn_agent description: {description:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
description.contains("Reasoning efforts: low, medium (default), high."),
|
||||
"expected default reasoning effort in spawn_agent description: {description:?}"
|
||||
listing.contains("Reasoning efforts: low, medium (default), high."),
|
||||
"expected default reasoning effort in model catalog: {catalog:?}"
|
||||
);
|
||||
assert!(
|
||||
description.contains("Service tiers: priority."),
|
||||
"expected service tier guidance in spawn_agent description: {description:?}"
|
||||
listing.contains("Service tiers: priority."),
|
||||
"expected service tier guidance in model catalog: {catalog:?}"
|
||||
);
|
||||
assert!(
|
||||
!description.contains("hidden-model"),
|
||||
"hidden picker model should be omitted from spawn_agent description: {description:?}"
|
||||
!listing.contains("hidden-model"),
|
||||
"hidden picker model should be omitted from model catalog: {catalog:?}"
|
||||
);
|
||||
assert!(
|
||||
if multi_agent_version == MultiAgentVersion::V1 {
|
||||
assert!(
|
||||
description.contains(
|
||||
"Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work."
|
||||
),
|
||||
"expected explicit authorization rule in spawn_agent description: {description:?}"
|
||||
);
|
||||
assert!(
|
||||
assert!(
|
||||
description.contains(
|
||||
"Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn."
|
||||
) && description.contains("### When to delegate vs. do the subtask yourself"),
|
||||
"expected delegation decision guidance in spawn_agent description: {description:?}"
|
||||
);
|
||||
assert!(
|
||||
description.contains(
|
||||
"Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself."
|
||||
),
|
||||
"expected agent-role clarification in spawn_agent description: {description:?}"
|
||||
);
|
||||
assert!(
|
||||
!description.contains("A mini model can solve many tasks faster than the main model."),
|
||||
"spawn_agent description should not encourage choosing a smaller model by default: {description:?}"
|
||||
);
|
||||
assert!(
|
||||
!description.contains("A mini model can solve many tasks faster than the main model."),
|
||||
"spawn_agent description should not encourage choosing a smaller model by default: {description:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Only the opt-in path keeps the tool schema stable across a catalog refresh.
|
||||
let manager = test.thread_manager.get_models_manager();
|
||||
let mut refreshed = manager
|
||||
.raw_model_catalog(RefreshStrategy::Offline, test.config.http_client_factory())
|
||||
.await;
|
||||
refreshed
|
||||
.models
|
||||
.iter_mut()
|
||||
.find(|model| model.slug == "visible-model")
|
||||
.expect("visible model")
|
||||
.description = Some("Updated picker copy".to_string());
|
||||
mount_models_once(&server, refreshed.clone()).await;
|
||||
assert_eq!(
|
||||
manager
|
||||
.raw_model_catalog(RefreshStrategy::Online, test.config.http_client_factory())
|
||||
.await
|
||||
.models,
|
||||
refreshed.models
|
||||
);
|
||||
let expected_catalogs = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<model_catalog>"))
|
||||
.flat_map(|catalog| {
|
||||
[
|
||||
catalog.clone(),
|
||||
catalog.replace("Fast and capable", "Updated picker copy"),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut requests = vec![request];
|
||||
for turn in ["after catalog refresh", "without another catalog change"] {
|
||||
let response = mount_sse_once(&server, sse(vec![ev_completed(turn)])).await;
|
||||
test.submit_turn(turn).await?;
|
||||
let current = response.single_request();
|
||||
if model_catalog_in_context {
|
||||
assert_eq!(current.body_json()["tools"], body["tools"]);
|
||||
} else {
|
||||
let expected_tools = serde_json::to_string(&body["tools"])?
|
||||
.replace("Fast and capable", "Updated picker copy");
|
||||
assert_eq!(
|
||||
current.body_json()["tools"],
|
||||
serde_json::from_str::<Value>(&expected_tools)?
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
current
|
||||
.input()
|
||||
.starts_with(&requests.last().unwrap().input())
|
||||
);
|
||||
assert_eq!(
|
||||
current
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<model_catalog>"))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_catalogs,
|
||||
);
|
||||
requests.push(current);
|
||||
}
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
#[test_case(false, false, MULTI_AGENT_V1_NAMESPACE; "v1 hides agent type without roles")]
|
||||
|
||||
@@ -211,6 +211,8 @@ pub enum Feature {
|
||||
Collab,
|
||||
/// Enable task-path-based multi-agent routing.
|
||||
MultiAgentV2,
|
||||
/// Keep spawn model choices in append-only context instead of tool descriptions.
|
||||
ModelCatalogInContext,
|
||||
/// Keep sampling through reasoning and commentary boundaries when agent mail arrives.
|
||||
/// Pending mail is delivered at the next normal input boundary instead.
|
||||
DeferMailboxPreemption,
|
||||
@@ -1360,6 +1362,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ModelCatalogInContext,
|
||||
key: "model_catalog_in_context",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::DeferMailboxPreemption,
|
||||
key: "defer_mailbox_preemption",
|
||||
|
||||
Reference in New Issue
Block a user