deputy is an agentic AI framework for R that builds on ellmer. It enables you to create AI agents that can use tools to accomplish multi-step tasks, with built-in support for permissions, hooks, and streaming output.
Installation
# Install from GitHub
# install.packages("pak")
pak::pak("JamesHWade/deputy")You’ll also need ellmer for the underlying LLM functionality:
pak::pak("tidyverse/ellmer")Quick Start
Streaming Output
For real-time feedback, use the run() method which
returns a generator:
for (event in agent$run("Analyze the structure of this project")) {
switch(
event$type,
"text" = cat(event$text),
"tool_start" = cli::cli_alert_info("Calling {event$tool_name}..."),
"tool_end" = cli::cli_alert_success("Done"),
"stop" = cli::cli_alert("Finished! Cost: ${event$cost$total}")
)
}Tools
deputy provides built-in tools in convenient bundles:
| Bundle | Tools | Purpose |
|---|---|---|
tools_file() |
read_file, write_file,
list_files
|
File operations |
tools_code() |
run_r_code, run_bash
|
Code execution |
tools_data() |
read_csv, read_file
|
Data reading |
tools_all() |
All of the above | Everything |
# Combine multiple tool bundles
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = c(tools_file(), tools_code())
)Custom Tools
You can create custom tools using ellmer’s tool()
function:
# Create a custom tool
tool_weather <- ellmer::tool(
name = "get_weather",
description = "Get the current weather for a location",
arguments = list(
location = ellmer::tool_arg(
type = "string",
description = "City name"
)
),
.fun = function(location) {
# Your implementation here
paste("Weather in", location, "is sunny, 72F")
}
)
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = list(tool_weather)
)Permissions
Permissions control what an agent is allowed to do. deputy provides three preset configurations:
Standard Permissions (default)
# Allows: file read/write (in working dir), R code execution
# Denies: bash commands, web access, package installation
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_file(),
permissions = permissions_standard()
)Read-Only Permissions
# Only allows reading files - no writes, no code execution
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_file(),
permissions = permissions_readonly()
)Full Permissions
# Allows everything - use with caution!
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_all(),
permissions = permissions_full()
)Custom Permissions
For fine-grained control:
perms <- Permissions$new(
file_read = TRUE,
file_write = "/path/to/allowed/dir", # Restrict to specific directory
bash = FALSE,
r_code = TRUE,
web = FALSE,
max_turns = 10,
max_cost_usd = 0.50
)
agent <- Agent$new(
chat = ellmer::chat("openai"),
permissions = perms
)Custom Permission Callbacks
For complex permission logic:
perms <- Permissions$new(
can_use_tool = function(tool_name, tool_input, context) {
# Block any file writes to sensitive directories
if (tool_name == "write_file") {
if (grepl("^\\.env|secrets|credentials", tool_input$path)) {
return(PermissionResultDeny(
reason = "Cannot write to sensitive files"
))
}
}
PermissionResultAllow()
}
)Hooks
Hooks let you intercept and customize agent behavior at key points:
Available Hook Events
| Event | When it Fires | Can Modify |
|---|---|---|
PreToolUse |
Before a tool executes | Allow/deny, input |
PostToolUse |
After a tool executes | Continue flag |
Stop |
When agent stops | - |
UserPromptSubmit |
When user sends a message | - |
PreCompact |
Before conversation compaction | Summary |
Example: Logging All Tool Calls
agent$add_hook(HookMatcher$new(
event = "PostToolUse",
callback = function(tool_name, tool_result, context) {
cli::cli_alert_info("Tool {tool_name} completed")
HookResultPostToolUse()
}
))Example: Block Dangerous Commands
agent$add_hook(HookMatcher$new(
event = "PreToolUse",
pattern = "^run_bash$", # Only match bash tool
callback = function(tool_name, tool_input, context) {
if (grepl("rm -rf|sudo|chmod 777", tool_input$command)) {
HookResultPreToolUse(
permission = "deny",
reason = "Dangerous command pattern detected"
)
} else {
HookResultPreToolUse(permission = "allow")
}
}
))Multi-Agent Systems
For complex tasks, you can create a lead agent that delegates to specialized sub-agents:
# Define specialized sub-agents
code_agent <- agent_definition(
name = "code_analyst",
description = "Analyzes R code and suggests improvements",
prompt = "You are an expert R programmer. Analyze code for best practices.",
tools = tools_file()
)
data_agent <- agent_definition(
name = "data_analyst",
description = "Analyzes data files and provides statistical summaries",
prompt = "You are a data analyst. Provide clear statistical insights.",
tools = tools_data()
)
# Create a lead agent that can delegate
lead <- LeadAgent$new(
chat = ellmer::chat("openai"),
sub_agents = list(code_agent, data_agent),
system_prompt = "You coordinate between specialized agents to complete tasks."
)
# The lead agent will automatically delegate to sub-agents as needed
result <- lead$run_sync(
"Review the R code in src/ and analyze the data in data/"
)Working with Results
The AgentResult object contains useful information:
result <- agent$run_sync("Analyze this project")
# The final response
cat(result$response)
# Cost information
result$cost
#> $input
#> [1] 1250
#> $output
#> [1] 450
#> $total
#> [1] 0.0045
# Execution duration
result$duration
#> [1] 3.45 # seconds
# Stop reason
result$stop_reason
#> [1] "complete"
# All events (for detailed analysis)
length(result$events)
#> [1] 12Provider Support
deputy works with any LLM provider that ellmer supports:
# OpenAI
agent <- Agent$new(chat = ellmer::chat("openai"))
# Anthropic
agent <- Agent$new(chat = ellmer::chat("anthropic/claude-sonnet-4-5-20250929"))
# Google
agent <- Agent$new(chat = ellmer::chat("google/gemini-1.5-pro"))
# Local models via Ollama
agent <- Agent$new(chat = ellmer::chat("ollama/llama3.1"))Best Practices
Start with minimal permissions - Use
permissions_readonly()orpermissions_standard()and only expand as needed.Use hooks for logging - Add a
PostToolUsehook to track what your agent does.Set cost limits - Use
max_cost_usdin permissions to prevent runaway costs.Save sessions - For long-running tasks, save sessions periodically.
Use streaming for UX - The
run()method provides real-time feedback.
# Example combining best practices
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_file(),
permissions = Permissions$new(
file_write = getwd(),
max_turns = 20,
max_cost_usd = 1.00
)
)
agent$add_hook(HookMatcher$new(
event = "PostToolUse",
callback = function(tool_name, tool_result, context) {
message(sprintf("[%s] %s", Sys.time(), tool_name))
HookResultPostToolUse()
}
))
for (event in agent$run("Organize the files in this directory")) {
if (event$type == "text") cat(event$text)
}