deputy is a provider-agnostic agent runtime for R, built 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. It also ships an Anthropic-compatible Claude Agent SDK facade for teams that want Claude-style entrypoints, tool aliases, settings, and persisted session workflows.
Note: deputy keeps its R-native
AgentandLeadAgentAPIs as the canonical runtime. The Claude-compatible layer is opt-in viaclaude_sdk_query(),claude_sdk_options(), andClaudeSDKClient.
Features
- Provider-agnostic - Works with OpenAI, Anthropic, Google, Ollama, and any provider ellmer supports
- Anthropic-compatible facade - Claude-style entrypoints, permission modes, tool aliases, and session semantics
- Tool bundles - Pre-built tools for file operations, code execution, and data analysis
- Permission system - Fine-grained control over what agents can do
- Hooks - Intercept and customize agent behavior at key points
- Streaming output - Real-time feedback as agents work
- Multi-agent - Coordinate specialized sub-agents for complex tasks
- Session persistence - Save and restore agent conversations, including Claude-compatible session snapshots
Installation
You can install the development version of deputy from GitHub:
# install.packages("pak")
pak::pak("JamesHWade/deputy")You’ll also need ellmer:
pak::pak("tidyverse/ellmer")Quick Start
CLI (exec/deputy)
deputy ships an executable CLI app at exec/deputy (Rapp front-end). Install launchers with:
Rapp::install_pkg_cli_apps("deputy")Then run:
Single-task mode (non-interactive):
Common options include short aliases (-p, -m, -t, -P, -n, -c, -d, -v, etc.), and repeatable flags for Rapp 0.3 style inputs:
deputy --setting-source project --setting-source user
deputy --mcp-server github --mcp-server slack
deputy --debug --debug-file /tmp/deputy-debug.log--mcp-servers "github,slack" is still accepted for backward compatibility, but --mcp-server is preferred.
Claude-compatible session controls are available when you want persistent session ids and resume or fork behavior:
Anthropic-Compatible API
Use the compatibility facade when you want Claude-style options, tool names, and session persistence without giving up deputy’s provider-agnostic runtime:
options <- claude_sdk_options(
chat = ellmer::chat("openai/gpt-4o"),
setting_sources = "project",
permission_mode = "plan"
)
# One-shot query
result <- claude_sdk_query(
"Summarize the current repository state",
options = options
)
result$session_id
# Stateful client with resume/fork semantics
client <- ClaudeSDKClient$new(options)
client$query("Inspect the package structure")
client$resume(result$session_id, fork = TRUE)The compatibility layer exposes Anthropic-style tool aliases such as Read, Write, Edit, MultiEdit, Glob, Grep, LS, TodoRead, TodoWrite, WebFetch, WebSearch, and Task (when sub-agents are registered).
Tools
deputy provides tool presets for common use cases:
# Use presets for quick setup
tools_preset("minimal") # read_file, list_files
tools_preset("standard") # + write_file, run_r_code
tools_preset("dev") # + run_bash (full development)
tools_preset("data") # read_file, list_files, read_csv, run_r_code
tools_preset("full") # all tools
# Or use individual bundles
tools_file() # File operations
tools_code() # Code execution
tools_data() # Data reading
tools_all() # Everything
# List available presets
list_presets()
# Optional: read selected pages from a PDF
tool_read_file("report.pdf", pages = "1,3-5")
# Optional: convert rich docs to markdown with MarkItDown
tool_read_markdown("slides.pptx")PDF page extraction uses pdftools when available, with a fallback to reticulate + Python pypdf. tool_read_markdown() uses reticulate + Python markitdown.
Permissions
Control what your agent can do:
# Read-only: no writes, no code execution
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_file(),
permissions = permissions_readonly()
)
# Standard: file read/write in working dir, R code, no bash
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_file(),
permissions = permissions_standard()
)
# Custom permissions with limits
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_all(),
permissions = Permissions$new(
file_write = getwd(),
bash = FALSE,
r_code = TRUE,
max_turns = 10,
max_cost_usd = 0.50
)
)
# Claude SDK-style tool policy gates
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_all(),
permissions = Permissions$new(
tool_allowlist = c("read_file", "list_files", "run_r_code"),
tool_denylist = c("run_bash"),
permission_prompt_tool_name = "AskUserQuestion"
)
)
# Planning mode: only annotated read-only tools plus AskUserQuestion
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_all(),
permissions = permissions_plan()
)When both are set, tool_denylist takes precedence over tool_allowlist. permission_prompt_tool_name is always allowed and is included in deny messages so the model can request explicit approval. permissions_plan() mirrors Claude’s plan mode by allowing only read-only annotated tools and AskUserQuestion.
Hooks
Intercept agent behavior:
# Log all tool calls
agent$add_hook(HookMatcher$new(
event = "PostToolUse",
callback = function(tool_name, tool_result, context) {
message("[", Sys.time(), "] ", tool_name)
HookResultPostToolUse()
}
))
# Block dangerous bash commands
agent$add_hook(HookMatcher$new(
event = "PreToolUse",
pattern = "^run_bash$",
callback = function(tool_name, tool_input, context) {
if (grepl("rm -rf|sudo", tool_input$command)) {
HookResultPreToolUse(permission = "deny", reason = "Dangerous command")
} else {
HookResultPreToolUse(permission = "allow")
}
}
))
# Track session lifecycle for metrics/logging
agent$add_hook(HookMatcher$new(
event = "SessionStart",
callback = function(context) {
message("Session started with ", context$tools_count, " tools")
HookResultSessionStart()
}
))
agent$add_hook(HookMatcher$new(
event = "SessionEnd",
callback = function(reason, context) {
message("Session ended: ", reason, " after ", context$total_turns, " turns")
HookResultSessionEnd()
}
))Skills
Skills bundle tools and prompt extensions. You can load them from disk or create them programmatically. deputy ships with a built-in example skill under inst/skills.
# Discover bundled skills
skills_dir <- system.file("skills", package = "deputy")
skills_list(skills_dir)
# Load a skill by path
agent$load_skill(file.path(skills_dir, "data_analysis"))
# Inspect loaded skills
agent$skills()
# Create and load a skill programmatically
custom <- skill_create(
name = "my_skill",
description = "Custom helpers",
prompt = "You are a focused assistant for project-specific tasks."
)
agent$load_skill(custom)Claude Settings (settingSources)
deputy can load Claude-style settings from .claude directories, including CLAUDE.md memory, .claude/skills, .claude/commands slash commands, and .claude/agents definitions. Tool policy settings from settings.json are also applied to agent$permissions (allowedTools, disallowedTools, and permissionPromptToolName).
agent <- Agent$new(
chat = ellmer::chat("openai"),
tools = tools_file(),
setting_sources = c("project", "user")
)
# Inspect loaded slash commands
agent$slash_commands()Settings-defined agents are applied automatically when you use LeadAgent$new(setting_sources = ...) or the Claude-compatible client.
Example .claude/settings.json tool policy:
Error Handling
deputy provides structured error types for programmatic error handling:
# Catch specific error types
tryCatch(
agent$run_sync("task"),
deputy_budget_exceeded = function(e) {
message("Budget exceeded: $", e$current_cost, " > $", e$max_cost)
},
deputy_session_load = function(e) {
message("Failed to load session from: ", e$path)
},
deputy_error = function(e) {
message("Deputy error: ", conditionMessage(e))
}
)Multi-Agent Systems
Coordinate specialized agents:
# Define sub-agents
code_agent <- agent_definition(
name = "code_analyst",
description = "Analyzes R code",
prompt = "You are an expert R programmer.",
tools = tools_file()
)
# Create lead agent that can delegate
lead <- LeadAgent$new(
chat = ellmer::chat("openai"),
sub_agents = list(code_agent)
)
result <- lead$run_sync("Review the R code in this project")Learn More
-
vignette("getting-started")- Comprehensive introduction -
vignette("claude-sdk-parity")- Anthropic-compatible surface area and gaps - ellmer documentation - Underlying LLM framework