-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Subagent and Custom Agent Tools #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
simonpcouch
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is stellar. Only providing the last assistant message as the output, allowing the main agent to configure which tools the subagent will access to, the tool UI, etc.
The only additional feature that I might consider is allowing the main agent to toggle whether the subagent should inherit the turns from the main agent. There are many situations where I'm fine to pay for the cost of the subagents "re-reading" tokens in exchange for it knowing enough to do the task right.
Resolves full available tools at the start of the `btw_app()` session, then adds/removes those tools directly
…recursion The subagent tool's description is dynamically generated based on available tools. This caused infinite recursion when `btw_tools()` tried to instantiate the subagent tool, which called `btw_tools()` again to build its description. Changes: - Add `can_register` argument to `.btw_add_to_tools()` for filtering tools before instantiation (preventing recursion) - Update `as_ellmer_tools()` to check `can_register` before calling `$tool()`, then propagate it to the `btw_can_register` annotation - Add `btw_can_register_subagent_tool()` that returns FALSE during subagent description generation (via `.btw_resolving_for_subagent` option) - Migrate git, github, run tools to use wrapper pattern: `can_register = function() btw_can_register_*()` for mockability - Add explicit error when subagent tool is directly requested by name (e.g., `tools = "btw_tool_subagent"`) - Simplify `subagent_disallow_recursion()` to silently filter without warnings The wrapper pattern `function() btw_can_register_*()` ensures the real function is looked up by name at call time, allowing test mocks to work correctly.
to avoid CRAN outrage
|
@simonpcouch Would you mind giving this another look? It's now full of big changes and features that probably should be two separate PRs but got too entangled to keep apart. I took the foundation of the subagent tool (now Additionally, we auto-discover Claude Code's custom subagent files in |
Avoid picking up local btw.md configs
simonpcouch
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in support of these new changes, as well! Looks great.
I'll underscore that request on being able to optionally send along the full conversation history to subagents. This has been a limiter for my own use of subagents with Claude Code--I often find myself wanting to build up context that would be useful to all subagents, then dispatching to them, as I've been bitten before by the main agent learning some important subtleties from context but failing to articulate all of it the subagents, whose sloppy work ends up thrown away.
| #' @param agent_config List with agent configuration | ||
| #' @return Function that implements the tool | ||
| #' @noRd | ||
| btw_tool_agent_custom_from_config <- function(agent_config) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My gut feeling is that many of these @noRd entries give the "what" rather than the "why" of each of these helpers and ought to be removed, maybe sometimes in favor of a short line motivating / explaining some design choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree. Claude really likes writing these but they don't really add anything useful.
| #' directory for compatibility. However, some Claude Code fields are not | ||
| #' supported: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #' directory for compatibility. However, some Claude Code fields are not | |
| #' supported: | |
| #' directory: |
I initially read this as "all of the things in the list below are not supported"
Does CC "converse" with subagents? i.e. can it return to a subagent to refine the output? Answer: yes, subgaents are resumable, but you have to prompt Claude to re-use an agent by asking directly and mentioning the agent id.
The biggest design hesitation for me here is where and how the "optionally" is decided. For custom agents, we could make in an option that's set in its YAML front matter and a custom agent would either inherit the main chat history or not. For subagents, it's unclear to me how that would be decided. I can also see situational advantages to sometimes including full history for custom agents, which would also be tricky. Resuming conversation with a custom/subagent introduces another wrinkle, but I think it'd be reasonable to say that subagents inherit history when first created and that it wouldn't be updated when resumed. |
This PR introduces two complementary features that enable hierarchical agent workflows: a built-in subagent tool for task delegation, and custom agent tools that can be defined via markdown files.
Overview
Subagent Tool (
btw_tool_agent_subagent)The subagent tool allows an orchestrating LLM agent to delegate tasks to specialized subagents. Each subagent runs in its own isolated chat session with restricted tool access and maintains conversation state that can be resumed across multiple calls.
Key behaviors:
btw.subagent.client)"swift_falcon"(adjective_noun pattern)btw.subagent.tools_allowed(security whitelist - hard boundary)btw.subagent.tools_default(convenience - what you get when nothing specified)Custom Agent Tools
Custom agents are specialized assistants defined via
agent-*.mdfiles that are automatically discovered and registered as callable tools. These integrate seamlessly with the existingbtw_tools()infrastructure.Key behaviors:
.btw/agent-*.md~/.btw/agent-*.mdand~/.config/btw/agent-*.md.claude/agents/*.md~/.claude/agents/*.mdname,description,client,tools,iconbtw_tools()call to ensure changes arereflected immediately
Architecture Integration
Shared Infrastructure
Custom agents are built on top of the subagent infrastructure. When a custom agent tool is called, it uses the same session management, client configuration, and result format as the built-in subagent tool.
Recursion Prevention Strategy
The built-in subagent tool cannot spawn other subagents to prevent infinite recursion. This is enforced at multiple levels:
btw_can_register_subagent_tool()prevents the tool from being included when building a subagent's tool description (via thecan_registerpattern in.btw_add_to_tools())"btw_tool_agent_subagent"in the tools list, an error is thrown immediately"agent"), it's silently filtered outHowever, custom agents CAN call other agents (including other custom agents). This is intentional — users define custom agents and accept responsibility for avoiding cycles. Natural limits (token limits, request limits) provide guardrails against runaway chains.
Tool Discovery and Registration
Custom agents are merged into
btw_tools()at call time rather than being statically registered in.btw_tools. This allows working directory changes to affect discovery and maintains clean separation between built-in and custom tools.Duplicate agent names are detected across all discovery locations, and warnings are issued with the lower-priority file being skipped.
System Prompt Composition
Custom agent prompts are appended to the base
btw-subagent.mdprompt rather than replacing it. This ensures all agents follow core btw conventions while allowing specialization. A visual separator (\n\n---\n\n) makes the composition clear.Icon System
Custom agents support three icon formats:
shiny::icon():icon: robotfontawesome::home,bsicons::house,phosphoricons::house, etc.icon: '<svg>...</svg>'If a package isn't installed or an icon name is invalid, the system warns and falls back to the default agent icon.
Claude Code Compatibility
btw can load agent files from Claude Code's
.claude/agents/directory with automatictranslation:
code-reviewer→code_reviewer) for R identifier compatibilitymodelfield is ignored in favor of btw'sclientfieldtools,permissionMode, andskillstrigger warnings but don't break loadingConfiguration Precedence
The configuration system follows a consistent precedence pattern:
For client:
clienttobtw_agent_tool())clientfieldbtw.subagent.clientoptionbtw.clientoptionFor tools:
toolsfield (btw-style agents only)btw.subagent.tools_defaultoptionbtw.toolsoptionThe
btw.subagent.tools_allowedoption always acts as a final security filter regardless of source.Important Implementation Details
The
can_registerPatternTools can define a
can_registerfunction that's checked before$tool()is called. This prevents recursion when a tool's$tool()function needs to resolve other tools (like the subagent tool building its description of available tools). The pattern looks like:Session Management
Sessions are stored in the package-level environment
.btw_subagent_sessionsas a list containingid,chat, andcreatedtimestamp. Sessions intentionally have no serialization or complex cleanup logic - they persist for the R session duration and are automatically cleaned up when R exits.Error Handling Philosophy
For custom agents, the system warns and skips invalid agent files rather than failing completely. One bad file shouldn't break all tools. Users get feedback via warnings while other valid agents continue to work.
Client Cloning
Chat client objects are always cloned before being used in a subagent session to prevent state pollution between the subagent and parent session, or between different subagent sessions.