Skip to main content

codex_protocol/
dynamic_tools.rs

1use schemars::JsonSchema;
2use serde::Deserialize;
3use serde::Deserializer;
4use serde::Serialize;
5use serde::de::Error as _;
6use serde_json::Value as JsonValue;
7use std::collections::HashMap;
8use ts_rs::TS;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
11#[serde(tag = "type", rename_all = "camelCase")]
12#[ts(tag = "type", export_to = "v2/")]
13pub enum DynamicToolSpec {
14    Function(DynamicToolFunctionSpec),
15    Namespace(DynamicToolNamespaceSpec),
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
19#[serde(rename_all = "camelCase")]
20#[ts(export_to = "v2/")]
21pub struct DynamicToolFunctionSpec {
22    pub name: String,
23    pub description: String,
24    pub input_schema: JsonValue,
25    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
26    pub defer_loading: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
30#[serde(rename_all = "camelCase")]
31#[ts(export_to = "v2/")]
32pub struct DynamicToolNamespaceSpec {
33    pub name: String,
34    pub description: String,
35    pub tools: Vec<DynamicToolNamespaceTool>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
39#[serde(tag = "type", rename_all = "camelCase")]
40#[ts(tag = "type", export_to = "v2/")]
41pub enum DynamicToolNamespaceTool {
42    Function(DynamicToolFunctionSpec),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
46#[serde(rename_all = "camelCase")]
47pub struct DynamicToolCallRequest {
48    pub call_id: String,
49    pub turn_id: String,
50    #[serde(default)]
51    pub started_at_ms: i64,
52    #[serde(default)]
53    pub namespace: Option<String>,
54    pub tool: String,
55    pub arguments: JsonValue,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
59#[serde(rename_all = "camelCase")]
60pub struct DynamicToolResponse {
61    pub content_items: Vec<DynamicToolCallOutputContentItem>,
62    pub success: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
66#[serde(tag = "type", rename_all = "camelCase")]
67#[ts(tag = "type")]
68pub enum DynamicToolCallOutputContentItem {
69    #[serde(rename_all = "camelCase")]
70    InputText { text: String },
71    #[serde(rename_all = "camelCase")]
72    InputImage { image_url: String },
73    #[serde(rename_all = "camelCase")]
74    InputAudio { audio_url: String },
75}
76
77/// Former flat `SessionMeta` shape, including the old `exposeToContext` flag.
78/// Kept so new builds can resume sessions written before explicit namespaces.
79#[derive(Deserialize)]
80#[serde(rename_all = "camelCase")]
81struct LegacyDynamicToolSpec {
82    namespace: Option<String>,
83    name: String,
84    description: String,
85    input_schema: JsonValue,
86    defer_loading: Option<bool>,
87    expose_to_context: Option<bool>,
88}
89
90pub fn normalize_dynamic_tool_specs(
91    values: Vec<JsonValue>,
92) -> Result<Vec<DynamicToolSpec>, serde_json::Error> {
93    let has_legacy_fields = |value: &JsonValue| {
94        value.get("namespace").is_some()
95            || value.get("exposeToContext").is_some()
96            || value.get("type").is_none()
97    };
98    let has_legacy_format = values.iter().any(|value| {
99        has_legacy_fields(value)
100            || value
101                .get("tools")
102                .and_then(JsonValue::as_array)
103                .is_some_and(|tools| tools.iter().any(&has_legacy_fields))
104    });
105    let has_canonical_format = values.iter().any(|value| value.get("type").is_some());
106    if has_legacy_format && has_canonical_format {
107        return Err(serde_json::Error::custom(
108            "dynamic tools must use either canonical or legacy format consistently",
109        ));
110    }
111    if !has_legacy_format {
112        return values.into_iter().map(serde_json::from_value).collect();
113    }
114
115    let tools = values
116        .into_iter()
117        .map(|value| {
118            let tool: LegacyDynamicToolSpec = serde_json::from_value(value)?;
119            let function = DynamicToolFunctionSpec {
120                name: tool.name,
121                description: tool.description,
122                input_schema: tool.input_schema,
123                defer_loading: tool.defer_loading.unwrap_or_else(|| {
124                    tool.expose_to_context
125                        .map(|visible| !visible)
126                        .unwrap_or(false)
127                }),
128            };
129            Ok((tool.namespace, function))
130        })
131        .collect::<Result<Vec<_>, serde_json::Error>>()?;
132    Ok(group_dynamic_tools_by_namespace(tools))
133}
134
135pub fn group_dynamic_tools_by_namespace(
136    tools: Vec<(Option<String>, DynamicToolFunctionSpec)>,
137) -> Vec<DynamicToolSpec> {
138    let mut grouped_tools = Vec::with_capacity(tools.len());
139    let mut namespace_indices = HashMap::<String, usize>::new();
140    for (namespace, function) in tools {
141        let Some(namespace) = namespace else {
142            grouped_tools.push(DynamicToolSpec::Function(function));
143            continue;
144        };
145        let function = DynamicToolNamespaceTool::Function(function);
146        if let Some(index) = namespace_indices.get(&namespace).copied() {
147            let DynamicToolSpec::Namespace(namespace) = &mut grouped_tools[index] else {
148                unreachable!("namespace index must point to a namespace");
149            };
150            namespace.tools.push(function);
151            continue;
152        }
153        namespace_indices.insert(namespace.clone(), grouped_tools.len());
154        grouped_tools.push(DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
155            name: namespace,
156            description: String::new(),
157            tools: vec![function],
158        }));
159    }
160    grouped_tools
161}
162
163pub fn deserialize_dynamic_tool_specs<'de, D>(
164    deserializer: D,
165) -> Result<Option<Vec<DynamicToolSpec>>, D::Error>
166where
167    D: Deserializer<'de>,
168{
169    let Some(values) = Option::<Vec<JsonValue>>::deserialize(deserializer)? else {
170        return Ok(None);
171    };
172    normalize_dynamic_tool_specs(values)
173        .map(Some)
174        .map_err(D::Error::custom)
175}