> ## Documentation Index
> Fetch the complete documentation index at: https://dsrs.herumbshandilya.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools and Agents

> Host and sandboxed tools, the #[agent] loop and its options, stop tools, and standalone versus in-module execution

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:

```rust theme={null}
use dspy_rs::tool;

/// Uppercase text.
#[tool(caps("demo:shout"))]
fn shout(text: String) -> String {
    text.to_uppercase()
}
```

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](/docs/components/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`:

```rust theme={null}
/// Fetch a page and return its text.
#[tool(caps("net:http"))]
async fn fetch(url: String) -> Result<String, String> {
    // your implementation
}
```

`#[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.

```rust theme={null}
use dspy_rs::agent;

/// Research the question. Use the shout tool when volume is needed.
#[agent(tools(shout), max_turns = 3, budget(tokens = 50_000, on_exhausted = finalize))]
fn research(question: String) -> String;
```

The bodyless-fn rules match `#[predict]`: no `async`, no generics, no `self`, plain identifier parameters, at least one parameter, an explicit return type.

### Options

| Option        | Form                                                                          | Meaning                                                                                                                                             |
| ------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`       | `model = "@name"`                                                             | Model reference for the loop. Binds only inside `#[module]` programs; setting it removes the standalone fn (calling one is a compile error).        |
| `tools`       | `tools(a, b)`                                                                 | The `#[tool]` functions the loop may call, in order.                                                                                                |
| `stop_tools`  | `stop_tools(a)`                                                               | Tools whose call ends the loop. Each must also appear in `tools(...)`.                                                                              |
| `max_turns`   | `max_turns = N`                                                               | Turn bound for the lowered loop node (IR default is 8 when omitted).                                                                                |
| `until_parse` | `until_parse = bool`                                                          | Stop when an assistant turn parses as the signature outputs (IR default is true).                                                                   |
| `budget`      | `budget(calls = N, tokens = N, deadline_ms = N, on_exhausted = finalize)`     | Per-node spend limits. `on_exhausted` is `fail` (the default, the run fails) or `finalize` (one forced final round trip without tools, then parse). |
| `context`     | `context(max_history_turns = N, tool_result_max_bytes = N, playbook = "...")` | Context policy: history window, tool-result truncation, and a free-text playbook.                                                                   |

## 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(...)`:

```rust theme={null}
/// Submit the final answer. Call this exactly once, when you are done.
#[tool]
fn submit(research: String) -> String {
    research
}

/// Research the question. Call submit when you have the answer.
#[agent(tools(shout, submit), stop_tools(submit), max_turns = 6)]
fn research(question: String) -> String;
```

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.

```rust theme={null}
use dspy_rs::module;

#[dspy_rs::Schema]
#[derive(Debug)]
pub struct AOut {
    pub answer: String,
}

#[module(caps("demo:shout"))]
async fn agentic(question: String) -> Result<AOut, dspy_rs::ir::RunError> {
    let researcher = research(question).await?;
    Ok(AOut {
        answer: researcher.research,
    })
}
```

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:

````
tool shout "Uppercase the text" {
  in  text: string
  out shout: string
} js```
(args) => ({ shout: args.text.toUpperCase() })
```
````

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](/docs/components/dsrs-file) for the full text format.

## Related surfaces

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](/docs/components/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](/docs/components/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](/docs/components/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](/docs/components/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](/docs/components/capabilities) for tool needs, program ceilings, and host grants
* [Code Mode](/docs/components/code-mode) for presenting many tools as one script surface
* [Modules](/docs/components/modules) for the struct-lane strategies
* [The module macro](/docs/components/module-macro) for the body rules that lower agent calls
* [The .dsrs file](/docs/components/dsrs-file) for sandboxed tool syntax in the artifact
