Support CodeMode ToolDisclosure Variants (#7926)
Signed-off-by: Elias Posen <elias@posen.ch>
This commit is contained in:
@@ -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"}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
+10
-15
@@ -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; }"
|
||||
|
||||
Reference in New Issue
Block a user