7 Commits

28 changed files with 449 additions and 234 deletions
+3 -1
View File
@@ -243,7 +243,9 @@ instructions: |
When you write or modify files yourself (rather than delegating to coder):
- **For writing files**, ALWAYS use `fs_write` (new file / full overwrite) or `fs_patch` (surgical edit). NEVER write files via `execute_command`. Do not use:
- **For editing an existing file**, prefer `fs_patch`. It's a surgical edit that preserves unchanged content. Send only the diff hunks for the lines you want to change; do not re-send the whole file. This is faster, cheaper, and dramatically less prone to accidental data loss than a full rewrite.
- **For writing a NEW file or doing a COMPLETE rewrite**, use `fs_write`. Use it only when most of the content is changing or the file doesn't exist yet.
- **NEVER write files via `execute_command`.** Do not use:
- `cat > file`, `cat >> file`, `tee`
- `echo >`, `printf >`
- Heredocs (`<<EOF`, `<<-EOF`, `<<'EOF'`)
+3 -2
View File
@@ -32,7 +32,7 @@ def main():
agent_data = parse_raw_data(raw_data)
root_dir = "{config_dir}"
setup_env(root_dir, agent_func)
setup_env(root_dir, agent_func, raw_data)
agent_tools_path = os.path.join(root_dir, "agents/{agent_name}/tools.py")
run(agent_tools_path, agent_func, agent_data)
@@ -65,13 +65,14 @@ def parse_argv():
return agent_func, agent_data
def setup_env(root_dir, agent_func):
def setup_env(root_dir, agent_func, raw_data):
load_env(os.path.join(root_dir, ".env"))
os.environ["LLM_ROOT_DIR"] = root_dir
os.environ["LLM_AGENT_NAME"] = "{agent_name}"
os.environ["LLM_AGENT_FUNC"] = agent_func
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", "{agent_name}")
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}")
os.environ["LLM_AGENT_RAW_JSON"] = raw_data
def load_env(file_path):
+1
View File
@@ -32,6 +32,7 @@ setup_env() {
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/{agent_name}"
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}"
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
export LLM_AGENT_RAW_JSON="$agent_data"
}
load_env() {
+3 -2
View File
@@ -11,7 +11,7 @@ async function main(): Promise<void> {
const agentData = parseRawData(rawData);
const configDir = "{config_dir}";
setupEnv(configDir, agentFunc);
setupEnv(configDir, agentFunc, rawData);
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
await run(agentToolsPath, agentFunc, agentData);
@@ -48,13 +48,14 @@ function parseArgv(): { agentFunc: string; rawData: string } {
return { agentFunc, rawData: agentData };
}
function setupEnv(configDir: string, agentFunc: string): void {
function setupEnv(configDir: string, agentFunc: string, rawData: string): void {
loadEnv(join(configDir, ".env"));
process.env["LLM_ROOT_DIR"] = configDir;
process.env["LLM_AGENT_NAME"] = "{agent_name}";
process.env["LLM_AGENT_FUNC"] = agentFunc;
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
process.env["LLM_AGENT_RAW_JSON"] = rawData;
}
function loadEnv(filePath: string): void {
+3 -2
View File
@@ -32,7 +32,7 @@ def main():
tool_data = parse_raw_data(raw_data)
root_dir = "{root_dir}"
setup_env(root_dir)
setup_env(root_dir, raw_data)
tool_path = "{tool_path}.py"
run(tool_path, "run", tool_data)
@@ -65,11 +65,12 @@ def parse_argv():
return tool_data
def setup_env(root_dir):
def setup_env(root_dir, raw_data):
load_env(os.path.join(root_dir, ".env"))
os.environ["LLM_ROOT_DIR"] = root_dir
os.environ["LLM_TOOL_NAME"] = "{function_name}"
os.environ["LLM_TOOL_CACHE_DIR"] = os.path.join(root_dir, "cache", "{function_name}")
os.environ["LLM_TOOL_RAW_JSON"] = raw_data
def load_env(file_path):
+1
View File
@@ -29,6 +29,7 @@ setup_env() {
export LLM_TOOL_NAME="{function_name}"
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}"
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
export LLM_TOOL_RAW_JSON="$tool_data"
}
load_env() {
+3 -2
View File
@@ -11,7 +11,7 @@ async function main(): Promise<void> {
const toolData = parseRawData(rawData);
const rootDir = "{root_dir}";
setupEnv(rootDir);
setupEnv(rootDir, rawData);
const toolPath = "{tool_path}.ts";
await run(toolPath, "run", toolData);
@@ -45,11 +45,12 @@ function parseArgv(): string {
return toolData;
}
function setupEnv(rootDir: string): void {
function setupEnv(rootDir: string, rawData: string): void {
loadEnv(join(rootDir, ".env"));
process.env["LLM_ROOT_DIR"] = rootDir;
process.env["LLM_TOOL_NAME"] = "{function_name}";
process.env["LLM_TOOL_CACHE_DIR"] = join(rootDir, "cache", "{function_name}");
process.env["LLM_TOOL_RAW_JSON"] = rawData;
}
function loadEnv(filePath: string): void {
@@ -10,6 +10,9 @@ set -e
source "$LLM_PROMPT_UTILS_FILE"
main() {
# shellcheck disable=SC2154
argc_command="$(jq -r '.command' <<< "$LLM_TOOL_RAW_JSON")"
guard_operation
local script
script="$(mktemp)"
@@ -14,6 +14,8 @@ source "$LLM_PROMPT_UTILS_FILE"
# shellcheck disable=SC2154
main() {
argc_code="$(jq -r '.code' <<< "$LLM_TOOL_RAW_JSON")"
if ! grep -qi '^select' <<<"$argc_code"; then
guard_operation ""
fi
+7 -2
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env bash
set -e
# @describe Apply a patch to a file at the specified path.
# This can be used to edit a file without having to rewrite the whole file.
# @describe Apply a unified-diff patch to a file at the specified path. Use this for editing an existing file. It's the
# PREFERRED way to modify a file. Prefer this over fs_write whenever the file already exists: it sends less data,
# preserves unchanged content automatically, and is less prone to accidental data loss from full rewrites.
# Use fs_write only when you are creating a new file or doing a complete rewrite where most of the content changes.
# @option --path! The path of the file to apply the patch to
# @option --contents! The patch to apply to the file
@@ -14,6 +16,9 @@ source "$LLM_PROMPT_UTILS_FILE"
# shellcheck disable=SC2154
main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
if [[ ! -f "$argc_path" ]]; then
error "Unable to find the specified file: $argc_path"
exit 1
+6 -1
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -e
# @describe Write the full file contents to a file at the specified path.
# @describe Write the FULL file contents to a file at the specified path. Use this for NEW files or COMPLETE rewrites
# only. For editing an existing file, prefer fs_patch. It's a surgical edit that preserves unchanged content, requires
# sending less data, and is less prone to accidental data loss.
# @option --path! The path of the file to write to
# @option --contents! The full contents to write to the file
@@ -13,6 +15,9 @@ source "$LLM_PROMPT_UTILS_FILE"
# shellcheck disable=SC2154
main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
if [[ -f "$argc_path" ]]; then
printf "%s" "$argc_contents" | git diff --no-index "$argc_path" - || true
guard_operation "Apply changes?"
+4
View File
@@ -14,6 +14,10 @@ set -e
# shellcheck disable=SC2154
main() {
argc_recipient="$(jq -r '.recipient' <<< "$LLM_TOOL_RAW_JSON")"
argc_subject="$(jq -r '.subject' <<< "$LLM_TOOL_RAW_JSON")"
argc_body="$(jq -r '.body' <<< "$LLM_TOOL_RAW_JSON")"
sender_name="${EMAIL_SENDER_NAME:-$(echo "$EMAIL_SMTP_USER" | awk -F'@' '{print $1}')}"
printf "%s\n" "From: $sender_name <$EMAIL_SMTP_USER>
To: $argc_recipient
+20 -3
View File
@@ -82,7 +82,14 @@ vault_password_file: null # Path to a file containing the password for th
function_calling_support: true # Enables or disables function calling (Globally).
mapping_tools: # Alias for a tool or toolset
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
enabled_tools: null # Which tools to enable by default. (e.g. 'fs,web_search_coyote')
enabled_tools: null # Which tools to enable by default.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Example (list form):
# enabled_tools:
# - fs
# - web_search_coyote
# Example (comma-separated form):
# enabled_tools: fs,web_search_coyote
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
# - demo_py.py
# - demo_sh.sh
@@ -118,7 +125,14 @@ visible_tools: # Which tools are visible to be compiled (and a
mcp_server_support: true # Enables or disables MCP servers (globally).
mapping_mcp_servers: # Alias for an MCP server or set of servers
git: github,gitmcp
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
enabled_mcp_servers: null # Which MCP servers to enable by default.
# Accepts either a YAML list or a comma-separated string. Use 'all' to enable everything.
# Example (list form):
# enabled_mcp_servers:
# - github
# - slack
# Example (comma-separated form):
# enabled_mcp_servers: github,slack,ddg-search
# ---- Skills ----
# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation.
@@ -131,10 +145,13 @@ visible_skills: # The universe of skills allowed to be enabled
- frontend-ui-ux
- git-master
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:
# - git-master
# - ai-slop-remover
# Example (comma-separated form):
# enabled_skills: git-master,ai-slop-remover
# ---- Auto-Continue (Todo System) ----
# The auto-continue system provides built-in task tracking for improved reliability.
+9 -4
View File
@@ -8,12 +8,17 @@ name: <role-name> # The name of the role
model: openai:gpt-4o # The model to use for this role
temperature: 0.2 # The temperature to use for this role when querying the model
top_p: 0 # The top_p to use for this role when querying the model
enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enable for this role
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
enabled_tools: # Tools to enable for this role. Accepts a YAML list (preferred)
- fs_ls # or a comma-separated string (e.g. `enabled_tools: fs_ls,fs_cat`).
- fs_cat # Use `all` to enable every visible tool.
enabled_mcp_servers: # MCP servers to enable for this role. Accepts a YAML list (preferred)
- github # or a comma-separated string (e.g. `enabled_mcp_servers: github,gitmcp`).
- gitmcp # Use `all` to enable every configured MCP server.
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.
enabled_skills: git-master,ai-slop-remover # Comma-separated list of skills available when this role is active.
# Must be a subset of global `visible_skills`. Omit to inherit the global default.
enabled_skills: # Skills available when this role is active. Accepts a YAML list (preferred)
- 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
# the model for output instead of using the instructions below
# Auto-Continue (Todo System)
+9 -11
View File
@@ -548,12 +548,12 @@ impl RoleLike for Agent {
self.config.top_p
}
fn enabled_tools(&self) -> Option<String> {
fn enabled_tools(&self) -> Option<Vec<String>> {
None
}
fn enabled_mcp_servers(&self) -> Option<String> {
self.config.mcp_servers.clone().join(",").into()
fn enabled_mcp_servers(&self) -> Option<Vec<String>> {
Some(self.config.mcp_servers.clone())
}
fn set_model(&mut self, model: Model) {
@@ -569,15 +569,14 @@ impl RoleLike for Agent {
self.config.top_p = value;
}
fn set_enabled_tools(&mut self, value: Option<String>) {
fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
match value {
Some(tools) => {
let tools = tools
.split(',')
self.config.global_tools = tools
.into_iter()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.collect::<Vec<_>>();
self.config.global_tools = tools;
}
None => {
self.config.global_tools.clear();
@@ -585,15 +584,14 @@ impl RoleLike for Agent {
}
}
fn set_enabled_mcp_servers(&mut self, value: Option<String>) {
fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>) {
match value {
Some(servers) => {
let servers = servers
.split(',')
self.config.mcp_servers = servers
.into_iter()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.collect::<Vec<_>>();
self.config.mcp_servers = servers;
}
None => {
self.config.mcp_servers.clear();
+11 -8
View File
@@ -34,16 +34,19 @@ pub struct AppConfig {
pub function_calling_support: bool,
pub mapping_tools: IndexMap<String, String>,
pub enabled_tools: Option<String>,
#[serde(default, deserialize_with = "super::deserialize_csv_or_vec")]
pub enabled_tools: Option<Vec<String>>,
pub visible_tools: Option<Vec<String>>,
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 mcp_server_support: bool,
pub mapping_mcp_servers: IndexMap<String, String>,
pub enabled_mcp_servers: Option<String>,
#[serde(default, deserialize_with = "super::deserialize_csv_or_vec")]
pub enabled_mcp_servers: Option<Vec<String>>,
pub auto_continue: bool,
pub max_auto_continues: usize,
@@ -392,7 +395,7 @@ impl AppConfig {
self.mapping_tools = v;
}
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_tools")) {
self.enabled_tools = v;
self.enabled_tools = v.map(|raw| super::csv_to_vec(&raw));
}
if let Some(Some(v)) = super::read_env_bool(&get_env_name("skills_enabled")) {
@@ -400,7 +403,7 @@ impl AppConfig {
}
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")) {
@@ -412,7 +415,7 @@ impl AppConfig {
self.mapping_mcp_servers = v;
}
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_mcp_servers")) {
self.enabled_mcp_servers = v;
self.enabled_mcp_servers = v.map(|raw| super::csv_to_vec(&raw));
}
if let Some(v) = super::read_env_value::<String>(&get_env_name("repl_prelude")) {
@@ -514,12 +517,12 @@ impl AppConfig {
}
#[allow(dead_code)]
pub fn set_enabled_tools_default(&mut self, value: Option<String>) {
pub fn set_enabled_tools_default(&mut self, value: Option<Vec<String>>) {
self.enabled_tools = value;
}
#[allow(dead_code)]
pub fn set_enabled_mcp_servers_default(&mut self, value: Option<String>) {
pub fn set_enabled_mcp_servers_default(&mut self, value: Option<Vec<String>>) {
self.enabled_mcp_servers = value;
}
+2 -2
View File
@@ -33,8 +33,8 @@ pub async fn macro_execute(
let mut app_config = (*ctx.app.config).clone();
app_config.temperature = role.temperature();
app_config.top_p = role.top_p();
app_config.enabled_tools = role.enabled_tools().clone();
app_config.enabled_mcp_servers = role.enabled_mcp_servers().clone();
app_config.enabled_tools = role.enabled_tools();
app_config.enabled_mcp_servers = role.enabled_mcp_servers();
let mut app_state = (*ctx.app).clone();
app_state.config = Arc::new(app_config);
+72 -3
View File
@@ -196,16 +196,19 @@ pub struct Config {
pub function_calling_support: bool,
pub mapping_tools: IndexMap<String, String>,
pub enabled_tools: Option<String>,
#[serde(default, deserialize_with = "deserialize_csv_or_vec")]
pub enabled_tools: Option<Vec<String>>,
pub visible_tools: Option<Vec<String>>,
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 mcp_server_support: bool,
pub mapping_mcp_servers: IndexMap<String, String>,
pub enabled_mcp_servers: Option<String>,
#[serde(default, deserialize_with = "deserialize_csv_or_vec")]
pub enabled_mcp_servers: Option<Vec<String>>,
pub auto_continue: bool,
pub max_auto_continues: usize,
@@ -783,6 +786,72 @@ where
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>> {
let value = env::var(key).ok()?;
Some(parse_bool(&value))
+73 -44
View File
@@ -700,7 +700,7 @@ impl RequestContext {
}
}
pub fn set_enabled_tools_on_role_like(&mut self, value: Option<String>) -> bool {
pub fn set_enabled_tools_on_role_like(&mut self, value: Option<Vec<String>>) -> bool {
match self.role_like_mut() {
Some(role_like) => {
role_like.set_enabled_tools(value);
@@ -710,7 +710,7 @@ impl RequestContext {
}
}
pub fn set_enabled_mcp_servers_on_role_like(&mut self, value: Option<String>) -> bool {
pub fn set_enabled_mcp_servers_on_role_like(&mut self, value: Option<Vec<String>>) -> bool {
match self.role_like_mut() {
Some(role_like) => {
role_like.set_enabled_mcp_servers(value);
@@ -854,11 +854,11 @@ impl RequestContext {
("top_p", super::format_option_value(&role.top_p())),
(
"enabled_tools",
super::format_option_value(&role.enabled_tools()),
super::format_option_value(&role.enabled_tools().map(|v| v.join(","))),
),
(
"enabled_mcp_servers",
super::format_option_value(&role.enabled_mcp_servers()),
super::format_option_value(&role.enabled_mcp_servers().map(|v| v.join(","))),
),
(
"max_output_tokens",
@@ -1148,10 +1148,10 @@ impl RequestContext {
}
let mut tool_names: HashSet<String> = Default::default();
if enabled_tools == "all" {
if enabled_tools.iter().any(|s| s.trim() == "all") {
tool_names.extend(declaration_names);
} else {
for item in enabled_tools.split(',') {
for item in enabled_tools.iter() {
let item = item.trim();
if item.is_empty() {
continue;
@@ -1279,10 +1279,10 @@ impl RequestContext {
}
let mut server_names: HashSet<String> = Default::default();
if enabled_mcp_servers == "all" {
if enabled_mcp_servers.iter().any(|s| s.trim() == "all") {
server_names.extend(mcp_declaration_names);
} else {
for item in enabled_mcp_servers.split(',') {
for item in enabled_mcp_servers.iter() {
let item = item.trim();
if item.is_empty() {
continue;
@@ -1714,14 +1714,29 @@ impl RequestContext {
}
}
"enabled_tools" => {
let value = super::parse_value(value)?;
if !self.set_enabled_tools_on_role_like(value.clone()) {
self.update_app_config(|app| app.enabled_tools = value);
let raw: Option<String> = super::parse_value(value)?;
let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
if !self.set_enabled_tools_on_role_like(parsed.clone()) {
self.update_app_config(|app| app.enabled_tools = parsed.clone());
}
}
"enabled_skills" => {
let raw: Option<String> = super::parse_value(value)?;
let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
self.update_app_config(|app| app.enabled_skills = parsed.clone());
}
"skills_enabled" => {
let value: Option<bool> = super::parse_value(value)?;
if let Some(session) = self.session.as_mut() {
session.set_skills_enabled(value);
} else {
self.update_app_config(|app| app.skills_enabled = value.unwrap_or(true));
}
}
"enabled_mcp_servers" => {
let value: Option<String> = super::parse_value(value)?;
if let Some(servers) = value.as_ref() {
let raw: Option<String> = super::parse_value(value)?;
let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
if let Some(servers) = parsed.as_ref() {
let Some(mcp_config) = &self.app.mcp_config else {
bail!(
"No MCP servers are configured. Please configure MCP servers first before setting 'enabled_mcp_servers'."
@@ -1733,7 +1748,7 @@ impl RequestContext {
);
}
if !servers.split(',').all(|s| {
if !servers.iter().all(|s| {
let server = s.trim();
server == "all" || mcp_config.mcp_servers.contains_key(server)
}) {
@@ -1742,8 +1757,8 @@ impl RequestContext {
);
}
}
if !self.set_enabled_mcp_servers_on_role_like(value.clone()) {
self.update_app_config(|app| app.enabled_mcp_servers = value.clone());
if !self.set_enabled_mcp_servers_on_role_like(parsed.clone()) {
self.update_app_config(|app| app.enabled_mcp_servers = parsed.clone());
}
if self.app.config.mcp_server_support {
let app = Arc::clone(&self.app.config);
@@ -1965,6 +1980,7 @@ impl RequestContext {
"dry_run",
"function_calling_support",
"mcp_server_support",
"skills_enabled",
"stream",
"save",
"highlight",
@@ -2063,6 +2079,14 @@ impl RequestContext {
.collect()
}
"mcp_server_support" => super::complete_bool(app.mcp_server_support),
"skills_enabled" => {
let current = if let Some(session) = &self.session {
session.skills_enabled()
} else {
Some(app.skills_enabled)
};
super::complete_option_bool(current)
}
"enabled_mcp_servers" => {
let mut prefix = String::new();
let mut ignores = HashSet::new();
@@ -2141,7 +2165,7 @@ impl RequestContext {
async fn rebuild_tool_scope(
&mut self,
app: &AppConfig,
enabled_mcp_servers: Option<String>,
enabled_mcp_servers: Option<Vec<String>>,
abort_signal: AbortSignal,
) -> Result<()> {
let policy = SkillPolicy::effective(
@@ -2153,21 +2177,23 @@ impl RequestContext {
let enabled_mcp_servers = if policy.skills_enabled && app.mcp_server_support {
let skill_mcps = self.skill_registry.loaded_mcp_servers();
match (enabled_mcp_servers.as_deref(), skill_mcps.is_empty()) {
(Some("all"), _) | (_, true) => enabled_mcp_servers,
(base, false) => {
let mut merged: BTreeSet<String> = skill_mcps;
if let Some(s) = base {
for token in s.split(',') {
let t = token.trim();
if !t.is_empty() {
merged.insert(t.to_string());
}
let has_all = enabled_mcp_servers
.as_ref()
.map(|v| v.iter().any(|s| s.trim() == "all"))
.unwrap_or(false);
if has_all || skill_mcps.is_empty() {
enabled_mcp_servers
} else {
let mut merged: BTreeSet<String> = skill_mcps;
if let Some(servers) = &enabled_mcp_servers {
for token in servers {
let t = token.trim();
if !t.is_empty() {
merged.insert(t.to_string());
}
}
Some(merged.into_iter().collect::<Vec<_>>().join(","))
}
Some(merged.into_iter().collect())
}
} else {
enabled_mcp_servers
@@ -2179,12 +2205,12 @@ impl RequestContext {
&& let Some(mcp_config) = &self.app.mcp_config
{
let server_ids: Vec<String> = match &enabled_mcp_servers {
Some(servers) if servers == "all" => {
Some(servers) if servers.iter().any(|s| s.trim() == "all") => {
mcp_config.mcp_servers.keys().cloned().collect()
}
Some(servers) => {
let mut ids = Vec::new();
for item in servers.split(',').map(|s| s.trim()) {
for item in servers.iter().map(|s| s.trim()) {
if mcp_config.mcp_servers.contains_key(item) {
ids.push(item.to_string());
} else if let Some(mapped) = app.mapping_mcp_servers.get(item) {
@@ -2263,7 +2289,7 @@ impl RequestContext {
if names.is_empty() {
None
} else {
Some(names.join(","))
Some(names.to_vec())
}
} else if let Some(role) = &self.role {
role.enabled_mcp_servers()
@@ -2423,7 +2449,7 @@ impl RequestContext {
}
let mcp_servers = if app.mcp_server_support {
(!agent.mcp_server_names().is_empty()).then(|| agent.mcp_server_names().join(","))
(!agent.mcp_server_names().is_empty()).then(|| agent.mcp_server_names().to_vec())
} else {
if !agent.mcp_server_names().is_empty() {
bail!(
@@ -2599,7 +2625,7 @@ impl RequestContext {
let skill = Skill::load(name)?;
let needs_mcps = skill
.enabled_mcp_servers()
.map(|s| !s.trim().is_empty())
.map(|v| !v.is_empty())
.unwrap_or(false);
if needs_mcps && !self.app.config.mcp_server_support {
@@ -2706,13 +2732,13 @@ impl RequestContext {
&self,
app: &AppConfig,
start_mcp_servers: bool,
) -> Option<String> {
) -> Option<Vec<String>> {
if !start_mcp_servers || !app.mcp_server_support {
return None;
}
if let Some(agent) = self.agent.as_ref() {
return (!agent.mcp_server_names().is_empty())
.then(|| agent.mcp_server_names().join(","));
.then(|| agent.mcp_server_names().to_vec());
}
if let Some(session) = self.session.as_ref() {
return session.enabled_mcp_servers();
@@ -3205,7 +3231,7 @@ mod tests {
let app = ctx.app.config.clone();
let abort = utils::create_abort_signal();
run_async(ctx.rebuild_tool_scope(&app, Some("all".to_string()), abort)).unwrap();
run_async(ctx.rebuild_tool_scope(&app, Some(vec!["all".to_string()]), abort)).unwrap();
assert!(ctx.tool_scope.mcp_runtime.is_empty());
}
@@ -3233,7 +3259,7 @@ mod tests {
let app = ctx.app.config.clone();
let abort = utils::create_abort_signal();
run_async(ctx.rebuild_tool_scope(&app, Some("all".to_string()), abort)).unwrap();
run_async(ctx.rebuild_tool_scope(&app, Some(vec!["all".to_string()]), abort)).unwrap();
assert!(ctx.tool_scope.mcp_runtime.is_empty());
}
@@ -3341,7 +3367,7 @@ mod tests {
};
let ctx = RequestContext::new(app_state, WorkingMode::Cmd);
let mut role = Role::new("r", "p");
role.set_enabled_tools(Some("all".to_string()));
role.set_enabled_tools(Some(vec!["all".to_string()]));
assert!(ctx.select_functions(&role).is_none());
}
@@ -3352,7 +3378,7 @@ mod tests {
ctx.tool_scope.functions.append_user_interaction_functions();
let mut role = Role::new("r", "p");
role.set_enabled_tools(Some("all".to_string()));
role.set_enabled_tools(Some(vec!["all".to_string()]));
let fns = ctx.select_functions(&role).unwrap();
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
@@ -3366,7 +3392,10 @@ mod tests {
ctx.tool_scope.functions.append_todo_functions();
let mut role = Role::new("r", "p");
role.set_enabled_tools(Some("todo__init, todo__add".to_string()));
role.set_enabled_tools(Some(vec![
"todo__init".to_string(),
"todo__add".to_string(),
]));
let fns = ctx.select_functions(&role).unwrap();
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
@@ -3395,7 +3424,7 @@ mod tests {
};
let ctx = RequestContext::new(app_state, WorkingMode::Cmd);
let mut role = Role::new("r", "p");
role.set_enabled_mcp_servers(Some("all".to_string()));
role.set_enabled_mcp_servers(Some(vec!["all".to_string()]));
let result = ctx.select_enabled_mcp_servers(&role);
assert!(result.is_empty());
}
@@ -3408,7 +3437,7 @@ mod tests {
.append_mcp_meta_functions(vec!["github".into(), "slack".into()]);
let mut role = Role::new("r", "p");
role.set_enabled_mcp_servers(Some("all".to_string()));
role.set_enabled_mcp_servers(Some(vec!["all".to_string()]));
let fns = ctx.select_enabled_mcp_servers(&role);
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
@@ -3425,7 +3454,7 @@ mod tests {
.append_mcp_meta_functions(vec!["github".into(), "slack".into()]);
let mut role = Role::new("r", "p");
role.set_enabled_mcp_servers(Some("github".to_string()));
role.set_enabled_mcp_servers(Some(vec!["github".to_string()]));
let fns = ctx.select_enabled_mcp_servers(&role);
let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
+71 -27
View File
@@ -28,13 +28,13 @@ pub trait RoleLike {
fn model(&self) -> &Model;
fn temperature(&self) -> Option<f64>;
fn top_p(&self) -> Option<f64>;
fn enabled_tools(&self) -> Option<String>;
fn enabled_mcp_servers(&self) -> Option<String>;
fn enabled_tools(&self) -> Option<Vec<String>>;
fn enabled_mcp_servers(&self) -> Option<Vec<String>>;
fn set_model(&mut self, model: Model);
fn set_temperature(&mut self, value: Option<f64>);
fn set_top_p(&mut self, value: Option<f64>);
fn set_enabled_tools(&mut self, value: Option<String>);
fn set_enabled_mcp_servers(&mut self, value: Option<String>);
fn set_enabled_tools(&mut self, value: Option<Vec<String>>);
fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>);
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
@@ -51,14 +51,26 @@ pub struct Role {
temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_mcp_servers: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_tools: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
skills_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_skills: Option<String>,
#[serde(
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")]
auto_continue: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -98,12 +110,12 @@ impl Role {
"model" => role.model_id = value.as_str().map(|v| v.to_string()),
"temperature" => role.temperature = value.as_f64(),
"top_p" => role.top_p = value.as_f64(),
"enabled_tools" => role.enabled_tools = value.as_str().map(|v| v.to_string()),
"enabled_tools" => role.enabled_tools = parse_string_or_array(value),
"enabled_mcp_servers" => {
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
role.enabled_mcp_servers = parse_string_or_array(value)
}
"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_string_or_array(value),
"auto_continue" => role.auto_continue = value.as_bool(),
"max_auto_continues" => {
role.max_auto_continues = value.as_u64().map(|v| v as usize)
@@ -147,17 +159,21 @@ impl Role {
if let Some(top_p) = self.top_p() {
metadata.push(format!("top_p: {top_p}"));
}
if let Some(enabled_tools) = self.enabled_tools() {
metadata.push(format!("enabled_tools: {enabled_tools}"));
if let Some(enabled_tools) = &self.enabled_tools {
let inline = serde_json::to_string(enabled_tools).unwrap_or_else(|_| "[]".to_string());
metadata.push(format!("enabled_tools: {inline}"));
}
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}"));
if let Some(enabled_mcp_servers) = &self.enabled_mcp_servers {
let inline =
serde_json::to_string(enabled_mcp_servers).unwrap_or_else(|_| "[]".to_string());
metadata.push(format!("enabled_mcp_servers: {inline}"));
}
if let Some(skills_enabled) = self.skills_enabled {
metadata.push(format!("skills_enabled: {skills_enabled}"));
}
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 {
metadata.push(format!("auto_continue: {auto_continue}"));
@@ -225,8 +241,8 @@ impl Role {
model: &Model,
temperature: Option<f64>,
top_p: Option<f64>,
enabled_tools: Option<String>,
enabled_mcp_servers: Option<String>,
enabled_tools: Option<Vec<String>>,
enabled_mcp_servers: Option<Vec<String>>,
) {
self.set_model(model.clone());
if temperature.is_some() {
@@ -287,7 +303,7 @@ impl Role {
self.skills_enabled
}
pub fn enabled_skills(&self) -> Option<&str> {
pub fn enabled_skills(&self) -> Option<&[String]> {
self.enabled_skills.as_deref()
}
@@ -360,11 +376,11 @@ impl RoleLike for Role {
self.top_p
}
fn enabled_tools(&self) -> Option<String> {
fn enabled_tools(&self) -> Option<Vec<String>> {
self.enabled_tools.clone()
}
fn enabled_mcp_servers(&self) -> Option<String> {
fn enabled_mcp_servers(&self) -> Option<Vec<String>> {
self.enabled_mcp_servers.clone()
}
@@ -383,15 +399,37 @@ impl RoleLike for Role {
self.top_p = value;
}
fn set_enabled_tools(&mut self, value: Option<String>) {
fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
self.enabled_tools = value;
}
fn set_enabled_mcp_servers(&mut self, value: Option<String>) {
fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>) {
self.enabled_mcp_servers = value;
}
}
fn parse_string_or_array(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)>) {
let mut text = prompt;
let mut search_input = true;
@@ -466,14 +504,20 @@ mod tests {
fn role_new_parses_enabled_tools() {
let content = "---\nenabled_tools: tool1,tool2\n---\nPrompt";
let role = Role::new("test", content);
assert_eq!(role.enabled_tools(), Some("tool1,tool2".to_string()));
assert_eq!(
role.enabled_tools(),
Some(vec!["tool1".to_string(), "tool2".to_string()])
);
}
#[test]
fn role_new_parses_enabled_mcp_servers() {
let content = "---\nenabled_mcp_servers: github,jira\n---\nPrompt";
let role = Role::new("test", content);
assert_eq!(role.enabled_mcp_servers(), Some("github,jira".to_string()));
assert_eq!(
role.enabled_mcp_servers(),
Some(vec!["github".to_string(), "jira".to_string()])
);
}
#[test]
+48 -15
View File
@@ -24,14 +24,26 @@ pub struct Session {
temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_mcp_servers: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_tools: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::deserialize_csv_or_vec"
)]
enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
skills_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_skills: Option<String>,
#[serde(
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")]
save_session: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -83,10 +95,17 @@ impl Session {
self.skills_enabled
}
pub fn enabled_skills(&self) -> Option<&str> {
pub fn enabled_skills(&self) -> Option<&[String]> {
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 {
let role = ctx.extract_role(app);
let mut session = Self {
@@ -182,10 +201,16 @@ impl Session {
data["top_p"] = top_p.into();
}
if let Some(enabled_tools) = self.enabled_tools() {
data["enabled_tools"] = enabled_tools.into();
data["enabled_tools"] = json!(enabled_tools);
}
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
data["enabled_mcp_servers"] = enabled_mcp_servers.into();
data["enabled_mcp_servers"] = json!(enabled_mcp_servers);
}
if let Some(skills_enabled) = self.skills_enabled() {
data["skills_enabled"] = skills_enabled.into();
}
if let Some(enabled_skills) = self.enabled_skills() {
data["enabled_skills"] = json!(enabled_skills);
}
if let Some(save_session) = self.save_session() {
data["save_session"] = save_session.into();
@@ -242,11 +267,19 @@ impl Session {
}
if let Some(enabled_tools) = self.enabled_tools() {
items.push(("enabled_tools", enabled_tools));
items.push(("enabled_tools", enabled_tools.join(",")));
}
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
items.push(("enabled_mcp_servers", enabled_mcp_servers));
items.push(("enabled_mcp_servers", enabled_mcp_servers.join(",")));
}
if let Some(skills_enabled) = self.skills_enabled() {
items.push(("skills_enabled", skills_enabled.to_string()));
}
if let Some(enabled_skills) = self.enabled_skills() {
items.push(("enabled_skills", enabled_skills.join(",")));
}
if let Some(save_session) = self.save_session() {
@@ -682,11 +715,11 @@ impl RoleLike for Session {
self.top_p
}
fn enabled_tools(&self) -> Option<String> {
fn enabled_tools(&self) -> Option<Vec<String>> {
self.enabled_tools.clone()
}
fn enabled_mcp_servers(&self) -> Option<String> {
fn enabled_mcp_servers(&self) -> Option<Vec<String>> {
self.enabled_mcp_servers.clone()
}
@@ -713,14 +746,14 @@ impl RoleLike for Session {
}
}
fn set_enabled_tools(&mut self, value: Option<String>) {
fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
if self.enabled_tools != value {
self.enabled_tools = value;
self.dirty = true;
}
}
fn set_enabled_mcp_servers(&mut self, value: Option<String>) {
fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>) {
if self.enabled_mcp_servers != value {
self.enabled_mcp_servers = value;
self.dirty = true;
+33 -9
View File
@@ -33,9 +33,9 @@ pub struct Skill {
#[serde(default)]
body: String,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<String>,
enabled_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_mcp_servers: Option<String>,
enabled_mcp_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_unload: Option<bool>,
}
@@ -69,10 +69,10 @@ impl Skill {
}
}
"enabled_tools" => {
skill.enabled_tools = value.as_str().map(|v| v.to_string());
skill.enabled_tools = parse_skill_string_or_array(value);
}
"enabled_mcp_servers" => {
skill.enabled_mcp_servers = value.as_str().map(|v| v.to_string());
skill.enabled_mcp_servers = parse_skill_string_or_array(value);
}
"auto_unload" => {
skill.auto_unload = value.as_bool();
@@ -134,11 +134,11 @@ impl Skill {
&self.body
}
pub fn enabled_tools(&self) -> Option<&str> {
pub fn enabled_tools(&self) -> Option<&[String]> {
self.enabled_tools.as_deref()
}
pub fn enabled_mcp_servers(&self) -> Option<&str> {
pub fn enabled_mcp_servers(&self) -> Option<&[String]> {
self.enabled_mcp_servers.as_deref()
}
@@ -157,11 +157,29 @@ impl Skill {
fn declares_mcp_servers(&self) -> bool {
self.enabled_mcp_servers
.as_deref()
.map(|s| !s.trim().is_empty())
.map(|servers| !servers.is_empty())
.unwrap_or(false)
}
}
fn parse_skill_string_or_array(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
}
#[cfg(test)]
mod tests {
use super::*;
@@ -189,8 +207,14 @@ mod tests {
assert_eq!(skill.name(), "git-master");
assert_eq!(skill.description(), "Atomic commits, rebase surgery");
assert_eq!(skill.enabled_tools(), Some("shell,fs"));
assert_eq!(skill.enabled_mcp_servers(), Some("github"));
assert_eq!(
skill.enabled_tools(),
Some(["shell".to_string(), "fs".to_string()].as_slice())
);
assert_eq!(
skill.enabled_mcp_servers(),
Some(["github".to_string()].as_slice())
);
assert!(skill.auto_unload());
assert_eq!(skill.body(), "You are a git expert");
}
+5 -13
View File
@@ -67,10 +67,10 @@ impl SkillPolicy {
.map(|v| v.iter().cloned().collect());
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(|| role.and_then(|r| parse_csv_opt(r.enabled_skills())))
.or_else(|| parse_csv_opt(global.enabled_skills.as_deref()));
.or_else(|| role.and_then(|r| r.enabled_skills().map(|v| v.to_vec())))
.or_else(|| global.enabled_skills.clone());
let enabled: HashSet<String> = match enabled_raw {
Some(explicit) => {
@@ -107,17 +107,9 @@ 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)]
mod tests {
use super::super::csv_to_vec;
use super::*;
fn always_true(_: &str) -> bool {
@@ -135,7 +127,7 @@ mod tests {
) -> AppConfig {
AppConfig {
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()),
..AppConfig::default()
}
+26 -33
View File
@@ -38,8 +38,8 @@ impl SkillRegistry {
pub fn loaded_mcp_servers(&self) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for skill in self.loaded.values() {
if let Some(csv) = skill.enabled_mcp_servers() {
for token in csv.split(',') {
if let Some(servers) = skill.enabled_mcp_servers() {
for token in servers {
let t = token.trim();
if !t.is_empty() {
out.insert(t.to_string());
@@ -69,12 +69,22 @@ impl SkillRegistry {
let base_tools_set = effective.enabled_tools().is_some();
let base_mcps_set = effective.enabled_mcp_servers().is_some();
let mut tools = parse_csv(effective.enabled_tools().as_deref());
let mut mcps = parse_csv(effective.enabled_mcp_servers().as_deref());
let mut tools: BTreeSet<String> = effective
.enabled_tools()
.map(|v| v.into_iter().collect())
.unwrap_or_default();
let mut mcps: BTreeSet<String> = effective
.enabled_mcp_servers()
.map(|v| v.into_iter().collect())
.unwrap_or_default();
for (_, skill) in &self.loaded {
tools.extend(parse_csv(skill.enabled_tools()));
mcps.extend(parse_csv(skill.enabled_mcp_servers()));
if let Some(skill_tools) = skill.enabled_tools() {
tools.extend(skill_tools.iter().cloned());
}
if let Some(servers) = skill.enabled_mcp_servers() {
mcps.extend(servers.iter().cloned());
}
if !skip_body && !skill.body().is_empty() {
let separator = if effective.is_empty_prompt() {
""
@@ -87,34 +97,17 @@ impl SkillRegistry {
}
if base_tools_set || !tools.is_empty() {
effective.set_enabled_tools(Some(join_csv(&tools)));
effective.set_enabled_tools(Some(tools.into_iter().collect()));
}
if base_mcps_set || !mcps.is_empty() {
effective.set_enabled_mcp_servers(Some(join_csv(&mcps)));
effective.set_enabled_mcp_servers(Some(mcps.into_iter().collect()));
}
effective
}
}
fn parse_csv(s: Option<&str>) -> BTreeSet<String> {
let mut set = BTreeSet::new();
if let Some(raw) = s {
for token in raw.split(',') {
let trimmed = token.trim();
if !trimmed.is_empty() {
set.insert(trimmed.to_string());
}
}
}
set
}
fn join_csv(set: &BTreeSet<String>) -> String {
set.iter().cloned().collect::<Vec<_>>().join(",")
}
#[cfg(test)]
impl SkillRegistry {
fn insert_for_test(&mut self, skill: Skill) {
@@ -194,7 +187,7 @@ mod tests {
assert_eq!(effective.prompt(), "Process: __INPUT__");
let tools = effective.enabled_tools().expect("tools set by skill");
assert!(tools.contains("shell"));
assert!(tools.iter().any(|s| s == "shell"));
}
#[test]
@@ -223,16 +216,16 @@ mod tests {
));
let mut base = Role::new("test", "body");
base.set_enabled_tools(Some("web_search".to_string()));
base.set_enabled_tools(Some(vec!["web_search".to_string()]));
let effective = registry.effective_role(&base);
let tools_str = effective.enabled_tools().unwrap();
let tools: BTreeSet<&str> = tools_str.split(',').collect();
let tools_vec = effective.enabled_tools().unwrap();
let tools: BTreeSet<&str> = tools_vec.iter().map(|s| s.as_str()).collect();
assert_eq!(tools, BTreeSet::from(["fs", "git", "shell", "web_search"]));
let mcps_str = effective.enabled_mcp_servers().unwrap();
let mcps: BTreeSet<&str> = mcps_str.split(',').collect();
let mcps_vec = effective.enabled_mcp_servers().unwrap();
let mcps: BTreeSet<&str> = mcps_vec.iter().map(|s| s.as_str()).collect();
assert_eq!(mcps, BTreeSet::from(["github", "jira"]));
}
@@ -254,10 +247,10 @@ mod tests {
registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge"));
let mut base = Role::new("test", "Base");
base.set_enabled_tools(Some(String::new()));
base.set_enabled_tools(Some(Vec::new()));
let effective = registry.effective_role(&base);
assert_eq!(effective.enabled_tools().as_deref(), Some(""));
assert_eq!(effective.enabled_tools().as_deref(), Some([].as_slice()));
}
#[test]
+4 -28
View File
@@ -127,8 +127,8 @@ fn handle_list(ctx: &RequestContext, policy: &SkillPolicy) -> Result<Value> {
entries.push(json!({
"name": skill.name(),
"description": skill.description(),
"grants_tools": csv_to_vec(skill.enabled_tools()),
"grants_mcp_servers": csv_to_vec(skill.enabled_mcp_servers()),
"grants_tools": skill.enabled_tools().unwrap_or_default(),
"grants_mcp_servers": skill.enabled_mcp_servers().unwrap_or_default(),
"loaded": ctx.skill_registry.is_loaded(skill.name()),
}));
}
@@ -166,11 +166,11 @@ async fn handle_load(
let tools_declared = skill
.enabled_tools()
.map(|s| !s.trim().is_empty())
.map(|v| !v.is_empty())
.unwrap_or(false);
let mcps_declared = skill
.enabled_mcp_servers()
.map(|s| !s.trim().is_empty())
.map(|v| !v.is_empty())
.unwrap_or(false);
if tools_declared && !function_calling_on {
@@ -226,16 +226,6 @@ async fn handle_unload(ctx: &mut RequestContext, args: &Value) -> Result<Value>
}))
}
fn csv_to_vec(csv: Option<&str>) -> Vec<String> {
csv.map(|raw| {
raw.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -293,18 +283,4 @@ mod tests {
assert!(required, "skill__list should have no required parameters");
}
#[test]
fn csv_to_vec_empty_input() {
assert!(csv_to_vec(None).is_empty());
assert!(csv_to_vec(Some("")).is_empty());
assert!(csv_to_vec(Some(" ")).is_empty());
}
#[test]
fn csv_to_vec_parses_and_trims() {
let v = csv_to_vec(Some("a, b ,c,, d"));
assert_eq!(v, vec!["a", "b", "c", "d"]);
}
}
+6 -6
View File
@@ -256,18 +256,18 @@ fn build_inline_role(
}
if node.tools.as_deref().unwrap_or_default().is_empty() {
role.set_enabled_tools(Some(String::new()));
role.set_enabled_mcp_servers(Some(String::new()));
role.set_enabled_tools(Some(Vec::new()));
role.set_enabled_mcp_servers(Some(Vec::new()));
} else {
if !regular_tools.is_empty() {
role.set_enabled_tools(Some(regular_tools.join(",")));
role.set_enabled_tools(Some(regular_tools.to_vec()));
} else {
role.set_enabled_tools(Some(String::new()));
role.set_enabled_tools(Some(Vec::new()));
}
if !mcp_servers.is_empty() {
role.set_enabled_mcp_servers(Some(mcp_servers.join(",")));
role.set_enabled_mcp_servers(Some(mcp_servers.to_vec()));
} else {
role.set_enabled_mcp_servers(Some(String::new()));
role.set_enabled_mcp_servers(Some(Vec::new()));
}
}
+4 -4
View File
@@ -55,8 +55,8 @@ async fn extract_via_extractor(
fn build_extractor_role() -> Result<Role> {
let mut role = Role::new(EXTRACTOR_ROLE_NAME, EXTRACTOR_ROLE_PROMPT);
role.set_enabled_tools(Some(String::new()));
role.set_enabled_mcp_servers(Some(String::new()));
role.set_enabled_tools(Some(Vec::new()));
role.set_enabled_mcp_servers(Some(Vec::new()));
Ok(role)
}
@@ -183,7 +183,7 @@ mod tests {
fn build_extractor_role_disables_tools_and_mcp() {
let role = build_extractor_role().expect("builtin role must exist");
assert_eq!(role.enabled_tools().as_deref(), Some(""));
assert_eq!(role.enabled_mcp_servers().as_deref(), Some(""));
assert_eq!(role.enabled_tools().as_deref(), Some([].as_slice()));
assert_eq!(role.enabled_mcp_servers().as_deref(), Some([].as_slice()));
}
}
+17 -12
View File
@@ -146,7 +146,7 @@ impl McpRegistry {
pub async fn init(
log_path: Option<PathBuf>,
start_mcp_servers: bool,
enabled_mcp_servers: Option<String>,
enabled_mcp_servers: Option<Vec<String>>,
abort_signal: AbortSignal,
app_config: &AppConfig,
vault: &Vault,
@@ -216,7 +216,7 @@ impl McpRegistry {
async fn start_select_mcp_servers(
&mut self,
enabled_mcp_servers: Option<String>,
enabled_mcp_servers: Option<Vec<String>>,
) -> Result<()> {
if self.config.is_none() {
debug!(
@@ -292,15 +292,15 @@ impl McpRegistry {
Ok((id.to_string(), service, catalog))
}
fn resolve_server_ids(&self, enabled_mcp_servers: Option<String>) -> Vec<String> {
fn resolve_server_ids(&self, enabled_mcp_servers: Option<Vec<String>>) -> Vec<String> {
if let Some(config) = &self.config
&& let Some(servers) = enabled_mcp_servers
{
if servers == "all" {
if servers.iter().any(|s| s.trim() == "all") {
config.mcp_servers.keys().cloned().collect()
} else {
let enabled_servers: HashSet<String> =
servers.split(',').map(|s| s.trim().to_string()).collect();
servers.into_iter().map(|s| s.trim().to_string()).collect();
config
.mcp_servers
.keys()
@@ -754,7 +754,7 @@ mod tests {
#[test]
fn resolve_all_returns_all_configured_servers() {
let registry = make_registry_with_config(&["github", "slack", "jira"]);
let mut ids = registry.resolve_server_ids(Some("all".to_string()));
let mut ids = registry.resolve_server_ids(Some(vec!["all".to_string()]));
ids.sort();
assert_eq!(ids, vec!["github", "jira", "slack"]);
}
@@ -762,7 +762,8 @@ mod tests {
#[test]
fn resolve_comma_separated_returns_matching_servers() {
let registry = make_registry_with_config(&["github", "slack", "jira"]);
let mut ids = registry.resolve_server_ids(Some("github, jira".to_string()));
let mut ids =
registry.resolve_server_ids(Some(vec!["github".to_string(), "jira".to_string()]));
ids.sort();
assert_eq!(ids, vec!["github", "jira"]);
}
@@ -770,7 +771,7 @@ mod tests {
#[test]
fn resolve_single_server_name() {
let registry = make_registry_with_config(&["github", "slack"]);
let ids = registry.resolve_server_ids(Some("slack".to_string()));
let ids = registry.resolve_server_ids(Some(vec!["slack".to_string()]));
assert_eq!(ids, vec!["slack"]);
}
@@ -784,28 +785,32 @@ mod tests {
#[test]
fn resolve_no_config_returns_empty() {
let registry = McpRegistry::default();
let ids = registry.resolve_server_ids(Some("all".to_string()));
let ids = registry.resolve_server_ids(Some(vec!["all".to_string()]));
assert!(ids.is_empty());
}
#[test]
fn resolve_nonexistent_server_filtered_out() {
let registry = make_registry_with_config(&["github"]);
let ids = registry.resolve_server_ids(Some("github, nonexistent".to_string()));
let ids = registry
.resolve_server_ids(Some(vec!["github".to_string(), "nonexistent".to_string()]));
assert_eq!(ids, vec!["github"]);
}
#[test]
fn resolve_all_nonexistent_returns_empty() {
let registry = make_registry_with_config(&["github"]);
let ids = registry.resolve_server_ids(Some("foo, bar".to_string()));
let ids = registry.resolve_server_ids(Some(vec!["foo".to_string(), "bar".to_string()]));
assert!(ids.is_empty());
}
#[test]
fn resolve_trims_whitespace() {
let registry = make_registry_with_config(&["github", "slack"]);
let mut ids = registry.resolve_server_ids(Some(" github , slack ".to_string()));
let mut ids = registry.resolve_server_ids(Some(vec![
" github ".to_string(),
" slack ".to_string(),
]));
ids.sort();
assert_eq!(ids, vec!["github", "slack"]);
}