diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 26316eddbd..14dce02c64 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -81,6 +81,7 @@ pub fn create_fake_rollout_with_source( source, model_provider: model_provider.map(str::to_string), base_instructions: None, + dynamic_tools: None, }; let payload = serde_json::to_value(SessionMetaLine { meta, @@ -159,6 +160,7 @@ pub fn create_fake_rollout_with_text_elements( source: SessionSource::Cli, model_provider: model_provider.map(str::to_string), base_instructions: None, + dynamic_tools: None, }; let payload = serde_json::to_value(SessionMetaLine { meta, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 1b7b7312c7..1fb5572183 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -324,6 +324,12 @@ impl Codex { .clone() .or_else(|| conversation_history.get_base_instructions().map(|s| s.text)) .unwrap_or_else(|| model_info.get_model_instructions(config.model_personality)); + // Respect explicit thread-start tools; fall back to persisted tools when resuming a thread. + let dynamic_tools = if dynamic_tools.is_empty() { + conversation_history.get_dynamic_tools().unwrap_or_default() + } else { + dynamic_tools + }; // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode // to avoid extracting these fields separately and constructing CollaborationMode here. @@ -715,6 +721,7 @@ impl Session { BaseInstructions { text: session_configuration.base_instructions.clone(), }, + session_configuration.dynamic_tools.clone(), ), ) } diff --git a/codex-rs/core/src/rollout/metadata.rs b/codex-rs/core/src/rollout/metadata.rs index d08e77e241..5523274bde 100644 --- a/codex-rs/core/src/rollout/metadata.rs +++ b/codex-rs/core/src/rollout/metadata.rs @@ -303,6 +303,7 @@ mod tests { source: SessionSource::default(), model_provider: Some("openai".to_string()), base_instructions: None, + dynamic_tools: None, }; let session_meta_line = SessionMetaLine { meta: session_meta, diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index cc35850544..8157133165 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -7,6 +7,7 @@ use std::path::Path; use std::path::PathBuf; use codex_protocol::ThreadId; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::BaseInstructions; use serde_json::Value; use time::OffsetDateTime; @@ -68,6 +69,7 @@ pub enum RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: BaseInstructions, + dynamic_tools: Vec, }, Resume { path: PathBuf, @@ -91,12 +93,14 @@ impl RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: BaseInstructions, + dynamic_tools: Vec, ) -> Self { Self::Create { conversation_id, forked_from_id, source, base_instructions, + dynamic_tools, } } @@ -259,6 +263,7 @@ impl RolloutRecorder { forked_from_id, source, base_instructions, + dynamic_tools, } => { let LogFileInfo { file, @@ -288,6 +293,11 @@ impl RolloutRecorder { source, model_provider: Some(config.model_provider_id.clone()), base_instructions: Some(base_instructions), + dynamic_tools: if dynamic_tools.is_empty() { + None + } else { + Some(dynamic_tools) + }, }), ) } diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 34afae2c94..cdf16291d3 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -873,6 +873,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { source: SessionSource::VSCode, model_provider: Some("test-provider".into()), base_instructions: None, + dynamic_tools: None, }, git: None, }), diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index bf3b5a4f51..2582f90cbe 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -93,6 +93,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { source: SessionSource::default(), model_provider: None, base_instructions: None, + dynamic_tools: None, }, git: None, }; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index aea7d3f013..7ba01fa4bd 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -20,6 +20,7 @@ use crate::config_types::WindowsSandboxLevel; use crate::custom_prompts::CustomPrompt; use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; +use crate::dynamic_tools::DynamicToolSpec; use crate::items::TurnItem; use crate::message_history::HistoryEntry; use crate::models::BaseInstructions; @@ -1513,6 +1514,22 @@ impl InitialHistory { }), } } + + pub fn get_dynamic_tools(&self) -> Option> { + match self { + InitialHistory::New => None, + InitialHistory::Resumed(resumed) => { + resumed.history.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(), + _ => None, + }) + } + InitialHistory::Forked(items) => items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(), + _ => None, + }), + } + } } fn session_cwd_from_items(items: &[RolloutItem]) -> Option { @@ -1599,6 +1616,8 @@ pub struct SessionMeta { /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions /// from ModelsManager. pub base_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_tools: Option>, } impl Default for SessionMeta { @@ -1613,6 +1632,7 @@ impl Default for SessionMeta { source: SessionSource::default(), model_provider: None, base_instructions: None, + dynamic_tools: None, } } }