.dsrs program file.
Host tools
A host tool is a normal Rust function marked#[tool]. The body is the implementation:
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.
#[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 instop_tools(...):
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.
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:
Related surfaces
The struct-lane way to give a model tools is to attach them to aPredict (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’stools 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
- Capabilities for tool needs, program ceilings, and host grants
- Code Mode for presenting many tools as one script surface
- Modules for the struct-lane strategies
- The module macro for the body rules that lower agent calls
- The .dsrs file for sandboxed tool syntax in the artifact
