A tool is an R function the model can ask to call. Deputy ships tools
for files, data, the web and code, and any R function becomes a tool
once you describe it with ellmer::tool(). Registering a
tool only makes it available: the agent’s permissions decide whether each call
runs.
Built-in tools
| Tool | What it does | Effects |
|---|---|---|
tool_read_file |
Read a text file, or pages of a PDF | read-only |
tool_read_markdown |
Convert a PDF, Word, PowerPoint or HTML file to Markdown (needs the
Python markitdown module) |
read-only |
tool_list_files |
List a directory | read-only |
tool_glob_files |
Find files by glob pattern | read-only |
tool_grep_files |
Search file contents | read-only |
tool_read_csv |
Read a CSV file and summarise it | read-only |
tool_write_file |
Write or append to a file | destructive |
tool_edit_file |
Replace text in a file | destructive |
tool_multi_edit |
Make several replacements in one file | destructive |
tool_run_r_code |
Run R code in a new R process | destructive, open world |
tool_run_bash |
Run a shell command | destructive, open world |
tool_web_fetch |
Fetch a web page as text | read-only, open world |
tool_web_search |
Search the web with DuckDuckGo | read-only, open world |
tool_ask_user |
Ask the person running the agent a question | read-only |
run_r_code and run_bash are trusted-code
tools: the code runs with your user account’s access to files and the
network. The default permissions deny both. Running R code covers when to enable them
and how to sandbox model-written code instead.
Bundles and presets
Bundles group related tools:
library(deputy)
tools_file() # read_file, read_markdown, write_file, list_files
tools_data() # read_csv, read_file, read_markdown
tools_code() # run_r_code, run_bash
tools_web() # web_fetch, web_search
tools_all() # every built-in tool except ask_userCombine them with c():
agent <- Agent$new(
chat = ellmer::chat("openai/gpt-6-luna"),
tools = c(tools_file(), tools_web()),
permissions = Permissions(web = TRUE)
)Presets are named combinations for common jobs:
| Preset | Tools |
|---|---|
tools_preset("minimal") |
read_file, read_markdown,
list_files
|
tools_preset("standard") |
minimal plus write_file
|
tools_preset("data") |
minimal plus read_csv and
run_r_code
|
tools_preset("dev") |
standard plus run_r_code and
run_bash
|
tools_preset("full") |
tools_all() |
The data and dev presets include
trusted-code tools, so they only do something useful with permissions
that allow code (r_code = TRUE,
bash = TRUE).
Turn an R function into a tool
A tool needs a function, a name, a description the model reads, and
the types of its arguments. This one wraps toupper():
library(deputy)
uppercase <- ellmer::tool(
base::toupper,
name = "uppercase",
description = "Convert text to upper case.",
arguments = list(x = ellmer::type_string()),
annotations = ellmer::tool_annotations(
read_only_hint = TRUE,
destructive_hint = FALSE,
open_world_hint = FALSE,
idempotent_hint = TRUE
)
)
uppercase("hello")
#> [1] "HELLO"Pass it to the agent like any other tool:
agent <- Agent$new(
chat = ellmer::chat("openai/gpt-6-luna"),
tools = list(uppercase)
)
agent$run_sync("Use the uppercase tool to shout 'hello'.")$responseThe annotations tell Deputy what the function does. Permissions are decided from them:
| Annotation |
TRUE means |
|---|---|
read_only_hint |
the tool changes nothing |
destructive_hint |
the tool may delete or overwrite data |
open_world_hint |
the tool reaches outside the machine, for example the network |
idempotent_hint |
calling it twice has the same effect as calling it once |
Deputy takes annotations at their word; it doesn’t inspect the
function to check them. When you leave one out, Deputy assumes the risky
answer: the tool might destroy data, might reach the network, and isn’t
safe to retry. A tool that might reach the network then needs a policy
with web = TRUE, and one that might destroy data needs a
policy that allows file writes or shell commands. So declare all four.
In "readonly" mode a custom tool must also be listed in the
policy’s tool_allowlist.
Add, replace and inspect tools
Add tools to an existing agent with $register_tool() or
$register_tools(). Registering a name that already exists
is an error unless you pass replace = TRUE.
$set_tools() replaces all tools at once. Deputy checks the
whole batch before changing anything, so one bad tool leaves the agent’s
tools as they were.
agent$register_tool(tool_list_files)
agent$register_tool(uppercase, replace = TRUE)tool_metadata() shows where a tool came from, which
annotations it declares and which defaults Deputy will assume, without
running it:
tool_metadata(uppercase)
#> $name
#> [1] "uppercase"
#>
#> $source
#> $source$type
#> [1] "package"
#>
#> $source$package
#> [1] "base"
#>
#>
#> $annotations
#> $annotations$read_only_hint
#> [1] TRUE
#>
#> $annotations$open_world_hint
#> [1] FALSE
#>
#> $annotations$idempotent_hint
#> [1] TRUE
#>
#> $annotations$destructive_hint
#> [1] FALSE
#>
#>
#> $missing_annotations
#> character(0)
#>
#> $effective_annotations
#> $effective_annotations$read_only_hint
#> [1] TRUE
#>
#> $effective_annotations$destructive_hint
#> [1] FALSE
#>
#> $effective_annotations$idempotent_hint
#> [1] TRUE
#>
#> $effective_annotations$open_world_hint
#> [1] FALSEWeb tools
tools_web() returns Deputy’s own web_fetch
and web_search, which work with any provider. Pass it your
chat to use the provider’s built-in web tools instead, where there are
any (Anthropic, Google, and OpenAI search):
chat <- ellmer::chat("anthropic/claude-sonnet-5")
agent <- Agent$new(
chat = chat,
tools = tools_web(chat),
permissions = Permissions(
web = TRUE,
tool_allowlist = c("web_search", "web_fetch")
)
)Provider tools run on the provider’s servers, so Deputy can’t check
each call. It checks them once, when they’re registered: the policy must
allow web access and list them in tool_allowlist. A policy
with a can_use_tool callback can’t use them at all, because
the callback would never see their arguments. Use
tools_web(chat, use_native = FALSE) when you need per-call
checks.
Skills
A skill packages instructions, tools and the packages they need, so you can give the same abilities to several agents. Create one in code:
concise <- skill_create(
"concise",
prompt = "Answer in one short paragraph.",
requires = list(packages = "base")
)
skill_check_requirements(concise)$ok
#> [1] TRUEagent$load_skill(concise) appends the prompt to the
agent’s system prompt and registers the skill’s tools under the agent’s
current permissions. Missing packages produce a warning, and a tool name
that clashes with an existing tool is an error unless you pass
allow_conflicts = TRUE. agent$skills() lists
what’s loaded.
Skills can also live in a directory with a SKILL.md
prompt and a SKILL.yaml that lists tools and requirements.
Deputy ships two: data_analysis, with exploratory analysis
tools, and debate, a prompt for weighing opposing
arguments.
agent$load_skill(system.file("skills", "data_analysis", package = "deputy"))Loading a skill directory sources the R files its tools are defined
in, so load only skills you trust. skills_list() lists the
skills in a directory.
More tools
- Running R code: trusted R execution, persistent R sessions, and sandboxed interpreters through mcp-repl and MCP Console.
- MCP servers: tools, resources and prompts from Model Context Protocol servers.
-
Human input and approvals:
tools_interactive()lets the model ask a person questions.