This example assigns separate reviewers to bugs, code style, and documentation in an R package. A lead agent combines their findings into a report. Each reviewer has its own prompt and conversation, so you can inspect its results separately.
Define the Reviewer Agents
Each reviewer agent focuses on one dimension of code quality. They
all use tools_file() for read-only file access:
library(deputy)
bug_hunter <- agent_definition(
name = "bug_hunter",
description = "Finds logic errors, edge cases, off-by-one errors,
and potential runtime failures in R code",
prompt = "You are a bug-hunting specialist for R code. Focus on:
- Logic errors and incorrect conditions
- Edge cases (NULL, NA, empty inputs, zero-length vectors)
- Off-by-one errors in indexing
- Missing error handling for external calls
- Type coercion issues
Read the source files carefully. Report specific issues with
file paths and line numbers. Be precise -- only report real bugs,
not style preferences.",
tools = tools_file()
)
style_reviewer <- agent_definition(
name = "style_reviewer",
description = "Checks code against tidyverse style conventions,
naming patterns, and code organisation",
prompt = "You are an R style reviewer following tidyverse conventions.
Focus on:
- snake_case naming for functions and variables
- Consistent use of <- for assignment (not =)
- Function length (flag functions over ~50 lines)
- Clear, descriptive names
- Proper use of R6 conventions for classes
Be pragmatic -- flag patterns that hurt readability, not
minor nitpicks.",
tools = tools_file()
)
doc_checker <- agent_definition(
name = "doc_checker",
description = "Reviews roxygen2 documentation for completeness,
accuracy, and clarity",
prompt = "You are a documentation reviewer for R packages. Focus on:
- Missing roxygen2 tags (@param, @return, @export)
- Inaccurate parameter descriptions
- Missing @examples sections on exported functions
- Unclear or misleading descriptions
- Missing @seealso cross-references
Read both the roxygen comments and the function bodies to verify
that documentation matches the actual behaviour.",
tools = tools_file()
)Create the Lead Agent
The LeadAgent coordinates the reviewers. It has a
delegate_to_agent tool that lets it assign tasks to any
registered sub-agent:
chat <- ellmer::chat_openai(model = "gpt-5.6-luna")
lead <- LeadAgent$new(
chat = chat,
sub_agents = list(bug_hunter, style_reviewer, doc_checker),
system_prompt = "You are a lead code reviewer coordinating a team of
specialists. For each review request:
1. Delegate to each specialist with a clear, specific task
2. Collect their findings
3. Synthesise into a unified review with findings sorted by severity
4. Remove duplicate findings across reviewers
Severity levels: critical, warning, suggestion.
Always include the file path and a concrete suggestion for each finding.",
permissions = permissions_readonly()
)
lead$available_sub_agents()
#> [1] "bug_hunter" "style_reviewer" "doc_checker"Add Monitoring Hooks
Hooks provide real-time visibility as the agent works. Use them for
logging and blocking; use AgentResult for post-hoc
analysis.
# Log when each sub-agent finishes
hook_sub_agent_log <- HookMatcher(
event = "SubagentStop",
callback = function(agent_name, task, result, context) {
cli::cli_alert_info("Sub-agent {.val {agent_name}} finished")
NULL
}
)
# Log tool calls with the built-in hook
lead$add_hook(hook_sub_agent_log)
lead$add_hook(hook_log_tools(verbose = TRUE))Run the Review
result <- lead$run_sync(
"Review the R source files in R/ for code quality issues.
Have each specialist review the code from their perspective,
then synthesise the findings into a unified report."
)
cat(result$response)Structured Review Output
For machine-readable results, add a structured output schema to capture individual findings:
review_schema <- list(
type = "object",
properties = list(
summary = list(type = "string"),
total_findings = list(type = "integer"),
findings = list(
type = "array",
items = list(
type = "object",
properties = list(
severity = list(
type = "string",
enum = c("critical", "warning", "suggestion")
),
category = list(
type = "string",
enum = c("bug", "style", "documentation")
),
file = list(type = "string"),
line = list(type = "integer"),
issue = list(type = "string"),
suggestion = list(type = "string"),
reviewer = list(type = "string")
),
required = c(
"severity",
"category",
"file",
"issue",
"suggestion",
"reviewer"
)
)
)
),
required = c("summary", "total_findings", "findings")
)
chat <- ellmer::chat_openai(model = "gpt-5.6-luna")
lead <- LeadAgent$new(
chat = chat,
sub_agents = list(bug_hunter, style_reviewer, doc_checker),
system_prompt = "You coordinate code reviewers. Delegate to each
specialist, then combine their findings into structured JSON.
Deduplicate findings across reviewers.",
permissions = permissions_readonly()
)
result <- lead$run_sync(
"Review the R/ directory and return structured findings.",
type = ellmer::type_from_schema(jsonlite::toJSON(review_schema, auto_unbox = TRUE))
)Processing the Results
With structured output, you can filter, sort, and summarise findings programmatically:
review <- result$structured_output
# Summary
cli::cli_h1("Code Review: {review$summary}")
cli::cli_alert_info("Total findings: {review$total_findings}")
# Filter by severity
critical <- Filter(
function(f) f$severity == "critical",
review$findings
)
warnings <- Filter(
function(f) f$severity == "warning",
review$findings
)
cli::cli_alert_danger("{length(critical)} critical issue(s)")
cli::cli_alert_warning("{length(warnings)} warning(s)")
# Display critical findings
for (finding in critical) {
cli::cli_h2("{finding$file}")
cli::cli_alert_danger("{finding$issue}")
cli::cli_alert_info("Suggestion: {finding$suggestion}")
cli::cli_text("Found by: {finding$reviewer}")
}
# Summarise by category
findings_df <- do.call(rbind, lapply(review$findings, as.data.frame))
table(findings_df$category, findings_df$severity)Cost and Performance
AgentResult captures cost and tool usage for post-hoc
analysis:
# Total cost and duration
result$cost
result$duration
# Never substitute zero for an incomplete provider total
if (!isTRUE(result$cost$complete)) {
cli::cli_alert_warning(
"Provider omitted {result$cost$missing} cost record{?s}."
)
}
# All tool calls made during the review
tool_calls <- result_tool_calls(result)
cli::cli_alert_info("Total tool calls: {length(tool_calls)}")
# How many model requests were used?
result$usage$requestsNext Steps
-
vignette("multi-agent")– LeadAgent and agent_definition() reference -
vignette("hooks")– Hook events and pre-built hooks -
vignette("structured-output")– JSON schema output and validation