mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
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:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user