deputy is an agentic AI framework for R that builds on ellmer. It enables you to create AI agents that use tools to accomplish multi-step tasks, with built-in support for permissions, hooks, and streaming output.
Installation
# Install from GitHub
pak::pak("JamesHWade/deputy")Quick Start
An agent wraps an ellmer chat object and gives it tools, permissions, and lifecycle hooks:
library(deputy)
chat <- ellmer::chat_anthropic(model = "claude-sonnet-4-20250514")
agent <- Agent$new(chat = chat, tools = tools_file())
result <- agent$run_sync("List the files in the current directory")
cat(result$response)The agent sends your task to the LLM, which may call tools, and loops
until the task is complete. run_sync() blocks until done
and returns an AgentResult.
Tool Bundles
deputy organises built-in tools into bundles you can mix and match:
| 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_web() |
web_fetch, web_search
|
Web access |
tools_all() |
All of the above | Everything |
# Combine bundles
agent <- Agent$new(
chat = ellmer::chat_anthropic(),
tools = c(tools_file(), tools_code())
)There are also named presets available via
tools_preset():
list_presets()
tools_preset("dev")See vignette("tools") for custom tools, web tools, MCP
integration, and human-in-the-loop.
Streaming
For real-time feedback, use run() which returns a
generator:
chat <- ellmer::chat_anthropic(model = "claude-sonnet-4-20250514")
agent <- Agent$new(chat = chat, tools = tools_file())
for (event in agent$run("What is the name of this package?")) {
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!")
)
}Events stream as they happen – you see text tokens arrive, tool calls start and finish, and a final stop event.
Permissions
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()
}
)Tool Allow/Deny Lists (Claude SDK-style)
Use allow/deny lists when you want an explicit tool policy that is separate from the broader mode flags:
perms <- Permissions$new(
mode = "default",
tool_allowlist = c("read_file", "list_files", "run_r_code"),
tool_denylist = c("run_bash"),
permission_prompt_tool_name = "AskUserQuestion"
)
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_all(),
permissions = perms
)If both lists are present, tool_denylist wins. The
permission_prompt_tool_name tool is always allowed and
appears in deny reasons so the model can request approval.
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
)
)Next Steps
-
vignette("tools")– Custom tools, web tools, MCP, and human-in-the-loop -
vignette("permissions")– Permission presets, modes, and custom policies -
vignette("hooks")– Lifecycle hooks for logging, blocking, and auditing -
vignette("multi-agent")– Multi-agent delegation with LeadAgent -
vignette("structured-output")– JSON schema output and validation -
vignette("agent-configuration")– Settings, skills, sessions, and AgentResult