Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2c38c90
initial implementation
gadenbuie Dec 19, 2025
c7724c5
refactor: create complete client
gadenbuie Dec 19, 2025
25492da
drop max_turns
gadenbuie Dec 19, 2025
f4d2048
docs: add user-facing docs
gadenbuie Dec 19, 2025
2a726d9
expand tool options for default and allowed
gadenbuie Dec 19, 2025
1fba6d7
refactor: clean up message code
gadenbuie Dec 19, 2025
2289da9
format tool result and appearance
gadenbuie Dec 19, 2025
b0a4569
steer toward efficient subagents
gadenbuie Dec 19, 2025
d55ae08
docs: tweak description
gadenbuie Dec 19, 2025
b92faec
chore: prevent subagent tool from being included in the subagent
gadenbuie Dec 19, 2025
93e76be
chore: Update AGENTS.md
gadenbuie Dec 19, 2025
8567a3e
docs: fix noRd
gadenbuie Dec 19, 2025
162143f
Merged origin/main into feat/subagents
gadenbuie Dec 22, 2025
c4d4956
refactor: Pull out `chat_get_tokens()`
gadenbuie Dec 22, 2025
51cc620
feat: Limit token summary to just this round
gadenbuie Dec 22, 2025
73bd999
feat: close over config for tool in `btw_tools()`
gadenbuie Dec 22, 2025
879de1f
feat: Use reference tool list in `btw_app()`
gadenbuie Dec 22, 2025
ca54f07
wip: trying to address load_all() vs run-time tool config
gadenbuie Dec 22, 2025
6de2930
feat: Add pre-instantiation `can_register` check to prevent subagent …
gadenbuie Jan 1, 2026
8d76699
chore: Rename `btw_tool_agent_subagent`
gadenbuie Jan 1, 2026
fe6b471
tests: fix tests
gadenbuie Jan 5, 2026
9871cea
feat: custom agents
gadenbuie Jan 2, 2026
9a92f38
refactor: Consolidate subagent code, improve tests
gadenbuie Jan 5, 2026
8688c40
feat: include full response in display
gadenbuie Jan 5, 2026
84193fb
fix(app): Attach group to tool annotation for use in app
gadenbuie Jan 5, 2026
3bafaad
fix(app): Use bare_client to avoid additional messages
gadenbuie Jan 5, 2026
62e5583
fix: Respect client in agent md frontmatter
gadenbuie Jan 5, 2026
8913f52
fix: `client` in options can also be a list now
gadenbuie Jan 5, 2026
d4ae276
chore: refactor memoids data
gadenbuie Jan 5, 2026
0b2507b
refactor: rename functions for consistency
gadenbuie Jan 5, 2026
0bcd31d
chore: support additional custom icon packs
gadenbuie Jan 5, 2026
fb3f97f
chore: move custom agent code up top
gadenbuie Jan 5, 2026
cc8a47d
fix: better warning for unsupported icons
gadenbuie Jan 5, 2026
df5e276
chore: document()
gadenbuie Jan 5, 2026
90edc70
feat: Support Claude Code subagents
gadenbuie Jan 5, 2026
596fa83
docs: gut/refactor subagent example
gadenbuie Jan 5, 2026
9e7f546
tests: Use "env" instead of "github"
gadenbuie Jan 5, 2026
6406c09
chore: Fix param docs issues
gadenbuie Jan 5, 2026
ee04d57
fix: make sure tools are always added into package namespace
gadenbuie Jan 5, 2026
bc7662c
tests: fixup for no skips
gadenbuie Jan 6, 2026
6f37673
tests: Another fix for skipped tests
gadenbuie Jan 6, 2026
09f99a6
chore: clean up roxygen comments in internal fns
gadenbuie Jan 10, 2026
c57f76e
docs: clarify list is about differences
gadenbuie Jan 10, 2026
9faaef3
chore: Add NEWS item
gadenbuie Jan 10, 2026
fa15a58
chore: edit news item
gadenbuie Jan 10, 2026
200c5e0
Merge 'origin/main' into feat/subagents
gadenbuie Jan 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
^CRAN-SUBMISSION$
^\.prettierrc$
^node_modules$
^data-raw$
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,15 @@ Tools are defined in `R/tool-*.R` files following a consistent pattern:
3. **Tool registration** - Called via `.btw_add_to_tools()` to register with ellmer

Tools are grouped by capability:
- **agent** - Hierarchical workflows via `btw_tool_agent_subagent()` to delegate tasks to specialized subagents
- **docs** - Package documentation, help pages, vignettes, NEWS
- **env** - Describe data frames and environments
- **files** - Read, write, list files; search code
- **git** - Git repository status, diffs, logs
- **github** - GitHub issues and pull requests
- **ide** - Read current editor/selection in RStudio/Positron
_ **pkg** - Package testing, checking and documentation tasks
- **run** - Run R code
- **search** - Search CRAN packages
- **session** - Platform info, installed packages
- **web** - Read web pages as markdown
Expand Down
3 changes: 3 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Config/testthat/edition: 3
Config/testthat/parallel: true
Config/testthat/start-first: web, news, covr, search
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Collate:
Expand All @@ -96,6 +97,8 @@ Collate:
'task_create_btw_md.R'
'task_create_readme.R'
'tool-result.R'
'tool-agent-subagent.R'
'tool-agent-custom.R'
'tool-docs-news.R'
'tool-docs.R'
'tool-env-df.R'
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ S3method(btw_this,pkg_search_result)
S3method(btw_this,tbl)
S3method(btw_this,vignette)
export(btw)
export(btw_agent_tool)
export(btw_app)
export(btw_client)
export(btw_mcp_server)
export(btw_mcp_session)
export(btw_task_create_btw_md)
export(btw_task_create_readme)
export(btw_this)
export(btw_tool_agent_subagent)
export(btw_tool_docs_available_vignettes)
export(btw_tool_docs_help_page)
export(btw_tool_docs_package_help_topics)
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# btw (development version)

* New `btw_tool_agent_subagent()` tool enables hierarchical agent workflows by allowing an orchestrating LLM to delegate tasks to subagents. Each subagent runs in its own isolated chat session with restricted tool access and maintains conversation state that can be resumed via `session_id`. This allows you to delegate tasks to smaller cheaper models or reduce context bloat in the main conversation (#149).

* New `btw_agent_tool()` allows you to create specialized custom subagents from `btw.md` style markdown files. Agent files are automatically discovered from `.btw/agent-*.md` (project and user directories) and `.claude/agents/` (for Claude Code compatibility), and are registered as callable tools in `btw_tools()`. Custom agents can specify their own system prompts, icons, models, and available tools (#149).

# btw 1.1.0

* `btw_client()` now supports reading `CLAUDE.md` files as project context files. `CLAUDE.md` files are searched after `AGENTS.md` but before user-level `btw.md`. YAML frontmatter in `CLAUDE.md` files is stripped but not used for configuration (#146).
Expand Down
7 changes: 5 additions & 2 deletions R/aaa-tools.R
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
.btw_tools <- list()

.btw_add_to_tools <- function(name, group = name, tool) {
.btw_add_to_tools <- function(name, group = name, tool, can_register = NULL) {
check_string(name)
check_string(group)
check_function(can_register, allow_null = TRUE)

if (!is_function(tool)) {
abort(
"`tool` must be a function to ensure `ellmer::tool()` is called at run time."
Expand All @@ -16,7 +18,8 @@
.btw_tools[[name]] <<- list(
name = name,
group = group,
tool = tool
tool = tool,
can_register = can_register
)

invisible(tool)
Expand Down
72 changes: 43 additions & 29 deletions R/btw_client.R
Original file line number Diff line number Diff line change
Expand Up @@ -174,28 +174,17 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) {
}

if (!is.null(config$client)) {
if (is_string(config$client)) {
config$client <- as_ellmer_client(config$client)
return(config)
}

chat_args <- utils::modifyList(
list(echo = "output"), # defaults
config$client
)

chat_fn <- gsub(" ", "_", tolower(chat_args$provider))
if (!grepl("^chat_", chat_fn)) {
chat_fn <- paste0("chat_", chat_fn)
}
chat_args$provider <- NULL
# Show informational message for list configs with model specified
show_model_info <-
is.list(config$client) &&
!is.null(config$client$model) &&
!isTRUE(getOption("btw.client.quiet"))

chat_client <- call2(.ns = "ellmer", chat_fn, !!!chat_args)
config$client <- eval(chat_client)
config$client <- as_ellmer_client(config$client)

if (!is.null(chat_args$model) && !isTRUE(getOption("btw.client.quiet"))) {
if (show_model_info) {
cli::cli_inform(
"Using {.field {chat_args$model}} from {.strong {config$client$get_provider()@name}}."
"Using {.field {config$client$get_model()}} from {.strong {config$client$get_provider()@name}}."
)
}
return(config)
Expand All @@ -214,14 +203,33 @@ as_ellmer_client <- function(client) {
return(client)
}

if (!is_string(client)) {
cli::cli_abort(c(
"{.arg client} must be an {.help ellmer::Chat} client or a string naming a chat provider and model to pass to {.fn ellmer::chat}, not {.obj_type_friendly {client}}.",
"i" = "Examples: {.or {.val {c('openai/gpt-5-mini', 'anthropic/claude-3-7-sonnet-20250219')}}}."
))
if (is_string(client)) {
return(ellmer::chat(client, echo = "output"))
}

ellmer::chat(client, echo = "output")
# Handle list/mapping configuration (e.g., from YAML frontmatter)
# Example: client: {provider: aws_bedrock, model: claude-sonnet-4}
if (is.list(client) && !is.null(client$provider)) {
chat_args <- utils::modifyList(
list(echo = "output"),
client
)

chat_fn <- gsub(" ", "_", tolower(chat_args$provider))
if (!grepl("^chat_", chat_fn)) {
chat_fn <- paste0("chat_", chat_fn)
}
chat_args$provider <- NULL

chat_client <- call2(.ns = "ellmer", chat_fn, !!!chat_args)
return(eval(chat_client))
}

cli::cli_abort(c(
"{.arg client} must be an {.help ellmer::Chat} client, a {.val provider/model} string, or a list with {.field provider} (and optionally {.field model}).",
"i" = "Examples: {.or {.val {c('openai/gpt-4.1-mini', 'anthropic/claude-sonnet-4-20250514')}}}.",
"i" = "Or as a list: {.code list(provider = 'anthropic', model = 'claude-sonnet-4-20250514')}"
))
}

flatten_and_check_tools <- function(tools) {
Expand Down Expand Up @@ -270,11 +278,17 @@ flatten_and_check_tools <- function(tools) {
}

flatten_config_options <- function(opts, prefix = "btw", sep = ".") {
# Keys that should be treated as leaf values (not recursed into)
# even if they contain nested lists
leaf_keys <- c("client")

out <- list()

recurse <- function(x, key_prefix) {
# If x is a list, dive deeper
if (is.list(x) && !is.data.frame(x)) {
recurse <- function(x, key_prefix, current_key = "") {
is_leaf_key <- current_key %in% leaf_keys

# If x is a list and not a leaf key, dive deeper
if (is.list(x) && !is.data.frame(x) && !is_leaf_key) {
nm <- names2(x)
if (!all(nzchar(nm))) {
cli::cli_abort("All options must be named.")
Expand All @@ -286,7 +300,7 @@ flatten_config_options <- function(opts, prefix = "btw", sep = ".") {
} else {
new_key <- nm[i]
}
recurse(x[[i]], new_key)
recurse(x[[i]], new_key, current_key = nm[i])
}
} else {
# Leaf: assign it directly
Expand Down
Loading