mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
## Why MCP tool parsing eagerly cloned structured output schemas and built full call-result envelopes. Cloning tool definitions also copied their output-schema JSON, even before a consumer needed it. ## What changed - Introduce `ToolOutputSchema` with immutable `Arc` storage so tool definitions share output schemas when cloned. - Retain MCP structured output schemas and materialize the call-result envelope only when JSON is requested. - Update code-mode consumers and schema mutation sites to materialize JSON explicitly, reusing uniquely owned storage when possible. - Move structured content into the MCP envelope without an extra clone, preserving property order. ## Testing Add tests for JSON preservation, mutation isolation, equality between lazy and materialized schemas, reuse of uniquely owned storage, and equivalent code-mode definitions. GitOrigin-RevId: e98ba4c2f0efedc99f7cbc7bba206cc63a3bd8f4
114 lines
3.8 KiB
Rust
114 lines
3.8 KiB
Rust
use std::sync::Arc;
|
|
|
|
use codex_extension_api::FunctionCallError;
|
|
use codex_extension_api::ResponsesApiTool;
|
|
use codex_extension_api::ToolCall;
|
|
use codex_extension_api::ToolExecutor;
|
|
use codex_extension_api::ToolName;
|
|
use codex_extension_api::ToolSpec;
|
|
use codex_extension_api::parse_tool_input_schema;
|
|
use codex_otel::MetricsClient;
|
|
use codex_tools::ResponsesApiNamespace;
|
|
use codex_tools::ResponsesApiNamespaceTool;
|
|
use codex_tools::default_namespace_description;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde_json::Value;
|
|
|
|
use crate::MEMORY_TOOLS_NAMESPACE;
|
|
use crate::backend::MemoriesBackend;
|
|
use crate::backend::MemoriesBackendError;
|
|
use crate::schema;
|
|
|
|
mod ad_hoc_note;
|
|
mod list;
|
|
mod read;
|
|
mod search;
|
|
|
|
pub(crate) fn memory_tools<B>(
|
|
backend: B,
|
|
metrics_client: Option<MetricsClient>,
|
|
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>>
|
|
where
|
|
B: MemoriesBackend,
|
|
{
|
|
vec![
|
|
Arc::new(ad_hoc_note::AddAdHocNoteTool {
|
|
backend: backend.clone(),
|
|
metrics_client: metrics_client.clone(),
|
|
}),
|
|
Arc::new(list::ListTool {
|
|
backend: backend.clone(),
|
|
metrics_client: metrics_client.clone(),
|
|
}),
|
|
Arc::new(read::ReadTool {
|
|
backend: backend.clone(),
|
|
metrics_client: metrics_client.clone(),
|
|
}),
|
|
Arc::new(search::SearchTool {
|
|
backend,
|
|
metrics_client,
|
|
}),
|
|
]
|
|
}
|
|
|
|
pub(super) fn memory_tool_name(name: &str) -> ToolName {
|
|
ToolName::namespaced(MEMORY_TOOLS_NAMESPACE, name)
|
|
}
|
|
|
|
pub(super) fn memory_function_tool<I: JsonSchema, O: JsonSchema>(
|
|
name: &str,
|
|
description: &str,
|
|
) -> ToolSpec {
|
|
let tool = ResponsesApiTool {
|
|
name: name.to_string(),
|
|
description: description.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: parse_tool_input_schema(&schema::input_schema_for::<I>())
|
|
.unwrap_or_else(|err| panic!("generated input schema for {name} should parse: {err}")),
|
|
output_schema: Some(schema::output_schema_for::<O>().into()),
|
|
};
|
|
|
|
ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: MEMORY_TOOLS_NAMESPACE.to_string(),
|
|
description: default_namespace_description(MEMORY_TOOLS_NAMESPACE),
|
|
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
|
|
})
|
|
}
|
|
|
|
fn parse_args<T: for<'de> Deserialize<'de>>(call: &ToolCall<'_>) -> Result<T, FunctionCallError> {
|
|
let arguments = call.function_arguments()?;
|
|
let value = if arguments.trim().is_empty() {
|
|
Value::Object(serde_json::Map::new())
|
|
} else {
|
|
serde_json::from_str(arguments)
|
|
.map_err(|err| FunctionCallError::RespondToModel(err.to_string()))?
|
|
};
|
|
serde_json::from_value(value).map_err(|err| FunctionCallError::RespondToModel(err.to_string()))
|
|
}
|
|
|
|
fn clamp_max_results(requested: Option<usize>, default: usize, max: usize) -> usize {
|
|
requested.unwrap_or(default).clamp(1, max)
|
|
}
|
|
|
|
fn backend_error_to_function_call(err: MemoriesBackendError) -> FunctionCallError {
|
|
match err {
|
|
MemoriesBackendError::InvalidPath { .. }
|
|
| MemoriesBackendError::InvalidCursor { .. }
|
|
| MemoriesBackendError::InvalidFilename { .. }
|
|
| MemoriesBackendError::NotFound { .. }
|
|
| MemoriesBackendError::InvalidLineOffset
|
|
| MemoriesBackendError::InvalidMaxLines
|
|
| MemoriesBackendError::LineOffsetExceedsFileLength
|
|
| MemoriesBackendError::NotFile { .. }
|
|
| MemoriesBackendError::EmptyQuery
|
|
| MemoriesBackendError::EmptyAdHocNote
|
|
| MemoriesBackendError::AdHocNoteAlreadyExists { .. }
|
|
| MemoriesBackendError::InvalidMatchWindow => {
|
|
FunctionCallError::RespondToModel(err.to_string())
|
|
}
|
|
MemoriesBackendError::Io(_) => FunctionCallError::Fatal(err.to_string()),
|
|
}
|
|
}
|