Skip to main content
A tool is a function the model may call while it works: you describe what the tool does, the model decides when to use it. An agent is a step that runs the model in a loop with tools; the model thinks, calls tools, reads the results, and answers when it is done. DSRs has two tool kinds: host tools are Rust functions in your binary, and sandboxed tools are JavaScript snippets that travel inside a .dsrs program file.

Host tools

A host tool is a normal Rust function marked #[tool]. The body is the implementation:
The doc comment is the tool’s description and the main thing the model uses to pick a tool, so write it for the model, like a short label on a button. caps("...") is optional and declares what the tool is allowed to touch, for example net:search or fs:read; the host checks these labels before it runs a program, so nothing gets network or file access silently. See Capabilities for the full model. The function stays plain Rust; you can still call shout("hi".to_string()) yourself in tests. Tools can be async, and they can fail. A fallible tool returns Result<T, E> where E: Display:
#[tool] generates the original function unchanged, plus a module carrying the tool’s signature, a rig tool wrapper for host binding, and __dsrs_tool() metadata consumed by #[agent] and #[module]. The rules: no generics, no self, plain identifier parameters, at least one parameter, an explicit return type. The attribute accepts only caps("...", ...); anything else is a compile error.

Declaring an agent

An agent is the tool-loop sibling of #[predict]: a bodyless function whose doc comment is the instruction, whose parameters are the input fields, and whose return type is a single output field named after the function.
The bodyless-fn rules match #[predict]: no async, no generics, no self, plain identifier parameters, at least one parameter, an explicit return type.

Options

Stop tools

Sometimes the model should end the loop with one explicit call instead of writing a final message. Declare a tool whose inputs match the agent’s output fields and list it in stop_tools(...):
When the model calls a stop tool, the loop ends right there. The arguments of that call become the step’s output, and the tool body does not run. That is why the stop tool’s inputs must be named after the agent’s outputs (research here). A stop tool missing from tools(...) is a compile error: “stop tool name is not in tools(…)”.

Standalone or inside a module

#[agent] generates a standalone async fn returning Result<Predicted<research::SigOutput>, PredictError>. Called directly, it executes the same 1-node AgentLoop program the #[module] lowering produces, with the loop options honored on both paths: max_turns/stop_tools/until_parse land in the node’s StopSpec, budget in its NodeBudget, and context in its ContextPolicy. The one exception is model: model refs bind only inside a #[module] program, so setting model = "…" removes the standalone fn — calling it is a compile error rather than a silent fallback to the globally configured LM.
The module’s caps(...) ceiling must cover what its tools declare; caps("demo:shout") on the module matches caps("demo:shout") on the tool, and a tool whose needs exceed the ceiling is a load error. Use the standalone form for a quick agent call, and the module form when the agent is one step in a larger pipeline.

Sandboxed tools

A sandboxed tool is not Rust. It is a small piece of JavaScript stored inside the .dsrs text file itself, executed in a sandbox, so it cannot reach the network or the disk unless the file declares a capability and the host grants it. In a .dsrs file it looks like this:
Because the code is inside the file, the program is fully portable: anyone who can run the file gets the tool too. Host tools are the opposite: the file only names them, and the binary that serves the program must supply the implementation. See The .dsrs file for the full text format. The struct-lane way to give a model tools is to attach them to a Predict (PredictBuilder::add_tool/with_tools): a tooled predictor executes as a 1-node agent program through the interpreter, with the default stop behavior (until_parse, max_turns = 8). See Predict. There is no separate ReAct module. Code Mode is the many-tools-to-one-script alternative. Instead of advertising N tool schemas and paying one round trip per call, the model sees a single run_js meta-tool whose description lists your tools as a JavaScript API; it writes one script that calls them as plain functions and returns one value. See Code Mode.

Tool membership is optimizable

Which tools a loop carries is a tuned value, not just structure. The agent node’s tools list is the declaration — the loop’s capability footprint, checked against the program ceiling at load. Which of those tools the loop actually presents to the model is the ToolSet parameter ("<leaf>.tool_set"), a slot like instruction or demos: an optimizer’s candidate can drop a distracting tool or bring a declared one back, and the descriptions the model sees are themselves ToolDesc slots. The alphabet is closed — a candidate can never smuggle in a tool the declaration doesn’t cover; that is refused at load, not at call time. Absent selection means the full declared list, so nothing changes until an optimizer says so. See Program and nodes for the slot machinery.

Executing tool calls yourself

To execute tool calls yourself instead of letting the loop dispatch them (a REPL the agent drives, tools that need caller-side state), run the agent through the interpreter’s caller-managed conversation surface: Interpreter::run_conversation_caller_managed suspends the loop on tool calls and resume_conversation feeds your results back, with the same spans, budgets, and stop-tool behavior as the dispatching loop. The suspended surface presents the ToolSet-selected tools per call, same as the dispatching loop. See Runtime.

Common mistakes

Forgetting the body on a host tool. #[tool] needs a real function with a body. A bodyless function is a step, not a tool. A stop tool with the wrong input names. The stop tool’s arguments become the agent’s output. If the names do not match the agent’s output fields, the output cannot be filled in. Skipping the doc comment. The doc comment is the tool’s description. It is the main thing the model uses to pick a tool, so write it for the model, not for other programmers.

See also