diff --git a/R/tool-skills.R b/R/tool-skills.R index f2996c7a..9599f237 100644 --- a/R/tool-skills.R +++ b/R/tool-skills.R @@ -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. @@ -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() { diff --git a/man/btw_skill_prompt.Rd b/man/btw_skill_prompt.Rd index a8f59751..84d98560 100644 --- a/man/btw_skill_prompt.Rd +++ b/man/btw_skill_prompt.Rd @@ -14,8 +14,8 @@ A single string with the skill's YAML-style entry. } \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 \code{\link[=btw_client]{btw_client()}} writes into its system prompt. +and location. This is the same entry that \code{\link[=btw_client]{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. diff --git a/tests/testthat/test-tool_skills.R b/tests/testthat/test-tool_skills.R index 0d690d10..f5e6f3cd 100644 --- a/tests/testthat/test-tool_skills.R +++ b/tests/testthat/test-tool_skills.R @@ -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 ---------------------------------------------------------- @@ -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") }) @@ -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", {