use std::path::PathBuf;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::subagent::{HookDef, MemoryScope, PermissionMode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelSpec {
Inherit,
Named(String),
}
impl ModelSpec {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
ModelSpec::Inherit => "inherit",
ModelSpec::Named(s) => s.as_str(),
}
}
}
impl Serialize for ModelSpec {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
ModelSpec::Inherit => serializer.serialize_str("inherit"),
ModelSpec::Named(s) => serializer.serialize_str(s),
}
}
}
impl<'de> Deserialize<'de> for ModelSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s == "inherit" {
Ok(ModelSpec::Inherit)
} else {
Ok(ModelSpec::Named(s))
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ContextInjectionMode {
None,
#[default]
LastAssistantTurn,
Summary,
}
fn default_max_tool_iterations() -> usize {
10
}
fn default_auto_update_check() -> bool {
true
}
fn default_focus_compression_interval() -> usize {
12
}
fn default_focus_reminder_interval() -> usize {
15
}
fn default_focus_min_messages_per_focus() -> usize {
8
}
fn default_focus_max_knowledge_tokens() -> usize {
4096
}
fn default_max_tool_retries() -> usize {
2
}
fn default_max_retry_duration_secs() -> u64 {
30
}
fn default_tool_repeat_threshold() -> usize {
2
}
fn default_tool_filter_top_k() -> usize {
6
}
fn default_tool_filter_min_description_words() -> usize {
5
}
fn default_tool_filter_always_on() -> Vec<String> {
vec![
"memory_search".into(),
"memory_save".into(),
"load_skill".into(),
"bash".into(),
"read".into(),
"edit".into(),
]
}
fn default_instruction_auto_detect() -> bool {
true
}
fn default_max_concurrent() -> usize {
5
}
fn default_context_window_turns() -> usize {
10
}
fn default_max_spawn_depth() -> u32 {
3
}
fn default_transcript_enabled() -> bool {
true
}
fn default_transcript_max_files() -> usize {
50
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct FocusConfig {
pub enabled: bool,
#[serde(default = "default_focus_compression_interval")]
pub compression_interval: usize,
#[serde(default = "default_focus_reminder_interval")]
pub reminder_interval: usize,
#[serde(default = "default_focus_min_messages_per_focus")]
pub min_messages_per_focus: usize,
#[serde(default = "default_focus_max_knowledge_tokens")]
pub max_knowledge_tokens: usize,
}
impl Default for FocusConfig {
fn default() -> Self {
Self {
enabled: false,
compression_interval: default_focus_compression_interval(),
reminder_interval: default_focus_reminder_interval(),
min_messages_per_focus: default_focus_min_messages_per_focus(),
max_knowledge_tokens: default_focus_max_knowledge_tokens(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ToolFilterConfig {
pub enabled: bool,
#[serde(default = "default_tool_filter_top_k")]
pub top_k: usize,
#[serde(default = "default_tool_filter_always_on")]
pub always_on: Vec<String>,
#[serde(default = "default_tool_filter_min_description_words")]
pub min_description_words: usize,
}
impl Default for ToolFilterConfig {
fn default() -> Self {
Self {
enabled: false,
top_k: default_tool_filter_top_k(),
always_on: default_tool_filter_always_on(),
min_description_words: default_tool_filter_min_description_words(),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AgentConfig {
pub name: String,
#[serde(default = "default_max_tool_iterations")]
pub max_tool_iterations: usize,
#[serde(default = "default_auto_update_check")]
pub auto_update_check: bool,
#[serde(default)]
pub instruction_files: Vec<std::path::PathBuf>,
#[serde(default = "default_instruction_auto_detect")]
pub instruction_auto_detect: bool,
#[serde(default = "default_max_tool_retries")]
pub max_tool_retries: usize,
#[serde(default = "default_tool_repeat_threshold")]
pub tool_repeat_threshold: usize,
#[serde(default = "default_max_retry_duration_secs")]
pub max_retry_duration_secs: u64,
#[serde(default)]
pub focus: FocusConfig,
#[serde(default)]
pub tool_filter: ToolFilterConfig,
#[serde(default = "default_budget_hint_enabled")]
pub budget_hint_enabled: bool,
#[serde(default)]
pub supervisor: TaskSupervisorConfig,
}
fn default_budget_hint_enabled() -> bool {
true
}
fn default_enrichment_limit() -> usize {
4
}
fn default_telemetry_limit() -> usize {
8
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct TaskSupervisorConfig {
#[serde(default = "default_enrichment_limit")]
pub enrichment_limit: usize,
#[serde(default = "default_telemetry_limit")]
pub telemetry_limit: usize,
#[serde(default)]
pub abort_enrichment_on_turn: bool,
}
impl Default for TaskSupervisorConfig {
fn default() -> Self {
Self {
enrichment_limit: default_enrichment_limit(),
telemetry_limit: default_telemetry_limit(),
abort_enrichment_on_turn: false,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct SubAgentConfig {
pub enabled: bool,
#[serde(default = "default_max_concurrent")]
pub max_concurrent: usize,
pub extra_dirs: Vec<PathBuf>,
#[serde(default)]
pub user_agents_dir: Option<PathBuf>,
pub default_permission_mode: Option<PermissionMode>,
#[serde(default)]
pub default_disallowed_tools: Vec<String>,
#[serde(default)]
pub allow_bypass_permissions: bool,
#[serde(default)]
pub default_memory_scope: Option<MemoryScope>,
#[serde(default)]
pub hooks: SubAgentLifecycleHooks,
#[serde(default)]
pub transcript_dir: Option<PathBuf>,
#[serde(default = "default_transcript_enabled")]
pub transcript_enabled: bool,
#[serde(default = "default_transcript_max_files")]
pub transcript_max_files: usize,
#[serde(default = "default_context_window_turns")]
pub context_window_turns: usize,
#[serde(default = "default_max_spawn_depth")]
pub max_spawn_depth: u32,
#[serde(default)]
pub context_injection_mode: ContextInjectionMode,
}
impl Default for SubAgentConfig {
fn default() -> Self {
Self {
enabled: false,
max_concurrent: default_max_concurrent(),
extra_dirs: Vec::new(),
user_agents_dir: None,
default_permission_mode: None,
default_disallowed_tools: Vec::new(),
allow_bypass_permissions: false,
default_memory_scope: None,
hooks: SubAgentLifecycleHooks::default(),
transcript_dir: None,
transcript_enabled: default_transcript_enabled(),
transcript_max_files: default_transcript_max_files(),
context_window_turns: default_context_window_turns(),
max_spawn_depth: default_max_spawn_depth(),
context_injection_mode: ContextInjectionMode::default(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct SubAgentLifecycleHooks {
pub start: Vec<HookDef>,
pub stop: Vec<HookDef>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subagent_config_defaults() {
let cfg = SubAgentConfig::default();
assert_eq!(cfg.context_window_turns, 10);
assert_eq!(cfg.max_spawn_depth, 3);
assert_eq!(
cfg.context_injection_mode,
ContextInjectionMode::LastAssistantTurn
);
}
#[test]
fn subagent_config_deserialize_new_fields() {
let toml_str = r#"
enabled = true
context_window_turns = 5
max_spawn_depth = 2
context_injection_mode = "none"
"#;
let cfg: SubAgentConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.context_window_turns, 5);
assert_eq!(cfg.max_spawn_depth, 2);
assert_eq!(cfg.context_injection_mode, ContextInjectionMode::None);
}
#[test]
fn model_spec_deserialize_inherit() {
let spec: ModelSpec = serde_json::from_str("\"inherit\"").unwrap();
assert_eq!(spec, ModelSpec::Inherit);
}
#[test]
fn model_spec_deserialize_named() {
let spec: ModelSpec = serde_json::from_str("\"fast\"").unwrap();
assert_eq!(spec, ModelSpec::Named("fast".to_owned()));
}
#[test]
fn model_spec_as_str() {
assert_eq!(ModelSpec::Inherit.as_str(), "inherit");
assert_eq!(ModelSpec::Named("x".to_owned()).as_str(), "x");
}
}