A LeadAgent can hand parts of a task to subagents:
specialists with their own prompt, tools, model and permissions. Each
delegated task runs in a fresh conversation, so a subagent sees only the
brief the lead writes for it, and its tool calls don’t fill up the
lead’s context. The lead gets the subagent’s answer back and decides
what to do next.
Subagents help when parts of a task need different tools or instructions, when some work should run with tighter permissions, or when you want several independent opinions. For a task one agent can handle with the right tools, one agent is simpler and cheaper.
Define subagents
agent_definition() describes a subagent. The lead’s
model reads the description to decide when to use it:
library(deputy)
code_reviewer <- agent_definition(
name = "code_reviewer",
description = "Reviews R code for bugs and unclear style.",
prompt = "You review R code. Report specific problems with file and line.",
tools = tools_file(),
permission_mode = "readonly"
)
data_analyst <- agent_definition(
name = "data_analyst",
description = "Summarises data files.",
prompt = "You read data files and summarise them in a few sentences.",
tools = tools_data()
)| Field | Meaning |
|---|---|
name |
routing name; converted to lower case, starts with a letter, then
letters, digits, _ or -
|
description |
what the subagent is for, shown to the lead |
prompt |
the subagent’s system prompt |
tools |
the subagent’s tools; it doesn’t inherit the lead’s |
model |
which model to use (see below) |
skills |
skills to load into the subagent |
disallowed_tools |
tool names the subagent may never call |
memory |
text added to the subagent’s system prompt |
mcp_servers |
MCP servers for the subagent (see Briefing and inspecting subagents) |
initial_prompt |
text put in front of every task the subagent receives |
max_requests |
model request limit for each delegated task |
permission_mode |
a mode at least as strict as the lead’s |
Definitions are read-only values:
library(deputy)
reviewer <- agent_definition(
"Reviewer",
"Reviews files without changing them.",
"Review the files you are given and report problems.",
max_requests = 5
)
reviewer$name
#> [1] "reviewer"
reviewer$max_requests
#> [1] 5Create a lead agent
LeadAgent is an Agent with one extra tool,
delegate_to_agent, and a list of subagents the model can
choose from:
lead <- LeadAgent$new(
chat = ellmer::chat("anthropic/claude-sonnet-5"),
sub_agents = list(code_reviewer, data_analyst),
system_prompt = "You coordinate analysis tasks. Delegate to the right
specialist, then combine their findings."
)
lead$available_sub_agents()
#> [1] "code_reviewer" "data_analyst"
result <- lead$run_sync(
"Ask the code reviewer to read R/utils.R and summarise its problems."
)
result$responseAdd a subagent later with
lead$register_sub_agent(definition), which also updates the
list of subagents in the lead’s system prompt.
When the lead delegates, the subagent runs to completion and its
answer comes back as the result of the delegate_to_agent
call. When the model delegates several tasks in one turn, they can run
concurrently.
Choose each subagent’s model
model accepts three forms:
-
"inherit", the default, uses a copy of the lead’s chat: same provider, model and settings, without the lead’s conversation or tools. - A bare model id such as
"gpt-6-luna"uses the lead’s provider, endpoint and credentials with a different model, for example a cheaper one for simple work. -
"provider/model", such as"anthropic/claude-sonnet-5", creates a new chat withellmer::chat(). Use this form for model ids that contain/.
Permissions and budgets
A subagent can never do more than its lead. Deputy combines the
subagent’s permission_mode with the lead’s policy, keeping
the lead’s capabilities, write directory, allow and deny lists, and
callback, and adds the definition’s disallowed_tools. The
allowed modes follow the same order as
set_permission_mode():
| Lead mode | Subagent may use |
|---|---|
"readonly" |
"readonly" |
"standard" |
"standard", "readonly"
|
"plan" |
"plan", "readonly"
|
"full" |
any mode |
A lead in read-only or plan mode can still delegate, because its
subagents can’t use a less strict mode. A lead created with
permissions_readonly() and all its subagents can read files
but can’t write them or run code.
The lead’s PreToolUse, PostToolUse and
PostToolUseFailure hooks also run for subagent tool calls,
and each subagent call is checked against the lead’s current
permissions, so narrowing the lead mid-run narrows its subagents
too.
Subagents spend from the lead run’s budget. Each delegated task gets
whatever is left of the lead’s UsageLimits, further capped
by the definition’s max_requests, and its usage is added to
the lead’s result$usage. When the model delegates several
tasks at once, each reserves its share before it starts, so together
they can’t overspend.
A few things subagents can’t do. They can’t use tools that run on the
provider’s side, because Deputy can’t check those calls. They can’t
pause for a durable approval: a lead created with
approval_dir refuses to delegate, and a subagent permission
callback that returns PermissionResultPending() denies the
call. And they share the lead’s working directory and file checkpoints;
nothing about delegation isolates the file system.
Ask several subagents at once
To get independent answers to the same question, call
parallel_delegate() yourself instead of waiting for the
model to delegate. Each named definition gets the task in a fresh
conversation and answers with a single model request. The lead makes no
request of its own, and its conversation is unchanged.
lead <- LeadAgent$new(
ellmer::chat("openai/gpt-6-luna"),
sub_agents = list(
agent_definition("benefits", "Finds benefits", "Make the strongest case for."),
agent_definition("risks", "Finds risks", "Make the strongest case against.")
)
)
batch <- lead$parallel_delegate(
c(
benefits = "Should our team adopt a four-day week?",
risks = "Should our team adopt a four-day week?"
),
max_active = 2,
usage_limits = UsageLimits(max_requests = 2)
)
batch$status
batch$results$benefits$response
batch$run$usageThe subagents in a batch can’t have tools, skills or MCP servers. The result keeps the order of the tasks:
| Field | Contents |
|---|---|
status |
"completed", "failed",
"stopped" or "not_started" for each task |
results |
each subagent’s AgentResult, or NULL
|
outcomes |
each subagent’s compact outcome |
errors |
each failure’s condition, or NULL
|
run |
an AgentResult for the whole batch, with the combined
usage |
One failure doesn’t discard the other answers, so check
status before using them. max_active sets how
many run at once; the batch works through the tasks in waves of that
size. Request limits are reserved before each wave starts, and token and
cost limits are split across it, so a wave can overshoot a token or cost
limit by up to one response per subagent. With the default
on_exceed = "stop", a limit returns the answers so far.
In Shiny, use parallel_delegate_async(), which returns a
promise. lead$interrupt() stops tasks that haven’t started
and asks running ones to stop.
For a complete example that compares two opposing answers and asks a separate moderator to weigh them, run the bundled debate script with your OpenAI key set:
source(system.file("examples", "standalone", "09-debate.R", package = "deputy"))See what the subagents did
lead$list_subagents() returns a data frame with one row
per delegated task, including tasks still running: the subagent’s name,
its status, the exact stop_reason, timestamps
and identifiers.
| Status | Meaning |
|---|---|
queued, running
|
not finished yet |
completed |
the subagent’s run ended with "complete"
|
stopped |
a limit or an interruption ended the run |
failed |
the subagent errored |
not_started |
the task never started |
"completed" means the run ended normally. It doesn’t
mean the subagent did the task well; read its answer.
lead$get_subagent_results() returns each task’s
AgentResult, and lead$get_subagent_messages()
returns each subagent’s conversation, including what it managed before a
failure. Filter both by agent_name, or by
delegation_id and session_id respectively.
Reading them doesn’t add anything to the lead’s context. These records
live in the LeadAgent object and disappear with it.
To react while delegations happen, add SubagentStart and
SubagentStop hooks to the lead:
lead$add_hook(HookMatcher(
event = "SubagentStop",
callback = function(agent_name, task, result, context) {
cli::cli_inform("{agent_name} finished: {context$status}")
NULL
}
))These accessors are for your own code. Before you show subagent conversations to users, read Briefing and inspecting subagents, which covers authorising and redacting what each user may see.
Keep definitions in files
Definitions can live as YAML files, conventionally in
.deputy/agents/, so they can be reviewed and versioned like
any other configuration:
version: 1
name: reviewer
description: Reviews local text for gaps
prompt: |
Read the supplied text using read_file.
Report unsupported claims and missing evidence concisely.
tools: [read_file]
model: inherit
disallowed_tools: [write_file, run_bash]
initial_prompt: Keep the review grounded in the supplied text.
max_requests: 2
permission_mode: readonlyOnly version, name,
description and prompt are required; the other
fields default as in agent_definition(). A file can’t
contain R code. Tools and skills are named by keys that your code maps
to real objects, so a file can only use the tools you offer it:
tool_registry <- list(read_file = tool_read_file)
definition_dir <- system.file("examples", "agent-definitions", package = "deputy")
definitions <- agent_definitions(definition_dir, tools = tool_registry)
names(definitions)
#> [1] "reviewer"agent_definitions() reads every .yaml and
.yml file in a directory (.deputy/agents/ by
default), in file name order. A missing directory gives an empty list;
an invalid file, an unknown key or a duplicate name is an error. Reading
files never runs code, loads skills, or connects to servers.
agent_definition_read() reads one file, and
agent_definition_write() writes a definition back, given
the same registries:
path <- tempfile(fileext = ".yaml")
agent_definition_write(definitions$reviewer, path, tools = tool_registry)
restored <- agent_definition_read(path, tools = tool_registry)
identical(restored, definitions$reviewer)
#> [1] TRUEUnknown fields, unsupported versions and YAML !expr tags
are errors. Quote strings such as "yes" and
"123" that YAML would otherwise convert. Files are limited
to 1 MiB, and a file can’t give a subagent more permissions than its
lead.
Going further
- Briefing and inspecting subagents: pass structured briefs and evidence, give each subagent its own resources, and show subagent conversations to users safely.
- Retained specialists and agent graphs: keep a specialist’s conversation across several tasks, and let specialists delegate to each other.
- Multi-agent code review: a complete example.