mirror of
https://github.com/openai/codex.git
synced 2026-04-29 00:55:38 +00:00
752 lines
27 KiB
Rust
752 lines
27 KiB
Rust
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde_json::Value as JsonValue;
|
|
use std::collections::BTreeMap;
|
|
|
|
use crate::PUBLIC_TOOL_NAME;
|
|
|
|
const MAX_JS_SAFE_INTEGER: u64 = (1_u64 << 53) - 1;
|
|
const CODE_MODE_ONLY_PREFACE: &str =
|
|
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly";
|
|
const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/compose tool calls
|
|
- Evaluates the provided JavaScript code in a fresh V8 isolate as an async module.
|
|
- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`.
|
|
- Nested tool methods take either a string or an object as their input argument.
|
|
- Nested tools return either an object or a string, based on the description.
|
|
- 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 after that many milliseconds if the script is still running.
|
|
- `max_output_tokens` sets the token budget for direct `exec` results. By default the result is truncated 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 })`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL.
|
|
- `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session.
|
|
- `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing.
|
|
- `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`.
|
|
- `setTimeout(callback: () => void, delayMs?: number)`: schedules a callback to run later and returns a timeout id. Pending timeouts do not keep `exec` alive by themselves; await an explicit promise if you need to wait for one.
|
|
- `clearTimeout(timeoutId?: number)`: cancels a timeout created by `setTimeout`.
|
|
- `ALL_TOOLS`: metadata for the enabled nested tools as `{ name, description }` entries.
|
|
- `yield_control()`: yields the accumulated output to the model immediately while the script keeps running."#;
|
|
const WAIT_DESCRIPTION_TEMPLATE: &str = r#"- Use `wait` only after `exec` returns `Script running with cell ID ...`.
|
|
- `cell_id` identifies the running `exec` cell to resume.
|
|
- `yield_time_ms` controls how long to wait for more output before yielding again. If omitted, `wait` uses its default wait timeout.
|
|
- `max_tokens` limits how much new output this wait call returns.
|
|
- `terminate: true` stops the running cell instead of waiting for more output.
|
|
- `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."#;
|
|
|
|
pub const CODE_MODE_PRAGMA_PREFIX: &str = "// @exec:";
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CodeModeToolKind {
|
|
Function,
|
|
Freeform,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct ToolDefinition {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub kind: CodeModeToolKind,
|
|
pub input_schema: Option<JsonValue>,
|
|
pub output_schema: Option<JsonValue>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct ToolNamespaceDescription {
|
|
pub name: String,
|
|
pub description: String,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct CodeModeExecPragma {
|
|
#[serde(default)]
|
|
yield_time_ms: Option<u64>,
|
|
#[serde(default)]
|
|
max_output_tokens: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct ParsedExecSource {
|
|
pub code: String,
|
|
pub yield_time_ms: Option<u64>,
|
|
pub max_output_tokens: Option<usize>,
|
|
}
|
|
|
|
pub fn parse_exec_source(input: &str) -> Result<ParsedExecSource, String> {
|
|
if input.trim().is_empty() {
|
|
return Err(
|
|
"exec expects raw JavaScript source text (non-empty). Provide JS only, optionally with first-line `// @exec: {\"yield_time_ms\": 10000, \"max_output_tokens\": 1000}`.".to_string(),
|
|
);
|
|
}
|
|
|
|
let mut args = ParsedExecSource {
|
|
code: input.to_string(),
|
|
yield_time_ms: None,
|
|
max_output_tokens: None,
|
|
};
|
|
|
|
let mut lines = input.splitn(2, '\n');
|
|
let first_line = lines.next().unwrap_or_default();
|
|
let rest = lines.next().unwrap_or_default();
|
|
let trimmed = first_line.trim_start();
|
|
let Some(pragma) = trimmed.strip_prefix(CODE_MODE_PRAGMA_PREFIX) else {
|
|
return Ok(args);
|
|
};
|
|
|
|
if rest.trim().is_empty() {
|
|
return Err(
|
|
"exec pragma must be followed by JavaScript source on subsequent lines".to_string(),
|
|
);
|
|
}
|
|
|
|
let directive = pragma.trim();
|
|
if directive.is_empty() {
|
|
return Err(
|
|
"exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`"
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
let value: serde_json::Value = serde_json::from_str(directive).map_err(|err| {
|
|
format!(
|
|
"exec pragma must be valid JSON with supported fields `yield_time_ms` and `max_output_tokens`: {err}"
|
|
)
|
|
})?;
|
|
let object = value.as_object().ok_or_else(|| {
|
|
"exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`"
|
|
.to_string()
|
|
})?;
|
|
for key in object.keys() {
|
|
match key.as_str() {
|
|
"yield_time_ms" | "max_output_tokens" => {}
|
|
_ => {
|
|
return Err(format!(
|
|
"exec pragma only supports `yield_time_ms` and `max_output_tokens`; got `{key}`"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let pragma: CodeModeExecPragma = serde_json::from_value(value).map_err(|err| {
|
|
format!(
|
|
"exec pragma fields `yield_time_ms` and `max_output_tokens` must be non-negative safe integers: {err}"
|
|
)
|
|
})?;
|
|
if pragma
|
|
.yield_time_ms
|
|
.is_some_and(|yield_time_ms| yield_time_ms > MAX_JS_SAFE_INTEGER)
|
|
{
|
|
return Err(
|
|
"exec pragma field `yield_time_ms` must be a non-negative safe integer".to_string(),
|
|
);
|
|
}
|
|
if pragma.max_output_tokens.is_some_and(|max_output_tokens| {
|
|
u64::try_from(max_output_tokens)
|
|
.map(|max_output_tokens| max_output_tokens > MAX_JS_SAFE_INTEGER)
|
|
.unwrap_or(true)
|
|
}) {
|
|
return Err(
|
|
"exec pragma field `max_output_tokens` must be a non-negative safe integer".to_string(),
|
|
);
|
|
}
|
|
|
|
args.code = rest.to_string();
|
|
args.yield_time_ms = pragma.yield_time_ms;
|
|
args.max_output_tokens = pragma.max_output_tokens;
|
|
Ok(args)
|
|
}
|
|
|
|
pub fn is_code_mode_nested_tool(tool_name: &str) -> bool {
|
|
tool_name != crate::PUBLIC_TOOL_NAME && tool_name != crate::WAIT_TOOL_NAME
|
|
}
|
|
|
|
pub fn build_exec_tool_description(
|
|
enabled_tools: &[(String, String)],
|
|
namespace_descriptions: &BTreeMap<String, ToolNamespaceDescription>,
|
|
code_mode_only: bool,
|
|
) -> String {
|
|
if !code_mode_only {
|
|
return EXEC_DESCRIPTION_TEMPLATE.to_string();
|
|
}
|
|
|
|
let mut sections = vec![
|
|
CODE_MODE_ONLY_PREFACE.to_string(),
|
|
EXEC_DESCRIPTION_TEMPLATE.to_string(),
|
|
];
|
|
|
|
if !enabled_tools.is_empty() {
|
|
let mut current_namespace: Option<&str> = None;
|
|
let mut nested_tool_sections = Vec::with_capacity(enabled_tools.len());
|
|
|
|
for (name, nested_description) in enabled_tools {
|
|
let next_namespace = namespace_descriptions
|
|
.get(name)
|
|
.map(|namespace_description| namespace_description.name.as_str());
|
|
if next_namespace != current_namespace {
|
|
if let Some(namespace_description) = namespace_descriptions.get(name) {
|
|
let namespace_description_text = namespace_description.description.trim();
|
|
if !namespace_description_text.is_empty() {
|
|
nested_tool_sections.push(format!(
|
|
"## {}\n{namespace_description_text}",
|
|
namespace_description.name
|
|
));
|
|
}
|
|
}
|
|
current_namespace = next_namespace;
|
|
}
|
|
|
|
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}`)"));
|
|
} else {
|
|
nested_tool_sections.push(format!(
|
|
"### `{global_name}` (`{name}`)\n{nested_description}"
|
|
));
|
|
}
|
|
}
|
|
|
|
let nested_tool_reference = nested_tool_sections.join("\n\n");
|
|
sections.push(nested_tool_reference);
|
|
}
|
|
|
|
sections.join("\n\n")
|
|
}
|
|
|
|
pub fn build_wait_tool_description() -> &'static str {
|
|
WAIT_DESCRIPTION_TEMPLATE
|
|
}
|
|
|
|
pub fn normalize_code_mode_identifier(tool_key: &str) -> String {
|
|
let mut identifier = String::new();
|
|
|
|
for (index, ch) in tool_key.chars().enumerate() {
|
|
let is_valid = if index == 0 {
|
|
ch == '_' || ch == '$' || ch.is_ascii_alphabetic()
|
|
} else {
|
|
ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()
|
|
};
|
|
|
|
if is_valid {
|
|
identifier.push(ch);
|
|
} else {
|
|
identifier.push('_');
|
|
}
|
|
}
|
|
|
|
if identifier.is_empty() {
|
|
"_".to_string()
|
|
} else {
|
|
identifier
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
pub fn enabled_tool_metadata(definition: &ToolDefinition) -> EnabledToolMetadata {
|
|
EnabledToolMetadata {
|
|
tool_name: definition.name.clone(),
|
|
global_name: normalize_code_mode_identifier(&definition.name),
|
|
description: definition.description.clone(),
|
|
kind: definition.kind,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
pub struct EnabledToolMetadata {
|
|
pub tool_name: String,
|
|
pub global_name: String,
|
|
pub description: String,
|
|
pub kind: CodeModeToolKind,
|
|
}
|
|
|
|
pub fn append_code_mode_sample(
|
|
description: &str,
|
|
tool_name: &str,
|
|
input_name: &str,
|
|
input_type: String,
|
|
output_type: String,
|
|
) -> String {
|
|
let declaration = format!(
|
|
"declare const tools: {{ {} }};",
|
|
render_code_mode_tool_declaration(tool_name, input_name, input_type, output_type)
|
|
);
|
|
format!("{description}\n\nexec tool declaration:\n```ts\n{declaration}\n```")
|
|
}
|
|
|
|
fn append_code_mode_sample_for_definition(definition: &ToolDefinition) -> String {
|
|
let input_name = match definition.kind {
|
|
CodeModeToolKind::Function => "args",
|
|
CodeModeToolKind::Freeform => "input",
|
|
};
|
|
let input_type = match definition.kind {
|
|
CodeModeToolKind::Function => definition
|
|
.input_schema
|
|
.as_ref()
|
|
.map(render_json_schema_to_typescript)
|
|
.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(
|
|
&definition.description,
|
|
&definition.name,
|
|
input_name,
|
|
input_type,
|
|
output_type,
|
|
)
|
|
}
|
|
|
|
fn render_code_mode_tool_declaration(
|
|
tool_name: &str,
|
|
input_name: &str,
|
|
input_type: String,
|
|
output_type: String,
|
|
) -> String {
|
|
let tool_name = normalize_code_mode_identifier(tool_name);
|
|
format!("{tool_name}({input_name}: {input_type}): Promise<{output_type}>;")
|
|
}
|
|
|
|
pub fn render_json_schema_to_typescript(schema: &JsonValue) -> String {
|
|
render_json_schema_to_typescript_inner(schema)
|
|
}
|
|
|
|
fn render_json_schema_to_typescript_inner(schema: &JsonValue) -> String {
|
|
match schema {
|
|
JsonValue::Bool(true) => "unknown".to_string(),
|
|
JsonValue::Bool(false) => "never".to_string(),
|
|
JsonValue::Object(map) => {
|
|
if let Some(value) = map.get("const") {
|
|
return render_json_schema_literal(value);
|
|
}
|
|
|
|
if let Some(values) = map.get("enum").and_then(JsonValue::as_array) {
|
|
let rendered = values
|
|
.iter()
|
|
.map(render_json_schema_literal)
|
|
.collect::<Vec<_>>();
|
|
if !rendered.is_empty() {
|
|
return rendered.join(" | ");
|
|
}
|
|
}
|
|
|
|
for key in ["anyOf", "oneOf"] {
|
|
if let Some(variants) = map.get(key).and_then(JsonValue::as_array) {
|
|
let rendered = variants
|
|
.iter()
|
|
.map(render_json_schema_to_typescript_inner)
|
|
.collect::<Vec<_>>();
|
|
if !rendered.is_empty() {
|
|
return rendered.join(" | ");
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(variants) = map.get("allOf").and_then(JsonValue::as_array) {
|
|
let rendered = variants
|
|
.iter()
|
|
.map(render_json_schema_to_typescript_inner)
|
|
.collect::<Vec<_>>();
|
|
if !rendered.is_empty() {
|
|
return rendered.join(" & ");
|
|
}
|
|
}
|
|
|
|
if let Some(schema_type) = map.get("type") {
|
|
if let Some(types) = schema_type.as_array() {
|
|
let rendered = types
|
|
.iter()
|
|
.filter_map(JsonValue::as_str)
|
|
.map(|schema_type| render_json_schema_type_keyword(map, schema_type))
|
|
.collect::<Vec<_>>();
|
|
if !rendered.is_empty() {
|
|
return rendered.join(" | ");
|
|
}
|
|
}
|
|
|
|
if let Some(schema_type) = schema_type.as_str() {
|
|
return render_json_schema_type_keyword(map, schema_type);
|
|
}
|
|
}
|
|
|
|
if map.contains_key("properties")
|
|
|| map.contains_key("additionalProperties")
|
|
|| map.contains_key("required")
|
|
{
|
|
return render_json_schema_object(map);
|
|
}
|
|
|
|
if map.contains_key("items") || map.contains_key("prefixItems") {
|
|
return render_json_schema_array(map);
|
|
}
|
|
|
|
"unknown".to_string()
|
|
}
|
|
_ => "unknown".to_string(),
|
|
}
|
|
}
|
|
|
|
fn render_json_schema_type_keyword(
|
|
map: &serde_json::Map<String, JsonValue>,
|
|
schema_type: &str,
|
|
) -> String {
|
|
match schema_type {
|
|
"string" => "string".to_string(),
|
|
"number" | "integer" => "number".to_string(),
|
|
"boolean" => "boolean".to_string(),
|
|
"null" => "null".to_string(),
|
|
"array" => render_json_schema_array(map),
|
|
"object" => render_json_schema_object(map),
|
|
_ => "unknown".to_string(),
|
|
}
|
|
}
|
|
|
|
fn render_json_schema_array(map: &serde_json::Map<String, JsonValue>) -> String {
|
|
if let Some(items) = map.get("items") {
|
|
let item_type = render_json_schema_to_typescript_inner(items);
|
|
return format!("Array<{item_type}>");
|
|
}
|
|
|
|
if let Some(items) = map.get("prefixItems").and_then(JsonValue::as_array) {
|
|
let item_types = items
|
|
.iter()
|
|
.map(render_json_schema_to_typescript_inner)
|
|
.collect::<Vec<_>>();
|
|
if !item_types.is_empty() {
|
|
return format!("[{}]", item_types.join(", "));
|
|
}
|
|
}
|
|
|
|
"unknown[]".to_string()
|
|
}
|
|
|
|
fn append_additional_properties_line(
|
|
lines: &mut Vec<String>,
|
|
map: &serde_json::Map<String, JsonValue>,
|
|
properties: &serde_json::Map<String, JsonValue>,
|
|
line_prefix: &str,
|
|
) {
|
|
if let Some(additional_properties) = map.get("additionalProperties") {
|
|
let property_type = match additional_properties {
|
|
JsonValue::Bool(true) => Some("unknown".to_string()),
|
|
JsonValue::Bool(false) => None,
|
|
value => Some(render_json_schema_to_typescript_inner(value)),
|
|
};
|
|
|
|
if let Some(property_type) = property_type {
|
|
lines.push(format!("{line_prefix}[key: string]: {property_type};"));
|
|
}
|
|
} else if properties.is_empty() {
|
|
lines.push(format!("{line_prefix}[key: string]: unknown;"));
|
|
}
|
|
}
|
|
|
|
fn has_property_description(value: &JsonValue) -> bool {
|
|
value
|
|
.get("description")
|
|
.and_then(JsonValue::as_str)
|
|
.is_some_and(|description| !description.is_empty())
|
|
}
|
|
|
|
fn render_json_schema_object_property(name: &str, value: &JsonValue, required: &[&str]) -> String {
|
|
let optional = if required.iter().any(|required_name| required_name == &name) {
|
|
""
|
|
} else {
|
|
"?"
|
|
};
|
|
let property_name = render_json_schema_property_name(name);
|
|
let property_type = render_json_schema_to_typescript_inner(value);
|
|
format!("{property_name}{optional}: {property_type};")
|
|
}
|
|
|
|
fn render_json_schema_object(map: &serde_json::Map<String, JsonValue>) -> String {
|
|
let required = map
|
|
.get("required")
|
|
.and_then(JsonValue::as_array)
|
|
.map(|items| {
|
|
items
|
|
.iter()
|
|
.filter_map(JsonValue::as_str)
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
let properties = map
|
|
.get("properties")
|
|
.and_then(JsonValue::as_object)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
let mut sorted_properties = properties.iter().collect::<Vec<_>>();
|
|
sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
|
|
if sorted_properties
|
|
.iter()
|
|
.any(|(_, value)| has_property_description(value))
|
|
{
|
|
let mut lines = vec!["{".to_string()];
|
|
for (name, value) in sorted_properties {
|
|
if let Some(description) = value.get("description").and_then(JsonValue::as_str) {
|
|
for description_line in description
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
{
|
|
lines.push(format!(" // {description_line}"));
|
|
}
|
|
}
|
|
|
|
lines.push(format!(
|
|
" {}",
|
|
render_json_schema_object_property(name, value, &required)
|
|
));
|
|
}
|
|
|
|
append_additional_properties_line(&mut lines, map, &properties, " ");
|
|
lines.push("}".to_string());
|
|
return lines.join("\n");
|
|
}
|
|
|
|
let mut lines = sorted_properties
|
|
.into_iter()
|
|
.map(|(name, value)| render_json_schema_object_property(name, value, &required))
|
|
.collect::<Vec<_>>();
|
|
|
|
append_additional_properties_line(&mut lines, map, &properties, "");
|
|
|
|
if lines.is_empty() {
|
|
return "{}".to_string();
|
|
}
|
|
|
|
format!("{{ {} }}", lines.join(" "))
|
|
}
|
|
|
|
fn render_json_schema_property_name(name: &str) -> String {
|
|
if normalize_code_mode_identifier(name) == name {
|
|
name.to_string()
|
|
} else {
|
|
serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\"")))
|
|
}
|
|
}
|
|
|
|
fn render_json_schema_literal(value: &JsonValue) -> String {
|
|
serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::CodeModeToolKind;
|
|
use super::ParsedExecSource;
|
|
use super::ToolDefinition;
|
|
use super::ToolNamespaceDescription;
|
|
use super::augment_tool_definition;
|
|
use super::build_exec_tool_description;
|
|
use super::normalize_code_mode_identifier;
|
|
use super::parse_exec_source;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[test]
|
|
fn parse_exec_source_without_pragma() {
|
|
assert_eq!(
|
|
parse_exec_source("text('hi')").unwrap(),
|
|
ParsedExecSource {
|
|
code: "text('hi')".to_string(),
|
|
yield_time_ms: None,
|
|
max_output_tokens: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_exec_source_with_pragma() {
|
|
assert_eq!(
|
|
parse_exec_source("// @exec: {\"yield_time_ms\": 10}\ntext('hi')").unwrap(),
|
|
ParsedExecSource {
|
|
code: "text('hi')".to_string(),
|
|
yield_time_ms: Some(10),
|
|
max_output_tokens: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn normalize_identifier_rewrites_invalid_characters() {
|
|
assert_eq!(
|
|
"mcp__ologs__get_profile",
|
|
normalize_code_mode_identifier("mcp__ologs__get_profile")
|
|
);
|
|
assert_eq!(
|
|
"hidden_dynamic_tool",
|
|
normalize_code_mode_identifier("hidden-dynamic-tool")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn augment_tool_definition_appends_typed_declaration() {
|
|
let definition = ToolDefinition {
|
|
name: "hidden_dynamic_tool".to_string(),
|
|
description: "Test tool".to_string(),
|
|
kind: CodeModeToolKind::Function,
|
|
input_schema: Some(json!({
|
|
"type": "object",
|
|
"properties": { "city": { "type": "string" } },
|
|
"required": ["city"],
|
|
"additionalProperties": false
|
|
})),
|
|
output_schema: Some(json!({
|
|
"type": "object",
|
|
"properties": { "ok": { "type": "boolean" } },
|
|
"required": ["ok"]
|
|
})),
|
|
};
|
|
|
|
let description = augment_tool_definition(definition).description;
|
|
assert!(description.contains("declare const tools"));
|
|
assert!(
|
|
description.contains(
|
|
"hidden_dynamic_tool(args: { city: string; }): Promise<{ ok: boolean; }>;"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn augment_tool_definition_includes_property_descriptions_as_comments() {
|
|
let definition = ToolDefinition {
|
|
name: "weather_tool".to_string(),
|
|
description: "Weather tool".to_string(),
|
|
kind: CodeModeToolKind::Function,
|
|
input_schema: Some(json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"weather": {
|
|
"type": "array",
|
|
"description": "look up weather for a given list of locations",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"location": { "type": "string" }
|
|
},
|
|
"required": ["location"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["weather"]
|
|
})),
|
|
output_schema: Some(json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"forecast": {
|
|
"type": "string",
|
|
"description": "human readable weather forecast"
|
|
}
|
|
},
|
|
"required": ["forecast"]
|
|
})),
|
|
};
|
|
|
|
let description = augment_tool_definition(definition).description;
|
|
assert!(description.contains(
|
|
r#"weather_tool(args: {
|
|
// look up weather for a given list of locations
|
|
weather: Array<{ location: string; }>;
|
|
}): Promise<{
|
|
// human readable weather forecast
|
|
forecast: string;
|
|
}>;"#
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_description_includes_nested_tools() {
|
|
let description = build_exec_tool_description(
|
|
&[("foo".to_string(), "bar".to_string())],
|
|
&BTreeMap::new(),
|
|
/*code_mode_only*/ true,
|
|
);
|
|
assert!(description.contains("### `foo` (`foo`)"));
|
|
}
|
|
|
|
#[test]
|
|
fn exec_description_mentions_timeout_helpers() {
|
|
let description =
|
|
build_exec_tool_description(&[], &BTreeMap::new(), /*code_mode_only*/ false);
|
|
assert!(description.contains("`setTimeout(callback: () => void, delayMs?: number)`"));
|
|
assert!(description.contains("`clearTimeout(timeoutId?: number)`"));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_description_groups_namespace_instructions_once() {
|
|
let namespace_descriptions = BTreeMap::from([
|
|
(
|
|
"mcp__sample__alpha".to_string(),
|
|
ToolNamespaceDescription {
|
|
name: "mcp__sample".to_string(),
|
|
description: "Shared namespace guidance.".to_string(),
|
|
},
|
|
),
|
|
(
|
|
"mcp__sample__beta".to_string(),
|
|
ToolNamespaceDescription {
|
|
name: "mcp__sample".to_string(),
|
|
description: "Shared namespace guidance.".to_string(),
|
|
},
|
|
),
|
|
]);
|
|
let description = build_exec_tool_description(
|
|
&[
|
|
("mcp__sample__alpha".to_string(), "First tool".to_string()),
|
|
("mcp__sample__beta".to_string(), "Second tool".to_string()),
|
|
],
|
|
&namespace_descriptions,
|
|
/*code_mode_only*/ true,
|
|
);
|
|
assert_eq!(description.matches("## mcp__sample").count(), 1);
|
|
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"#
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_description_omits_empty_namespace_sections() {
|
|
let namespace_descriptions = BTreeMap::from([(
|
|
"mcp__sample__alpha".to_string(),
|
|
ToolNamespaceDescription {
|
|
name: "mcp__sample".to_string(),
|
|
description: String::new(),
|
|
},
|
|
)]);
|
|
let description = build_exec_tool_description(
|
|
&[("mcp__sample__alpha".to_string(), "First tool".to_string())],
|
|
&namespace_descriptions,
|
|
/*code_mode_only*/ true,
|
|
);
|
|
|
|
assert!(!description.contains("## mcp__sample"));
|
|
assert!(description.contains("### `mcp__sample__alpha` (`mcp__sample__alpha`)"));
|
|
}
|
|
}
|