[feat] persist dynamic tools in session rollout file (#10130)

Add dynamic tools to rollout file for persistence & read from rollout on
resume. Ran a real example and spotted the following in the rollout
file:
```
{"timestamp":"2026-01-29T01:27:57.468Z","type":"session_meta","payload":{"id":"019c075d-3f0b-77e3-894e-c1c159b04b1e","timestamp":"2026-01-29T01:27:57.451Z","...."dynamic_tools":[{"name":"demo_tool","description":"Demo dynamic tool","inputSchema":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"}}],"git":{"commit_hash":"ebc573f15c01b8af158e060cfedd401f043e9dfa","branch":"dev/cc/dynamic-tools","repository_url":"https://github.com/openai/codex.git"}}}
```
This commit is contained in:
Celia Chen
2026-01-29 17:10:00 -08:00
committed by GitHub
parent c6e1288ef1
commit 7151387474
7 changed files with 42 additions and 0 deletions

View File

@@ -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,

View File

@@ -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(),
),
)
}

View File

@@ -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,

View File

@@ -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<ThreadId>,
source: SessionSource,
base_instructions: BaseInstructions,
dynamic_tools: Vec<DynamicToolSpec>,
},
Resume {
path: PathBuf,
@@ -91,12 +93,14 @@ impl RolloutRecorderParams {
forked_from_id: Option<ThreadId>,
source: SessionSource,
base_instructions: BaseInstructions,
dynamic_tools: Vec<DynamicToolSpec>,
) -> 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)
},
}),
)
}

View File

@@ -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,
}),

View File

@@ -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,
};

View File

@@ -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<Vec<DynamicToolSpec>> {
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<PathBuf> {
@@ -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<BaseInstructions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
}
impl Default for SessionMeta {
@@ -1613,6 +1632,7 @@ impl Default for SessionMeta {
source: SessionSource::default(),
model_provider: None,
base_instructions: None,
dynamic_tools: None,
}
}
}