From 7e802b22f13e2714efd2fb2a6e396319958d6506 Mon Sep 17 00:00:00 2001 From: sayan-oai Date: Thu, 21 May 2026 18:11:47 -0700 Subject: [PATCH] Expose conversation history to extension tools (#23963) ## Why Extension tools that need conversation context should be able to read it from the live tool invocation instead of reaching into thread persistence themselves. ## What changed - Add a `ConversationHistory` snapshot to extension `ToolCall`s and populate it from the current raw in-memory response history. - Expose all history items at this boundary so each extension can filter and bound the subset it needs before consuming or forwarding it. - Cover the adapter and registry dispatch paths and update existing extension tests that construct `ToolCall` literals. ## Test plan - `cargo test -p codex-tools` - `cargo test -p codex-extension-api` - `cargo test -p codex-goal-extension` - `cargo test -p codex-memories-extension` - `cargo test -p codex-core passes_turn_fields_to_extension_call` - `cargo test -p codex-core extension_tool_executors_are_model_visible_and_dispatchable` --- codex-rs/core/src/context_manager/history.rs | 5 ++++ .../src/tools/handlers/extension_tools.rs | 25 +++++++++++++++++-- codex-rs/core/src/tools/router_tests.rs | 14 +++++++++++ codex-rs/ext/extension-api/src/lib.rs | 1 + .../ext/goal/tests/goal_extension_backend.rs | 1 + codex-rs/ext/memories/src/tests.rs | 4 +++ codex-rs/tools/src/lib.rs | 1 + codex-rs/tools/src/tool_call.rs | 21 ++++++++++++++++ 8 files changed, 70 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 6a05535488..e5f6e4132d 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -126,6 +126,11 @@ impl ContextManager { &self.items } + /// Returns raw items in the history and consumes the snapshot. + pub(crate) fn into_raw_items(self) -> Vec { + self.items + } + pub(crate) fn history_version(&self) -> u64 { self.history_version } diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index 764e66f0ce..470b32beae 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use codex_tools::ConversationHistory; use codex_tools::ToolCall as ExtensionToolCall; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -53,7 +54,7 @@ impl ToolExecutor for ExtensionToolAdapter { &self, invocation: ToolInvocation, ) -> Result, FunctionCallError> { - self.0.handle(to_extension_call(&invocation)).await + self.0.handle(to_extension_call(&invocation).await).await } } @@ -86,12 +87,15 @@ impl CoreToolRuntime for ExtensionToolAdapter { } } -fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall { +async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall { + let conversation_history = + ConversationHistory::new(invocation.session.clone_history().await.into_raw_items()); ExtensionToolCall { turn_id: invocation.turn.sub_id.clone(), call_id: invocation.call_id.clone(), tool_name: invocation.tool_name.clone(), truncation_policy: invocation.turn.truncation_policy, + conversation_history, payload: invocation.payload.clone(), } } @@ -108,6 +112,8 @@ fn extension_tool_hook_input(arguments: &str) -> Value { mod tests { use std::sync::Arc; + use codex_protocol::models::ContentItem; + use codex_protocol::models::ResponseItem; use pretty_assertions::assert_eq; use serde_json::json; use tokio::sync::Mutex; @@ -236,6 +242,17 @@ mod tests { let (session, turn) = crate::session::tests::make_session_and_context().await; let turn_id = turn.sub_id.clone(); let truncation_policy = turn.truncation_policy; + let history_item = ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "extension history".to_string(), + }], + phase: None, + }; + session + .record_into_history(std::slice::from_ref(&history_item), &turn) + .await; let invocation = ToolInvocation { session: session.into(), turn: turn.into(), @@ -261,6 +278,10 @@ mod tests { codex_tools::ToolName::plain("extension_echo") ); assert_eq!(captured_call.truncation_policy, truncation_policy); + assert_eq!( + captured_call.conversation_history.items(), + std::slice::from_ref(&history_item) + ); match captured_call.payload { ToolPayload::Function { arguments } => { assert_eq!(arguments, json!({ "message": "hello" }).to_string()); diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index 375faba248..9d685e53b6 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -11,6 +11,7 @@ use codex_extension_api::ResponsesApiTool; use codex_extension_api::ToolCall as ExtensionToolCall; use codex_extension_api::ToolExecutor; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; @@ -81,6 +82,7 @@ impl ToolExecutor for ExtensionEchoExecutor { Ok(Box::new(codex_tools::JsonToolOutput::new(json!({ "arguments": arguments, "callId": call.call_id, + "conversationHistory": call.conversation_history.items(), "ok": true, })))) } @@ -327,6 +329,17 @@ fn mcp_tool_info( async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow::Result<()> { let (mut session, turn) = make_session_and_context().await; session.services.extensions = extension_tool_test_registry(); + let history_item = ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "extension history".to_string(), + }], + phase: None, + }; + session + .record_into_history(std::slice::from_ref(&history_item), &turn) + .await; let router = ToolRouter::from_turn_context( &turn, @@ -384,6 +397,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow json!({ "arguments": { "message": "hello" }, "callId": "call-extension", + "conversationHistory": [history_item], "ok": true, }) ); diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index 06cbc6f889..a7c5c87e52 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -10,6 +10,7 @@ pub use capabilities::NoopExtensionEventSink; pub use capabilities::NoopResponseItemInjector; pub use capabilities::ResponseItemInjectionFuture; pub use capabilities::ResponseItemInjector; +pub use codex_tools::ConversationHistory; pub use codex_tools::FunctionCallError; pub use codex_tools::JsonToolOutput; pub use codex_tools::ResponsesApiTool; diff --git a/codex-rs/ext/goal/tests/goal_extension_backend.rs b/codex-rs/ext/goal/tests/goal_extension_backend.rs index 28e55064fd..cdeacbebe6 100644 --- a/codex-rs/ext/goal/tests/goal_extension_backend.rs +++ b/codex-rs/ext/goal/tests/goal_extension_backend.rs @@ -625,6 +625,7 @@ fn tool_call(tool_name: &str, call_id: &str, arguments: serde_json::Value) -> To call_id: call_id.to_string(), tool_name: codex_extension_api::ToolName::plain(tool_name), truncation_policy: TruncationPolicy::Bytes(1024), + conversation_history: codex_extension_api::ConversationHistory::default(), payload: ToolPayload::Function { arguments: arguments.to_string(), }, diff --git a/codex-rs/ext/memories/src/tests.rs b/codex-rs/ext/memories/src/tests.rs index e88d7d8db9..c2e90e6520 100644 --- a/codex-rs/ext/memories/src/tests.rs +++ b/codex-rs/ext/memories/src/tests.rs @@ -139,6 +139,7 @@ async fn read_tool_reads_memory_file() { call_id: "call-1".to_string(), tool_name: memory_tool_name(crate::READ_TOOL_NAME), truncation_policy: TruncationPolicy::Bytes(1024), + conversation_history: codex_extension_api::ConversationHistory::default(), payload: payload.clone(), }) .await @@ -183,6 +184,7 @@ async fn search_tool_accepts_multiple_queries() { call_id: "call-1".to_string(), tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME), truncation_policy: TruncationPolicy::Bytes(1024), + conversation_history: codex_extension_api::ConversationHistory::default(), payload: payload.clone(), }) .await @@ -253,6 +255,7 @@ async fn search_tool_accepts_windowed_all_match_mode() { call_id: "call-1".to_string(), tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME), truncation_policy: TruncationPolicy::Bytes(1024), + conversation_history: codex_extension_api::ConversationHistory::default(), payload: payload.clone(), }) .await @@ -303,6 +306,7 @@ async fn search_tool_rejects_legacy_single_query() { call_id: "call-1".to_string(), tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME), truncation_policy: TruncationPolicy::Bytes(1024), + conversation_history: codex_extension_api::ConversationHistory::default(), payload, }) .await; diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 7b64776dca..c141bfb37a 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -57,6 +57,7 @@ pub use responses_api::dynamic_tool_to_responses_api_tool; pub use responses_api::mcp_tool_to_deferred_responses_api_tool; pub use responses_api::mcp_tool_to_responses_api_tool; pub use responses_api::tool_definition_to_responses_api_tool; +pub use tool_call::ConversationHistory; pub use tool_call::ToolCall; pub use tool_config::ShellCommandBackendConfig; pub use tool_config::ToolEnvironmentMode; diff --git a/codex-rs/tools/src/tool_call.rs b/codex-rs/tools/src/tool_call.rs index f92c92f979..32d428648f 100644 --- a/codex-rs/tools/src/tool_call.rs +++ b/codex-rs/tools/src/tool_call.rs @@ -1,7 +1,27 @@ use crate::FunctionCallError; use crate::ToolName; use crate::ToolPayload; +use codex_protocol::models::ResponseItem; use codex_utils_output_truncation::TruncationPolicy; +use std::sync::Arc; + +/// Raw response history snapshot available when an extension tool is invoked. +#[derive(Clone, Debug, Default)] +pub struct ConversationHistory { + items: Arc<[ResponseItem]>, +} + +impl ConversationHistory { + pub fn new(items: Vec) -> Self { + Self { + items: items.into(), + } + } + + pub fn items(&self) -> &[ResponseItem] { + &self.items + } +} // TODO: this is temporary and will disappear in the next PR (as we make codex-extension-api generic on Invocation. #[derive(Clone, Debug)] @@ -10,6 +30,7 @@ pub struct ToolCall { pub call_id: String, pub tool_name: ToolName, pub truncation_policy: TruncationPolicy, + pub conversation_history: ConversationHistory, pub payload: ToolPayload, }