Support model-catalog overrides for Code Mode tool messages (#47118)

## What changed

- Add `tools.code_mode` catalog messages for `exec`, `wait`, deferred-tool guidance, and shared MCP TypeScript definitions.
- Render `{{ default_exec_yield_time_ms }}` and `{{ image_helper }}` in `exec` descriptions while preserving runtime tool declarations and the freeform grammar.
- Resolve `wait` descriptions and parameter schemas independently, falling back to bundled parameters for invalid or unsupported schemas.
- Use the active model's Code Mode messages, including after mid-turn model changes. Missing overrides retain bundled defaults; empty text overrides suppress the corresponding text.

## Testing

Add coverage for literal template substitution, runtime section preservation, independent `wait` overrides and fallback behavior, mid-turn model changes, and a yielded cell completing through `wait` with catalog messages.

GitOrigin-RevId: e542a0cd47a7138a414871192cee47790ed1214e
This commit is contained in:
rhan-oai
2026-09-21 22:49:17 +00:00
committed by copyberry
parent 3fd5160cd6
commit 5aa4939be4
19 changed files with 652 additions and 48 deletions
+45 -15
View File
@@ -1,4 +1,8 @@
//! Composes Code Mode descriptions, retaining runtime-owned tool declarations.
//! Exec templates substitute only the documented literal placeholders.
use codex_protocol::ToolName;
use codex_protocol::openai_models::CodeModeToolMessages;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
@@ -20,14 +24,14 @@ const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/co
- Runs raw JavaScript -- no Node, no file system, no network access, no console.
- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences.
- You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`.
- `yield_time_ms` asks `exec` to yield early if the script is still running. Defaults to 10000 ms.
- `yield_time_ms` asks `exec` to yield early if the script is still running. Defaults to {{ default_exec_yield_time_ms }} ms.
- `max_output_tokens` sets the token budget for direct `exec` results. Defaults to 10000 tokens.
- When the JS code is fully evaluated, the isolate's lifetime ends and unawaited promises are silently discarded.
- Global helpers:
- `exit()`: Immediately ends the current script successfully (like an early return from the top level).
- `text(value: string | number | boolean | undefined | null)`: Appends a text item. Non-string values are stringified with `JSON.stringify(...)` when possible.
- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null } | ImageContent, detail?: "auto" | "low" | "high" | "original" | null)`: Appends an image item. `image_url` should be a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument.
- {{ image_helper }}
- `audio(audioUrlOrItem: string | { audio_url: string } | AudioContent)`: Appends an audio item. `audio_url` should be a base64-encoded `data:` URL. To forward an MCP tool audio block, pass an individual `AudioContent` block from `result.content`, for example `audio(result.content[0])`.
- `generatedImage(result: { image_url: string; output_hint?: string })`: Appends an image-generation result and its optional output hint. HTTP(S) URLs are not supported.
- `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session.
@@ -265,20 +269,32 @@ pub fn build_exec_tool_description(
default_exec_yield_time_ms: u64,
code_mode_only: bool,
image_detail_visibility: ImageDetailVisibility,
messages: Option<&CodeModeToolMessages>,
) -> String {
let mut sections = Vec::new();
sections.push(EXEC_DESCRIPTION_TEMPLATE.replace(
"Defaults to 10000 ms.",
&format!("Defaults to {default_exec_yield_time_ms} ms."),
));
if image_detail_visibility == ImageDetailVisibility::Hidden {
sections[0] = sections[0].replace(
LEGACY_IMAGE_HELPER_DESCRIPTION,
UNIFIED_IMAGE_HELPER_DESCRIPTION,
);
let image_helper = match image_detail_visibility {
ImageDetailVisibility::Visible => LEGACY_IMAGE_HELPER_DESCRIPTION,
ImageDetailVisibility::Hidden => UNIFIED_IMAGE_HELPER_DESCRIPTION,
};
let description = messages
.and_then(|messages| messages.exec.as_ref())
.and_then(|exec| exec.description.as_deref())
.unwrap_or(EXEC_DESCRIPTION_TEMPLATE)
.replace(
"{{ default_exec_yield_time_ms }}",
&default_exec_yield_time_ms.to_string(),
)
.replace("{{ image_helper }}", image_helper);
if !description.is_empty() {
sections.push(description);
}
if !deferred_tools.is_empty() {
sections.push(DEFERRED_NESTED_TOOLS_GUIDANCE.to_string());
let guidance = messages
.and_then(|messages| messages.deferred_nested_tools_guidance.as_deref())
.unwrap_or(DEFERRED_NESTED_TOOLS_GUIDANCE);
if !guidance.is_empty() {
sections.push(guidance.to_string());
}
}
if !code_mode_only {
return sections.join("\n\n");
@@ -289,9 +305,12 @@ pub fn build_exec_tool_description(
.chain(deferred_tools)
.any(|tool| mcp_structured_content_schema(tool.output_schema.as_ref()).is_some());
if has_mcp_tools {
sections.push(format!(
"Shared MCP Types:\n```ts\n{MCP_TYPESCRIPT_PREAMBLE}\n```"
));
let preamble = messages
.and_then(|messages| messages.mcp_typescript_preamble.as_deref())
.unwrap_or(MCP_TYPESCRIPT_PREAMBLE);
if !preamble.is_empty() {
sections.push(format!("Shared MCP Types:\n```ts\n{preamble}\n```"));
}
}
if !enabled_tools.is_empty() {
@@ -502,6 +521,10 @@ fn mcp_structured_content_schema(output_schema: Option<&JsonValue>) -> Option<&J
)
}
#[cfg(test)]
#[path = "description_override_tests.rs"]
mod description_override_tests;
#[cfg(test)]
mod tests {
use super::CodeModeToolKind;
@@ -709,6 +732,7 @@ mod tests {
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert!(description.contains(
"### `foo`
@@ -726,6 +750,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ false,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert!(description.contains("`audio(audioUrlOrItem:"));
assert!(description.contains("`setTimeout(callback: () => void, delayMs?: number)`"));
@@ -781,6 +806,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert_eq!(description.matches("## mcp__sample").count(), 1);
assert!(description.contains("## mcp__sample\nShared namespace guidance."));
@@ -823,6 +849,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert!(!description.contains("## mcp__sample"));
@@ -924,6 +951,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert_eq!(
@@ -961,6 +989,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert!(description.contains("Some deferred nested tools may be omitted"));
@@ -984,6 +1013,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ false,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
assert!(description.contains("Some deferred nested tools may be omitted"));
@@ -0,0 +1,159 @@
//! Tests catalog exec template rendering and runtime description composition.
use super::CodeModeToolKind;
use super::DEFERRED_NESTED_TOOLS_GUIDANCE;
use super::ImageDetailVisibility;
use super::LEGACY_IMAGE_HELPER_DESCRIPTION;
use super::MCP_TYPESCRIPT_PREAMBLE;
use super::ToolDefinition;
use super::UNIFIED_IMAGE_HELPER_DESCRIPTION;
use super::build_exec_tool_description;
use codex_protocol::ToolName;
use codex_protocol::openai_models::CodeModeToolMessages;
use codex_protocol::openai_models::ToolMessage;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn exec_override_renders_only_known_literal_placeholders() {
for (image_detail_visibility, image_helper) in [
(
ImageDetailVisibility::Visible,
LEGACY_IMAGE_HELPER_DESCRIPTION,
),
(
ImageDetailVisibility::Hidden,
UNIFIED_IMAGE_HELPER_DESCRIPTION,
),
] {
let description = build_exec_tool_description(
&[],
&[],
&BTreeMap::new(),
/*default_exec_yield_time_ms*/ 4567,
/*code_mode_only*/ false,
image_detail_visibility,
Some(&CodeModeToolMessages {
exec: Some(ToolMessage {
description: Some(" \nDefaults to 10000 ms. {{ default_exec_yield_time_ms }} ms.\n{{ image_helper }}\n{{ unknown }} {{default_exec_yield_time_ms}}\t ".to_string()),
..Default::default()
}),
..Default::default()
}),
);
assert_eq!(
description,
format!(
" \nDefaults to 10000 ms. 4567 ms.\n{image_helper}\n{{{{ unknown }}}} {{{{default_exec_yield_time_ms}}}}\t "
),
);
}
}
#[test]
fn exec_override_preserves_empty_and_whitespace_only_text() {
for description_override in ["", " \n\t "] {
assert_eq!(
build_exec_tool_description(
&[],
&[],
&BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
Some(&CodeModeToolMessages {
exec: Some(ToolMessage {
description: Some(description_override.to_string()),
..Default::default()
}),
deferred_nested_tools_guidance: Some(
"No deferred tools; omit this.".to_string()
),
mcp_typescript_preamble: Some("No MCP tools; omit this.".to_string()),
..Default::default()
}),
),
description_override,
);
}
}
#[test]
fn exec_override_preserves_runtime_sections() {
let enabled_tools = [ToolDefinition {
name: "alpha".to_string(),
tool_name: ToolName::plain("alpha"),
description: "First tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: None,
output_schema: None,
}];
let deferred_tools = [ToolDefinition {
name: "mcp__sample__beta".to_string(),
tool_name: ToolName::namespaced("mcp__sample__", "beta"),
description: "Deferred tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: None,
output_schema: Some(json!({
"type": "object",
"properties": {
"content": { "type": "array", "items": { "type": "object" } },
"isError": { "type": "boolean" },
"_meta": { "type": "object" }
}
})),
}];
let declaration = "### `alpha`\nFirst tool\n\nexec tool declaration:\n```ts\ndeclare const tools: { alpha(args: unknown): Promise<unknown>; };\n```";
for (code_mode_only, guidance, preamble, expected) in [
(true, Some(""), Some(""), declaration.to_string()),
(
true,
None,
Some(""),
format!("{DEFERRED_NESTED_TOOLS_GUIDANCE}\n\n{declaration}"),
),
(
true,
Some(""),
None,
format!("Shared MCP Types:\n```ts\n{MCP_TYPESCRIPT_PREAMBLE}\n```\n\n{declaration}"),
),
(
true,
Some(" {{ image_helper }}\n"),
Some(" {{ default_exec_yield_time_ms }}\n"),
format!(
" {{{{ image_helper }}}}\n\n\nShared MCP Types:\n```ts\n {{{{ default_exec_yield_time_ms }}}}\n\n```\n\n{declaration}"
),
),
(
false,
Some("Catalog discovery."),
Some("No types outside Code Mode Only."),
"Catalog discovery.".to_string(),
),
] {
assert_eq!(
build_exec_tool_description(
&enabled_tools,
&deferred_tools,
&BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
code_mode_only,
ImageDetailVisibility::Visible,
Some(&CodeModeToolMessages {
exec: Some(ToolMessage {
description: Some(String::new()),
..Default::default()
}),
deferred_nested_tools_guidance: guidance.map(str::to_string),
mcp_typescript_preamble: preamble.map(str::to_string),
..Default::default()
}),
),
expected,
);
}
}