From 605b4d965b69a9f1cdc892f3a0fb20fad920b409 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 25 Sep 2026 17:18:26 -0400 Subject: [PATCH 1/2] Shorten skill system prompt and listing --- R/mcp.R | 4 +- R/tool-skills.R | 60 +++++++++++----------------- inst/prompts/skills.md | 14 +------ man/btw_skill_prompt.Rd | 12 +++--- tests/testthat/_snaps/tool_skills.md | 24 ++--------- tests/testthat/test-tool_skills.R | 59 ++++++++++++++------------- 6 files changed, 68 insertions(+), 105 deletions(-) diff --git a/R/mcp.R b/R/mcp.R index db9d554c..9e13a198 100644 --- a/R/mcp.R +++ b/R/mcp.R @@ -185,10 +185,10 @@ btw_mcp_session <- function() { btw_mcp_tools <- function() { # Skills are excluded from MCP by default: the skill system prompt - # (with metadata) is injected by btw_client(), not by + # (with the available-skills listing) is injected by btw_client(), not by # MCP. Without that context the model has no way to know which skills are # available. Filesystem-based agents (e.g. Claude Code) can read SKILL.md - # files directly via their paths in the system prompt. + # files directly via their listed locations in the system prompt. all_tools <- btw_tools() Filter( function(tool) !identical(tool@annotations$btw_group, "skills"), diff --git a/R/tool-skills.R b/R/tool-skills.R index c8427cb3..f2996c7a 100644 --- a/R/tool-skills.R +++ b/R/tool-skills.R @@ -151,18 +151,18 @@ btw_skill_resolve <- function(skill_name) { #' Render a skill's entry for a system prompt #' #' @description -#' Returns the `` block for one skill: its name, description, and -#' location, plus its compatibility notes and allowed tools when present. This -#' is the same block that [btw_client()] writes into its system prompt. +#' Returns a compact YAML-style entry for one skill: its name, description, +#' and location, plus compatibility notes and allowed tools when present. This +#' is the same entry that [btw_client()] writes into its system prompt. #' -#' Compose the listing yourself. For example, wrap the blocks for all skills -#' in an `` element, the way btw does it. +#' Compose the listing yourself by joining the entries under an +#' "Available skills:" heading, the way btw does it. #' #' If the skill doesn't exist, the error lists the available skill names. #' #' @param skill_name The name of the skill, e.g. `"skill-creator"`. #' -#' @return A single string with the skill's `` block. +#' @return A single string with the skill's YAML-style entry. #' #' @examples #' cat(btw_skill_prompt("skill-creator")) @@ -749,16 +749,6 @@ format_resources_listing <- function(resources, base_dir) { paste(parts, collapse = "") } -# Escapes &, <, > for use in XML text content. This XML is decorative -# formatting for LLM system prompts, not parsed by an XML parser, so we -# intentionally omit attribute-level escapes (" and '). -xml_escape <- function(x) { - x <- gsub("&", "&", x, fixed = TRUE) - x <- gsub("<", "<", x, fixed = TRUE) - x <- gsub(">", ">", x, fixed = TRUE) - x -} - # R Package Build Ignore --------------------------------------------------- maybe_use_build_ignore <- function(target_parent, project_dir = getwd()) { @@ -835,37 +825,36 @@ escape_for_rbuildignore <- function(path) { # System Prompt ------------------------------------------------------------ -# Renders one skill as a block: name, description, location, plus -# compatibility notes and allowed tools when present. Callers compose the -# blocks into their own system prompt; btw_skills_system_prompt() wraps them -# in , btw_skill_prompt() returns a single block. +# Keep frontmatter block scalars on one line so each entry stays together. +skill_prompt_line <- function(x) { + gsub("[\r\n]+[ \t]*", " ", x) +} + +# Renders one skill as a compact YAML-style entry. Both the system prompt and +# btw_skill_prompt() use the same entry format. format_skill_prompt <- function(skill) { parts <- sprintf( - "\n%s\n%s\n%s", - xml_escape(skill$name), - xml_escape(skill$description), - xml_escape(skill$path) + "- %s: %s\n location: %s", + skill_prompt_line(skill$name), + skill_prompt_line(skill$description), + skill_prompt_line(skill$path) ) if (!is.null(skill$compatibility)) { parts <- paste0( parts, - sprintf( - "\n%s", - xml_escape(skill$compatibility) - ) + "\n compatibility: ", + skill_prompt_line(skill$compatibility) ) } if (!is.null(skill$allowed_tools)) { allowed_tools <- paste(skill$allowed_tools, collapse = ", ") parts <- paste0( parts, - sprintf( - "\n%s", - xml_escape(allowed_tools) - ) + "\n allowed-tools: ", + skill_prompt_line(allowed_tools) ) } - paste0(parts, "\n") + parts } btw_skills_system_prompt <- function() { @@ -886,9 +875,8 @@ btw_skills_system_prompt <- function() { paste0( explanation, - "\n\n\n", - paste(skill_items, collapse = "\n"), - "\n" + "\n\nAvailable skills:\n", + paste(skill_items, collapse = "\n") ) } diff --git a/inst/prompts/skills.md b/inst/prompts/skills.md index e3ce8ce5..e4a018ad 100644 --- a/inst/prompts/skills.md +++ b/inst/prompts/skills.md @@ -1,15 +1,3 @@ ## Skills -You have access to specialized skills that provide detailed guidance for specific tasks. Skills are loaded on-demand to provide domain-specific expertise without consuming context until needed. - -### Using Skills - -1. **Check available skills**: Review the `` listing below -2. **Load when relevant**: When you recognize that a task matches a skill's description, call `btw_tool_skill(name)` to load the full skill instructions -3. **Don't reload**: If a skill has already been loaded in this conversation, follow its instructions directly without loading it again -4. **Access resources**: After loading, use file read tools to access references - -Skills may include bundled resources: -- **Scripts**: Code bundled with the skill. Scripts are not directly executable by btw; read them for reference or adapt their logic into R code for use with the R code execution tool. -- **References**: Additional documentation to consult as needed -- **Assets**: Templates and files for use in outputs +When a task matches an available skill, call `btw_tool_skill(name)` to load its instructions. Don't reload skills already loaded. Resolve relative paths in a skill against the directory containing its SKILL.md. Use file read tools for bundled references; bundled scripts are for reference or adaptation into R, not directly executable by btw. diff --git a/man/btw_skill_prompt.Rd b/man/btw_skill_prompt.Rd index 4438c3bc..a8f59751 100644 --- a/man/btw_skill_prompt.Rd +++ b/man/btw_skill_prompt.Rd @@ -10,15 +10,15 @@ btw_skill_prompt(skill_name) \item{skill_name}{The name of the skill, e.g. \code{"skill-creator"}.} } \value{ -A single string with the skill's \verb{} block. +A single string with the skill's YAML-style entry. } \description{ -Returns the \verb{} block for one skill: its name, description, and -location, plus its compatibility notes and allowed tools when present. This -is the same block that \code{\link[=btw_client]{btw_client()}} writes into its system prompt. +Returns a compact YAML-style entry for one skill: its name, description, +and location, plus compatibility notes and allowed tools when present. This +is the same entry that \code{\link[=btw_client]{btw_client()}} writes into its system prompt. -Compose the listing yourself. For example, wrap the blocks for all skills -in an \verb{} element, the way btw does it. +Compose the listing yourself by joining the entries under an +"Available skills:" heading, the way btw does it. If the skill doesn't exist, the error lists the available skill names. } diff --git a/tests/testthat/_snaps/tool_skills.md b/tests/testthat/_snaps/tool_skills.md index b006f26b..16064ed6 100644 --- a/tests/testthat/_snaps/tool_skills.md +++ b/tests/testthat/_snaps/tool_skills.md @@ -5,27 +5,11 @@ Output ## Skills - You have access to specialized skills that provide detailed guidance for specific tasks. Skills are loaded on-demand to provide domain-specific expertise without consuming context until needed. + When a task matches an available skill, call `btw_tool_skill(name)` to load its instructions. Don't reload skills already loaded. Resolve relative paths in a skill against the directory containing its SKILL.md. Use file read tools for bundled references; bundled scripts are for reference or adaptation into R, not directly executable by btw. - ### Using Skills - - 1. **Check available skills**: Review the `` listing below - 2. **Load when relevant**: When you recognize that a task matches a skill's description, call `btw_tool_skill(name)` to load the full skill instructions - 3. **Don't reload**: If a skill has already been loaded in this conversation, follow its instructions directly without loading it again - 4. **Access resources**: After loading, use file read tools to access references - - Skills may include bundled resources: - - **Scripts**: Code bundled with the skill. Scripts are not directly executable by btw; read them for reference or adapt their logic into R code for use with the R code execution tool. - - **References**: Additional documentation to consult as needed - - **Assets**: Templates and files for use in outputs - - - - skill-creator - Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations. - SKILL_PATH - - + Available skills: + - skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations. + location: SKILL_PATH # btw_skill_prompt() errors for unknown or invalid skills diff --git a/tests/testthat/test-tool_skills.R b/tests/testthat/test-tool_skills.R index 17d4ba56..0d690d10 100644 --- a/tests/testthat/test-tool_skills.R +++ b/tests/testthat/test-tool_skills.R @@ -807,9 +807,9 @@ test_that("btw_skills_system_prompt() includes skill metadata", { local_skill_dirs(dir) prompt <- btw_skills_system_prompt() - expect_match(prompt, "prompt-test") - expect_match(prompt, "A skill for testing prompts.") - expect_match(prompt, "Needs R 4.2") + expect_match(prompt, "- prompt-test: A skill for testing prompts.", fixed = TRUE) + expect_match(prompt, " location: ", fixed = TRUE) + expect_match(prompt, " compatibility: Needs R 4.2", fixed = TRUE) }) # select_skill_dir ---------------------------------------------------------- @@ -1291,7 +1291,7 @@ test_that("btw_skills_system_prompt() works", { expect_snapshot( cat(btw_skills_system_prompt()), transform = function(x) { - gsub(".*?", "SKILL_PATH", x) + gsub(" location: .*SKILL\\.md", " location: SKILL_PATH", x) } ) }) @@ -1384,9 +1384,9 @@ test_that("install_skill_from_dir() overwrites with overwrite = TRUE", { expect_true(any(grepl("Version 2", content))) }) -# xml_escape() in system prompt --------------------------------------------- +# Description formatting in skill listing ---------------------------------- -test_that("btw_skills_system_prompt() escapes XML special characters", { +test_that("btw_skills_system_prompt() preserves special characters", { dir <- withr::local_tempdir() create_temp_skill( name = "esc-test", @@ -1396,9 +1396,21 @@ test_that("btw_skills_system_prompt() escapes XML special characters", { local_skill_dirs(dir) prompt <- btw_skills_system_prompt() - expect_match(prompt, "<tags>", fixed = TRUE) - expect_match(prompt, "& ampersands", fixed = TRUE) - expect_no_match(prompt, "", fixed = TRUE) + expect_match(prompt, "Uses & ampersands.", fixed = TRUE) +}) + +test_that("btw_skills_system_prompt() keeps multiline descriptions in one entry", { + dir <- withr::local_tempdir() + create_temp_skill( + name = "multiline-test", + description = "First line.\nSecond line.", + dir = dir + ) + local_skill_dirs(dir) + + prompt <- btw_skills_system_prompt() + expect_match(prompt, "- multiline-test: First line. Second line.", fixed = TRUE) + expect_match(prompt, " location: .*multiline-test.*SKILL\\.md") }) # maybe_use_build_ignore() ------------------------------------------------- @@ -1570,9 +1582,9 @@ test_that("skills prompt is included in btw_client() system prompt", { system_prompt <- chat$get_system_prompt() expect_match(system_prompt, "## Skills", fixed = TRUE) - expect_match(system_prompt, "skill-creator", fixed = TRUE) + expect_match(system_prompt, "- skill-creator:", fixed = TRUE) }) -test_that("btw_skill_prompt() renders the block for one skill", { +test_that("btw_skill_prompt() renders a YAML-style entry for one skill", { dir <- withr::local_tempdir() create_temp_skill( "demo-skill", @@ -1589,22 +1601,13 @@ test_that("btw_skill_prompt() renders the block for one skill", { expect_type(text, "character") expect_length(text, 1) - # the block carries the same fields as the btw_client() system prompt - expect_match(text, "demo-skill", fixed = TRUE) - expect_match( - text, - "A test skill for unit testing.", - fixed = TRUE - ) - expect_match(text, ".*SKILL\\.md") - expect_match( - text, - "Requires Python 3", - fixed = TRUE - ) - expect_match(text, "Read Bash", fixed = TRUE) + # the entry carries the same fields as the btw_client() system prompt + expect_match(text, "- demo-skill: A test skill for unit testing.", fixed = TRUE) + expect_match(text, " location: .*SKILL\\.md") + expect_match(text, " compatibility: Requires Python 3", fixed = TRUE) + expect_match(text, " allowed-tools: Read Bash", fixed = TRUE) - # the skill body is not part of the block + # the skill body is not part of the entry expect_false(grepl("Do the thing.", text, fixed = TRUE)) }) @@ -1614,8 +1617,8 @@ test_that("btw_skill_prompt() omits optional fields when absent", { local_skill_dirs(dir) text <- btw_skill_prompt("demo-skill") - expect_false(grepl("", text, fixed = TRUE)) - expect_false(grepl("", text, fixed = TRUE)) + expect_false(grepl(" compatibility:", text, fixed = TRUE)) + expect_false(grepl(" allowed-tools:", text, fixed = TRUE)) }) test_that("btw_skill_prompt() errors for unknown or invalid skills", { From bbcbf63ee2c5e7dd89865ff579088d6582221522 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 25 Sep 2026 17:22:12 -0400 Subject: [PATCH 2/2] docs: Add NEWS bullet for compact skill listing --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 68823705..9451bf8c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # btw (development version) +* Skill listings in the system prompt now use compact YAML-style entries instead of `` XML blocks, reducing prompt tokens while conveying the same information. + * Package tests now support a compact reporter that shows per-file progress, timing, warning and failure details, and final counts. `btw pkg test` uses it by default, while `btw_tool_pkg_test()` defaults to concise output for non-streaming clients; both accept other testthat reporter names (#220). # btw 1.5.0