mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Add output_schema to code mode render (#17210)
This updates code-mode tool rendering so MCP tools can surface
structured output types from their `outputSchema`.
What changed:
- Detect MCP tool-call result wrappers from the output schema shape
instead of relying on tool-name parsing or provenance flags.
- Render shared TypeScript aliases once for MCP tool results
(`CallToolResult`, `ContentBlock`, etc.) so multiple MCP tool
declarations stay compact.
- Type `structuredContent` from the tool definition's `outputSchema`
instead of rendering it as `unknown`.
- Update the shared MCP aliases to match the MCP draft `CallToolResult`
schema more closely.
Example:
- Before: `declare const tools: { mcp__rmcp__echo(args: { env_var?:
string; message: string; }): Promise<{ _meta?: unknown; content:
Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };`
- After: `declare const tools: { mcp__rmcp__echo(args: { env_var?:
string; message: string; }): Promise<CallToolResult<{ echo: string; env:
string | null; }>>; };`
This commit is contained in:
@@ -39,6 +39,83 @@ const WAIT_DESCRIPTION_TEMPLATE: &str = r#"- Use `wait` only after `exec` return
|
||||
- `wait` returns only the new output since the last yield, or the final completion or termination result for that cell.
|
||||
- If the cell is still running, `wait` may yield again with the same `cell_id`.
|
||||
- If the cell has already finished, `wait` returns the completed result and closes the cell."#;
|
||||
// Based off of https://modelcontextprotocol.io/specification/draft/schema#calltoolresult
|
||||
const MCP_TYPESCRIPT_PREAMBLE: &str = r#"type Role = "user" | "assistant";
|
||||
type MetaObject = Record<string, unknown>;
|
||||
type Annotations = {
|
||||
audience?: Role[];
|
||||
priority?: number;
|
||||
lastModified?: string;
|
||||
};
|
||||
type Icon = {
|
||||
src: string;
|
||||
mimeType?: string;
|
||||
sizes?: string[];
|
||||
theme?: "light" | "dark";
|
||||
};
|
||||
type TextResourceContents = {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
_meta?: MetaObject;
|
||||
text: string;
|
||||
};
|
||||
type BlobResourceContents = {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
_meta?: MetaObject;
|
||||
blob: string;
|
||||
};
|
||||
type TextContent = {
|
||||
type: "text";
|
||||
text: string;
|
||||
annotations?: Annotations;
|
||||
_meta?: MetaObject;
|
||||
};
|
||||
type ImageContent = {
|
||||
type: "image";
|
||||
data: string;
|
||||
mimeType: string;
|
||||
annotations?: Annotations;
|
||||
_meta?: MetaObject;
|
||||
};
|
||||
type AudioContent = {
|
||||
type: "audio";
|
||||
data: string;
|
||||
mimeType: string;
|
||||
annotations?: Annotations;
|
||||
_meta?: MetaObject;
|
||||
};
|
||||
type ResourceLink = {
|
||||
icons?: Icon[];
|
||||
name: string;
|
||||
title?: string;
|
||||
uri: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
annotations?: Annotations;
|
||||
size?: number;
|
||||
_meta?: MetaObject;
|
||||
type: "resource_link";
|
||||
};
|
||||
type EmbeddedResource = {
|
||||
type: "resource";
|
||||
resource: TextResourceContents | BlobResourceContents;
|
||||
annotations?: Annotations;
|
||||
_meta?: MetaObject;
|
||||
};
|
||||
type ContentBlock =
|
||||
| TextContent
|
||||
| ImageContent
|
||||
| AudioContent
|
||||
| ResourceLink
|
||||
| EmbeddedResource;
|
||||
type CallToolResult<TStructured = { [key: string]: unknown }> = {
|
||||
_meta?: MetaObject;
|
||||
content: ContentBlock[];
|
||||
isError?: boolean;
|
||||
structuredContent?: TStructured;
|
||||
[key: string]: unknown;
|
||||
};"#;
|
||||
|
||||
pub const CODE_MODE_PRAGMA_PREFIX: &str = "// @exec:";
|
||||
|
||||
@@ -169,7 +246,7 @@ pub fn is_code_mode_nested_tool(tool_name: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn build_exec_tool_description(
|
||||
enabled_tools: &[(String, String)],
|
||||
enabled_tools: &[ToolDefinition],
|
||||
namespace_descriptions: &BTreeMap<String, ToolNamespaceDescription>,
|
||||
code_mode_only: bool,
|
||||
) -> String {
|
||||
@@ -185,8 +262,13 @@ pub fn build_exec_tool_description(
|
||||
if !enabled_tools.is_empty() {
|
||||
let mut current_namespace: Option<&str> = None;
|
||||
let mut nested_tool_sections = Vec::with_capacity(enabled_tools.len());
|
||||
let has_mcp_tools = enabled_tools
|
||||
.iter()
|
||||
.any(|tool| mcp_structured_content_schema(tool.output_schema.as_ref()).is_some());
|
||||
|
||||
for (name, nested_description) in enabled_tools {
|
||||
for tool in enabled_tools {
|
||||
let name = tool.name.as_str();
|
||||
let nested_description = render_code_mode_sample_for_definition(tool);
|
||||
let next_namespace = namespace_descriptions
|
||||
.get(name)
|
||||
.map(|namespace_description| namespace_description.name.as_str());
|
||||
@@ -206,14 +288,20 @@ pub fn build_exec_tool_description(
|
||||
let global_name = normalize_code_mode_identifier(name);
|
||||
let nested_description = nested_description.trim();
|
||||
if nested_description.is_empty() {
|
||||
nested_tool_sections.push(format!("### `{global_name}` (`{name}`)"));
|
||||
nested_tool_sections.push(render_tool_heading(&global_name, name));
|
||||
} else {
|
||||
nested_tool_sections.push(format!(
|
||||
"### `{global_name}` (`{name}`)\n{nested_description}"
|
||||
"{}\n{nested_description}",
|
||||
render_tool_heading(&global_name, name)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if has_mcp_tools {
|
||||
sections.push(format!(
|
||||
"Shared MCP Types:\n```ts\n{MCP_TYPESCRIPT_PREAMBLE}\n```"
|
||||
));
|
||||
}
|
||||
let nested_tool_reference = nested_tool_sections.join("\n\n");
|
||||
sections.push(nested_tool_reference);
|
||||
}
|
||||
@@ -251,7 +339,7 @@ pub fn normalize_code_mode_identifier(tool_key: &str) -> String {
|
||||
|
||||
pub fn augment_tool_definition(mut definition: ToolDefinition) -> ToolDefinition {
|
||||
if definition.name != PUBLIC_TOOL_NAME {
|
||||
definition.description = append_code_mode_sample_for_definition(&definition);
|
||||
definition.description = render_code_mode_sample_for_definition(&definition);
|
||||
}
|
||||
definition
|
||||
}
|
||||
@@ -273,7 +361,7 @@ pub struct EnabledToolMetadata {
|
||||
pub kind: CodeModeToolKind,
|
||||
}
|
||||
|
||||
pub fn append_code_mode_sample(
|
||||
pub fn render_code_mode_sample(
|
||||
description: &str,
|
||||
tool_name: &str,
|
||||
input_name: &str,
|
||||
@@ -287,7 +375,7 @@ pub fn append_code_mode_sample(
|
||||
format!("{description}\n\nexec tool declaration:\n```ts\n{declaration}\n```")
|
||||
}
|
||||
|
||||
fn append_code_mode_sample_for_definition(definition: &ToolDefinition) -> String {
|
||||
fn render_code_mode_sample_for_definition(definition: &ToolDefinition) -> String {
|
||||
let input_name = match definition.kind {
|
||||
CodeModeToolKind::Function => "args",
|
||||
CodeModeToolKind::Freeform => "input",
|
||||
@@ -300,12 +388,23 @@ fn append_code_mode_sample_for_definition(definition: &ToolDefinition) -> String
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
CodeModeToolKind::Freeform => "string".to_string(),
|
||||
};
|
||||
let output_type = definition
|
||||
.output_schema
|
||||
.as_ref()
|
||||
.map(render_json_schema_to_typescript)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
append_code_mode_sample(
|
||||
let output_type = if let Some(structured_content_schema) =
|
||||
mcp_structured_content_schema(definition.output_schema.as_ref())
|
||||
{
|
||||
let structured_content_type = render_json_schema_to_typescript(structured_content_schema);
|
||||
if structured_content_type == "unknown" {
|
||||
"CallToolResult".to_string()
|
||||
} else {
|
||||
format!("CallToolResult<{structured_content_type}>")
|
||||
}
|
||||
} else {
|
||||
definition
|
||||
.output_schema
|
||||
.as_ref()
|
||||
.map(render_json_schema_to_typescript)
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
};
|
||||
render_code_mode_sample(
|
||||
&definition.description,
|
||||
&definition.name,
|
||||
input_name,
|
||||
@@ -324,10 +423,59 @@ fn render_code_mode_tool_declaration(
|
||||
format!("{tool_name}({input_name}: {input_type}): Promise<{output_type}>;")
|
||||
}
|
||||
|
||||
fn render_tool_heading(global_name: &str, raw_name: &str) -> String {
|
||||
if global_name == raw_name {
|
||||
format!("### `{global_name}`")
|
||||
} else {
|
||||
format!("### `{global_name}` (`{raw_name}`)")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_json_schema_to_typescript(schema: &JsonValue) -> String {
|
||||
render_json_schema_to_typescript_inner(schema)
|
||||
}
|
||||
|
||||
fn mcp_structured_content_schema(output_schema: Option<&JsonValue>) -> Option<&JsonValue> {
|
||||
let output_schema = output_schema?;
|
||||
let properties = output_schema
|
||||
.get("properties")
|
||||
.and_then(JsonValue::as_object)?;
|
||||
let content_schema = properties.get("content").and_then(JsonValue::as_object)?;
|
||||
if content_schema.get("type").and_then(JsonValue::as_str) != Some("array") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if content_schema
|
||||
.get("items")
|
||||
.and_then(JsonValue::as_object)
|
||||
.is_none_or(|items| items.get("type").and_then(JsonValue::as_str) != Some("object"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if properties
|
||||
.get("isError")
|
||||
.and_then(JsonValue::as_object)
|
||||
.is_none_or(|schema| schema.get("type").and_then(JsonValue::as_str) != Some("boolean"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if properties
|
||||
.get("_meta")
|
||||
.and_then(JsonValue::as_object)
|
||||
.is_none_or(|schema| schema.get("type").and_then(JsonValue::as_str) != Some("object"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
properties
|
||||
.get("structuredContent")
|
||||
.unwrap_or(&JsonValue::Bool(true)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_json_schema_to_typescript_inner(schema: &JsonValue) -> String {
|
||||
match schema {
|
||||
JsonValue::Bool(true) => "unknown".to_string(),
|
||||
@@ -559,9 +707,29 @@ mod tests {
|
||||
use super::normalize_code_mode_identifier;
|
||||
use super::parse_exec_source;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn mcp_call_tool_result_schema(structured_content_schema: JsonValue) -> JsonValue {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"structuredContent": structured_content_schema,
|
||||
"isError": { "type": "boolean" },
|
||||
"_meta": { "type": "object" }
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exec_source_without_pragma() {
|
||||
assert_eq!(
|
||||
@@ -676,11 +844,20 @@ mod tests {
|
||||
#[test]
|
||||
fn code_mode_only_description_includes_nested_tools() {
|
||||
let description = build_exec_tool_description(
|
||||
&[("foo".to_string(), "bar".to_string())],
|
||||
&[ToolDefinition {
|
||||
name: "foo".to_string(),
|
||||
description: "bar".to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
}],
|
||||
&BTreeMap::new(),
|
||||
/*code_mode_only*/ true,
|
||||
);
|
||||
assert!(description.contains("### `foo` (`foo`)"));
|
||||
assert!(description.contains(
|
||||
"### `foo`
|
||||
bar"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -711,22 +888,47 @@ mod tests {
|
||||
]);
|
||||
let description = build_exec_tool_description(
|
||||
&[
|
||||
("mcp__sample__alpha".to_string(), "First tool".to_string()),
|
||||
("mcp__sample__beta".to_string(), "Second tool".to_string()),
|
||||
ToolDefinition {
|
||||
name: "mcp__sample__alpha".to_string(),
|
||||
description: "First tool".to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
})),
|
||||
output_schema: Some(mcp_call_tool_result_schema(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}))),
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "mcp__sample__beta".to_string(),
|
||||
description: "Second tool".to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
})),
|
||||
output_schema: Some(mcp_call_tool_result_schema(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}))),
|
||||
},
|
||||
],
|
||||
&namespace_descriptions,
|
||||
/*code_mode_only*/ true,
|
||||
);
|
||||
assert_eq!(description.matches("## mcp__sample").count(), 1);
|
||||
assert!(description.contains("## mcp__sample\nShared namespace guidance."));
|
||||
assert!(description.contains(
|
||||
r#"## mcp__sample
|
||||
Shared namespace guidance.
|
||||
|
||||
### `mcp__sample__alpha` (`mcp__sample__alpha`)
|
||||
First tool
|
||||
|
||||
### `mcp__sample__beta` (`mcp__sample__beta`)
|
||||
Second tool"#
|
||||
"declare const tools: { mcp__sample__alpha(args: {}): Promise<CallToolResult<{}>>; };"
|
||||
));
|
||||
assert!(description.contains(
|
||||
"declare const tools: { mcp__sample__beta(args: {}): Promise<CallToolResult<{}>>; };"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -740,12 +942,125 @@ Second tool"#
|
||||
},
|
||||
)]);
|
||||
let description = build_exec_tool_description(
|
||||
&[("mcp__sample__alpha".to_string(), "First tool".to_string())],
|
||||
&[ToolDefinition {
|
||||
name: "mcp__sample__alpha".to_string(),
|
||||
description: "First tool".to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
})),
|
||||
output_schema: Some(mcp_call_tool_result_schema(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}))),
|
||||
}],
|
||||
&namespace_descriptions,
|
||||
/*code_mode_only*/ true,
|
||||
);
|
||||
|
||||
assert!(!description.contains("## mcp__sample"));
|
||||
assert!(description.contains("### `mcp__sample__alpha` (`mcp__sample__alpha`)"));
|
||||
assert!(description.contains("### `mcp__sample__alpha`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_only_description_renders_shared_mcp_types_once() {
|
||||
let first_tool = augment_tool_definition(ToolDefinition {
|
||||
name: "mcp__sample__alpha".to_string(),
|
||||
description: "First tool".to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
})),
|
||||
output_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"structuredContent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"echo": { "type": "string" }
|
||||
},
|
||||
"required": ["echo"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"isError": { "type": "boolean" },
|
||||
"_meta": { "type": "object" }
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": false
|
||||
})),
|
||||
});
|
||||
let second_tool = augment_tool_definition(ToolDefinition {
|
||||
name: "mcp__sample__beta".to_string(),
|
||||
description: "Second tool".to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
})),
|
||||
output_schema: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"structuredContent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": { "type": "integer" }
|
||||
},
|
||||
"required": ["count"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"isError": { "type": "boolean" },
|
||||
"_meta": { "type": "object" }
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": false
|
||||
})),
|
||||
});
|
||||
|
||||
let description = build_exec_tool_description(
|
||||
&[
|
||||
ToolDefinition {
|
||||
name: first_tool.name,
|
||||
description: "First tool".to_string(),
|
||||
kind: first_tool.kind,
|
||||
input_schema: first_tool.input_schema,
|
||||
output_schema: first_tool.output_schema,
|
||||
},
|
||||
ToolDefinition {
|
||||
name: second_tool.name,
|
||||
description: "Second tool".to_string(),
|
||||
kind: second_tool.kind,
|
||||
input_schema: second_tool.input_schema,
|
||||
output_schema: second_tool.output_schema,
|
||||
},
|
||||
],
|
||||
&BTreeMap::new(),
|
||||
/*code_mode_only*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
description
|
||||
.matches("type CallToolResult<TStructured = { [key: string]: unknown }>")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(description.matches("Shared MCP Types:").count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ pub use description::CODE_MODE_PRAGMA_PREFIX;
|
||||
pub use description::CodeModeToolKind;
|
||||
pub use description::ToolDefinition;
|
||||
pub use description::ToolNamespaceDescription;
|
||||
pub use description::append_code_mode_sample;
|
||||
pub use description::augment_tool_definition;
|
||||
pub use description::build_exec_tool_description;
|
||||
pub use description::build_wait_tool_description;
|
||||
pub use description::is_code_mode_nested_tool;
|
||||
pub use description::normalize_code_mode_identifier;
|
||||
pub use description::parse_exec_source;
|
||||
pub use description::render_code_mode_sample;
|
||||
pub use description::render_json_schema_to_typescript;
|
||||
pub use response::FunctionCallOutputContentItem;
|
||||
pub use response::ImageDetail;
|
||||
|
||||
@@ -2280,7 +2280,14 @@ text(JSON.stringify(tool));
|
||||
parsed,
|
||||
serde_json::json!({
|
||||
"name": "mcp__rmcp__echo",
|
||||
"description": "Echo back the provided message and include environment data.\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };\n```",
|
||||
"description": concat!(
|
||||
"Echo back the provided message and include environment data.\n\n",
|
||||
"exec tool declaration:\n",
|
||||
"```ts\n",
|
||||
"declare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): ",
|
||||
"Promise<CallToolResult<{ echo: string; env: string | null; }>>; };\n",
|
||||
"```",
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -45,11 +45,29 @@ impl TestToolServer {
|
||||
}))
|
||||
.expect("echo tool schema should deserialize");
|
||||
|
||||
Tool::new(
|
||||
let mut tool = Tool::new(
|
||||
Cow::Borrowed("echo"),
|
||||
Cow::Borrowed("Echo back the provided message and include environment data."),
|
||||
Arc::new(schema),
|
||||
)
|
||||
);
|
||||
#[expect(clippy::expect_used)]
|
||||
let output_schema: JsonObject = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"echo": { "type": "string" },
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["echo", "env"],
|
||||
"additionalProperties": false
|
||||
}))
|
||||
.expect("echo tool output schema should deserialize");
|
||||
tool.output_schema = Some(Arc::new(output_schema));
|
||||
tool
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,23 @@ impl TestToolServer {
|
||||
Cow::Borrowed(description),
|
||||
Arc::new(schema),
|
||||
);
|
||||
#[expect(clippy::expect_used)]
|
||||
let output_schema: JsonObject = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"echo": { "type": "string" },
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["echo", "env"],
|
||||
"additionalProperties": false
|
||||
}))
|
||||
.expect("echo tool output schema should deserialize");
|
||||
tool.output_schema = Some(Arc::new(output_schema));
|
||||
tool.annotations = Some(ToolAnnotations::new().read_only(true));
|
||||
tool
|
||||
}
|
||||
|
||||
@@ -90,6 +90,23 @@ impl TestToolServer {
|
||||
Cow::Borrowed("Echo back the provided message and include environment data."),
|
||||
Arc::new(schema),
|
||||
);
|
||||
#[expect(clippy::expect_used)]
|
||||
let output_schema: JsonObject = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"echo": { "type": "string" },
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["echo", "env"],
|
||||
"additionalProperties": false
|
||||
}))
|
||||
.expect("echo tool output schema should deserialize");
|
||||
tool.output_schema = Some(Arc::new(output_schema));
|
||||
tool.annotations = Some(ToolAnnotations::new().read_only(true));
|
||||
tool
|
||||
}
|
||||
|
||||
@@ -49,6 +49,19 @@ pub fn collect_code_mode_tool_definitions<'a>(
|
||||
tool_definitions
|
||||
}
|
||||
|
||||
pub fn collect_code_mode_exec_prompt_tool_definitions<'a>(
|
||||
specs: impl IntoIterator<Item = &'a ToolSpec>,
|
||||
) -> Vec<CodeModeToolDefinition> {
|
||||
let mut tool_definitions = specs
|
||||
.into_iter()
|
||||
.filter_map(code_mode_tool_definition_for_spec)
|
||||
.filter(|definition| codex_code_mode::is_code_mode_nested_tool(&definition.name))
|
||||
.collect::<Vec<_>>();
|
||||
tool_definitions.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
tool_definitions.dedup_by(|left, right| left.name == right.name);
|
||||
tool_definitions
|
||||
}
|
||||
|
||||
pub fn create_wait_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
@@ -95,7 +108,7 @@ pub fn create_wait_tool() -> ToolSpec {
|
||||
}
|
||||
|
||||
pub fn create_code_mode_tool(
|
||||
enabled_tools: &[(String, String)],
|
||||
enabled_tools: &[CodeModeToolDefinition],
|
||||
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
|
||||
code_mode_only_enabled: bool,
|
||||
) -> ToolSpec {
|
||||
|
||||
@@ -182,7 +182,13 @@ fn create_wait_tool_matches_expected_spec() {
|
||||
|
||||
#[test]
|
||||
fn create_code_mode_tool_matches_expected_spec() {
|
||||
let enabled_tools = vec![("update_plan".to_string(), "Update the plan".to_string())];
|
||||
let enabled_tools = vec![codex_code_mode::ToolDefinition {
|
||||
name: "update_plan".to_string(),
|
||||
description: "Update the plan".to_string(),
|
||||
kind: codex_code_mode::CodeModeToolKind::Function,
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
create_code_mode_tool(
|
||||
|
||||
@@ -44,6 +44,7 @@ pub use apply_patch_tool::ApplyPatchToolArgs;
|
||||
pub use apply_patch_tool::create_apply_patch_freeform_tool;
|
||||
pub use apply_patch_tool::create_apply_patch_json_tool;
|
||||
pub use code_mode::augment_tool_spec_for_code_mode;
|
||||
pub use code_mode::collect_code_mode_exec_prompt_tool_definitions;
|
||||
pub use code_mode::collect_code_mode_tool_definitions;
|
||||
pub use code_mode::create_code_mode_tool;
|
||||
pub use code_mode::create_wait_tool;
|
||||
|
||||
@@ -42,13 +42,17 @@ pub fn mcp_call_tool_result_output_schema(structured_content_schema: JsonValue)
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"structuredContent": structured_content_schema,
|
||||
"isError": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"_meta": {}
|
||||
"_meta": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::ToolSpec;
|
||||
use crate::ToolsConfig;
|
||||
use crate::ViewImageToolOptions;
|
||||
use crate::WebSearchToolOptions;
|
||||
use crate::collect_code_mode_tool_definitions;
|
||||
use crate::collect_code_mode_exec_prompt_tool_definitions;
|
||||
use crate::collect_tool_search_source_infos;
|
||||
use crate::collect_tool_suggest_entries;
|
||||
use crate::create_apply_patch_freeform_tool;
|
||||
@@ -93,17 +93,14 @@ pub fn build_tool_registry_plan(
|
||||
..params
|
||||
},
|
||||
);
|
||||
let mut enabled_tools = collect_code_mode_tool_definitions(
|
||||
let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions(
|
||||
nested_plan
|
||||
.specs
|
||||
.iter()
|
||||
.map(|configured_tool| &configured_tool.spec),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|tool| (tool.name, tool.description))
|
||||
.collect::<Vec<_>>();
|
||||
enabled_tools.sort_by(|(left_name, _), (right_name, _)| {
|
||||
compare_code_mode_tool_names(left_name, right_name, &namespace_descriptions)
|
||||
);
|
||||
enabled_tools.sort_by(|left, right| {
|
||||
compare_code_mode_tool_names(&left.name, &right.name, &namespace_descriptions)
|
||||
});
|
||||
plan.push_spec(
|
||||
create_code_mode_tool(
|
||||
|
||||
@@ -1608,7 +1608,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
||||
|
||||
exec tool declaration:
|
||||
```ts
|
||||
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
|
||||
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<CallToolResult>; };
|
||||
```"#
|
||||
);
|
||||
}
|
||||
@@ -1694,7 +1694,7 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
|
||||
assert!(description.contains(
|
||||
r#"exec tool declaration:
|
||||
```ts
|
||||
declare const tools: { mcp__sample__fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
|
||||
declare const tools: { mcp__sample__fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<CallToolResult>; };
|
||||
```"#
|
||||
));
|
||||
}
|
||||
@@ -1769,8 +1769,8 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() {
|
||||
assert!(description.starts_with(
|
||||
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
|
||||
));
|
||||
assert!(description.contains("### `update_plan` (`update_plan`)"));
|
||||
assert!(description.contains("### `view_image` (`view_image`)"));
|
||||
assert!(description.contains("### `update_plan`"));
|
||||
assert!(description.contains("### `view_image`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1804,8 +1804,8 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only(
|
||||
assert!(!description.starts_with(
|
||||
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
|
||||
));
|
||||
assert!(!description.contains("### `update_plan` (`update_plan`)"));
|
||||
assert!(!description.contains("### `view_image` (`view_image`)"));
|
||||
assert!(!description.contains("### `update_plan`"));
|
||||
assert!(!description.contains("### `view_image`"));
|
||||
}
|
||||
|
||||
fn model_info() -> ModelInfo {
|
||||
@@ -1919,6 +1919,78 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() {
|
||||
let model_info = model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CodeMode);
|
||||
features.enable(Feature::CodeModeOnly);
|
||||
features.enable(Feature::UnifiedExec);
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
|
||||
let mut tool = mcp_tool(
|
||||
"echo",
|
||||
"Echo text",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"required": ["message"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
);
|
||||
tool.output_schema = Some(std::sync::Arc::new(rmcp::model::object(
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"echo": {"type": "string"},
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["echo", "env"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
)));
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([("mcp__sample__echo".to_string(), tool)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
||||
&find_tool(&tools, "mcp__sample__echo").spec
|
||||
else {
|
||||
panic!("expected function tool");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
description,
|
||||
r#"Echo text
|
||||
|
||||
exec tool declaration:
|
||||
```ts
|
||||
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<CallToolResult<{ echo: string; env: string | null; }>>; };
|
||||
```"#
|
||||
);
|
||||
}
|
||||
|
||||
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
||||
let slug = name.replace(' ', "-").to_lowercase();
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
|
||||
Reference in New Issue
Block a user