use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use super::{
DEFAULT_MODEL, capabilities::CapabilitiesConfig, mcp::McpServer, models::GeminiConfig,
};
use crate::{
hooks::HookEntry, policies::PolicyRule, tools::ToolDefinition, triggers::TriggerEntry,
};
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SystemInstructionSection {
pub content: String,
#[serde(default = "default_section_title")]
pub title: String,
}
fn default_section_title() -> String {
"user_system_instructions".to_owned()
}
fn default_model_name() -> String {
DEFAULT_MODEL.to_owned()
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemInstructions {
Custom(String),
Templated {
#[serde(default)]
identity: Option<String>,
#[serde(default)]
sections: Vec<SystemInstructionSection>,
},
}
impl SystemInstructions {
#[must_use]
pub fn custom(text: impl Into<String>) -> Self {
Self::Custom(text.into())
}
}
impl From<&str> for SystemInstructions {
fn from(s: &str) -> Self {
Self::custom(s)
}
}
impl From<String> for SystemInstructions {
fn from(s: String) -> Self {
Self::custom(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct JsonSchema(serde_json::Value);
impl JsonSchema {
#[must_use]
pub const fn new(value: serde_json::Value) -> Self {
Self(value)
}
pub fn from_schema<T: schemars::JsonSchema>() -> Result<Self, crate::error::Error> {
let schema_root = schemars::schema_for!(T);
let value =
serde_json::to_value(schema_root).map_err(|e| crate::error::Error::InvalidConfig {
message: format!("Failed to serialize JSON schema: {e}"),
})?;
Ok(Self(value))
}
#[must_use]
pub const fn as_value(&self) -> &serde_json::Value {
&self.0
}
pub fn validate(&self) -> Result<(), &'static str> {
if self.0.is_object() {
Ok(())
} else {
Err("JSON Schema must be a JSON object at the top level")
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(default))]
pub struct AgentConfig {
#[serde(default = "default_model_name")]
#[builder(default = DEFAULT_MODEL.to_owned(), setter(into))]
pub model: String,
#[serde(default)]
#[builder(setter(into, strip_option))]
pub api_key: Option<String>,
#[builder(setter(into, strip_option))]
pub system_instructions: Option<SystemInstructions>,
#[serde(default)]
#[builder(setter(strip_option))]
pub capabilities: Option<CapabilitiesConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<PathBuf>>| v.into_iter().map(Into::into).collect()))]
pub workspaces: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<ToolDefinition>>| v.into_iter().map(Into::into).collect()))]
pub tools: Vec<ToolDefinition>,
#[serde(default = "default_policies")]
#[builder(default = default_policies(), setter(transform = |v: impl IntoIterator<Item = impl Into<PolicyRule>>| v.into_iter().map(Into::into).collect()))]
pub policies: Vec<PolicyRule>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<TriggerEntry>>| v.into_iter().map(Into::into).collect()))]
pub triggers: Vec<TriggerEntry>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<HookEntry>>| v.into_iter().map(Into::into).collect()))]
pub hooks: Vec<HookEntry>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
rename = "skills_paths"
)]
#[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<PathBuf>>| v.into_iter().map(Into::into).collect()))]
pub skills: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default, setter(transform = |v: impl IntoIterator<Item = impl Into<super::subagents::InlineSkill>>| v.into_iter().map(Into::into).collect()))]
pub inline_skills: Vec<super::subagents::InlineSkill>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<McpServer>>| v.into_iter().map(Into::into).collect()))]
pub mcp_servers: Vec<McpServer>,
#[serde(default)]
#[builder(setter(into, strip_option))]
pub conversation_id: Option<String>,
#[serde(default)]
#[builder(setter(into, strip_option))]
pub save_dir: Option<PathBuf>,
#[serde(default)]
#[builder(setter(into, strip_option))]
pub app_data_dir: Option<PathBuf>,
#[serde(default)]
#[builder(setter(strip_option))]
pub response_schema: Option<JsonSchema>,
#[serde(
default,
rename = "gemini_config",
skip_serializing_if = "Option::is_none"
)]
#[builder(setter(strip_option))]
pub gemini: Option<GeminiConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default, setter(transform = |v: impl IntoIterator<Item = impl Into<crate::types::ConversationMessage>>| v.into_iter().map(Into::into).collect()))]
pub initial_history: Vec<crate::types::ConversationMessage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(strip_option))]
pub retry_config: Option<RetryConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(strip_option))]
pub budget_config: Option<super::budget::BudgetConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(strip_option))]
pub compaction_config: Option<super::compaction::CompactionConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default, setter(transform = |v: impl IntoIterator<Item = impl Into<super::subagents::SubagentConfig>>| v.into_iter().map(Into::into).collect()))]
pub subagents: Vec<super::subagents::SubagentConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub session_continuation_mode: Option<SessionContinuationMode>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SessionContinuationMode {
Resume,
#[default]
CreateOrResume,
CreateOnly,
}
impl SessionContinuationMode {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Resume => "resume",
Self::CreateOrResume => "create_or_resume",
Self::CreateOnly => "create_only",
}
}
#[must_use]
pub const fn to_proto_i32(self) -> i32 {
match self {
Self::Resume => 1,
Self::CreateOrResume => 2,
Self::CreateOnly => 3,
}
}
#[must_use]
pub const fn from_proto_i32(val: i32) -> Option<Self> {
match val {
1 => Some(Self::Resume),
2 => Some(Self::CreateOrResume),
3 => Some(Self::CreateOnly),
_ => None,
}
}
}
impl std::fmt::Display for SessionContinuationMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for SessionContinuationMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"resume" => Ok(Self::Resume),
"create_or_resume" => Ok(Self::CreateOrResume),
"create_only" => Ok(Self::CreateOnly),
other => Err(format!("Unrecognized SessionContinuationMode: {other}")),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
pub struct ModelAPIRetryConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub max_retries: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub initial_sleep_duration_ms: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub exponential_multiplier: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub jitter_range: Option<f64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
pub struct ModelOutputRetryConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub max_retries: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
pub struct RetryConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub api_retry: Option<ModelAPIRetryConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(into, strip_option))]
pub model_output_retry: Option<ModelOutputRetryConfig>,
}
impl RetryConfig {
#[must_use]
pub fn no_retries() -> Self {
Self {
api_retry: Some(ModelAPIRetryConfig {
max_retries: Some(0),
initial_sleep_duration_ms: None,
exponential_multiplier: None,
jitter_range: None,
}),
model_output_retry: Some(ModelOutputRetryConfig {
max_retries: Some(0),
}),
}
}
#[must_use]
pub const fn benchmark() -> Self {
Self {
api_retry: Some(ModelAPIRetryConfig {
max_retries: Some(u32::MAX),
initial_sleep_duration_ms: Some(1000),
exponential_multiplier: None,
jitter_range: None,
}),
model_output_retry: None,
}
}
}
impl Default for AgentConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl AgentConfig {
#[must_use]
pub fn lightweight(mut self) -> Self {
let caps = match self.capabilities.take() {
None => super::CapabilitiesConfig::minimal(),
Some(mut user_caps) => {
if user_caps.enabled_tools.is_none() {
if let Some(disabled) = user_caps.disabled_tools.take() {
user_caps.enabled_tools = Some(
super::BuiltinTools::minimal()
.iter()
.copied()
.filter(|t| !disabled.contains(t))
.collect(),
);
} else {
user_caps.enabled_tools = Some(super::BuiltinTools::minimal().to_vec());
}
}
user_caps.agent_behavior = super::AgentBehavior::Minimal;
user_caps.enable_subagents = false;
user_caps
}
};
self.capabilities = Some(caps);
self
}
#[must_use]
pub fn effective_api_key(&self) -> Option<String> {
self.gemini
.as_ref()
.and_then(|g| g.models.default.api_key.clone())
.or_else(|| self.gemini.as_ref().and_then(|g| g.api_key.clone()))
.or_else(|| self.api_key.clone())
.or_else(|| crate::env_var(super::ENV_GEMINI_API_KEY))
}
#[must_use]
pub fn effective_base_url(&self) -> Option<String> {
self.gemini
.as_ref()
.and_then(|g| g.base_url.clone())
.or_else(|| crate::env_var(super::ENV_GEMINI_API_BASE_URL))
}
#[must_use]
pub fn custom_tool_names(&self) -> Vec<String> {
self.tools.iter().map(|t| t.name.clone()).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LocalAgentConfig {
#[serde(flatten)]
pub agent: AgentConfig,
}
impl LocalAgentConfig {
#[must_use]
pub const fn new(agent: AgentConfig) -> Self {
Self { agent }
}
}
impl From<AgentConfig> for LocalAgentConfig {
fn from(agent: AgentConfig) -> Self {
Self::new(agent)
}
}
fn default_policies() -> Vec<PolicyRule> {
vec![
PolicyRule::Deny("run_command".to_string()),
PolicyRule::AllowAll,
]
}
#[cfg(test)]
mod tests;