Skip to main content
Plain JSON tool calling pays one model round trip per call: the model emits a single tool invocation, the host executes it, and the result goes back to the model before it can decide the next step. A task that needs several tool results spends a full LM turn of latency and tokens on each one, and every intermediate result flows through the model’s context even when only the final value matters. Code Mode replaces JSON tool calling with code execution. Instead of advertising N tool schemas and paying one model round trip per call, the model sees a single meta-tool named run_js. Its description lists your tools as a JavaScript API; the model writes a script that calls them as plain global functions, composes their results, and returns one value. One script replaces many round trips. Everything on this page lives in the dsrs-tools crate. The dspy-rs crate re-exports Capability, CodeModeTool, RUN_JS_TOOL_NAME, and SandboxConfig behind the code-mode cargo feature, which is on by default. The rest of the surface (executor, sources, errors) is imported from dsrs_tools directly. dsrs_tools also re-exports rig::tool::ToolDyn and rig::tool::ToolError (as RigToolError) so downstream crates do not need a version-matched rig dependency.

Two-tier design

The Executor trait is the seam between the tiers: subprocess, microVM, and remote executors can implement the same contract.

ToolSource and the tool lifecycle

ToolSource is the raw material for an ephemeral tool, before validation. js_source must be a single expression that evaluates to a function taking one argument (the parsed JSON args object) and returning a JSON-serializable value or a promise of one. Helpers go inside an IIFE that returns the tool function. Named function declarations are wrapped in parentheses and become expressions; one trailing ; is tolerated. The self-test runs with the global tool bound to the compiled function; it fails if it throws or completes with false. A source only becomes callable after passing every stage of Executor::register:

Executor, ToolInvocation, RegisteredTool

ToolInvocation is one call: { name: String, args: Value }, built with ToolInvocation::new(name, args). RegisteredTool is the metadata of a tool that survived the lifecycle: Executor is Send + Sync and deliberately narrow:

QuickJsExecutor

The Tier-1 executor. Cheap to share: wrap it in an Arc and clone across tasks. It requires a Tokio runtime for its blocking pool. The builder sets limits and capabilities before construction:

SandboxConfig

Resource limits applied to every sandbox instance. Copy, so it is passed by value everywhere.

CacheStats

Sources compile once per unique content (BLAKE3-keyed) and the bytecode is shared across calls and tool names. CacheStats carries entries: usize, hits: u64, misses: u64.

run_script

The Code Mode execution primitive: a free async function, not a method.
The source runs as the body of an async IIFE in a fresh, fully fenced sandbox: top-level return produces the result, undefined maps to null, and await is tolerated but only microtask-resolvable promises settle (there is no event loop; a promise waiting on timers or IO reports PendingPromise). Capabilities appear as plain global functions. Errors are attributed to the pseudo-tool name RUN_JS_TOOL_NAME; a syntax error surfaces as ExecError::Js so a generating model can repair the script.

Capabilities

A Capability is an async Rust function injected into the sandbox as a global JS function: the only doorway out. From JavaScript the call looks synchronous (const rows = query({q: "..."})); the executor bridges it onto the host Tokio runtime and blocks the sandbox thread until it resolves. Err(String) from the handler surfaces to JS as a catchable exception. Accessors: name() and description(). CapabilityHandler is the public handler alias: Arc<dyn Fn(Value) -> BoxFuture<'static, Result<Value, String>> + Send + Sync>. For wrapped tools, the args object is serialized to JSON, handed to ToolDyn::call, and the result string is parsed back to JSON (or returned as a plain string if it is not valid JSON). A tool error becomes a JS exception whose message names the original tool: tool `<name>` failed: <error>. Capability names become JS globals, so they must be valid identifiers; the __dsrs prefix is reserved by the runtime. js_identifier(name) mangles an arbitrary tool name into a valid identifier, in order:
  1. Every character outside [A-Za-z0-9_$] becomes _ (my-tool.v2 becomes my_tool_v2).
  2. A leading digit gets a _ prepended (2fast becomes _2fast).
  3. An empty name becomes _tool.
  4. A result starting with __dsrs gets one more leading _.
The mapping is not injective: distinct names can mangle to the same identifier, so every batch wrapper refuses collisions at registration or load time instead of silently shadowing a tool.

The run_js surface

RUN_JS_TOOL_NAME is the constant "run_js". run_js_parameters() returns its argument schema: one required string property, code, described as an async function body that must return a JSON-serializable value. ToolApi is one entry of the JS API listing shown to the model: code_mode_description(apis: &[ToolApi]) -> String generates the default run_js description: the execution contract (async function body, global functions, one arguments object each, failed calls throw, no filesystem or network or imports) plus a token-compact listing such as - search(args): Find documents. args: {query: string, limit?: integer} and a short example. It is deliberately a plain function: in the IR this description is an optimizable ToolDesc parameter, and this function supplies its default value. CodeModeTool packages the whole surface as one rig::tool::ToolDyn: Its ToolDyn implementation advertises itself as run_js with run_js_parameters(). The call contract: model-repairable failures (script errors, tool failures, deadline and memory kills, bad arguments) are returned as Ok with the typed error serialized to JSON, so an outer tool loop feeds them back to the model instead of aborting. Only ExecError::Internal surfaces as Err. SandboxTool is the other ToolDyn bridge: one validated ephemeral tool (not the collapsed surface) exposed under its registered name. Obtain one via QuickJsExecutor::rig_tool or register_rig; construct directly with SandboxTool::new(executor, meta) and read metadata with meta(). Every call round-trips through the owning Executor, so limits, capabilities, and the bytecode cache apply. Empty argument strings are treated as {}. Unlike CodeModeTool, it surfaces every failure as Err(ToolError::ToolCallError) carrying the structured error JSON.

Errors

Both enums serialize to tagged JSON, and both have a to_llm_json() method that produces the string fed back to the model for self-repair. That JSON shape is part of the contract: the tag tells a generating loop which artifact to regenerate. ExecError tags on kind, RegisterError tags on stage, both in snake_case. Example: {"kind":"timeout","name":"add","deadline_ms":500}.

ExecError

Raised while executing an already-registered tool or a run_js script.

RegisterError

Raised during the validate-then-register lifecycle.

Integration: the two lanes

The sandbox exposes exactly the injected capability globals and nothing else; which tools become capabilities is decided per lane. Module lane. ToolSet::code_mode(tools, config) (async, behind the code-mode feature) collapses a Vec<Arc<dyn ToolDyn>> into a ToolSet containing a single CodeModeTool. Drop it into any tool loop, LM::call_with_toolset or Predict, exactly like a normal ToolSet. It errors if two tool names mangle to the same JS identifier. See LM. IR lane. RuntimeEnv::with_code_mode(config) enables Code Mode for every AgentLoop in a loaded program: the loop’s non-stop tools are presented as one run_js definition instead of N JSON definitions, while stop tools keep their plain definitions so the loop can still terminate. Host tools are wrapped with Capability::wrap_tool; sandboxed tools route through the environment’s bound executor under their registered names. The run_js description is generated from the overlay-resolved tool descriptions, so optimizable ToolDesc parameters keep flowing into the surface the model sees. Identifier collisions are refused at load. This is a RuntimeEnv binding option, not a ToolKind variant: Code Mode is a host presentation strategy, not program semantics, so the same artifact (same tools, same program hash) runs identically either way. See Runtime.
In the IR lane the usual capability gates still apply on top of the sandbox fence: program.caps must be a subset of the environment’s grants at load, and a run that reaches for an unpermitted capability fails with RunError::CapabilityDenied.

See also