Skip to main content

agentic_core/tool/
codex.rs

1use std::collections::{HashMap, HashSet};
2
3use serde_json::Value;
4
5use crate::types::io::{FunctionTool, FunctionToolCall, OutputItem, ToolChoice};
6use crate::types::tools::{CodexNamespaceMember, CodexNamespaceToolParam, NonEmptyToolName, ResponsesTool};
7
8use super::handler::{ToolError, ToolHandler};
9use super::registry::ToolType;
10
11// Upstream Responses-compatible backends only see flat function names. Prefix
12// flattened Codex namespace members so generated names are recognizable,
13// unlikely to collide with user functions, and can be restored to
14// `{ namespace, name }` on the way back to the client.
15pub const MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX: &str = "agentic_ns__";
16
17#[must_use]
18pub fn model_visible_namespace_member_name(namespace: &str, member: &str) -> String {
19    format!("{MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX}{namespace}__{member}")
20}
21
22#[derive(Clone, Debug, Eq, Hash, PartialEq)]
23struct NamespaceMemberName {
24    namespace: String,
25    name: String,
26}
27
28#[derive(Clone, Debug)]
29struct NamespaceCallMapping {
30    member: NamespaceMemberName,
31    upstream_name: String,
32}
33
34/// A pre-built, reusable namespace rename map, computed once per request from
35/// the declared tools via [`CodexNamespaceHandler::build_namespace_map`].
36///
37/// Passing this into [`CodexNamespaceHandler::restore_output_items`],
38/// [`CodexNamespaceHandler::restore_response_value`], and
39/// [`CodexNamespaceHandler::resolve_tool_choice`] avoids
40/// rebuilding the map on every call — important for streaming responses,
41/// which call the restore path once per SSE line.
42#[derive(Clone, Debug, Default)]
43pub struct NamespaceMap {
44    calls: HashMap<String, NamespaceCallMapping>,
45    members: HashMap<NamespaceMemberName, String>,
46}
47
48impl NamespaceMap {
49    fn mapping_for_call(&self, name: &str) -> Option<&NamespaceCallMapping> {
50        self.calls.get(name)
51    }
52
53    fn mapping_for_member(&self, namespace: &str, name: &str) -> Option<&NamespaceCallMapping> {
54        let member = NamespaceMemberName {
55            namespace: namespace.to_string(),
56            name: name.to_string(),
57        };
58        self.members
59            .get(&member)
60            .and_then(|upstream_name| self.calls.get(upstream_name))
61    }
62}
63
64#[derive(Default)]
65struct NamespaceMapBuilder {
66    top_level_names: HashSet<String>,
67    map: NamespaceMap,
68}
69
70impl NamespaceMapBuilder {
71    fn new(top_level_names: HashSet<String>) -> Self {
72        Self {
73            top_level_names,
74            ..Self::default()
75        }
76    }
77
78    fn validate_and_record_flat_member(
79        &mut self,
80        namespace_name: &str,
81        member_name: &str,
82    ) -> Result<String, ToolError> {
83        let flat_name = model_visible_namespace_member_name(namespace_name, member_name);
84        if self.top_level_names.contains(&flat_name) {
85            return Err(ToolError::Config(format!(
86                "codex namespace member {namespace_name}.{member_name} collides with top-level function {flat_name}"
87            )));
88        }
89        if let Some(existing) = self.map.calls.get(&flat_name) {
90            if existing.member.namespace != namespace_name || existing.member.name != member_name {
91                return Err(ToolError::Config(format!(
92                    "codex namespace member {namespace_name}.{member_name} collides with {}.{} at generated name {flat_name}",
93                    existing.member.namespace, existing.member.name
94                )));
95            }
96        }
97        Ok(self.record_flat_member_with_flat_name(namespace_name, member_name, flat_name))
98    }
99
100    fn record_flat_member_with_flat_name(
101        &mut self,
102        namespace_name: &str,
103        member_name: &str,
104        flat_name: String,
105    ) -> String {
106        let member = NamespaceMemberName {
107            namespace: namespace_name.to_string(),
108            name: member_name.to_string(),
109        };
110        if let Some(existing) = self.map.calls.get(&flat_name) {
111            debug_assert!(
112                existing.member == member,
113                "namespace collisions must be validated before recording namespace members"
114            );
115            if existing.member != member {
116                tracing::warn!(
117                    upstream_name = %flat_name,
118                    namespace = %namespace_name,
119                    member = %member_name,
120                    existing_namespace = %existing.member.namespace,
121                    existing_member = %existing.member.name,
122                    "generated codex namespace member name collides with another namespace member"
123                );
124            }
125        }
126        let mapping = NamespaceCallMapping {
127            member: member.clone(),
128            upstream_name: flat_name.clone(),
129        };
130
131        self.map.members.insert(member, flat_name.clone());
132        self.map.calls.insert(flat_name.clone(), mapping);
133        flat_name
134    }
135
136    fn finish(self) -> NamespaceMap {
137        self.map
138    }
139}
140
141/// Handler for Codex `type: "namespace"` tools.
142///
143/// Namespace tools are client-owned, like plain function tools, but need a
144/// request-scoped normalization pass to flatten members into model-visible
145/// function names and restore model calls back to the public namespace shape.
146#[derive(Debug)]
147pub struct CodexNamespaceHandler;
148
149impl CodexNamespaceHandler {
150    /// Rewrites every `Namespace` tool's function members to their flat,
151    /// model-visible names (see [`model_visible_namespace_member_name`]),
152    /// given real collision detection against sibling top-level tool names.
153    ///
154    /// Tools stay `ResponsesTool::Namespace` — only the nested members'
155    /// `name` fields change — so [`ResponsesTool::to_function_tools`] and
156    /// [`super::registry::ToolRegistry::build_with_handlers`] can read each
157    /// member's already-flat name directly, with no further namespace logic.
158    ///
159    /// Request execution must handle the result so ambiguous declarations fail
160    /// instead of being normalized into an irreversible flat shape.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`ToolError::Config`] when a generated namespace member name
165    /// collides with a top-level function tool or with another namespace
166    /// member.
167    pub fn resolve_namespace_members(&self, tools: &[ResponsesTool]) -> Result<Vec<ResponsesTool>, ToolError> {
168        let mut builder = NamespaceMapBuilder::new(typed_top_level_tool_names(tools));
169        tools
170            .iter()
171            .map(|tool| match tool {
172                ResponsesTool::Namespace(namespace) => {
173                    rename_namespace_members(namespace, &mut builder).map(ResponsesTool::Namespace)
174                }
175                other => Ok(other.clone()),
176            })
177            .collect()
178    }
179
180    /// Builds a [`NamespaceMap`] once from a request's declared tools, for
181    /// reuse across every subsequent restore/rewrite call on that request —
182    /// see [`NamespaceMap`]'s docs for why this matters.
183    /// # Errors
184    ///
185    /// Returns [`ToolError::Config`] when a generated namespace member name
186    /// collides with a top-level function tool or with another namespace
187    /// member.
188    pub fn build_namespace_map(&self, tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
189        namespace_map_from_tools(tools)
190    }
191
192    /// Rejects namespace declarations that cannot be represented
193    /// unambiguously by the gateway-owned flat model-visible naming scheme.
194    ///
195    /// This must run before building the upstream request or request-scoped
196    /// registry. Otherwise the gateway could silently drop a namespace member
197    /// or restore a flat model call to the wrong public `{ namespace, name }`.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`ToolError::Config`] when a generated namespace member name
202    /// collides with a top-level function tool or with another namespace
203    /// member.
204    pub fn validate_namespace_collisions(&self, tools: Option<&[ResponsesTool]>) -> Result<(), ToolError> {
205        let Some(tools) = tools else {
206            return Ok(());
207        };
208        let mut builder = NamespaceMapBuilder::new(typed_top_level_tool_names(tools));
209        for tool in tools {
210            let ResponsesTool::Namespace(namespace) = tool else {
211                continue;
212            };
213            for member_name in typed_function_member_names(namespace) {
214                builder.validate_and_record_flat_member(&namespace.name, &member_name)?;
215            }
216        }
217        Ok(())
218    }
219
220    /// Resolves the request's `tool_choice` (defaulting to `ToolChoice::Auto`
221    /// when absent) and, if it's a namespaced `ToolChoice::Function {
222    /// namespace, name }`, rewrites it to the flattened, model-visible name
223    /// that [`ResponsesTool::to_function_tools`] produces for the matching
224    /// namespace member — so `tool_choice` agrees with the tool names
225    /// actually sent upstream.
226    ///
227    /// A no-op for `ToolChoice` variants other than `Function`, and for a
228    /// `Function` choice that doesn't match any declared namespace member.
229    #[must_use]
230    pub fn resolve_tool_choice(&self, map: Option<&NamespaceMap>, tool_choice: Option<&ToolChoice>) -> ToolChoice {
231        let tool_choice = tool_choice.unwrap_or(&ToolChoice::Auto);
232        let Some(map) = map else {
233            return tool_choice.clone();
234        };
235        rewrite_tool_choice_with_map(tool_choice, map)
236    }
237
238    pub fn restore_output_items(&self, output: &mut [OutputItem], map: Option<&NamespaceMap>) {
239        let Some(map) = map else {
240            return;
241        };
242        for item in output {
243            if let OutputItem::FunctionCall(call) = item {
244                restore_function_call_with_map(call, map);
245            }
246        }
247    }
248
249    #[must_use]
250    pub fn restore_response_value(&self, value: &mut Value, map: Option<&NamespaceMap>) -> bool {
251        let Some(map) = map else {
252            return false;
253        };
254        restore_response_value_with_map(value, map)
255    }
256}
257
258impl ToolHandler for CodexNamespaceHandler {
259    fn tool_type(&self) -> ToolType {
260        ToolType::CodexNamespace
261    }
262
263    fn validate(&self, param: &Value) -> Result<(), ToolError> {
264        serde_json::from_value::<CodexNamespaceToolParam>(param.clone())
265            .map(|_| ())
266            .map_err(|e| ToolError::Config(format!("invalid codex namespace tool config: {e}")))
267    }
268
269    /// Converts an already-renamed namespace's function members straight to
270    /// `FunctionTool`s. Callers must rename members to their flat, model-visible
271    /// names first via [`CodexNamespaceHandler::resolve_namespace_members`] —
272    /// this method has no sibling-tool context to do that itself.
273    fn normalize(&self, param: &Value) -> Vec<FunctionTool> {
274        let Ok(namespace) = serde_json::from_value::<CodexNamespaceToolParam>(param.clone()) else {
275            tracing::warn!("normalize() called with invalid codex namespace param - validate() must be called first");
276            return vec![];
277        };
278        namespace
279            .tools
280            .iter()
281            .filter_map(|member| match member {
282                CodexNamespaceMember::Function(function) => Some(FunctionTool::from(function)),
283                CodexNamespaceMember::Unknown => None,
284            })
285            .collect()
286    }
287}
288
289fn namespace_map_from_tools(tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
290    let Some(tools) = tools else {
291        return Ok(None);
292    };
293    let mut builder = NamespaceMapBuilder::new(typed_top_level_tool_names(tools));
294    for tool in tools {
295        if let ResponsesTool::Namespace(namespace) = tool {
296            let _ = rename_namespace_members(namespace, &mut builder)?;
297        }
298    }
299    Ok(Some(builder.finish()))
300}
301
302/// Returns `namespace` with its function members' names rewritten to their
303/// flat, model-visible form, recording each rename in `builder` along the
304/// way.
305///
306/// # Errors
307///
308/// Returns [`ToolError::Config`] when a generated namespace member name
309/// collides with a top-level function tool or with another namespace member.
310fn rename_namespace_members(
311    namespace: &CodexNamespaceToolParam,
312    builder: &mut NamespaceMapBuilder,
313) -> Result<CodexNamespaceToolParam, ToolError> {
314    let function_member_names = typed_function_member_names(namespace);
315    if function_member_names.is_empty() {
316        tracing::debug!(
317            namespace = %namespace.name,
318            "namespace tool has no function members to rename for upstream"
319        );
320        return Ok(namespace.clone());
321    }
322    let tools = namespace
323        .tools
324        .iter()
325        .map(|member| {
326            let CodexNamespaceMember::Function(function) = member else {
327                return Ok(member.clone());
328            };
329            let flat_name_text = builder.validate_and_record_flat_member(&namespace.name, function.name.as_str())?;
330            let flat_name = NonEmptyToolName::try_from(flat_name_text.clone())
331                .expect("generated namespace member names include a non-empty prefix");
332            tracing::debug!(
333                namespace = %namespace.name,
334                member = %function.name.as_str(),
335                upstream_name = %flat_name_text,
336                "renamed namespace tool member for upstream"
337            );
338            let mut function = function.clone();
339            function.name = flat_name;
340            Ok(CodexNamespaceMember::Function(function))
341        })
342        .collect::<Result<Vec<_>, ToolError>>()?;
343
344    Ok(CodexNamespaceToolParam {
345        tools,
346        ..namespace.clone()
347    })
348}
349
350fn typed_top_level_tool_names(tools: &[ResponsesTool]) -> HashSet<String> {
351    tools
352        .iter()
353        .filter_map(|tool| match tool {
354            ResponsesTool::Function(function) => Some(function.name.as_str().to_string()),
355            ResponsesTool::Mcp(_)
356            | ResponsesTool::WebSearch(_)
357            | ResponsesTool::FileSearch(_)
358            | ResponsesTool::CodeInterpreter(_)
359            | ResponsesTool::Namespace(_)
360            | ResponsesTool::Unknown => None,
361        })
362        .collect()
363}
364
365fn typed_function_member_names(namespace: &CodexNamespaceToolParam) -> Vec<String> {
366    namespace
367        .tools
368        .iter()
369        .filter_map(|member| match member {
370            CodexNamespaceMember::Function(function) => Some(function.name.as_str().to_string()),
371            CodexNamespaceMember::Unknown => None,
372        })
373        .collect()
374}
375
376fn restore_function_call_with_map(call: &mut FunctionToolCall, map: &NamespaceMap) -> bool {
377    if call.namespace.is_some() {
378        return false;
379    }
380    let Some(mapping) = map.mapping_for_call(&call.name) else {
381        return false;
382    };
383    let original_name = call.name.clone();
384
385    call.namespace = Some(mapping.member.namespace.clone());
386    call.name.clone_from(&mapping.member.name);
387    tracing::debug!(
388        upstream_name = %original_name,
389        namespace = %mapping.member.namespace,
390        member = %mapping.member.name,
391        "restored upstream namespace function call"
392    );
393    true
394}
395
396fn rewrite_tool_choice_with_map(choice: &ToolChoice, map: &NamespaceMap) -> ToolChoice {
397    let ToolChoice::Function { namespace, name } = choice else {
398        return choice.clone();
399    };
400    let mapping = namespace
401        .as_deref()
402        .and_then(|namespace| map.mapping_for_member(namespace, name.as_str()))
403        .or_else(|| {
404            namespace
405                .is_none()
406                .then(|| map.mapping_for_call(name.as_str()))
407                .flatten()
408        });
409    let Some(mapping) = mapping else {
410        return choice.clone();
411    };
412    let Ok(name) = NonEmptyToolName::try_from(mapping.upstream_name.clone()) else {
413        return choice.clone();
414    };
415
416    ToolChoice::Function { namespace: None, name }
417}
418
419fn restore_response_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool {
420    let mut changed = false;
421
422    if let Some(item) = value.as_object_mut().and_then(|object| object.get_mut("item")) {
423        changed |= restore_call_value_with_map(item, map);
424    }
425
426    changed |= restore_call_value_with_map(value, map);
427
428    for key in ["response", "payload"] {
429        if let Some(nested) = value.as_object_mut().and_then(|object| object.get_mut(key)) {
430            changed |= restore_response_value_with_map(nested, map);
431        }
432    }
433
434    if let Some(Value::Array(items)) = value.as_object_mut().and_then(|object| object.get_mut("output")) {
435        for item in items {
436            changed |= restore_call_value_with_map(item, map);
437        }
438    }
439
440    changed
441}
442
443fn restore_call_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool {
444    let Some(object) = value.as_object_mut() else {
445        return false;
446    };
447    if object.get("type").and_then(Value::as_str) != Some("function_call") {
448        return false;
449    }
450    if object.get("namespace").and_then(Value::as_str).is_some() {
451        return false;
452    }
453    let Some(name) = object.get("name").and_then(Value::as_str) else {
454        return false;
455    };
456    let Some(mapping) = map.mapping_for_call(name) else {
457        return false;
458    };
459    let original_name = name.to_string();
460
461    object.insert("namespace".to_string(), Value::String(mapping.member.namespace.clone()));
462    object.insert("name".to_string(), Value::String(mapping.member.name.clone()));
463    tracing::debug!(
464        upstream_name = %original_name,
465        namespace = %mapping.member.namespace,
466        member = %mapping.member.name,
467        "restored upstream namespace function call"
468    );
469    true
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use crate::types::event::MessageStatus;
476
477    fn completed_call(name: &str, arguments: &str) -> OutputItem {
478        OutputItem::FunctionCall(FunctionToolCall {
479            id: "fc_1".to_string(),
480            call_id: "call_1".to_string(),
481            name: name.to_string(),
482            namespace: None,
483            arguments: arguments.to_string(),
484            status: MessageStatus::Completed,
485        })
486    }
487
488    #[test]
489    fn unqualified_function_tool_choice_is_not_rewritten_to_namespace_member() {
490        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
491            {
492                "type": "namespace",
493                "name": "mcp__shell",
494                "tools": [{"type": "function", "name": "run"}]
495            }
496        ]))
497        .unwrap();
498        let choice = ToolChoice::Function {
499            namespace: None,
500            name: NonEmptyToolName::try_from("run").unwrap(),
501        };
502
503        let map = CodexNamespaceHandler
504            .build_namespace_map(Some(&tools))
505            .expect("valid namespace map");
506        let rewritten = CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice));
507
508        assert_eq!(
509            rewritten,
510            ToolChoice::Function {
511                namespace: None,
512                name: NonEmptyToolName::try_from("run").unwrap()
513            }
514        );
515        let resolved = CodexNamespaceHandler
516            .resolve_namespace_members(&tools)
517            .expect("valid namespace members");
518        assert!(matches!(
519            resolved.as_slice(),
520            [ResponsesTool::Namespace(namespace)]
521                if matches!(&namespace.tools[0], CodexNamespaceMember::Function(f) if f.name.as_str() == "agentic_ns__mcp__shell__run")
522        ));
523    }
524
525    #[test]
526    fn namespaced_function_tool_choice_flattens_exact_member() {
527        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
528            {
529                "type": "namespace",
530                "name": "mcp__shell",
531                "tools": [{"type": "function", "name": "run"}]
532            },
533            {
534                "type": "namespace",
535                "name": "mcp__git",
536                "tools": [{"type": "function", "name": "run"}]
537            }
538        ]))
539        .unwrap();
540        let choice: ToolChoice = serde_json::from_value(serde_json::json!({
541            "type": "function",
542            "namespace": "mcp__git",
543            "name": "run"
544        }))
545        .unwrap();
546
547        let map = CodexNamespaceHandler
548            .build_namespace_map(Some(&tools))
549            .expect("valid namespace map");
550        let rewritten = CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice));
551
552        assert_eq!(
553            rewritten,
554            ToolChoice::Function {
555                namespace: None,
556                name: NonEmptyToolName::try_from("agentic_ns__mcp__git__run").unwrap()
557            }
558        );
559    }
560
561    #[test]
562    fn validate_namespace_collisions_rejects_top_level_flat_name_collision() {
563        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
564            {"type": "function", "name": "agentic_ns__mcp__shell__run"},
565            {
566                "type": "namespace",
567                "name": "mcp__shell",
568                "tools": [{"type": "function", "name": "run"}]
569            }
570        ]))
571        .unwrap();
572
573        let err = CodexNamespaceHandler
574            .validate_namespace_collisions(Some(&tools))
575            .unwrap_err();
576
577        assert!(err.to_string().contains("collides with top-level function"));
578    }
579
580    #[test]
581    fn resolve_namespace_members_rejects_top_level_flat_name_collision() {
582        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
583            {"type": "function", "name": "agentic_ns__mcp__shell__run"},
584            {
585                "type": "namespace",
586                "name": "mcp__shell",
587                "tools": [{"type": "function", "name": "run"}]
588            }
589        ]))
590        .unwrap();
591
592        let err = CodexNamespaceHandler.resolve_namespace_members(&tools).unwrap_err();
593
594        assert!(err.to_string().contains("collides with top-level function"));
595    }
596
597    #[test]
598    fn validate_namespace_collisions_rejects_generated_name_collision_between_namespace_members() {
599        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
600            {
601                "type": "namespace",
602                "name": "a__b",
603                "tools": [{"type": "function", "name": "c"}]
604            },
605            {
606                "type": "namespace",
607                "name": "a",
608                "tools": [{"type": "function", "name": "b__c"}]
609            }
610        ]))
611        .unwrap();
612
613        let err = CodexNamespaceHandler
614            .validate_namespace_collisions(Some(&tools))
615            .unwrap_err();
616
617        assert!(err.to_string().contains("generated name"));
618    }
619
620    #[test]
621    #[cfg(debug_assertions)]
622    #[should_panic(expected = "namespace collisions must be validated before recording namespace members")]
623    fn namespace_map_builder_debug_asserts_when_member_collision_validation_is_skipped() {
624        let mut builder = NamespaceMapBuilder::new(HashSet::new());
625
626        assert_eq!(
627            builder.record_flat_member_with_flat_name("a__b", "c", "agentic_ns__a__b__c".to_owned()),
628            "agentic_ns__a__b__c"
629        );
630        let _ = builder.record_flat_member_with_flat_name("a", "b__c", "agentic_ns__a__b__c".to_owned());
631    }
632
633    #[test]
634    fn flat_namespace_member_call_preserves_tools_argument() {
635        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
636            {
637                "type": "namespace",
638                "name": "mcp__agentic_fixture",
639                "tools": [{"type": "function", "name": "run"}]
640            }
641        ]))
642        .unwrap();
643        let mut output = vec![completed_call(
644            "agentic_ns__mcp__agentic_fixture__run",
645            "{\"tools\":\"legitimate\",\"cmd\":\"pwd\"}",
646        )];
647
648        let map = CodexNamespaceHandler
649            .build_namespace_map(Some(&tools))
650            .expect("valid namespace map");
651        CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
652
653        let OutputItem::FunctionCall(call) = &output[0] else {
654            panic!("expected function call");
655        };
656        assert_eq!(call.namespace.as_deref(), Some("mcp__agentic_fixture"));
657        assert_eq!(call.name, "run");
658        assert_eq!(call.arguments, "{\"tools\":\"legitimate\",\"cmd\":\"pwd\"}");
659    }
660
661    #[test]
662    fn plain_function_call_round_trip() {
663        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
664            {
665                "type": "function",
666                "name": "get_weather",
667                "parameters": {"type": "object"}
668            }
669        ]))
670        .unwrap();
671        let resolved = CodexNamespaceHandler
672            .resolve_namespace_members(&tools)
673            .expect("valid namespace members");
674        let mut output = vec![completed_call("get_weather", "{\"city\":\"SF\"}")];
675
676        let map = CodexNamespaceHandler
677            .build_namespace_map(Some(&tools))
678            .expect("valid namespace map");
679        CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
680
681        assert!(matches!(
682            resolved.as_slice(),
683            [ResponsesTool::Function(function)] if function.name.as_str() == "get_weather"
684        ));
685        let OutputItem::FunctionCall(call) = &output[0] else {
686            panic!("expected function call");
687        };
688        assert!(call.namespace.is_none());
689        assert_eq!(call.name, "get_weather");
690        assert_eq!(call.arguments, "{\"city\":\"SF\"}");
691    }
692
693    #[test]
694    fn response_value_normalizes_nested_function_call_item() {
695        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
696            {
697                "type": "namespace",
698                "name": "mcp__agentic_fixture",
699                "tools": [{"type": "function", "name": "add_numbers"}]
700            }
701        ]))
702        .unwrap();
703        let mut value = serde_json::json!({
704            "type": "response.output_item.done",
705            "item": {
706                "type": "function_call",
707                "name": "agentic_ns__mcp__agentic_fixture__add_numbers",
708                "call_id": "call_1",
709                "arguments": "{\"numbers\":[8,0]}"
710            }
711        });
712
713        let map = CodexNamespaceHandler
714            .build_namespace_map(Some(&tools))
715            .expect("valid namespace map");
716        assert!(CodexNamespaceHandler.restore_response_value(&mut value, map.as_ref()));
717        assert_eq!(value["item"]["namespace"], "mcp__agentic_fixture");
718        assert_eq!(value["item"]["name"], "add_numbers");
719        assert_eq!(value["item"]["arguments"], "{\"numbers\":[8,0]}");
720    }
721}