feat: updated enabled_skills handling to support both list and comma-separated strings
This commit is contained in:
+4
-1
@@ -131,10 +131,13 @@ visible_skills: # The universe of skills allowed to be enabled
|
|||||||
- frontend-ui-ux
|
- frontend-ui-ux
|
||||||
- git-master
|
- git-master
|
||||||
enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
|
enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
|
||||||
# Example: only expose two skills in the bare REPL.
|
# Accepts either a YAML list or a comma-separated string.
|
||||||
|
# Example (list form):
|
||||||
# enabled_skills:
|
# enabled_skills:
|
||||||
# - git-master
|
# - git-master
|
||||||
# - ai-slop-remover
|
# - ai-slop-remover
|
||||||
|
# Example (comma-separated form):
|
||||||
|
# enabled_skills: git-master,ai-slop-remover
|
||||||
|
|
||||||
# ---- Auto-Continue (Todo System) ----
|
# ---- Auto-Continue (Todo System) ----
|
||||||
# The auto-continue system provides built-in task tracking for improved reliability.
|
# The auto-continue system provides built-in task tracking for improved reliability.
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enabl
|
|||||||
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
|
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
|
||||||
skills_enabled: true # Master switch for skills in this role (default: inherit from global).
|
skills_enabled: true # Master switch for skills in this role (default: inherit from global).
|
||||||
# Skills also require `function_calling_support: true` in the global config.
|
# Skills also require `function_calling_support: true` in the global config.
|
||||||
enabled_skills: git-master,ai-slop-remover # Comma-separated list of skills available when this role is active.
|
enabled_skills: # Skills available when this role is active. Accepts a YAML list (preferred)
|
||||||
# Must be a subset of global `visible_skills`. Omit to inherit the global default.
|
- git-master # or a comma-separated string (e.g. `enabled_skills: git-master,ai-slop-remover`).
|
||||||
|
- ai-slop-remover # Must be a subset of global `visible_skills`. Omit to inherit the global default.
|
||||||
prompt: null # A custom prompt to use for this role that will immediately query
|
prompt: null # A custom prompt to use for this role that will immediately query
|
||||||
# the model for output instead of using the instructions below
|
# the model for output instead of using the instructions below
|
||||||
# Auto-Continue (Todo System)
|
# Auto-Continue (Todo System)
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ pub struct AppConfig {
|
|||||||
pub visible_tools: Option<Vec<String>>,
|
pub visible_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
pub skills_enabled: bool,
|
pub skills_enabled: bool,
|
||||||
pub enabled_skills: Option<String>,
|
#[serde(default, deserialize_with = "super::deserialize_csv_or_vec")]
|
||||||
|
pub enabled_skills: Option<Vec<String>>,
|
||||||
pub visible_skills: Option<Vec<String>>,
|
pub visible_skills: Option<Vec<String>>,
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
pub mcp_server_support: bool,
|
||||||
@@ -400,7 +401,7 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_skills")) {
|
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_skills")) {
|
||||||
self.enabled_skills = v;
|
self.enabled_skills = v.map(|raw| super::csv_to_vec(&raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
||||||
|
|||||||
+68
-1
@@ -200,7 +200,8 @@ pub struct Config {
|
|||||||
pub visible_tools: Option<Vec<String>>,
|
pub visible_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
pub skills_enabled: bool,
|
pub skills_enabled: bool,
|
||||||
pub enabled_skills: Option<String>,
|
#[serde(default, deserialize_with = "deserialize_csv_or_vec")]
|
||||||
|
pub enabled_skills: Option<Vec<String>>,
|
||||||
pub visible_skills: Option<Vec<String>>,
|
pub visible_skills: Option<Vec<String>>,
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
pub mcp_server_support: bool,
|
||||||
@@ -783,6 +784,72 @@ where
|
|||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn csv_to_vec(raw: &str) -> Vec<String> {
|
||||||
|
raw.split(',')
|
||||||
|
.map(|t| t.trim().to_string())
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn deserialize_csv_or_vec<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> std::result::Result<Option<Vec<String>>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::{self, SeqAccess, Visitor};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
struct CsvOrVec;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for CsvOrVec {
|
||||||
|
type Value = Option<Vec<String>>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a comma-separated string, a list of strings, or null")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
|
||||||
|
Ok(Some(csv_to_vec(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E: de::Error>(self, value: String) -> std::result::Result<Self::Value, E> {
|
||||||
|
Ok(Some(csv_to_vec(&value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_none<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_some<D2: serde::Deserializer<'de>>(
|
||||||
|
self,
|
||||||
|
deserializer: D2,
|
||||||
|
) -> std::result::Result<Self::Value, D2::Error> {
|
||||||
|
deserializer.deserialize_any(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_unit<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A: SeqAccess<'de>>(
|
||||||
|
self,
|
||||||
|
mut seq: A,
|
||||||
|
) -> std::result::Result<Self::Value, A::Error> {
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
while let Some(item) = seq.next_element::<String>()? {
|
||||||
|
let trimmed = item.trim().to_string();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
vec.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(vec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_option(CsvOrVec)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_env_bool(key: &str) -> Option<Option<bool>> {
|
fn read_env_bool(key: &str) -> Option<Option<bool>> {
|
||||||
let value = env::var(key).ok()?;
|
let value = env::var(key).ok()?;
|
||||||
Some(parse_bool(&value))
|
Some(parse_bool(&value))
|
||||||
|
|||||||
+32
-5
@@ -57,8 +57,12 @@ pub struct Role {
|
|||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
skills_enabled: Option<bool>,
|
skills_enabled: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(
|
||||||
enabled_skills: Option<String>,
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
deserialize_with = "super::deserialize_csv_or_vec"
|
||||||
|
)]
|
||||||
|
enabled_skills: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
auto_continue: Option<bool>,
|
auto_continue: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -103,7 +107,7 @@ impl Role {
|
|||||||
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
"skills_enabled" => role.skills_enabled = value.as_bool(),
|
"skills_enabled" => role.skills_enabled = value.as_bool(),
|
||||||
"enabled_skills" => role.enabled_skills = value.as_str().map(|v| v.to_string()),
|
"enabled_skills" => role.enabled_skills = parse_enabled_skills_value(value),
|
||||||
"auto_continue" => role.auto_continue = value.as_bool(),
|
"auto_continue" => role.auto_continue = value.as_bool(),
|
||||||
"max_auto_continues" => {
|
"max_auto_continues" => {
|
||||||
role.max_auto_continues = value.as_u64().map(|v| v as usize)
|
role.max_auto_continues = value.as_u64().map(|v| v as usize)
|
||||||
@@ -157,7 +161,8 @@ impl Role {
|
|||||||
metadata.push(format!("skills_enabled: {skills_enabled}"));
|
metadata.push(format!("skills_enabled: {skills_enabled}"));
|
||||||
}
|
}
|
||||||
if let Some(enabled_skills) = &self.enabled_skills {
|
if let Some(enabled_skills) = &self.enabled_skills {
|
||||||
metadata.push(format!("enabled_skills: {enabled_skills}"));
|
let inline = serde_json::to_string(enabled_skills).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
metadata.push(format!("enabled_skills: {inline}"));
|
||||||
}
|
}
|
||||||
if let Some(auto_continue) = self.auto_continue {
|
if let Some(auto_continue) = self.auto_continue {
|
||||||
metadata.push(format!("auto_continue: {auto_continue}"));
|
metadata.push(format!("auto_continue: {auto_continue}"));
|
||||||
@@ -287,7 +292,7 @@ impl Role {
|
|||||||
self.skills_enabled
|
self.skills_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enabled_skills(&self) -> Option<&str> {
|
pub fn enabled_skills(&self) -> Option<&[String]> {
|
||||||
self.enabled_skills.as_deref()
|
self.enabled_skills.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +397,28 @@ impl RoleLike for Role {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_enabled_skills_value(value: &Value) -> Option<Vec<String>> {
|
||||||
|
if value.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = value.as_str() {
|
||||||
|
return Some(csv_to_vec(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(arr) = value.as_array() {
|
||||||
|
let items: Vec<String> = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(|s| s.trim().to_string()))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Some(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_structure_prompt(prompt: &str) -> (&str, Vec<(&str, &str)>) {
|
fn parse_structure_prompt(prompt: &str) -> (&str, Vec<(&str, &str)>) {
|
||||||
let mut text = prompt;
|
let mut text = prompt;
|
||||||
let mut search_input = true;
|
let mut search_input = true;
|
||||||
|
|||||||
+14
-3
@@ -30,8 +30,12 @@ pub struct Session {
|
|||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
skills_enabled: Option<bool>,
|
skills_enabled: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(
|
||||||
enabled_skills: Option<String>,
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
deserialize_with = "super::deserialize_csv_or_vec"
|
||||||
|
)]
|
||||||
|
enabled_skills: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
save_session: Option<bool>,
|
save_session: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -83,10 +87,17 @@ impl Session {
|
|||||||
self.skills_enabled
|
self.skills_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enabled_skills(&self) -> Option<&str> {
|
pub fn enabled_skills(&self) -> Option<&[String]> {
|
||||||
self.enabled_skills.as_deref()
|
self.enabled_skills.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_skills_enabled(&mut self, value: Option<bool>) {
|
||||||
|
if self.skills_enabled != value {
|
||||||
|
self.skills_enabled = value;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
|
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
|
||||||
let role = ctx.extract_role(app);
|
let role = ctx.extract_role(app);
|
||||||
let mut session = Self {
|
let mut session = Self {
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ impl SkillPolicy {
|
|||||||
.map(|v| v.iter().cloned().collect());
|
.map(|v| v.iter().cloned().collect());
|
||||||
|
|
||||||
let enabled_raw: Option<Vec<String>> = session
|
let enabled_raw: Option<Vec<String>> = session
|
||||||
.and_then(|s| parse_csv_opt(s.enabled_skills()))
|
.and_then(|s| s.enabled_skills().map(|v| v.to_vec()))
|
||||||
.or_else(|| agent.and_then(|a| a.enabled_skills().map(|v| v.to_vec())))
|
.or_else(|| agent.and_then(|a| a.enabled_skills().map(|v| v.to_vec())))
|
||||||
.or_else(|| role.and_then(|r| parse_csv_opt(r.enabled_skills())))
|
.or_else(|| role.and_then(|r| r.enabled_skills().map(|v| v.to_vec())))
|
||||||
.or_else(|| parse_csv_opt(global.enabled_skills.as_deref()));
|
.or_else(|| global.enabled_skills.clone());
|
||||||
|
|
||||||
let enabled: HashSet<String> = match enabled_raw {
|
let enabled: HashSet<String> = match enabled_raw {
|
||||||
Some(explicit) => {
|
Some(explicit) => {
|
||||||
@@ -107,18 +107,10 @@ impl SkillPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_csv_opt(s: Option<&str>) -> Option<Vec<String>> {
|
|
||||||
s.map(|raw| {
|
|
||||||
raw.split(',')
|
|
||||||
.map(|t| t.trim().to_string())
|
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use super::super::csv_to_vec;
|
||||||
|
|
||||||
fn always_true(_: &str) -> bool {
|
fn always_true(_: &str) -> bool {
|
||||||
true
|
true
|
||||||
@@ -135,7 +127,7 @@ mod tests {
|
|||||||
) -> AppConfig {
|
) -> AppConfig {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
skills_enabled,
|
skills_enabled,
|
||||||
enabled_skills: enabled.map(|s| s.to_string()),
|
enabled_skills: enabled.map(csv_to_vec),
|
||||||
visible_skills: visible.map(|v| v.iter().map(|s| s.to_string()).collect()),
|
visible_skills: visible.map(|v| v.iter().map(|s| s.to_string()).collect()),
|
||||||
..AppConfig::default()
|
..AppConfig::default()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user