Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 22 additions & 23 deletions R/tool-skills.R
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ btw_skill_resolve <- function(skill_name) {
#'
#' @description
#' 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.
#' and location. This is the same entry that [btw_client()] writes into its
#' system prompt.
#'
#' Compose the listing yourself by joining the entries under an
#' "Available skills:" heading, the way btw does it.
Expand Down Expand Up @@ -825,36 +825,35 @@ escape_for_rbuildignore <- function(path) {

# System Prompt ------------------------------------------------------------

# 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.
# Multi-line descriptions use a YAML literal block scalar (`|`) so the entry
# stays together; blank lines are dropped to keep the listing compact.
skill_description_lines <- function(x) {
lines <- strsplit(gsub("\r", "", x), "\n", fixed = TRUE)[[1]]
lines <- trimws(lines)
lines[nzchar(lines)]
}

# Renders one skill as a compact YAML-style entry with only name, description
# and location. Both the system prompt and btw_skill_prompt() use this format.
format_skill_prompt <- function(skill) {
parts <- sprintf(
lines <- skill_description_lines(skill$description)

description <- if (length(lines) > 1) {
paste0("|\n", paste0(" ", lines, collapse = "\n"))
} else {
lines
}

sprintf(
"- %s: %s\n location: %s",
skill_prompt_line(skill$name),
skill_prompt_line(skill$description),
description,
skill_prompt_line(skill$path)
)
if (!is.null(skill$compatibility)) {
parts <- paste0(
parts,
"\n compatibility: ",
skill_prompt_line(skill$compatibility)
)
}
if (!is.null(skill$allowed_tools)) {
allowed_tools <- paste(skill$allowed_tools, collapse = ", ")
parts <- paste0(
parts,
"\n allowed-tools: ",
skill_prompt_line(allowed_tools)
)
}
parts
}

btw_skills_system_prompt <- function() {
Expand Down
4 changes: 2 additions & 2 deletions man/btw_skill_prompt.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 22 additions & 9 deletions tests/testthat/test-tool_skills.R
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ test_that("btw_skills_system_prompt() includes skill metadata", {
prompt <- btw_skills_system_prompt()
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)
expect_false(grepl(" compatibility:", prompt, fixed = TRUE))
})

# select_skill_dir ----------------------------------------------------------
Expand Down Expand Up @@ -1403,13 +1403,17 @@ 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.",
description = "First line.\n\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,
"- multiline-test: |\n First line.\n Second line.\n location: ",
fixed = TRUE
)
expect_match(prompt, " location: .*multiline-test.*SKILL\\.md")
})

Expand Down Expand Up @@ -1604,21 +1608,30 @@ test_that("btw_skill_prompt() renders a YAML-style entry for one skill", {
# 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)

# fields beyond name, description and location are not part of the entry
expect_false(grepl(" compatibility:", text, fixed = TRUE))
expect_false(grepl(" allowed-tools:", text, fixed = TRUE))

# the skill body is not part of the entry
expect_false(grepl("Do the thing.", text, fixed = TRUE))
})

test_that("btw_skill_prompt() omits optional fields when absent", {
test_that("btw_skill_prompt() uses block scalar for multi-line descriptions", {
dir <- withr::local_tempdir()
create_temp_skill("demo-skill", dir = dir)
create_temp_skill(
"demo-skill",
dir = dir,
description = "First line.\nSecond line."
)
local_skill_dirs(dir)

text <- btw_skill_prompt("demo-skill")
expect_false(grepl(" compatibility:", text, fixed = TRUE))
expect_false(grepl(" allowed-tools:", text, fixed = TRUE))
expect_match(
text,
"- demo-skill: |\n First line.\n Second line.\n location: ",
fixed = TRUE
)
})

test_that("btw_skill_prompt() errors for unknown or invalid skills", {
Expand Down
Loading