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.
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
ACapability 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:
- Every character outside
[A-Za-z0-9_$]becomes_(my-tool.v2becomesmy_tool_v2). - A leading digit gets a
_prepended (2fastbecomes_2fast). - An empty name becomes
_tool. - A result starting with
__dsrsgets one more leading_.
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 ato_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
- LM:
ToolSetand the tool loop thatrun_jsdrops into. - Runtime:
RuntimeEnv,with_sandbox,with_code_mode, and the load-time checks. - Tools and agents: writing host and sandboxed tools.
- Capabilities: the grants that gate what sandboxed code may reach.
- Runnable example: https://github.com/krypticmouse/DSRs/blob/main/crates/dspy-rs/examples/15-tools.rs
- Sandbox latency microbench: https://github.com/krypticmouse/DSRs/blob/main/crates/dsrs-tools/examples/bench.rs
