This recipe reviews an R package with three specialist subagents, one each for bugs, style and documentation, and a lead agent that combines their findings into one report. Each reviewer works in its own conversation with its own prompt, so its instructions stay focused and you can inspect its work separately.
Define the reviewers
All three reviewers get the same read-only tools for finding and reading code:
library(deputy)
review_tools <- list(
tool_read_file,
tool_list_files,
tool_glob_files,
tool_grep_files
)
bug_hunter <- agent_definition(
name = "bug_hunter",
description = "Finds logic errors, unhandled edge cases and likely runtime
failures in R code.",
prompt = "You look for bugs in R code: wrong conditions, NULL/NA/empty
inputs that aren't handled, off-by-one indexing, unchecked external calls,
and surprising type coercion. Report only real bugs, each with a file path
and line number. Ignore style.",
tools = review_tools,
permission_mode = "readonly"
)
style_reviewer <- agent_definition(
name = "style_reviewer",
description = "Checks R code against the tidyverse style guide.",
prompt = "You review R code against the tidyverse style guide: naming,
assignment with <-, function length and clear names. Flag what hurts
readability; skip trivial nitpicks.",
tools = review_tools,
permission_mode = "readonly"
)
doc_checker <- agent_definition(
name = "doc_checker",
description = "Checks that roxygen2 documentation is complete and matches
the code.",
prompt = "You review roxygen2 documentation: missing @param, @return and
@examples, and descriptions that don't match what the function does. Read
the function body to check.",
tools = review_tools,
permission_mode = "readonly"
)Create the lead
The lead decides what to ask each reviewer and merges their answers:
lead <- LeadAgent$new(
chat = ellmer::chat("openai/gpt-6-luna"),
sub_agents = list(bug_hunter, style_reviewer, doc_checker),
system_prompt = "You lead a code review. Give each specialist a clear,
specific task, then merge their findings: remove duplicates, sort by
severity (critical, warning, suggestion), and give a file path and a
concrete fix for each finding.",
permissions = permissions_readonly(),
usage_limits = UsageLimits(max_requests = 20)
)
lead$available_sub_agents()
#> [1] "bug_hunter" "style_reviewer" "doc_checker"The lead and the reviewers all run in read-only mode, so nobody changes a file. Read-only mode still lets the lead delegate, because its subagents can’t use a less strict mode than the lead.
To follow along while it runs, log each reviewer as it finishes, and each tool call:
lead$add_hook(HookMatcher(
event = "SubagentStop",
callback = function(agent_name, task, result, context) {
cli::cli_alert_info("{agent_name} finished")
NULL
}
))
lead$add_hook(hook_log_tools())Run the review
result <- lead$run_sync(
"Review the R files in R/. Ask each specialist to review them from their
perspective, then combine the findings into one report."
)
cat(result$response)Get the findings as a data frame
For a report you can filter and count, ask for structured output. An array of objects comes back as a data frame, and enums as factors:
review_type <- ellmer::type_object(
summary = ellmer::type_string("Two or three sentences on overall quality."),
findings = ellmer::type_array(
ellmer::type_object(
severity = ellmer::type_enum(c("critical", "warning", "suggestion")),
category = ellmer::type_enum(c("bug", "style", "documentation")),
file = ellmer::type_string(),
line = ellmer::type_integer(required = FALSE),
issue = ellmer::type_string(),
fix = ellmer::type_string(),
reviewer = ellmer::type_string()
)
)
)
result <- lead$run_sync(
"Review the R files in R/ and return every finding.",
type = review_type
)
review <- result$structured_output
findings <- review$findingsFrom there it’s ordinary R:
cli::cli_h1("Code review")
cli::cli_text(review$summary)
table(findings$category, findings$severity)
critical <- findings[findings$severity == "critical", ]
for (i in seq_len(nrow(critical))) {
cli::cli_h2(critical$file[[i]])
cli::cli_alert_danger(critical$issue[[i]])
cli::cli_alert_info("Fix: {critical$fix[[i]]}")
}Cost and effort
The lead’s result includes the reviewers’ usage:
result$usage
result$duration
length(result_tool_calls(result))result$usage$cost_usd is NA when ellmer
couldn’t price every response, for example with a model newer than your
ellmer version. It’s never a partial total that looks complete.
lead$list_subagents() shows each delegation, with its
status and stop reason, if you want to see where the requests went.
Next steps
- Subagents: definitions, budgets and parallel delegation.
- Hooks: events and built-in hooks.
- Structured output: types and validation.