Skip to contents

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] 5

Create 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$response

Add 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 with ellmer::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$usage

The 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: readonly

Only 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] TRUE

Unknown 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