Share additional tool declarations across response item clones (#54945)

## Why

Cloning `ResponseItem::AdditionalTools` deep-copies its tool declarations for history and request copies.

## What changed

Store `tools` in an `Arc<[serde_json::Value]>` so clones share immutable declarations. Update constructors and existing test fixtures to use the shared representation.

GitOrigin-RevId: eb8831774afb45fe8431a796f34ae9d1243e884a
This commit is contained in:
Charlie Marsh
2026-10-17 23:17:51 +00:00
committed by copyberry
parent 8d9f6466ff
commit 1cc7dc8f2a
11 changed files with 23 additions and 19 deletions
+3 -3
View File
@@ -2880,7 +2880,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
let tool_declarations = ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![serde_json::json!({"type": "function", "name": "parent_tool"})],
tools: Arc::from([serde_json::json!({"type": "function", "name": "parent_tool"})]),
};
let tool_state = serde_json::json!({"top_level_tools": {"function:parent_tool": "hash"}})
.as_object()
@@ -3286,7 +3286,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history(marke
let tool_declarations = ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![serde_json::json!({"type": "function", "name": "compacted_tool"})],
tools: Arc::from([serde_json::json!({"type": "function", "name": "compacted_tool"})]),
};
let tools_update = ContextualUserFragment::into(IncrementalToolsHint(
codex_prompts::ResolvedIncrementalToolMessages::default().tool_update_hint,
@@ -3865,7 +3865,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
rollout_items.push(rollout_response_item(ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![serde_json::json!({"type": "function", "name": "parent_tool"})],
tools: Arc::from([serde_json::json!({"type": "function", "name": "parent_tool"})]),
}));
rollout_items.push(RolloutItem::TurnContext(
turn_context.to_turn_context_item(),
+1 -1
View File
@@ -986,7 +986,7 @@ impl ModelClient {
Uuid::new_v5(&prefix_namespace, &serde_json::to_vec(&tools)?),
)),
role: "developer".to_string(),
tools,
tools: Arc::from(tools),
});
}
None
@@ -213,7 +213,7 @@ impl WorldStateSection for TopLevelToolsState {
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools,
tools: Arc::from(tools),
},
));
}
@@ -25,7 +25,7 @@ fn item(tools: Vec<Value>) -> ResponseItem {
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools,
tools: Arc::from(tools),
}
}
@@ -9,6 +9,7 @@ use pretty_assertions::assert_ne;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use std::sync::Arc;
#[derive(Clone, Deserialize, Serialize)]
struct TestSection {
@@ -443,9 +444,9 @@ impl WorldStateSection for MixedOutputSection {
content: WorldStateUpdateContent::Item(Box::new(ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![
tools: Arc::from([
json!({"type": "function", "name": "example", "description": self.0.to_string()}),
],
]),
})),
},
WorldStateUpdate {
@@ -457,7 +458,7 @@ impl WorldStateSection for MixedOutputSection {
content: WorldStateUpdateContent::Item(Box::new(ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![json!({"type": "function", "name": "context_tool"})],
tools: Arc::from([json!({"type": "function", "name": "context_tool"})]),
})),
},
WorldStateUpdate::fragment(TestFragment("after tools".to_string())),
@@ -481,13 +482,13 @@ fn mixed_output_preserves_order_and_restores_its_snapshot() {
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![json!({"type": "function", "name": "example", "description": "2"})],
tools: Arc::from([json!({"type": "function", "name": "example", "description": "2"})]),
},
ContextualUserFragment::into(TestFragment("prefix instructions".to_string())),
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![json!({"type": "function", "name": "context_tool"})],
tools: Arc::from([json!({"type": "function", "name": "context_tool"})]),
},
ContextualUserFragment::into(TestFragment("after tools".to_string())),
];
@@ -8,6 +8,7 @@ use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use codex_protocol::models::input_text;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::sync::Arc;
struct TestFragment {
role: &'static str,
@@ -76,7 +77,7 @@ fn additional_tools() -> ResponseItem {
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![json!({"type": "function", "name": "lookup", "parameters": {}})],
tools: Arc::from([json!({"type": "function", "name": "lookup", "parameters": {}})]),
}
}
+1 -1
View File
@@ -365,7 +365,7 @@ fn assign_missing_response_item_ids_assigns_additional_tools_ids() {
let items = Cow::Owned(vec![ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: Vec::new(),
tools: Arc::from([]),
}]);
let items = Session::assign_missing_response_item_ids(items);
@@ -77,7 +77,7 @@ fn additional_tools(name: &str) -> ResponseItem {
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![json!({"type": "function", "name": name})],
tools: Arc::from([json!({"type": "function", "name": name})]),
}
}
@@ -189,7 +189,7 @@ impl LunaSampler {
ResponseItemEnvelope::new(ResponseItem::AdditionalTools {
id: None,
role: "developer".to_owned(),
tools: Vec::new(),
tools: Arc::from([]),
}),
ResponseItemEnvelope::new(ResponseItem::from(request.instructions.clone())),
];
+4 -2
View File
@@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
use codex_utils_image::PromptImageMode;
use codex_utils_image::data_url_from_bytes;
@@ -1055,7 +1056,8 @@ pub enum ResponseItem {
id: Option<ResponseItemId>,
role: String,
#[ts(type = "unknown[]")]
tools: Vec<serde_json::Value>,
// Share immutable declarations across history and request copies.
tools: Arc<[serde_json::Value]>,
},
Message {
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -2807,7 +2809,7 @@ mod tests {
let mut additional_tools = ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: Vec::new(),
tools: Arc::from([]),
};
additional_tools.set_id(Some(ResponseItemId::with_suffix("at", "test")));
assert_eq!(
+2 -2
View File
@@ -638,14 +638,14 @@ async fn recorder_preserves_additional_tools_in_both_history_modes() -> std::io:
ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools: vec![serde_json::json!({
tools: Arc::from([serde_json::json!({
"type": "namespace",
"name": "functions",
"description": "Available tools.",
"tools": [{"type": "function", "name": "lookup",
"description": "Look up a value.",
"parameters": {"type": "object", "properties": {}}}]
})],
})]),
}
.into(),
);