mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Add opt-in incremental tools for regular OpenAI Responses (#59832)
## Why Incremental tool declarations were limited to Responses Lite. Regular OpenAI Responses requests sent the full current tool catalog on each request. ## What changed - Add the disabled-by-default `incremental_tools_legacy` feature for regular OpenAI Responses, independently of the Responses Lite `incremental_tools` flag. - Support tool declarations and updates in conversation history, including during compaction, while preserving existing history prefixes. - Keep hosted `web_search` configuration in top-level `tools` rather than `additional_tools`, including in compaction requests. ## Testing Extend request-history scenarios and snapshots to cover regular Responses catalog updates, model switches, compaction, and MCP tool removal and restoration. Assert that incremental requests preserve input prefixes and keep web search out of history declarations. GitOrigin-RevId: fa347409e387aa973ce2e41df8505769b7db1ec2
This commit is contained in:
@@ -1129,6 +1129,9 @@
|
||||
"incremental_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"incremental_tools_legacy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"instant_interrupt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -8149,6 +8152,9 @@
|
||||
"incremental_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"incremental_tools_legacy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"instant_interrupt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_rollout_trace::CompactionTraceContext;
|
||||
use codex_tools::ToolSpec;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
|
||||
@@ -95,10 +96,8 @@ pub(super) async fn run_remote_compact_v2_attempt(
|
||||
let model_info = turn_context.model_info();
|
||||
let mut history = sess.clone_history().await;
|
||||
let input_goal_ids = UserGoalUpdate::message_ids(history.raw_items());
|
||||
let tools_in_history = step_context.settings.model_info.use_responses_lite
|
||||
&& history
|
||||
.raw_items()
|
||||
.any(|item| matches!(item, ResponseItem::AdditionalTools { .. }));
|
||||
let tools_in_history = step_context.incremental_tools_feature().is_some()
|
||||
&& history.has_tool_declarations();
|
||||
let base_instructions = if tools_in_history {
|
||||
BaseInstructions {
|
||||
text: String::new(),
|
||||
@@ -173,7 +172,7 @@ pub(super) async fn run_remote_compact_v2_attempt(
|
||||
let mut unavailable =
|
||||
rejected_audio::unavailable_file_audio(&input, sess.services.image_store.as_ref()).await;
|
||||
rejected_audio::project_compaction_audio(&mut input, &mut prompt_input_metadata, &unavailable);
|
||||
let tools_in_history = model_info.use_responses_lite
|
||||
let tools_in_history = step_context.incremental_tools_feature().is_some()
|
||||
&& input
|
||||
.iter()
|
||||
.any(|item| matches!(item, ResponseItem::AdditionalTools { .. }));
|
||||
@@ -181,7 +180,12 @@ pub(super) async fn run_remote_compact_v2_attempt(
|
||||
let mut prompt = Prompt {
|
||||
input,
|
||||
tools: if tools_in_history {
|
||||
Arc::default()
|
||||
tool_router
|
||||
.model_visible_specs()
|
||||
.iter()
|
||||
.filter(|tool| matches!(tool, ToolSpec::WebSearch { .. }))
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
tool_router.model_visible_specs()
|
||||
},
|
||||
@@ -284,18 +288,15 @@ pub(crate) async fn capture_background_input(
|
||||
auth_owner: Option<AuthOwner>,
|
||||
window: (String, u64, codex_protocol::ContextWindowId),
|
||||
) -> BackgroundCompactionInput {
|
||||
let base_instructions = if step_context.settings.model_info.use_responses_lite
|
||||
&& snapshot
|
||||
.raw_items()
|
||||
.any(|item| matches!(item, ResponseItem::AdditionalTools { .. }))
|
||||
{
|
||||
BaseInstructions {
|
||||
text: String::new(),
|
||||
provenance: None,
|
||||
}
|
||||
} else {
|
||||
base_instructions
|
||||
};
|
||||
let base_instructions =
|
||||
if step_context.incremental_tools_feature().is_some() && snapshot.has_tool_declarations() {
|
||||
BaseInstructions {
|
||||
text: String::new(),
|
||||
provenance: None,
|
||||
}
|
||||
} else {
|
||||
base_instructions
|
||||
};
|
||||
let prefix = CompactionPrefix {
|
||||
generation: snapshot.history_version(),
|
||||
len: snapshot.annotated_items().len(),
|
||||
|
||||
@@ -11,8 +11,10 @@ use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::tools::router::ToolRouter;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_features::Feature;
|
||||
use codex_file_system::EnvironmentAccess;
|
||||
use codex_mcp::McpBinding;
|
||||
use codex_model_provider_info::OPENAI_PROVIDER_ID;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_plugin::ExecutorPluginHookSource;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
@@ -53,14 +55,20 @@ pub(crate) struct StepContext {
|
||||
}
|
||||
|
||||
impl StepContext {
|
||||
/// Feature that selects the tool layout of new windows on this route.
|
||||
pub(crate) fn incremental_tools_feature(&self) -> Option<Feature> {
|
||||
if self.settings.model_info.use_responses_lite {
|
||||
Some(Feature::IncrementalTools)
|
||||
} else {
|
||||
(self.turn.config.model_provider_id == OPENAI_PROVIDER_ID)
|
||||
.then_some(Feature::IncrementalToolsLegacy)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether new context windows should record tool declarations in history.
|
||||
pub(crate) fn incremental_tools_enabled(&self) -> bool {
|
||||
self.settings.model_info.use_responses_lite
|
||||
&& self
|
||||
.turn
|
||||
.config
|
||||
.features
|
||||
.enabled(codex_features::Feature::IncrementalTools)
|
||||
self.incremental_tools_feature()
|
||||
.is_some_and(|feature| self.turn.config.features.enabled(feature))
|
||||
}
|
||||
|
||||
/// Pairs the step's environments with access using current session and originating-turn grants.
|
||||
|
||||
@@ -137,6 +137,7 @@ use codex_skills_extension::validate_required_skills;
|
||||
use codex_thread_store::PersistContext;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::filter_request_plugin_install_discoverable_tools_for_client;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_stream_parser::AssistantTextChunk;
|
||||
@@ -1848,7 +1849,14 @@ pub(crate) fn build_prompt(
|
||||
Prompt {
|
||||
input,
|
||||
tools: if incremental_tools {
|
||||
Arc::default()
|
||||
// Hosted tools still require top-level Responses configuration.
|
||||
step_context
|
||||
.tool_router
|
||||
.model_visible_specs()
|
||||
.iter()
|
||||
.filter(|tool| matches!(tool, ToolSpec::WebSearch { .. }))
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
step_context.tool_router.model_visible_specs()
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ use codex_protocol::models::BaseInstructionsProvenance;
|
||||
use codex_protocol::models::ContentItemNamespace;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
impl Session {
|
||||
pub(super) fn host_roster_reader(
|
||||
@@ -60,7 +61,7 @@ impl Session {
|
||||
&self,
|
||||
step_context: &StepContext,
|
||||
) -> bool {
|
||||
if !step_context.settings.model_info.use_responses_lite {
|
||||
if step_context.incremental_tools_feature().is_none() {
|
||||
return false;
|
||||
}
|
||||
let state = self.state.lock().await;
|
||||
@@ -201,6 +202,11 @@ impl Session {
|
||||
!new_window && self.preserve_parent_tool_declarations();
|
||||
if incremental_tools && !preserve_parent_tool_declarations {
|
||||
let specs = step_context.tool_router.model_visible_specs();
|
||||
let specs = specs
|
||||
.iter()
|
||||
.filter(|tool| !matches!(tool, ToolSpec::WebSearch { .. }))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let definitions = codex_tools::create_tools_json_for_responses_lite(&specs)?;
|
||||
world_state.add_section(TopLevelToolsState::new(
|
||||
model_messages.incremental_tools(),
|
||||
|
||||
@@ -142,6 +142,10 @@ async fn catalog_and_model_changes_preserve_the_window_base(
|
||||
.features
|
||||
.disable(Feature::IncrementalTools)
|
||||
.expect("disable incremental tools");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::IncrementalToolsLegacy)
|
||||
.expect("enable only the regular Responses flag");
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Request-history coverage for incremental tools on Responses Lite.
|
||||
//! Request-history coverage for incremental tools on Responses and Responses Lite.
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::ForkSnapshot;
|
||||
@@ -148,19 +148,21 @@ async fn prediction_fork_keeps_inherited_tools() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(true, None; "responses_lite")]
|
||||
#[test_case(true, Some(true); "announcements_suppressed")]
|
||||
#[test_case(false, None; "responses_api")]
|
||||
#[test_case(true, None, true; "responses_lite")]
|
||||
#[test_case(true, Some(true), true; "announcements_suppressed")]
|
||||
#[test_case(false, None, false; "responses_api")]
|
||||
#[test_case(false, None, true; "responses_api_incremental")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
use_responses_lite: bool,
|
||||
suppress_model_change_announcements: Option<bool>,
|
||||
incremental_tools: bool,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = responses::mount_sse_sequence(
|
||||
&server,
|
||||
(1..=if use_responses_lite { 6 } else { 4 })
|
||||
(1..=if incremental_tools { 6 } else { 4 })
|
||||
.map(|index| {
|
||||
let mut events = Vec::new();
|
||||
if index == 5 {
|
||||
@@ -191,7 +193,7 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
.with_model_info_override("gpt-5.4", move |model| {
|
||||
model.use_responses_lite = use_responses_lite;
|
||||
model.tool_mode = Some(ToolMode::CodeMode);
|
||||
if use_responses_lite {
|
||||
if incremental_tools {
|
||||
model
|
||||
.model_messages
|
||||
.as_mut()
|
||||
@@ -201,13 +203,23 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
}
|
||||
})
|
||||
.with_config(move |config| {
|
||||
if !use_responses_lite && incremental_tools {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::IncrementalTools)
|
||||
.expect("disable the Lite flag");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::IncrementalToolsLegacy)
|
||||
.expect("enable incremental tools on regular Responses");
|
||||
}
|
||||
if let Some(suppress) = suppress_model_change_announcements {
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::SuppressModelChangeAnnouncements, suppress)
|
||||
.expect("configure model announcements");
|
||||
}
|
||||
if !use_responses_lite {
|
||||
if !incremental_tools {
|
||||
config.base_instructions =
|
||||
Some("Use the available tools to help the user.".to_string());
|
||||
}
|
||||
@@ -220,7 +232,7 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
.expect("source model")
|
||||
.clone();
|
||||
updated.slug = "updated-tools-model".to_string();
|
||||
if use_responses_lite {
|
||||
if incremental_tools {
|
||||
updated
|
||||
.model_messages
|
||||
.as_mut()
|
||||
@@ -263,7 +275,7 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
.await?;
|
||||
test.submit_text_turn("Continue with the same updated tools.")
|
||||
.await?;
|
||||
if use_responses_lite {
|
||||
if incremental_tools {
|
||||
test.codex.submit(Op::Compact).await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
@@ -273,7 +285,44 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
}
|
||||
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), if use_responses_lite { 6 } else { 4 });
|
||||
assert_eq!(requests.len(), if incremental_tools { 6 } else { 4 });
|
||||
if incremental_tools {
|
||||
for pair in requests[..4].windows(2) {
|
||||
let previous = pair[0].body_json();
|
||||
let current = pair[1].body_json();
|
||||
let prefix = previous["input"].as_array().expect("previous input");
|
||||
assert!(
|
||||
current["input"]
|
||||
.as_array()
|
||||
.expect("current input")
|
||||
.starts_with(prefix)
|
||||
);
|
||||
}
|
||||
for request in &requests {
|
||||
let body = request.body_json();
|
||||
if !use_responses_lite {
|
||||
assert!(
|
||||
body["tools"]
|
||||
.as_array()
|
||||
.expect("hosted tools")
|
||||
.iter()
|
||||
.any(|tool| tool["type"] == "web_search")
|
||||
);
|
||||
}
|
||||
if let Some(tools) = body["tools"].as_array() {
|
||||
assert!(tools.iter().all(|tool| tool["type"] == "web_search"));
|
||||
}
|
||||
assert!(
|
||||
body["input"]
|
||||
.as_array()
|
||||
.expect("input")
|
||||
.iter()
|
||||
.filter(|item| item["type"] == "additional_tools")
|
||||
.flat_map(|item| item["tools"].as_array().expect("declarations"))
|
||||
.all(|tool| tool["type"] != "web_search")
|
||||
);
|
||||
}
|
||||
}
|
||||
let snapshot = metrics.snapshot()?;
|
||||
let mut registration_samples = 0;
|
||||
let mut counts = BTreeMap::<_, u64>::new();
|
||||
@@ -291,7 +340,7 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
.attributes()
|
||||
.find(|tag| tag.key.as_str() == "incremental_tools")
|
||||
.map(|tag| tag.value.to_string()),
|
||||
Some(use_responses_lite.to_string())
|
||||
Some(incremental_tools.to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -331,9 +380,9 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
// Six exposure samples per sampling request, excluding compaction.
|
||||
assert_eq!(
|
||||
registration_samples,
|
||||
6 * if use_responses_lite { 5 } else { 4 }
|
||||
6 * if incremental_tools { 5 } else { 4 }
|
||||
);
|
||||
if use_responses_lite {
|
||||
if incremental_tools {
|
||||
let body = requests[2].body_json();
|
||||
let exec = body["input"]
|
||||
.as_array()
|
||||
@@ -362,7 +411,7 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
assert_eq!(counts, BTreeMap::new());
|
||||
assert_eq!(bytes, BTreeMap::new());
|
||||
}
|
||||
if use_responses_lite {
|
||||
if incremental_tools {
|
||||
for request in &requests[..5] {
|
||||
assert_eq!(
|
||||
request.instructions_text(),
|
||||
@@ -378,15 +427,17 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
insta::assert_snapshot!(
|
||||
if suppress_model_change_announcements == Some(true) {
|
||||
"incremental_tools_suppressed_announcements"
|
||||
} else if use_responses_lite {
|
||||
} else if incremental_tools && use_responses_lite {
|
||||
"incremental_tools"
|
||||
} else if incremental_tools {
|
||||
"incremental_tools_regular_responses"
|
||||
} else {
|
||||
"incremental_tools_responses_api"
|
||||
},
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
if suppress_model_change_announcements == Some(true) {
|
||||
"A model switch suppresses the model ID announcement while appending changed tools and replacement instructions. Repeated turns preserve the prefix; compaction starts a new window with the updated base."
|
||||
} else if use_responses_lite {
|
||||
} else if incremental_tools {
|
||||
"Tool definitions precede the initial base instructions. A model switch appends one developer notice, changed tools, and replacement instructions without rewriting the prefix; compaction starts a new window with the updated base."
|
||||
} else {
|
||||
"Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests."
|
||||
@@ -401,11 +452,13 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(false; "bundled")]
|
||||
#[test_case(true; "catalog_messages")]
|
||||
#[test_case(false, true; "bundled")]
|
||||
#[test_case(true, true; "catalog_messages")]
|
||||
#[test_case(false, false; "responses_api")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_removal_appends_a_standalone_notice_without_rewriting_history(
|
||||
catalog_messages: bool,
|
||||
use_responses_lite: bool,
|
||||
) -> Result<()> {
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
@@ -429,7 +482,7 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history(
|
||||
let restored_server = server_config(&["echo", "cwd"], "Echo service instructions.")?;
|
||||
let test = test_codex()
|
||||
.with_model_info_override("gpt-5.5", move |model| {
|
||||
model.use_responses_lite = true;
|
||||
model.use_responses_lite = use_responses_lite;
|
||||
model.supports_search_tool = false;
|
||||
model.tool_mode = Some(ToolMode::Direct);
|
||||
if catalog_messages {
|
||||
@@ -453,7 +506,11 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history(
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::IncrementalTools)
|
||||
.enable(if use_responses_lite {
|
||||
Feature::IncrementalTools
|
||||
} else {
|
||||
Feature::IncrementalToolsLegacy
|
||||
})
|
||||
.expect("enable incremental tools");
|
||||
if catalog_messages {
|
||||
config
|
||||
@@ -611,6 +668,11 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history(
|
||||
"MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Removal notices append alone; restoration appends one developer notice before both namespace declarations. Unchanged turns do not repeat updates.",
|
||||
)
|
||||
};
|
||||
let snapshot_name = if use_responses_lite {
|
||||
snapshot_name
|
||||
} else {
|
||||
"incremental_tool_removals_regular_responses"
|
||||
};
|
||||
insta::assert_snapshot!(
|
||||
snapshot_name,
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_incremental_tools.rs
|
||||
assertion_line: 644
|
||||
expression: "context_snapshot::format_request_history_snapshot(scenario, &requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Removal notices append alone; restoration appends one developer notice before both namespace declarations. Unchanged turns do not repeat updates.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "gpt-5.5"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (1; hash=f359ca6f02a419d6):
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (3; hash=c6476125e1d67bb3):
|
||||
- namespace/functions; hash=6475578c7f948d18
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/list_mcp_resources
|
||||
- function/list_mcp_resource_templates
|
||||
- function/read_mcp_resource
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__echo_service: Echo service instructions.; hash=a5144e5d70d01799
|
||||
- function/cwd
|
||||
- function/echo
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer:
|
||||
<PERMISSIONS_INSTRUCTIONS>
|
||||
03:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
04:message/user:
|
||||
Begin with the echo service.
|
||||
-- request 2 (turn) --
|
||||
05:message/developer:
|
||||
The following tools are no longer available. Do not call them:
|
||||
- mcp__echo_service.cwd
|
||||
06:message/user:
|
||||
Continue after disabling the cwd tool.
|
||||
-- request 3 (turn) --
|
||||
07:message/user:
|
||||
Continue with the remaining echo tool.
|
||||
-- request 4 (turn) --
|
||||
08:message/developer:
|
||||
The following namespaces are no longer available. Do not call tools in them unless those tools are declared in a later update:
|
||||
- mcp__echo_service
|
||||
|
||||
The following tools are no longer available. Do not call them:
|
||||
- functions.list_mcp_resource_templates
|
||||
- functions.list_mcp_resources
|
||||
- functions.read_mcp_resource
|
||||
09:message/user:
|
||||
Continue after removing the echo service.
|
||||
-- request 5 (turn) --
|
||||
10:message/user:
|
||||
Continue without the echo service.
|
||||
-- request 6 (turn) --
|
||||
11:message/developer:
|
||||
This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c]
|
||||
12:additional_tools/developer (2; hash=ba0bceb82aa883e0):
|
||||
- namespace/functions; hash=88d1efdc7cb932a8
|
||||
- function/list_mcp_resources
|
||||
- function/list_mcp_resource_templates
|
||||
- function/read_mcp_resource
|
||||
- namespace/mcp__echo_service: Echo service instructions.; hash=a5144e5d70d01799
|
||||
- function/cwd
|
||||
- function/echo
|
||||
13:message/user:
|
||||
Continue with the restored echo service.
|
||||
-- request 7 (turn) --
|
||||
14:message/user:
|
||||
Continue with the same restored tools.
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_incremental_tools.rs
|
||||
assertion_line: 399
|
||||
expression: "context_snapshot::format_request_history_snapshot(if\nsuppress_model_change_announcements == Some(true)\n{\n \"A model switch suppresses the model ID announcement while appending changed tools and replacement instructions. Repeated turns preserve the prefix; compaction starts a new window with the updated base.\"\n} else if incremental_tools\n{\n \"Tool definitions precede the initial base instructions. A model switch appends one developer notice, changed tools, and replacement instructions without rewriting the prefix; compaction starts a new window with the updated base.\"\n} else\n{\n \"Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests.\"\n}, &requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Tool definitions precede the initial base instructions. A model switch appends one developer notice, changed tools, and replacement instructions without rewriting the prefix; compaction starts a new window with the updated base.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "gpt-5.4"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (1; hash=16d317804b13dd3d):
|
||||
- web_search; hash=e214d4db1bc49e89
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (2; hash=019c865b71a05fec):
|
||||
- namespace/functions; hash=f3355f5705e28592
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=b573ed69c0b0e6c7
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <tools>
|
||||
Deferred tool namespaces:
|
||||
- multi_agent_v1: Tools for spawning and managing sub-agents.
|
||||
</tools>
|
||||
03:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
04:message/user:
|
||||
Begin work.
|
||||
-- request 2 (turn) --
|
||||
05:message/user:
|
||||
Continue with the same tools.
|
||||
|
||||
## Window 2 (after request 2)
|
||||
Settings: relative to window 1
|
||||
model: "updated-tools-model"
|
||||
-- request 3 (turn) --
|
||||
00:additional_tools/developer (2; hash=019c865b71a05fec):
|
||||
- namespace/functions; hash=f3355f5705e28592
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=b573ed69c0b0e6c7
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <tools>
|
||||
Deferred tool namespaces:
|
||||
- multi_agent_v1: Tools for spawning and managing sub-agents.
|
||||
</tools>
|
||||
03:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
04:message/user:
|
||||
Begin work.
|
||||
05:message/user:
|
||||
Continue with the same tools.
|
||||
06:message/developer:
|
||||
This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c]
|
||||
07:additional_tools/developer (1; hash=26cd263a29587cc7):
|
||||
- namespace/functions; hash=771cad2ff250d2f3
|
||||
- custom/exec
|
||||
08:message/developer:
|
||||
<model_instructions>
|
||||
The current model is updated-tools-model.
|
||||
The following instructions replace the previous base instructions:
|
||||
|
||||
Review results before responding.
|
||||
</model_instructions>
|
||||
09:message/user:
|
||||
Continue with the updated execution instructions.
|
||||
-- request 4 (turn) --
|
||||
10:message/user:
|
||||
Continue with the same updated tools.
|
||||
-- request 5 (compaction) --
|
||||
11:compaction_trigger
|
||||
|
||||
## Window 3 (after request 5: input diverged at item 00)
|
||||
Settings: same as window 2
|
||||
-- request 6 (turn) --
|
||||
00:additional_tools/developer (2; hash=7df544e43dd38360):
|
||||
- namespace/functions; hash=1e51a9a9273be2f8
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/exec_command
|
||||
- function/write_stdin
|
||||
- function/request_user_input
|
||||
- custom/apply_patch
|
||||
- function/view_image
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=b573ed69c0b0e6c7
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/user:
|
||||
Begin work.
|
||||
03:message/user:
|
||||
Continue with the same tools.
|
||||
04:message/user:
|
||||
Continue with the updated execution instructions.
|
||||
05:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <tools>
|
||||
Deferred tool namespaces:
|
||||
- multi_agent_v1: Tools for spawning and managing sub-agents.
|
||||
</tools>
|
||||
06:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
07:message/user:
|
||||
Continue with the same updated tools.
|
||||
08:compaction:encrypted=true; chars=28; hash=e4a73156e0a498bc
|
||||
09:message/user:
|
||||
Continue after compaction.
|
||||
@@ -306,6 +306,8 @@ pub enum Feature {
|
||||
CommandSessionsWorldState,
|
||||
/// Track top-level tool definitions in world state and emit incremental context updates.
|
||||
IncrementalTools,
|
||||
/// Publish incremental tool declarations on regular OpenAI Responses.
|
||||
IncrementalToolsLegacy,
|
||||
/// Suppress the model ID announcement in incremental context after a model switch.
|
||||
SuppressModelChangeAnnouncements,
|
||||
/// Preserve inherited tool declarations in MCP-disabled prediction forks.
|
||||
@@ -1798,6 +1800,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::IncrementalToolsLegacy,
|
||||
key: "incremental_tools_legacy",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SuppressModelChangeAnnouncements,
|
||||
key: "suppress_model_change_announcements",
|
||||
|
||||
Reference in New Issue
Block a user