Represent dynamic tools with explicit namespaces internally (#27365)

Follow-up to #27356.

## Stack note

This PR changes Codex's internal dynamic-tool shape while leaving
`thread/start` unchanged. App-server therefore converts the existing
per-tool input into explicit functions and namespaces before passing it
to core.

[#27371](https://github.com/openai/codex/pull/27371) updates
`thread/start` to use the same explicit shape and removes this temporary
conversion.

## Why

Dynamic tools repeat namespace metadata on every function. Core should
keep one explicit namespace with its member tools so descriptions and
membership stay consistent across sessions and runtime planning.

## What changed

- Represent dynamic tools as top-level functions or explicit namespaces
in protocol and session state.
- Read old flat rollout metadata and write the canonical hierarchy.
- Flatten namespace members only when registering callable tools.
- Keep `thread/start.dynamicTools` flat for now and normalize it at the
app-server boundary.

New builds can read old rollout metadata. Older builds cannot read newly
written hierarchical metadata.

## Test plan

- `just test -p codex-app-server
thread_start_normalizes_legacy_dynamic_tools_into_model_request`
- `just test -p codex-protocol
session_meta_normalizes_legacy_dynamic_tools`
- `just test -p codex-core
resume_restores_dynamic_tools_from_rollout_with_sqlite_enabled`
- `just test -p codex-core
tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call`
- `just test -p codex-core code_mode_can_call_hidden_dynamic_tools`
- `just test -p codex-tools`
This commit is contained in:
sayan-oai
2026-06-15 08:06:14 -07:00
committed by GitHub
Unverified
parent b3f6f70b68
commit a292faae5a
20 changed files with 656 additions and 279 deletions
+58 -1
View File
@@ -2872,7 +2872,11 @@ 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")]
#[serde(
default,
deserialize_with = "crate::dynamic_tools::deserialize_dynamic_tool_specs",
skip_serializing_if = "Option::is_none"
)]
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mode: Option<String>,
@@ -4122,6 +4126,59 @@ mod tests {
Ok(())
}
#[test]
fn session_meta_normalizes_legacy_dynamic_tools() -> Result<()> {
let mut value = serde_json::to_value(SessionMeta::default())?;
value["dynamic_tools"] = json!([
{
"namespace": "legacy_app",
"name": "lookup_ticket",
"description": "Look up a ticket",
"inputSchema": {"type": "object", "properties": {}},
"exposeToContext": false
},
{
"namespace": "legacy_app",
"name": "update_ticket",
"description": "Update a ticket",
"inputSchema": {"type": "object", "properties": {}},
"deferLoading": false,
"exposeToContext": false
}
]);
let meta: SessionMeta = serde_json::from_value(value)?;
assert_eq!(
meta.dynamic_tools,
Some(vec![DynamicToolSpec::Namespace(
crate::dynamic_tools::DynamicToolNamespaceSpec {
name: "legacy_app".to_string(),
description: String::new(),
tools: vec![
crate::dynamic_tools::DynamicToolNamespaceTool::Function(
crate::dynamic_tools::DynamicToolFunctionSpec {
name: "lookup_ticket".to_string(),
description: "Look up a ticket".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
defer_loading: true,
},
),
crate::dynamic_tools::DynamicToolNamespaceTool::Function(
crate::dynamic_tools::DynamicToolFunctionSpec {
name: "update_ticket".to_string(),
description: "Update a ticket".to_string(),
input_schema: json!({"type": "object", "properties": {}}),
defer_loading: false,
},
),
],
},
)])
);
Ok(())
}
fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {
let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots
.into_iter()