mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Add support for incremental code mode tool definitions (#60157)
## What changed Render stable entries for nested tool definitions and namespace instructions alongside the full `functions.exec` description. Add optional catalog tracking that emits complete replacements for added or changed entries and explicit notices for removed entries, while keeping the existing exec declaration unchanged. Preserve entry hashes while exec is unavailable, then restore the full catalog and withdraw removed entries when it returns. Keep the existing snapshot format for ordinary tool catalogs. This change provides the rendering and state support; runtime activation remains disabled. ## Testing Add tests for definition rendering and namespace identity, batched additions and changes, removal notices, unchanged catalogs, and full catalog restoration after exec is hidden. GitOrigin-RevId: f472bd3e8e770a8cad9663c72e290abe7d0473be
This commit is contained in:
@@ -27,7 +27,7 @@ use crate::context::ManagedDeveloperInstructions;
|
||||
use crate::context::ModelInstructions;
|
||||
use crate::context::MultiAgentRoleInstructions;
|
||||
use crate::context::SubagentNotification;
|
||||
use crate::context::world_state::IncrementalToolsHint;
|
||||
use crate::context::world_state::IncrementalToolsUpdate;
|
||||
use crate::init_state_db;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
use crate::session::SessionSettingsUpdate;
|
||||
@@ -3351,7 +3351,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history(marke
|
||||
role: "developer".to_string(),
|
||||
tools: Arc::from([serde_json::json!({"type": "function", "name": "compacted_tool"})]),
|
||||
};
|
||||
let tools_update = ContextualUserFragment::into(IncrementalToolsHint(
|
||||
let tools_update = ContextualUserFragment::into(IncrementalToolsUpdate(
|
||||
codex_prompts::ResolvedIncrementalToolMessages::default().tool_update_hint,
|
||||
));
|
||||
let replacement_history = vec![
|
||||
@@ -3517,7 +3517,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history(marke
|
||||
|| matches!(item, ResponseItem::Message {
|
||||
internal_chat_message_metadata_passthrough: Some(metadata), ..
|
||||
} if metadata.content_item_kinds.as_ref().is_some_and(|kinds| {
|
||||
kinds.iter().any(|kind| kind.as_str() == IncrementalToolsHint::KIND)
|
||||
kinds.iter().any(|kind| kind.as_str() == IncrementalToolsUpdate::KIND)
|
||||
}))
|
||||
})
|
||||
.cloned()
|
||||
@@ -3929,7 +3929,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
|
||||
})),
|
||||
]);
|
||||
rollout_items.push(rollout_response_item(ContextualUserFragment::into(
|
||||
IncrementalToolsHint(
|
||||
IncrementalToolsUpdate(
|
||||
codex_prompts::ResolvedIncrementalToolMessages::default().tool_update_hint,
|
||||
),
|
||||
)));
|
||||
@@ -4001,7 +4001,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
|
||||
if matches!(&envelope.item, ResponseItem::Message {
|
||||
internal_chat_message_metadata_passthrough: Some(metadata), ..
|
||||
} if metadata.content_item_kinds.as_ref().is_some_and(|kinds| {
|
||||
kinds.iter().any(|kind| kind.as_str() == IncrementalToolsHint::KIND)
|
||||
kinds.iter().any(|kind| kind.as_str() == IncrementalToolsUpdate::KIND)
|
||||
})) =>
|
||||
Some(envelope.item.clone()),
|
||||
_ => None,
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::context::ModelInstructions;
|
||||
use crate::context::MultiAgentModeInstructions;
|
||||
use crate::context::MultiAgentRoleInstructions;
|
||||
use crate::context::MultiAgentUsageHint;
|
||||
use crate::context::world_state::IncrementalToolsHint;
|
||||
use crate::context::world_state::IncrementalToolsUpdate;
|
||||
use crate::context::world_state::PersistentModeState;
|
||||
use crate::session::multi_agents::resolve_usage_hints;
|
||||
use crate::tasks::InterruptedTurnHistoryMarker;
|
||||
@@ -320,7 +320,7 @@ pub(super) async fn prepare_forked_history(
|
||||
content.retain_mut(|content_item| {
|
||||
if matches!(
|
||||
content_item.kind().as_str(),
|
||||
BaseInstructionsFragment::KIND | IncrementalToolsHint::KIND
|
||||
BaseInstructionsFragment::KIND | IncrementalToolsUpdate::KIND
|
||||
) {
|
||||
return preserve_context_baselines;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ pub(crate) use persistent_mode::PersistentModeState;
|
||||
pub(crate) use plugins_instructions::PluginsInstructionsState;
|
||||
pub(crate) use realtime::RealtimeState;
|
||||
pub(crate) use tools::ToolsState;
|
||||
pub(crate) use top_level_tools::IncrementalToolsHint;
|
||||
pub(crate) use top_level_tools::IncrementalToolsUpdate;
|
||||
pub(crate) use top_level_tools::TopLevelToolsState;
|
||||
|
||||
/// One contribution to model context and its placement policy.
|
||||
|
||||
@@ -11,11 +11,14 @@ use super::WorldStateSection;
|
||||
use super::WorldStateUpdate;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::DeveloperInstructions;
|
||||
use codex_code_mode::ExecDefinition;
|
||||
use codex_code_mode::PUBLIC_TOOL_NAME;
|
||||
use codex_extension_api::ExtensionMetrics;
|
||||
use codex_otel::TOOL_INCREMENTAL_UPDATES_METRIC;
|
||||
use codex_otel::TOOL_SPEC_DESCRIPTION_BYTES_METRIC;
|
||||
use codex_otel::TOOL_SPEC_DESCRIPTION_BYTES_METRIC_BUCKETS;
|
||||
use codex_prompts::ResolvedIncrementalToolMessages;
|
||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -23,6 +26,8 @@ use codex_protocol::models::ContentItemKind;
|
||||
use codex_protocol::models::ContentItemMetadata;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::input_text;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
@@ -32,12 +37,31 @@ const ACTION_ADDED: &str = "added";
|
||||
const ACTION_REMOVED: &str = "removed";
|
||||
const ACTION_SCHEMA_CHANGED: &str = "schema_changed";
|
||||
|
||||
const EXEC_TOOLS_UPDATE_HINT: &str = "Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier version in full. Unlisted tools keep their latest definitions and `tools` methods.";
|
||||
const EXEC_TOOLS_REMOVED_DEFINITIONS_HEADER: &str = "Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged.";
|
||||
|
||||
/// The exact serialized Responses Lite declarations visible in one captured step.
|
||||
pub(crate) struct TopLevelToolsState {
|
||||
messages: ResolvedIncrementalToolMessages,
|
||||
definitions: Vec<Value>,
|
||||
hashes: BTreeMap<String, WorldStateHash>,
|
||||
metrics: Option<Arc<dyn ExtensionMetrics>>,
|
||||
/// Whether this captured step uses granular exec catalog updates.
|
||||
incremental_exec_tools: bool,
|
||||
/// Diffable entries for the current CodeModeOnly catalog; absent in other tool modes.
|
||||
exec_definitions: Option<Arc<Vec<ExecDefinition>>>,
|
||||
}
|
||||
|
||||
/// Ordinary catalogs keep their landed flat format; exec mode adds its own comparison state.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum TopLevelToolsSnapshot {
|
||||
WithExec {
|
||||
tools: BTreeMap<String, WorldStateHash>,
|
||||
/// Stable entry keys and definition hashes, retained while exec is unavailable.
|
||||
exec: BTreeMap<String, WorldStateHash>,
|
||||
},
|
||||
ToolsOnly(BTreeMap<String, WorldStateHash>),
|
||||
}
|
||||
|
||||
impl TopLevelToolsState {
|
||||
@@ -77,8 +101,24 @@ impl TopLevelToolsState {
|
||||
definitions,
|
||||
hashes,
|
||||
metrics,
|
||||
incremental_exec_tools: false,
|
||||
exec_definitions: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Track granular exec updates as part of this tool catalog.
|
||||
#[cfg_attr(
|
||||
not(test),
|
||||
expect(dead_code, reason = "Activated by the follow-up runtime integration")
|
||||
)]
|
||||
pub(crate) fn with_incremental_exec_tools(
|
||||
mut self,
|
||||
exec_definitions: Option<Arc<Vec<ExecDefinition>>>,
|
||||
) -> Self {
|
||||
self.incremental_exec_tools = true;
|
||||
self.exec_definitions = exec_definitions;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Input comes from create_tools_json_for_responses_lite: namespaces and their members have
|
||||
@@ -129,20 +169,66 @@ fn record_changed_tool_metrics(
|
||||
|
||||
impl WorldStateSection for TopLevelToolsState {
|
||||
const ID: &'static str = "top_level_tools";
|
||||
type Snapshot = BTreeMap<String, WorldStateHash>;
|
||||
type Snapshot = TopLevelToolsSnapshot;
|
||||
|
||||
fn render_diff(
|
||||
&self,
|
||||
previous: PreviousSectionState<'_, Self::Snapshot>,
|
||||
) -> SectionTransition<Self::Snapshot> {
|
||||
let previous = match previous {
|
||||
PreviousSectionState::Known(previous) => Some(previous),
|
||||
PreviousSectionState::Absent | PreviousSectionState::Unknown => None,
|
||||
let (previous, previous_exec) = match previous {
|
||||
PreviousSectionState::Known(TopLevelToolsSnapshot::WithExec { tools, exec }) => {
|
||||
(Some(tools), Some(exec))
|
||||
}
|
||||
PreviousSectionState::Known(TopLevelToolsSnapshot::ToolsOnly(tools)) => {
|
||||
(Some(tools), None)
|
||||
}
|
||||
PreviousSectionState::Absent | PreviousSectionState::Unknown => (None, None),
|
||||
};
|
||||
// Only an advertised CodeModeOnly exec declaration gets granular updates.
|
||||
let exec_catalog = self.exec_definitions.as_ref().and_then(|entries| {
|
||||
self.definitions.iter().find_map(|namespace| {
|
||||
if namespace["type"] != "namespace"
|
||||
|| namespace["name"] != DEFAULT_FUNCTION_NAMESPACE
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let exec = namespace["tools"]
|
||||
.as_array()?
|
||||
.iter()
|
||||
.find(|tool| tool["type"] == "custom" && tool["name"] == PUBLIC_TOOL_NAME)?;
|
||||
Some((namespace, exec, entries.as_slice()))
|
||||
})
|
||||
});
|
||||
let manages_exec = exec_catalog.is_some();
|
||||
let mut updates = self.render_declaration_updates(previous, manages_exec);
|
||||
self.append_removed_tools(previous, &mut updates);
|
||||
let snapshot = if self.incremental_exec_tools {
|
||||
let exec =
|
||||
self.append_exec_updates(previous, previous_exec, exec_catalog, &mut updates);
|
||||
TopLevelToolsSnapshot::WithExec {
|
||||
tools: self.hashes.clone(),
|
||||
exec,
|
||||
}
|
||||
} else {
|
||||
TopLevelToolsSnapshot::ToolsOnly(self.hashes.clone())
|
||||
};
|
||||
// Persist empty catalogs as known comparison baselines.
|
||||
(Some(snapshot), updates)
|
||||
}
|
||||
}
|
||||
|
||||
impl TopLevelToolsState {
|
||||
/// Render added or changed declarations and namespace instruction updates.
|
||||
fn render_declaration_updates(
|
||||
&self,
|
||||
previous: Option<&BTreeMap<String, WorldStateHash>>,
|
||||
manages_exec: bool,
|
||||
) -> Vec<WorldStateUpdate> {
|
||||
let changed =
|
||||
|name: &str| previous.and_then(|previous| previous.get(name)) != self.hashes.get(name);
|
||||
let mut namespace_updates = Vec::new();
|
||||
let mut tools = Vec::new();
|
||||
// Tool declarations: collect changed standalone tools and namespace members.
|
||||
for definition in &self.definitions {
|
||||
let name = definition_name(definition);
|
||||
let Some(members) = definition["tools"].as_array() else {
|
||||
@@ -160,6 +246,9 @@ impl WorldStateSection for TopLevelToolsState {
|
||||
let mut changed_members = Vec::new();
|
||||
for tool in members {
|
||||
let tool_name = format!("{name}.{}", definition_name(tool));
|
||||
if manages_exec && previous.is_some() && tool_name == "functions.exec" {
|
||||
continue;
|
||||
}
|
||||
if changed(&tool_name) {
|
||||
record_changed_tool_metrics(
|
||||
&tool_name,
|
||||
@@ -174,9 +263,8 @@ impl WorldStateSection for TopLevelToolsState {
|
||||
let mut namespace = definition.clone();
|
||||
namespace["tools"] = Value::Array(changed_members);
|
||||
tools.push(namespace);
|
||||
} else if changed(name) {
|
||||
// Namespace declarations require a member. Update metadata as text without
|
||||
// repeating unchanged tool definitions.
|
||||
} else if changed(name) && (previous.is_some() || !manages_exec) {
|
||||
// Namespace instruction updates use text because declarations require a member.
|
||||
let instructions = definition["description"].as_str().unwrap_or_default();
|
||||
let text = if instructions.is_empty() {
|
||||
self.messages
|
||||
@@ -201,12 +289,13 @@ impl WorldStateSection for TopLevelToolsState {
|
||||
}
|
||||
let mut updates = Vec::new();
|
||||
if !tools.is_empty() {
|
||||
// Incremental tool updates: introduce an appended batch of namespace declarations.
|
||||
if previous.is_some()
|
||||
&& tools.iter().any(|tool| tool["type"] == "namespace")
|
||||
&& !self.messages.tool_update_hint.is_empty()
|
||||
{
|
||||
updates.push(WorldStateUpdate::prefix_item(ContextualUserFragment::into(
|
||||
IncrementalToolsHint(self.messages.tool_update_hint.clone()),
|
||||
IncrementalToolsUpdate(self.messages.tool_update_hint.clone()),
|
||||
)));
|
||||
}
|
||||
updates.push(WorldStateUpdate::prefix_item(
|
||||
@@ -218,7 +307,17 @@ impl WorldStateSection for TopLevelToolsState {
|
||||
));
|
||||
}
|
||||
updates.extend(namespace_updates);
|
||||
updates
|
||||
}
|
||||
|
||||
/// Append a removal notice, listing removed namespaces instead of their individual members.
|
||||
fn append_removed_tools(
|
||||
&self,
|
||||
previous: Option<&BTreeMap<String, WorldStateHash>>,
|
||||
updates: &mut Vec<WorldStateUpdate>,
|
||||
) {
|
||||
if let Some(previous) = previous {
|
||||
// Removal notices: withdraw namespaces and tools absent from the current catalog.
|
||||
// Namespace and member names cannot contain dots. A qualified key identifies
|
||||
// its old namespace without changing the persisted header/member hash format.
|
||||
let namespaces = previous
|
||||
@@ -259,19 +358,94 @@ impl WorldStateSection for TopLevelToolsState {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Persist the empty map too: it is a known empty catalog, not missing state.
|
||||
(Some(self.hashes.clone()), updates)
|
||||
}
|
||||
|
||||
/// Append exec catalog updates and return the matching comparison snapshot.
|
||||
fn append_exec_updates(
|
||||
&self,
|
||||
previous: Option<&BTreeMap<String, WorldStateHash>>,
|
||||
previous_exec: Option<&BTreeMap<String, WorldStateHash>>,
|
||||
exec_catalog: Option<(&Value, &Value, &[ExecDefinition])>,
|
||||
updates: &mut Vec<WorldStateUpdate>,
|
||||
) -> BTreeMap<String, WorldStateHash> {
|
||||
// Retain entry hashes while functions.exec is not advertised.
|
||||
// When it returns, send the full current catalog and withdraw removed entries.
|
||||
let mut exec = BTreeMap::new();
|
||||
if let Some((namespace, declaration, entries)) = exec_catalog {
|
||||
exec = entries
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
(
|
||||
entry.key.clone(),
|
||||
WorldStateHash::from_json(&Value::String(entry.description.clone())),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if previous_exec.is_some()
|
||||
&& previous.is_some_and(|tools| tools.contains_key("functions.exec"))
|
||||
{
|
||||
// Incremental tool updates: append entries; the exec declaration stays frozen.
|
||||
let changed = entries
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
previous_exec.and_then(|previous| previous.get(&entry.key))
|
||||
!= exec.get(&entry.key)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !changed.is_empty() {
|
||||
updates.push(
|
||||
WorldStateUpdate::fragment(render_exec_update(&changed)).standalone(),
|
||||
);
|
||||
}
|
||||
} else if previous.is_some() {
|
||||
// Install the full catalog when enabling granular updates or restoring exec.
|
||||
if previous_exec.is_some()
|
||||
&& let Some(metrics) = self.metrics.as_deref()
|
||||
{
|
||||
metrics.counter(
|
||||
TOOL_INCREMENTAL_UPDATES_METRIC,
|
||||
/*inc*/ 1,
|
||||
&[("action", ACTION_ADDED), ("tool", "functions.exec")],
|
||||
);
|
||||
}
|
||||
let mut namespace = namespace.clone();
|
||||
namespace["tools"] = Value::Array(vec![declaration.clone()]);
|
||||
updates.push(WorldStateUpdate::prefix_item(
|
||||
ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
tools: Arc::from(vec![namespace]),
|
||||
},
|
||||
));
|
||||
}
|
||||
// Removal notices: include exec entries removed while it was unavailable.
|
||||
if let Some(previous) = previous_exec {
|
||||
let removed = previous
|
||||
.keys()
|
||||
.filter(|key| !exec.contains_key(*key))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if !removed.is_empty() {
|
||||
updates.push(
|
||||
WorldStateUpdate::fragment(RemovedExecDefinitions(removed)).standalone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Some(previous) = previous_exec {
|
||||
exec = previous.clone();
|
||||
}
|
||||
exec
|
||||
}
|
||||
}
|
||||
|
||||
/// Guidance for an appended catalog batch, retained or discarded with its declarations.
|
||||
pub(crate) struct IncrementalToolsHint(pub(crate) String);
|
||||
/// An appended catalog hint or exec definition update, retained or discarded with its baseline.
|
||||
pub(crate) struct IncrementalToolsUpdate(pub(crate) String);
|
||||
|
||||
impl IncrementalToolsHint {
|
||||
impl IncrementalToolsUpdate {
|
||||
pub(crate) const KIND: &str = "tools.incremental_update";
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for IncrementalToolsHint {
|
||||
impl ContextualUserFragment for IncrementalToolsUpdate {
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
@@ -344,6 +518,46 @@ impl ContextualUserFragment for RemovedTools {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a nonempty batch of complete replacement definitions with the exec update hint.
|
||||
fn render_exec_update(definitions: &[&ExecDefinition]) -> IncrementalToolsUpdate {
|
||||
let description = definitions
|
||||
.iter()
|
||||
.map(|entry| entry.description.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
IncrementalToolsUpdate(format!(
|
||||
"{EXEC_TOOLS_UPDATE_HINT}\n\n## Namespace: functions\n{description}"
|
||||
))
|
||||
}
|
||||
|
||||
/// Removal notice containing the stable keys of withdrawn tools or namespace instructions.
|
||||
struct RemovedExecDefinitions(Vec<String>);
|
||||
|
||||
impl ContextualUserFragment for RemovedExecDefinitions {
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn content_kind(&self) -> ContentItemKind {
|
||||
ContentItemKind("tools.removed_definition".to_string())
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
("", "")
|
||||
}
|
||||
|
||||
fn body(&self) -> ContentItem {
|
||||
input_text(format!(
|
||||
"{EXEC_TOOLS_REMOVED_DEFINITIONS_HEADER}\n- {}",
|
||||
self.0.join("\n- ")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "top_level_tools_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::*;
|
||||
use crate::context::world_state::test_support::render_section_cases;
|
||||
use crate::context_manager::updates::merge_world_state_updates;
|
||||
use crate::session::extension_metrics::from_session_telemetry;
|
||||
use codex_code_mode::ExecDefinition;
|
||||
use codex_otel::MetricsClient;
|
||||
use codex_otel::MetricsConfig;
|
||||
use codex_otel::SessionTelemetry;
|
||||
@@ -33,6 +34,12 @@ fn namespace(name: &str, tools: Vec<Value>) -> Value {
|
||||
json!({"type": "namespace", "name": name, "description": "Tools.", "tools": tools})
|
||||
}
|
||||
|
||||
fn exec_declaration(description: &str, grammar: &str) -> Value {
|
||||
json!({"type": "namespace", "name": "functions", "description": "",
|
||||
"tools": [{"type": "custom", "name": "exec", "description": description,
|
||||
"format": {"type": "grammar", "syntax": "lark", "definition": grammar}}]})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshots_catalog_transitions() {
|
||||
use PreviousSectionState::Absent;
|
||||
@@ -278,10 +285,17 @@ fn initial_catalog_preserves_order_and_unchanged_catalog_emits_nothing() {
|
||||
vec![item(definitions.clone())]
|
||||
);
|
||||
let snapshot = snapshot.unwrap();
|
||||
let saved = serde_json::to_value(&snapshot).unwrap();
|
||||
assert_eq!(
|
||||
snapshot.keys().map(String::as_str).collect::<Vec<_>>(),
|
||||
saved
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["alpha", "alpha.lookup", "zebra", "zebra.lookup"]
|
||||
);
|
||||
let snapshot: TopLevelToolsSnapshot = serde_json::from_value(saved).unwrap();
|
||||
let (unchanged_snapshot, updates) = state.render_diff(PreviousSectionState::Known(&snapshot));
|
||||
assert_eq!(unchanged_snapshot, Some(snapshot.clone()));
|
||||
assert!(updates.is_empty());
|
||||
@@ -321,7 +335,10 @@ fn empty_catalog_is_persisted_and_can_add_tools_again() {
|
||||
)
|
||||
.unwrap();
|
||||
let (snapshot, updates) = empty.render_diff(PreviousSectionState::Known(&previous));
|
||||
assert_eq!(snapshot, Some(BTreeMap::new()));
|
||||
assert_eq!(
|
||||
snapshot,
|
||||
Some(TopLevelToolsSnapshot::ToolsOnly(BTreeMap::new()))
|
||||
);
|
||||
assert_eq!(
|
||||
merge_world_state_updates(updates),
|
||||
vec![ContextualUserFragment::into(RemovedTools {
|
||||
@@ -334,7 +351,7 @@ fn empty_catalog_is_persisted_and_can_add_tools_again() {
|
||||
assert_eq!(
|
||||
merge_world_state_updates(updates),
|
||||
vec![
|
||||
ContextualUserFragment::into(IncrementalToolsHint(
|
||||
ContextualUserFragment::into(IncrementalToolsUpdate(
|
||||
ResolvedIncrementalToolMessages::default().tool_update_hint
|
||||
)),
|
||||
item(definitions),
|
||||
@@ -384,7 +401,7 @@ fn namespace_diff_contains_only_changed_and_added_tools() {
|
||||
assert_eq!(
|
||||
merge_world_state_updates(updates),
|
||||
vec![
|
||||
ContextualUserFragment::into(IncrementalToolsHint(
|
||||
ContextualUserFragment::into(IncrementalToolsUpdate(
|
||||
ResolvedIncrementalToolMessages::default().tool_update_hint
|
||||
)),
|
||||
item(vec![expected]),
|
||||
@@ -428,7 +445,7 @@ fn new_namespaces_share_one_notice_before_their_declarations() {
|
||||
assert_eq!(
|
||||
prefix,
|
||||
vec![
|
||||
ContextualUserFragment::into(IncrementalToolsHint(
|
||||
ContextualUserFragment::into(IncrementalToolsUpdate(
|
||||
ResolvedIncrementalToolMessages::default().tool_update_hint
|
||||
)),
|
||||
item(additions.clone()),
|
||||
@@ -543,7 +560,7 @@ fn tool_type_change_keeps_the_callable_name_available() {
|
||||
assert_eq!(
|
||||
merge_world_state_updates(updates),
|
||||
vec![
|
||||
ContextualUserFragment::into(IncrementalToolsHint(
|
||||
ContextualUserFragment::into(IncrementalToolsUpdate(
|
||||
ResolvedIncrementalToolMessages::default().tool_update_hint
|
||||
)),
|
||||
item(vec![updated]),
|
||||
@@ -633,3 +650,158 @@ fn namespace_replaced_by_builtin_keeps_the_replacement_available() {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_exec_is_not_reintroduced_and_returning_exec_restores_full_catalog() {
|
||||
let original = exec_declaration("Shared MCP Types:", "grammar");
|
||||
let exec_definitions = Arc::new(vec![ExecDefinition {
|
||||
key: "Tool: tools.a".to_string(),
|
||||
description: "### a\ndeclare const tools: { a(): Promise<string> };".to_string(),
|
||||
}]);
|
||||
let current = TopLevelToolsState::new(
|
||||
/*messages*/ Default::default(),
|
||||
vec![original.clone()],
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.unwrap()
|
||||
.with_incremental_exec_tools(Some(Arc::clone(&exec_definitions)));
|
||||
let (baseline, full) = current.render_diff(PreviousSectionState::Absent);
|
||||
assert_eq!(
|
||||
merge_world_state_updates(full),
|
||||
vec![ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
tools: Arc::from(vec![original.clone()])
|
||||
},]
|
||||
);
|
||||
let hidden = TopLevelToolsState::new(
|
||||
/*messages*/ Default::default(),
|
||||
Vec::new(),
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.unwrap()
|
||||
.with_incremental_exec_tools(Some(exec_definitions));
|
||||
let (empty, updates) = hidden.render_diff(PreviousSectionState::Known(&baseline.unwrap()));
|
||||
assert_eq!(
|
||||
merge_world_state_updates(updates),
|
||||
vec![ContextualUserFragment::into(RemovedTools {
|
||||
namespaces: vec!["functions".to_string()],
|
||||
..Default::default()
|
||||
})],
|
||||
);
|
||||
// Retain identities for removals on return, but require a fresh exec declaration.
|
||||
let restored: TopLevelToolsSnapshot =
|
||||
serde_json::from_value(serde_json::to_value(empty.unwrap()).unwrap()).unwrap();
|
||||
let namespace_instructions = ContextualUserFragment::into(
|
||||
DeveloperInstructions::new(
|
||||
"The functions namespace no longer has additional instructions.".to_string(),
|
||||
)
|
||||
.with_metadata(ContentItemMetadata::tool(Some(
|
||||
"functions".to_string().into(),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
merge_world_state_updates(
|
||||
current
|
||||
.render_diff(PreviousSectionState::Known(&restored))
|
||||
.1
|
||||
),
|
||||
vec![
|
||||
namespace_instructions.clone(),
|
||||
ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
tools: Arc::from(vec![original.clone()])
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
// A full restore also withdraws definitions removed while exec was hidden.
|
||||
let without_entries = TopLevelToolsState::new(
|
||||
/*messages*/ Default::default(),
|
||||
vec![original.clone()],
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.unwrap()
|
||||
.with_incremental_exec_tools(Some(Arc::new(Vec::new())));
|
||||
assert_eq!(
|
||||
merge_world_state_updates(
|
||||
without_entries
|
||||
.render_diff(PreviousSectionState::Known(&restored))
|
||||
.1
|
||||
),
|
||||
vec![
|
||||
namespace_instructions,
|
||||
ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
tools: Arc::from(vec![original]),
|
||||
},
|
||||
ContextualUserFragment::into(RemovedExecDefinitions(
|
||||
vec!["Tool: tools.a".to_string(),]
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_and_added_entries_share_one_update_and_removed_entries_are_withdrawn() {
|
||||
let definition = exec_declaration("Shared MCP Types:", "grammar");
|
||||
let original = TopLevelToolsState::new(
|
||||
/*messages*/ Default::default(),
|
||||
vec![definition.clone()],
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.unwrap()
|
||||
.with_incremental_exec_tools(Some(Arc::new(vec![
|
||||
ExecDefinition {
|
||||
key: "Tool: tools.a".to_string(),
|
||||
description: "### a\ndeclare const tools: { a(): Promise<string> };".to_string(),
|
||||
},
|
||||
ExecDefinition {
|
||||
key: "Tool: tools.b".to_string(),
|
||||
description: "### b\ndeclare const tools: { b(): Promise<string> };".to_string(),
|
||||
},
|
||||
])));
|
||||
let baseline = original
|
||||
.render_diff(PreviousSectionState::Absent)
|
||||
.0
|
||||
.unwrap();
|
||||
|
||||
let current = TopLevelToolsState::new(
|
||||
/*messages*/ Default::default(),
|
||||
vec![definition],
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.unwrap()
|
||||
.with_incremental_exec_tools(Some(Arc::new(vec![
|
||||
ExecDefinition {
|
||||
key: "Tool: tools.a".to_string(),
|
||||
description:
|
||||
"### a\nChanged a.\ndeclare const tools: { a(value: number): Promise<number> };"
|
||||
.to_string(),
|
||||
},
|
||||
ExecDefinition {
|
||||
key: "Tool: tools.c".to_string(),
|
||||
description: "### c\ndeclare const tools: { c(): Promise<boolean> };".to_string(),
|
||||
},
|
||||
])));
|
||||
let (snapshot, updates) = current.render_diff(PreviousSectionState::Known(&baseline));
|
||||
assert_eq!(
|
||||
merge_world_state_updates(updates),
|
||||
vec![
|
||||
ContextualUserFragment::into(IncrementalToolsUpdate(
|
||||
"Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier version in full. Unlisted tools keep their latest definitions and `tools` methods.\n\n## Namespace: functions\n### a\nChanged a.\ndeclare const tools: { a(value: number): Promise<number> };\n\n### c\ndeclare const tools: { c(): Promise<boolean> };".to_string(),
|
||||
)),
|
||||
ContextualUserFragment::into(RemovedExecDefinitions(vec![
|
||||
"Tool: tools.b".to_string(),
|
||||
])),
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
current
|
||||
.render_diff(PreviousSectionState::Known(&snapshot.unwrap()))
|
||||
.1
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1109,7 +1109,7 @@ fn register_code_mode_executors(
|
||||
}
|
||||
enabled_tools
|
||||
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
|
||||
let exec_description = codex_code_mode::build_exec_tool_description(
|
||||
let (exec_description, _) = codex_code_mode::build_exec_tool_description_and_definitions(
|
||||
&enabled_tools,
|
||||
&deferred_tools,
|
||||
&namespace_descriptions,
|
||||
@@ -1121,6 +1121,7 @@ fn register_code_mode_executors(
|
||||
codex_code_mode::ImageDetailVisibility::Visible
|
||||
},
|
||||
deferred_tool_discovery,
|
||||
/*incremental_exec_enabled*/ false,
|
||||
model_messages.code_mode(),
|
||||
);
|
||||
let execute_handler = CodeModeExecuteHandler::new(
|
||||
|
||||
Reference in New Issue
Block a user