Support CodeMode ToolDisclosure Variants (#7926)

Signed-off-by: Elias Posen <elias@posen.ch>
This commit is contained in:
Elias Posen
2026-03-19 04:55:46 -04:00
committed by GitHub
parent 5be96c1ab2
commit 28a52e36ff
14 changed files with 367 additions and 234 deletions
+1
View File
@@ -5,6 +5,7 @@ run_cli.sh
tokenizer_files/
.DS_Store
.idea
.vscode
*.log
tmp/
Generated
+79 -34
View File
@@ -111,7 +111,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-parse 0.2.7",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse 1.0.0",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
@@ -121,9 +136,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
@@ -134,6 +149,15 @@ dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
@@ -940,7 +964,7 @@ dependencies = [
"bincode",
"bytesize",
"clircle",
"console 0.16.2",
"console 0.16.3",
"content_inspector",
"encoding_rs",
"flate2",
@@ -964,7 +988,7 @@ dependencies = [
"syntect",
"terminal-colorsaurus",
"thiserror 2.0.18",
"toml",
"toml 0.9.12+spec-1.1.0",
"unicode-segmentation",
"unicode-width 0.2.2",
"walkdir",
@@ -1838,9 +1862,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1848,11 +1872,11 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstream 1.0.0",
"anstyle",
"clap_lex",
"strsim",
@@ -1860,18 +1884,18 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.66"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck",
"proc-macro2",
@@ -1881,15 +1905,15 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clap_mangen"
version = "0.2.31"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301"
checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78"
dependencies = [
"clap",
"roff",
@@ -1945,9 +1969,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
@@ -2029,9 +2053,9 @@ dependencies = [
[[package]]
name = "config"
version = "0.15.19"
version = "0.15.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6"
checksum = "4fe5feec195269515c4722937cd7ffcfe7b4205d18d2e6577b7223ecb159ab00"
dependencies = [
"async-trait",
"convert_case 0.6.0",
@@ -2042,7 +2066,7 @@ dependencies = [
"serde-untagged",
"serde_core",
"serde_json",
"toml",
"toml 1.0.6+spec-1.1.0",
"winnow",
"yaml-rust2",
]
@@ -2062,13 +2086,12 @@ dependencies = [
[[package]]
name = "console"
version = "0.16.2"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.2",
"windows-sys 0.61.2",
]
@@ -4373,7 +4396,7 @@ dependencies = [
name = "goose-cli"
version = "1.28.0"
dependencies = [
"anstream",
"anstream 0.6.21",
"anyhow",
"async-trait",
"base64 0.22.1",
@@ -4385,7 +4408,7 @@ dependencies = [
"clap_mangen",
"cliclack",
"comfy-table",
"console 0.16.2",
"console 0.16.3",
"dotenvy",
"etcetera 0.11.0",
"futures",
@@ -5224,7 +5247,7 @@ version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
dependencies = [
"console 0.16.2",
"console 0.16.3",
"portable-atomic",
"unicode-width 0.2.2",
"unit-prefix",
@@ -7922,9 +7945,9 @@ dependencies = [
[[package]]
name = "roff"
version = "0.2.2"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad"
[[package]]
name = "ron"
@@ -10349,12 +10372,25 @@ dependencies = [
"indexmap 2.13.0",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc"
dependencies = [
"serde_core",
"serde_spanned",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
@@ -10364,6 +10400,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
@@ -10580,9 +10625,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -1,4 +1,4 @@
data: {"id":"chatcmpl-D64NZp69RkEyXdUoDaCBj7fSYll8J","object":"chat.completion.chunk","created":1770339173,"model":"gpt-5-nano-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HCUq7OYIqj233H77wpqAtSGP","type":"function","function":{"name":"code_execution__execute","arguments":""}}],"refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"XbIx"}
data: {"id":"chatcmpl-D64NZp69RkEyXdUoDaCBj7fSYll8J","object":"chat.completion.chunk","created":1770339173,"model":"gpt-5-nano-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HCUq7OYIqj233H77wpqAtSGP","type":"function","function":{"name":"code_execution__execute_typescript","arguments":""}}],"refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"XbIx"}
data: {"id":"chatcmpl-D64NZp69RkEyXdUoDaCBj7fSYll8J","object":"chat.completion.chunk","created":1770339173,"model":"gpt-5-nano-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"finish_reason":null}],"usage":null,"obfuscation":"0WAOew1EJA"}
+3 -3
View File
@@ -480,7 +480,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) {
Ok(call) => match call.name.to_string().as_str() {
name if is_shell_tool_name(name) => render_shell_request(call, debug),
name if is_file_tool_name(name) => render_text_editor_request(call, debug),
"execute" | "execute_code" => render_execute_code_request(call, debug),
"execute_typescript" | "execute_code" => render_execute_code_request(call, debug),
"delegate" => render_delegate_request(call, debug),
"subagent" => render_delegate_request(call, debug),
"todo__write" => render_todo_request(call, debug),
@@ -822,7 +822,7 @@ pub fn render_subagent_tool_call(
arguments: Option<&JsonObject>,
debug: bool,
) {
if tool_name == "code_execution__execute_code" {
if tool_name == "code_execution__execute_typescript" {
let tool_graph = arguments
.and_then(|args| args.get("tool_graph"))
.and_then(Value::as_array)
@@ -851,7 +851,7 @@ fn render_subagent_tool_graph(subagent_id: &str, tool_graph: &[Value]) {
" {} {} {} {} tool call{}",
style("").dim(),
style(format!("[subagent:{}]", short_id)).dim(),
style("execute_code").dim(),
style("execute_typescript").dim(),
style(count).dim(),
plural,
);
@@ -4,11 +4,13 @@ use crate::agents::mcp_client::{Error, McpClientTrait};
use crate::agents::tool_execution::ToolCallContext;
use anyhow::Result;
use async_trait::async_trait;
use indoc::indoc;
use pctx_code_mode::config::ToolDisclosure;
use pctx_code_mode::model::{CallbackConfig, ExecuteInput, GetFunctionDetailsInput};
use pctx_code_mode::registry::{CallbackFn, PctxRegistry};
use pctx_code_mode::CodeMode;
use pctx_code_mode::{
config::ToolDisclosure,
descriptions::{tools as tool_descriptions, workflow::get_workflow_description},
model::{CallbackConfig, ExecuteBashInput, ExecuteInput, GetFunctionDetailsInput},
registry::{CallbackFn, PctxRegistry},
CodeMode,
};
use rmcp::model::{
CallToolRequestParams, CallToolResult, Content, Implementation, InitializeResult, JsonObject,
ListToolsResult, RawContent, Role, ServerCapabilities, Tool as McpTool, ToolAnnotations,
@@ -29,6 +31,7 @@ pub static EXTENSION_NAME: &str = "code_execution";
pub struct CodeExecutionClient {
info: InitializeResult,
context: PlatformExtensionContext,
disclosure: ToolDisclosure,
state: RwLock<Option<CodeModeState>>,
}
@@ -54,32 +57,18 @@ pub struct ExecuteWithToolGraph {
}
impl CodeExecutionClient {
pub fn new(context: PlatformExtensionContext) -> Result<Self> {
pub fn new(context: PlatformExtensionContext, disclosure: ToolDisclosure) -> Result<Self> {
let info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(
Implementation::new(EXTENSION_NAME.to_string(), "1.0.0".to_string())
.with_title("Code Mode"),
)
.with_instructions(indoc! {r#"
BATCH MULTIPLE TOOL CALLS INTO ONE execute CALL.
This extension exists to reduce round-trips. When a task requires multiple tool calls:
- WRONG: Multiple execute calls, each with one tool
- RIGHT: One execute call with a script that calls all needed tools
IMPORTANT: All tool calls are ASYNC. Use await for each call.
Workflow:
1. Use the list_functions and get_function_details tools to discover tools and signatures
2. Write ONE script that calls ALL tools needed for the task, no need to import anything,
all the namespaces returned by list_functions and get_function_details will be available
3. Chain results: use output from one tool as input to the next
4. Only return and console.log data you need, tools could have very large responses.
"#}.to_string());
.with_instructions(get_workflow_description(disclosure));
Ok(Self {
info,
context,
disclosure,
state: RwLock::new(None),
})
}
@@ -95,19 +84,20 @@ impl CodeExecutionClient {
.get_prefixed_tools_excluding(session_id, EXTENSION_NAME)
.await
.ok()?;
let mut cfgs = vec![];
for tool in tools {
let full_name = tool.name.to_string();
let (namespace, name) = if let Some((server, tool_name)) = full_name.split_once("__") {
(server.to_string(), tool_name.to_string())
let (name, namespace) = if let Some((prefix, tool_name)) = tool.name.split_once("__") {
(tool_name.to_string(), Some(prefix.to_string()))
} else if let Some(owner) = get_tool_owner(&tool) {
(owner, full_name)
(tool.name.to_string(), Some(owner))
} else {
continue;
(tool.name.to_string(), None)
};
cfgs.push(CallbackConfig {
name,
namespace: Some(namespace),
namespace,
description: tool.description.as_ref().map(|d| d.to_string()),
input_schema: Some(json!(tool.input_schema)),
output_schema: tool.output_schema.as_ref().map(|s| json!(s)),
@@ -146,10 +136,11 @@ impl CodeExecutionClient {
let state = CodeModeState::new(cfgs)?;
let code_mode = state.code_mode.clone();
*guard = Some(state);
Ok(code_mode)
}
/// Build a CallbackRegistry with all tool callbacks registered
/// Build a PctxRegistry with all tool callbacks registered
fn build_callback_registry(
&self,
session_id: &str,
@@ -164,7 +155,14 @@ impl CodeExecutionClient {
let registry = PctxRegistry::default();
for cfg in code_mode.callbacks() {
let full_name = format!("{}__{}", cfg.namespace.as_deref().unwrap_or(""), &cfg.name);
let full_name = format!(
"{}{}",
cfg.namespace
.clone()
.map(|n| format!("{n}__"))
.unwrap_or_default(),
&cfg.name
);
let callback = create_tool_callback(session_id.to_string(), full_name, manager.clone());
registry
.add_callback(&cfg.id(), callback)
@@ -200,21 +198,19 @@ impl CodeExecutionClient {
Ok(vec![Content::text(output.code)])
}
/// Handle the execute tool call
async fn handle_execute(
/// Handle the execute bash tool call
async fn handle_execute_bash(
&self,
session_id: &str,
arguments: Option<JsonObject>,
) -> Result<Vec<Content>, String> {
let args: ExecuteWithToolGraph = arguments
let input: ExecuteBashInput = arguments
.map(|args| serde_json::from_value(Value::Object(args)))
.transpose()
.map_err(|e| format!("Failed to parse arguments: {e}"))?
.ok_or("Missing arguments for execute")?;
.ok_or("Missing arguments for execute_bash")?;
let command = input.command;
let code_mode = self.get_code_mode(session_id).await?;
let registry = self.build_callback_registry(session_id, &code_mode)?;
let code = args.input.code.clone();
// Deno runtime is not Send, so we need to run it in a blocking task
// with its own tokio runtime
@@ -226,13 +222,51 @@ impl CodeExecutionClient {
rt.block_on(async move {
code_mode
.execute_typescript(&code, ToolDisclosure::default(), Some(registry))
.execute_bash(&command)
.await
.map_err(|e| format!("Execution error: {e}"))
.map_err(|e| format!("Typescript execution error: {e}"))
})
})
.await
.map_err(|e| format!("Execution task failed: {e}"))??;
.map_err(|e| format!("Typescript execution task failed: {e}"))??;
Ok(vec![Content::text(output.markdown())])
}
/// Handle the execute typescript tool call
async fn handle_execute_typescript(
&self,
session_id: &str,
arguments: Option<JsonObject>,
) -> Result<Vec<Content>, String> {
let args: ExecuteWithToolGraph = arguments
.map(|args| serde_json::from_value(Value::Object(args)))
.transpose()
.map_err(|e| format!("Failed to parse arguments: {e}"))?
.ok_or("Missing arguments for execute_typescript")?;
let code_mode = self.get_code_mode(session_id).await?;
let registry = self.build_callback_registry(session_id, &code_mode)?;
let code = args.input.code.clone();
let disclosure = self.disclosure;
// Deno runtime is not Send, so we need to run it in a blocking task
// with its own tokio runtime
let output = tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("Failed to create runtime: {e}"))?;
rt.block_on(async move {
code_mode
.execute_typescript(&code, disclosure, Some(registry))
.await
.map_err(|e| format!("Typescript execution error: {e}"))
})
})
.await
.map_err(|e| format!("Typescript execution task failed: {e}"))??;
Ok(vec![Content::text(output.markdown())])
}
@@ -316,97 +350,79 @@ impl McpClientTrait for CodeExecutionClient {
}))
.expect("valid schema");
Ok(ListToolsResult {
tools: vec![
McpTool::new(
"list_functions".to_string(),
indoc! {r#"
List all available functions across all namespaces.
This will not return function input and output types.
After determining which functions are needed use
get_function_details to get input and output type
information about specific functions.
"#}
.to_string(),
empty_schema,
)
.annotate(ToolAnnotations::from_raw(
Some("List functions".to_string()),
Some(true),
Some(false),
Some(true),
Some(false),
)),
McpTool::new(
"get_function_details".to_string(),
indoc! {r#"
Get detailed type information for specific functions.
Provide a list of function identifiers in the format "Namespace.functionName"
(e.g., "Developer.shell", "Github.createIssue").
Returns full TypeScript interface definitions with parameter types,
return types, and descriptions for the requested functions.
"#}
.to_string(),
schema::<GetFunctionDetailsInput>(),
)
.annotate(ToolAnnotations::from_raw(
Some("Get function details".to_string()),
Some(true),
Some(false),
Some(true),
Some(false),
)),
McpTool::new(
"execute".to_string(),
indoc! {r#"
Execute TypeScript code that calls available functions.
SYNTAX - TypeScript with async run() function:
```typescript
async function run() {
// Access functions via Namespace.functionName({ params }) — always camelCase
const files = await Developer.shell({ command: "ls -la" });
const readme = await Developer.shell({ command: "cat ./README.md" });
return { files, readme };
}
```
TOOL_GRAPH: Always provide tool_graph to describe the execution flow for the UI.
Each node has: tool (Namespace.functionName), description (what it does), depends_on (indices of dependencies).
Example for chained operations:
[
{"tool": "Developer.shell", "description": "list files", "depends_on": []},
{"tool": "Developer.shell", "description": "read README.md", "depends_on": []},
{"tool": "Developer.write", "description": "write output.txt", "depends_on": [0, 1]}
]
KEY RULES:
- Code MUST define an async function named `run()`
- All function calls are async - use `await`
- Function names are always camelCase (e.g., Developer.shell, Github.listIssues, Github.createIssue)
- Return value from `run()` is the result, all `console.log()` output will be returned as well.
- Only functions from `list_functions()` and `console` methods are available — no `fetch()`, `fs`, or other Node/Deno APIs
- Variables don't persist between `execute()` calls - return or log anything you need later
- Code runs in an isolated sandbox with restricted network access
HANDLING RETURN VALUES:
- If a function returns `any`, do NOT assume its shape - log it first: `console.log(JSON.stringify(result))`
- Many functions return wrapper objects, not raw arrays - check the response structure before calling .filter(), .map(), etc.
- Always inspect unfamiliar return values with console.log() before processing them
TOKEN USAGE WARNING: This tool could return LARGE responses if your code returns big objects.
To minimize tokens:
- Filter/map/reduce data IN YOUR CODE before returning
- Only return specific fields you need (e.g., return {id: result.id, count: items.length})
- Use console.log() for intermediate results instead of returning everything
- Avoid returning full API responses - extract just what you need
BEFORE CALLING: Use list_functions or get_function_details to check available functions and their parameters.
"#}
.to_string(),
let tools = match self.disclosure {
ToolDisclosure::Catalog => {
vec![
McpTool::new(
"list_functions".to_string(),
tool_descriptions::LIST_FUNCTIONS.to_string(),
empty_schema,
)
.annotate(ToolAnnotations::from_raw(
Some("List functions".to_string()),
Some(true),
Some(false),
Some(true),
Some(false),
)),
McpTool::new(
"get_function_details".to_string(),
tool_descriptions::GET_FUNCTION_DETAILS.to_string(),
schema::<GetFunctionDetailsInput>(),
)
.annotate(ToolAnnotations::from_raw(
Some("Get function details".to_string()),
Some(true),
Some(false),
Some(true),
Some(false),
)),
McpTool::new(
"execute_typescript".to_string(),
tool_descriptions::EXECUTE_TYPESCRIPT_CATALOG.to_string(),
schema::<ExecuteWithToolGraph>(),
)
.annotate(ToolAnnotations::from_raw(
Some("Execute TypeScript".to_string()),
Some(false),
Some(true),
Some(false),
Some(true),
)),
]
}
ToolDisclosure::Filesystem => {
vec![
McpTool::new(
"execute_bash".to_string(),
tool_descriptions::EXECUTE_BASH.to_string(),
schema::<ExecuteBashInput>(),
)
.annotate(ToolAnnotations::from_raw(
Some("Get function details".to_string()),
Some(true),
Some(false),
Some(true),
Some(false),
)),
McpTool::new(
"execute_typescript".to_string(),
tool_descriptions::EXECUTE_TYPESCRIPT_FILESYSTEM.to_string(),
schema::<ExecuteWithToolGraph>(),
)
.annotate(ToolAnnotations::from_raw(
Some("Execute TypeScript".to_string()),
Some(false),
Some(true),
Some(false),
Some(true),
)),
]
}
ToolDisclosure::Sidecar => {
vec![McpTool::new(
"execute_typescript".to_string(),
tool_descriptions::EXECUTE_TYPESCRIPT_SIDECAR.to_string(),
schema::<ExecuteWithToolGraph>(),
)
.annotate(ToolAnnotations::from_raw(
@@ -415,10 +431,14 @@ impl McpClientTrait for CodeExecutionClient {
Some(true),
Some(false),
Some(true),
)),
],
next_cursor: None,
))]
}
};
Ok(ListToolsResult {
meta: None,
next_cursor: None,
tools,
})
}
@@ -436,7 +456,8 @@ impl McpClientTrait for CodeExecutionClient {
self.handle_get_function_details(session_id, arguments)
.await
}
"execute" => self.handle_execute(session_id, arguments).await,
"execute_bash" => self.handle_execute_bash(session_id, arguments).await,
"execute_typescript" => self.handle_execute_typescript(session_id, arguments).await,
_ => Err(format!("Unknown tool: {name}")),
};
@@ -454,28 +475,48 @@ impl McpClientTrait for CodeExecutionClient {
async fn get_moim(&self, session_id: &str) -> Option<String> {
let code_mode = self.get_code_mode(session_id).await.ok()?;
let available: Vec<_> = code_mode
.list_functions()
.functions
.iter()
.map(|f| format!("{}.{}", &f.namespace, &f.name))
.collect();
let disclosure_style_moim = match self.disclosure {
ToolDisclosure::Catalog => {
let available_fns: Vec<_> = code_mode
.list_functions()
.functions
.iter()
.map(|f| format!("{}.{}", &f.namespace, &f.name))
.collect();
format!("Available functions: {}
Use the list_functions & get_function_details tools to see tool signatures and input/output types before calling execute_typescript.", available_fns.join(", "))
}
ToolDisclosure::Filesystem => {
let available_filepaths: Vec<_> = code_mode
.virtual_fs().keys().map(String::from).collect();
format!("Use execute_bash to search and read the tool signatures and input/output types before calling execute_typescript. The available files are: {}", available_filepaths.join(", "))
},
ToolDisclosure::Sidecar => "Prioritize calling tools with the execute_typescript tool, especially when multiple tools can be called in one script.".into(),
};
Some(format!(
indoc::indoc! {r#"
ALWAYS batch multiple tool operations into ONE execute call.
- WRONG: Separate execute calls for read file, then write file
- RIGHT: One execute with an async run() function that reads AND writes
ALWAYS batch multiple tool operations into ONE execute_typescript call.
- WRONG: Separate execute_typescript calls for read file, then write file
- RIGHT: One execute_typescript with an async run() function that reads AND writes AND logs/returns as little information as needed for the next step.
Available namespaces: {}
Use the list_functions & get_function_details tools to see tool signatures and input/output types before calling unfamiliar tools.
{}
"#},
available.join(", ")
disclosure_style_moim
))
}
}
pub fn get_tool_disclosure() -> ToolDisclosure {
let config = crate::config::Config::global();
let tool_disclosure_str: String = config
.get_param("CODE_MODE_TOOL_DISCLOSURE")
.unwrap_or_else(|_| "catalog".to_string());
serde_json::from_value(serde_json::json!(tool_disclosure_str)).unwrap_or_default()
}
struct CodeModeState {
code_mode: CodeMode,
hash: u64,
@@ -128,7 +128,13 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
default_enabled: false,
unprefixed_tools: true,
client_factory: |ctx| {
Box::new(code_execution::CodeExecutionClient::new(ctx).unwrap())
Box::new(
code_execution::CodeExecutionClient::new(
ctx,
code_execution::get_tool_disclosure(),
)
.unwrap(),
)
},
},
);
+44 -7
View File
@@ -155,13 +155,50 @@ impl Agent {
#[cfg(not(feature = "code-mode"))]
let code_execution_active = false;
if code_execution_active {
tools.retain(|tool| {
if let Some(owner) = crate::agents::extension_manager::get_tool_owner(tool) {
crate::agents::extension_manager::is_first_class_extension(&owner)
} else {
false
}
});
let disclosure_style =
crate::agents::platform_extensions::code_execution::get_tool_disclosure();
tools = tools
.into_iter()
.filter_map(|mut t| match disclosure_style {
pctx_code_mode::config::ToolDisclosure::Catalog
| pctx_code_mode::config::ToolDisclosure::Filesystem => {
// in catalog & filesystem styles, progressive search is handled
// by pctx, so we want to omit all non-first-class extensions
// from the standard tool list
if crate::agents::extension_manager::get_tool_owner(&t).is_some_and(|o| {
crate::agents::extension_manager::is_first_class_extension(&o)
}) {
Some(t)
} else {
None
}
}
pctx_code_mode::config::ToolDisclosure::Sidecar => {
// in sidecar style there is no progressive search, just a way to chain tools
// together with typescript
// add output schema to description since many model providers drop the
// output schema when presenting tools to the model
let output_schema = t
.output_schema
.as_ref()
.map(|s| serde_json::json!(s).to_string())
.unwrap_or("unknown".to_string());
let description_extension = format!(
"The successful return schema of this tool is:\n{output_schema}"
);
t.description = Some(
t.description
.map(|t| format!("{t}\n{description_extension}"))
.unwrap_or(description_extension)
.into(),
);
Some(t)
}
})
.collect();
}
// Stable tool ordering is important for multi session prompt caching.
@@ -64,21 +64,16 @@ Two modes:
## code_execution
### Instructions
BATCH MULTIPLE TOOL CALLS INTO ONE execute CALL.
This extension exists to reduce round-trips. When a task requires multiple tool calls:
- WRONG: Multiple execute calls, each with one tool
- RIGHT: One execute call with a script that calls all needed tools
IMPORTANT: All tool calls are ASYNC. Use await for each call.
Workflow:
1. Use the list_functions and get_function_details tools to discover tools and signatures
2. Write ONE script that calls ALL tools needed for the task, no need to import anything,
all the namespaces returned by list_functions and get_function_details will be available
3. Chain results: use output from one tool as input to the next
4. Only return and console.log data you need, tools could have very large responses.
General:
- BATCH MULTIPLE TOOL CALLS INTO ONE `execute_typescript` CALL.
- These tools exists to reduce round-trips. When a task requires multiple tool calls:
- WRONG: Multiple `execute_typescript` calls, each with one tool
- RIGHT: One `execute_typescript` call with a script that calls all needed tools
- Only `return` and `console.log` data you need, tools could have very large responses.
- IMPORTANT: All tool calls are ASYNC. Use await for each call.
WORKFLOW:
1. Use the `list_functions` and `get_function_details` tools to discover tools signatures and input/output types.
2. Write ONE script that calls ALL tools needed for the task and execute that script with `execute_typescript`, no need to import anything, all the namespaces returned by `list_functions` and `get_function_details` will be available globally.
## developer
### Instructions
@@ -39,7 +39,7 @@ use tokio::sync::Mutex;
use uuid::Uuid;
const SHELL_TOOL: &str = "developer__shell";
const CODE_EXECUTION_TOOL: &str = "code_execution__execute";
const CODE_EXECUTION_TOOL: &str = "code_execution__execute_typescript";
type ModelSlot = Arc<Mutex<Option<LoadedModel>>>;
@@ -210,10 +210,10 @@ fn build_openai_messages_json(system: &str, messages: &[Message]) -> String {
/// the model:
///
/// - `ToolRequest` with a `"command"` argument → `$ command`
/// - `ToolRequest` with a `"code"` argument → `` ```execute\n…\n``` ``
/// - `ToolRequest` with a `"code"` argument → `` ```execute_typescript\n…\n``` ``
/// - `ToolResponse` → `Command output:\n…`
///
/// Only `developer__shell` and `code_execution__execute` style tool calls are
/// Only `developer__shell` and `code_execution__execute_typescript` style tool calls are
/// recognized (by argument shape, not tool name). Tool calls from other extensions
/// (e.g. custom MCP tools made by a native-tool-calling model earlier in the
/// conversation) are silently dropped, since the emulator path has no syntax to
@@ -241,7 +241,7 @@ fn extract_text_content(msg: &Message) -> String {
.and_then(|a| a.get("code"))
.and_then(|v| v.as_str())
{
parts.push(format!("```execute\n{}\n```", code));
parts.push(format!("```execute_typescript\n{}\n```", code));
}
}
}
@@ -34,7 +34,7 @@ use super::inference_engine::{
};
use super::{finalize_usage, StreamSender, CODE_EXECUTION_TOOL, SHELL_TOOL};
const HOLD_BACK_CODE_MODE: usize = " ```execute\n".len();
const HOLD_BACK_CODE_MODE: usize = " ```execute_typescript\n".len();
const HOLD_BACK_SHELL_ONLY: usize = "\n$".len();
pub(super) fn load_tiny_model_prompt() -> String {
@@ -79,7 +79,7 @@ pub(super) fn build_emulator_tool_description(tools: &[Tool], code_mode_enabled:
The code runs immediately — do not explain it, just run it.\n\n",
);
tool_desc.push_str("Example — counting files in /tmp:\n\n");
tool_desc.push_str("```execute\nasync function run() {\n");
tool_desc.push_str("```execute_typescript\nasync function run() {\n");
tool_desc.push_str(
" const result = await Developer.shell({ command: \"ls -1 /tmp | wc -l\" });\n",
);
@@ -206,7 +206,9 @@ impl StreamingEmulatorParser {
ParserState::Normal => {
// Check for ```execute block (code mode)
if self.code_mode_enabled {
if let Some((before, after)) = self.buffer.split_once("```execute\n") {
if let Some((before, after)) =
self.buffer.split_once("```execute_typescript\n")
{
if !before.trim().is_empty() {
results.push(EmulatorAction::Text(before.to_string()));
}
@@ -215,8 +217,8 @@ impl StreamingEmulatorParser {
continue;
}
// Also handle without newline after tag (accumulating)
if self.buffer.ends_with("```execute") {
let before = self.buffer.trim_end_matches("```execute");
if self.buffer.ends_with("```execute_typescript") {
let before = self.buffer.trim_end_matches("```execute_typescript");
if !before.trim().is_empty() {
results.push(EmulatorAction::Text(before.to_string()));
}
@@ -561,7 +563,7 @@ mod tests {
#[test]
fn execute_block() {
let input = "Here's the code:\n```execute\nconsole.log('hi');\n```\n";
let input = "Here's the code:\n```execute_typescript\nconsole.log('hi');\n```\n";
let actions = parse_all(input, true);
assert!(actions.len() >= 2);
assert_text(&actions[0], "Here's the code:");
@@ -570,7 +572,7 @@ mod tests {
#[test]
fn execute_block_not_detected_without_code_mode() {
let input = "```execute\nconsole.log('hi');\n```\n";
let input = "```execute_typescript\nconsole.log('hi');\n```\n";
let actions = parse_all(input, false);
// Should be treated as plain text
for action in &actions {
@@ -592,7 +594,10 @@ mod tests {
#[test]
fn execute_fence_split_across_chunks() {
let actions = parse_chunks(&["Here:\n```ex", "ecute\nlet x = 1;\n", "```\n"], true);
let actions = parse_chunks(
&["Here:\n```ex", "ecute_typescript\nlet x = 1;\n", "```\n"],
true,
);
let executes: Vec<_> = actions
.iter()
.filter(|a| matches!(a, EmulatorAction::ExecuteCode(_)))
@@ -655,7 +660,7 @@ mod tests {
#[test]
fn execute_block_with_multiline_code() {
let input = "```execute\nasync function run() {\n const r = await Developer.shell({ command: \"ls\" });\n return r;\n}\n```\n";
let input = "```execute_typescript\nasync function run() {\n const r = await Developer.shell({ command: \"ls\" });\n return r;\n}\n```\n";
let actions = parse_all(input, true);
let executes: Vec<_> = actions
.iter()
@@ -674,7 +679,7 @@ mod tests {
#[test]
fn unclosed_execute_block_flushed() {
// Model stops generating mid-block
let input = "```execute\nlet x = 1;";
let input = "```execute_typescript\nlet x = 1;";
let actions = parse_all(input, true);
let executes: Vec<_> = actions
.iter()
@@ -436,13 +436,13 @@ mod tests {
#[test]
fn test_parse_glm_style_tool_call_multiple_args() {
let text = "Let me check.\n<tool_call>execute<arg_key>code</arg_key><arg_value>async function run() { return 1; }</arg_value><arg_key>tool_graph</arg_key><arg_value>[{\"tool\": \"shell\"}]</arg_value></tool_call>";
let text = "Let me check.\n<tool_call>execute_typescript<arg_key>code</arg_key><arg_value>async function run() { return 1; }</arg_value><arg_key>tool_graph</arg_key><arg_value>[{\"tool\": \"shell\"}]</arg_value></tool_call>";
let result = split_content_and_xml_tool_calls(text);
assert!(result.is_some());
let (content, calls) = result.unwrap();
assert_eq!(content, "Let me check.");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "execute");
assert_eq!(calls[0].0, "execute_typescript");
assert_eq!(
calls[0].1.get("code").unwrap(),
"async function run() { return 1; }"
@@ -15,13 +15,15 @@ This functionality requires the built-in [Code Mode extension](/docs/mcp/code-mo
:::
Code Mode controls how tools are discovered and called:
- Tools from enabled extensions are discovered on-demand and loaded into context as needed
- Multiple tool calls are batched in one execution
- Intermediate results are chained (output from one tool as input to the next)
## How Code Mode Works
The [Code Mode extension](/docs/mcp/code-mode-mcp) is an MCP server that uses the MCP protocol to expose three foundational meta-tools. When Code Mode is enabled, goose switches to Code Mode. For every request, the LLM writes JavaScript code that goose executes using [pctx (Port of Context)](https://github.com/AdrianCole/pctx), a custom Deno-based runtime, to:
The [Code Mode extension](/docs/mcp/code-mode-mcp) is an MCP server that uses the MCP protocol to expose three foundational meta-tools. When Code Mode is enabled, goose switches to Code Mode. For every request, the LLM writes JavaScript code that goose executes using [pctx (Port of Context)](https://portofcontext.com/) ([GitHub](https://github.com/portofcontext/pctx)), a custom Deno-based runtime, to:
- Discover available tools from your enabled extensions (if needed)
- Learn how to work with the tools it needs for the current task
- Call those tools programmatically to complete the task
@@ -32,7 +34,7 @@ Traditional MCP tool calling and Code Mode are two different approaches to the s
| Aspect | Traditional | Code Mode |
|--------|------------------|-----------|
| **Tool Discovery** | All tools from enabled extensions, for example:<br/>• `developer.shell`<br/>• `developer.text_editor`<br/>• `github.list_issues`<br/>• `github.get_pull_request`<br/>• `slack.send_message`<br/>• ... *potentially many more* | Code Mode extension's meta-tools:<br/>• `list_functions`<br/>• `get_function_details`<br/>• `execute`<br/><br/>The LLM uses these tools to discover tools from other enabled extensions as needed |
| **Tool Discovery** | All tools from enabled extensions, for example:<br/>• `developer.shell`<br/>• `developer.text_editor`<br/>• `github.list_issues`<br/>• `github.get_pull_request`<br/>• `slack.send_message`<br/>• ... *potentially many more* | Code Mode extension's meta-tools:<br/>• `list_functions`<br/>• `get_function_details`<br/>• `execute_typescript`<br/><br/>The LLM uses these tools to discover tools from other enabled extensions as needed |
| **Tool Calling** | • Sequential tool calls<br/>• Each result sent to the LLM before the next call | • May require tool discovery calls<br/>• Multiple tool calls batched in one execution<br/>• Intermediate results are chained and processed locally |
| **Context Window** | Every LLM call includes all tool definitions from enabled extensions | Every LLM call includes the 3 meta-tool definitions, plus any tool definitions previously discovered in the session |
| **Best For** | • 1-3 enabled extensions<br/>• Simple tasks using 1-2 tools | • 5+ extensions<br/>• Well-defined multi-step workflows |
@@ -68,4 +70,4 @@ import notMcpReplacement from '@site/blog/2025-12-21-code-mode-doesnt-replace-mc
duration: '8 min read'
}
]}
/>
/>
+3 -2
View File
@@ -27,10 +27,11 @@ run_test() {
cd "$testdir" && "$GOOSE_BIN" run --text "$prompt" --with-builtin "$BUILTINS" 2>&1
) > "$output_file" 2>&1
# Matches: "execute | code_execution", "get_function_details | code_execution",
# Matches: "execute_typescript | code_execution", "get_function_details | code_execution",
# "tool call | execute", "tool calls | execute" (old format)
# "▸ execute N tool call" (new format with tool_graph)
if grep -qE "(execute \| code_execution)|(get_function_details \| code_execution)|(tool calls? \| execute)|(▸.*execute.*tool call)" "$output_file"; then
# "▸ execute_typescript" (plain tool name in output)
if grep -qE "(execute_typescript \| code_execution)|(get_function_details \| code_execution)|(tool calls? \| execute)|(▸.*execute.*tool call)|(▸ execute_typescript)" "$output_file"; then
echo "success|code_execution tool called" > "$result_file"
else
echo "failure|no code_execution tool calls found" > "$result_file"
@@ -354,9 +354,9 @@ const formatSubagentToolCall = (data: SubagentToolRequestData): string => {
const extensionName = parts.slice(1).reverse().join('__') || '';
const toolGraph = toolCall.arguments?.tool_graph;
if (toolName === 'execute_code' && toolGraph && toolGraph.length > 0) {
if (toolName === 'execute_typescript' && toolGraph && toolGraph.length > 0) {
const plural = toolGraph.length === 1 ? '' : 's';
const header = `[subagent:${shortId}] ${toolGraph.length} tool call${plural} | execute_code`;
const header = `[subagent:${shortId}] ${toolGraph.length} tool call${plural} | execute_typescript`;
const lines = toolGraph.map((node, idx) => {
const deps =
node.depends_on && node.depends_on.length > 0
@@ -638,7 +638,7 @@ function ToolCallView({
case 'computer_control':
return `poking around...`;
case 'execute': {
case 'execute_typescript': {
const toolGraph = args.tool_graph as unknown as ToolGraphNode[] | undefined;
if (toolGraph && Array.isArray(toolGraph) && toolGraph.length > 0) {
if (toolGraph.length === 1) {
@@ -736,7 +736,7 @@ function ToolCallView({
const toolGraph = toolCall.arguments?.tool_graph as unknown as ToolGraphNode[] | undefined;
if (
toolCall.name === 'code_execution__execute' &&
toolCall.name === 'code_execution__execute_typescript' &&
(typeof code === 'string' || Array.isArray(toolGraph))
) {
return (