use std::collections::HashMap;
use crate::types::{MemoryConfiguration, SectionOverride, SystemMessageConfig};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClientMode {
#[default]
CopilotCli,
Empty,
}
pub(crate) fn resolve_custom_agents_local_only(
mode: ClientMode,
custom_agents_local_only: Option<bool>,
) -> Option<bool> {
custom_agents_local_only.or_else(|| (mode == ClientMode::Empty).then_some(true))
}
fn is_valid_tool_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn validate_name(kind: &str, name: &str) -> Result<(), crate::Error> {
if name == "*" {
return Ok(());
}
if !is_valid_tool_name(name) {
return Err(crate::Error::with_message(
crate::ErrorKind::InvalidConfig,
format!(
"Invalid {kind} tool name '{name}': tool names must match \
/^[a-zA-Z0-9_-]+$/ or be the wildcard '*'."
),
));
}
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct ToolSet {
items: Vec<String>,
}
impl ToolSet {
pub fn new() -> Self {
Self::default()
}
pub fn add_builtin(mut self, name: &str) -> Result<Self, crate::Error> {
validate_name("builtin", name)?;
self.items.push(format!("builtin:{name}"));
Ok(self)
}
pub fn add_builtin_many<I, S>(mut self, names: I) -> Result<Self, crate::Error>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for name in names {
let name = name.as_ref();
validate_name("builtin", name)?;
self.items.push(format!("builtin:{name}"));
}
Ok(self)
}
pub fn add_custom(mut self, name: &str) -> Result<Self, crate::Error> {
validate_name("custom", name)?;
self.items.push(format!("custom:{name}"));
Ok(self)
}
pub fn add_mcp(mut self, tool_name: &str) -> Result<Self, crate::Error> {
validate_name("mcp", tool_name)?;
self.items.push(format!("mcp:{tool_name}"));
Ok(self)
}
pub fn to_vec(&self) -> Vec<String> {
self.items.clone()
}
pub fn into_vec(self) -> Vec<String> {
self.items
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl From<ToolSet> for Vec<String> {
fn from(value: ToolSet) -> Self {
value.into_vec()
}
}
pub const BUILTIN_TOOLS_ISOLATED: &[&str] = &[
"ask_user",
"task_complete",
"exit_plan_mode",
"task",
"read_agent",
"write_agent",
"list_agents",
"send_inbox",
"context_board",
"skill",
];
pub(crate) fn validate_tool_filter_list(
field: &str,
list: Option<&[String]>,
) -> Result<(), crate::Error> {
let Some(list) = list else { return Ok(()) };
for item in list {
if item == "*" {
return Err(crate::Error::with_message(
crate::ErrorKind::InvalidConfig,
format!(
"{field} contains a bare '*' which matches no tool. Use \
source-qualified wildcards instead: \
ToolSet::new().add_builtin(\"*\").add_mcp(\"*\").add_custom(\"*\")."
),
));
}
}
Ok(())
}
pub(crate) fn system_message_for_mode(
mode: ClientMode,
supplied: Option<SystemMessageConfig>,
) -> Option<SystemMessageConfig> {
if mode != ClientMode::Empty {
return supplied;
}
let strip_env = || {
let mut sections = HashMap::new();
sections.insert(
"environment_context".to_string(),
SectionOverride {
action: Some("remove".to_string()),
content: None,
},
);
sections
};
let Some(supplied) = supplied else {
return Some(SystemMessageConfig {
mode: Some("customize".to_string()),
content: None,
sections: Some(strip_env()),
});
};
let mode_str = supplied.mode.as_deref().unwrap_or("append");
match mode_str {
"replace" => Some(supplied),
"customize" => {
if supplied
.sections
.as_ref()
.is_some_and(|s| s.contains_key("environment_context"))
{
Some(supplied)
} else {
let mut sections = supplied.sections.unwrap_or_default();
sections.insert(
"environment_context".to_string(),
SectionOverride {
action: Some("remove".to_string()),
content: None,
},
);
Some(SystemMessageConfig {
mode: Some("customize".to_string()),
content: supplied.content,
sections: Some(sections),
})
}
}
_ => Some(SystemMessageConfig {
mode: Some("customize".to_string()),
content: supplied.content,
sections: Some(strip_env()),
}),
}
}
pub(crate) fn memory_for_mode(
mode: ClientMode,
supplied: Option<MemoryConfiguration>,
) -> Option<MemoryConfiguration> {
match supplied {
Some(config) => Some(config),
None if mode == ClientMode::Empty => Some(MemoryConfiguration::disabled()),
None => None,
}
}
pub(crate) fn experimental_mode_for_mode(mode: ClientMode, supplied: Option<bool>) -> Option<bool> {
if mode == ClientMode::Empty {
Some(supplied.unwrap_or(false))
} else {
supplied
}
}
#[cfg(test)]
mod tests;