use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolRef {
pub extension_id: String,
pub tool_name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LlmProviderRef {
pub provider: String, pub model: String, #[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_ref: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MemoryProviderRef {
pub provider: String,
pub capability: String,
#[serde(default)]
pub params: serde_json::Map<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_ref: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct MemorySettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub short_term: Option<MemoryProviderRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub long_term: Option<MemoryProviderRef>,
}
pub(crate) const fn default_knowledge_top_k() -> usize {
5
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct KnowledgeSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub knowledge: Option<MemoryProviderRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<MemoryProviderRef>,
#[serde(default = "default_knowledge_top_k")]
pub top_k: usize,
}
impl Default for KnowledgeSettings {
fn default() -> Self {
Self {
knowledge: None,
embedding: None,
top_k: default_knowledge_top_k(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GuardrailRef {
pub cap_id: String,
#[serde(default)]
pub offer_id: Option<String>,
#[serde(default)]
pub config: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AgentConfig {
pub agent_id: String,
pub system_prompt: String,
pub tools: Vec<ToolRef>,
#[serde(default)]
pub guardrails: Vec<GuardrailRef>,
pub llm: LlmProviderRef,
#[serde(default)]
pub limits: AgentLimits,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<MemorySettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub knowledge: Option<KnowledgeSettings>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentLimits {
pub max_iter: u32,
#[serde(with = "duration_secs")]
pub timeout: Duration,
pub max_history_turns: u32,
pub llm_retry_attempts: u32,
#[serde(with = "duration_ms")]
pub llm_retry_backoff: Duration,
pub provider_failure_message: Option<String>,
pub daily_token_cap_per_tenant: Option<u32>,
}
impl Default for AgentLimits {
fn default() -> Self {
Self {
max_iter: 8,
timeout: Duration::from_secs(60),
max_history_turns: 20,
llm_retry_attempts: 3,
llm_retry_backoff: Duration::from_millis(250),
provider_failure_message: None,
daily_token_cap_per_tenant: None,
}
}
}
mod duration_secs {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_u64(d.as_secs())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
Ok(Duration::from_secs(u64::deserialize(d)?))
}
}
mod duration_ms {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
#[allow(clippy::cast_possible_truncation)]
let ms = d.as_millis() as u64;
s.serialize_u64(ms)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
Ok(Duration::from_millis(u64::deserialize(d)?))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod limits_serde_tests {
use super::*;
#[test]
fn agent_limits_fills_all_missing_fields_from_default() {
let limits: AgentLimits = serde_json::from_str("{}").unwrap();
assert_eq!(limits.max_iter, 8);
assert_eq!(limits.timeout, std::time::Duration::from_secs(60));
assert_eq!(limits.max_history_turns, 20);
assert_eq!(limits.llm_retry_attempts, 3);
assert_eq!(
limits.llm_retry_backoff,
std::time::Duration::from_millis(250)
);
assert_eq!(limits.provider_failure_message, None);
assert_eq!(limits.daily_token_cap_per_tenant, None);
}
#[test]
fn agent_limits_partial_keeps_given_overrides_defaults_rest() {
let limits: AgentLimits =
serde_json::from_str(r#"{ "max_iter": 99, "timeout": 5 }"#).unwrap();
assert_eq!(limits.max_iter, 99);
assert_eq!(limits.timeout, std::time::Duration::from_secs(5));
assert_eq!(limits.max_history_turns, 20);
assert_eq!(limits.llm_retry_attempts, 3);
}
#[test]
fn agent_config_allows_omitted_limits_block() {
let json = r#"{
"agent_id": "greeter",
"system_prompt": "hi",
"tools": [],
"llm": { "provider": "openai", "model": "gpt-4o-mini" }
}"#;
let cfg: AgentConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.limits.max_iter, 8);
assert_eq!(cfg.limits.timeout, std::time::Duration::from_secs(60));
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn llm_provider_ref_defaults_credential_ref_to_none() {
let r: LlmProviderRef =
serde_json::from_str(r#"{ "provider":"anthropic","model":"claude-3" }"#).unwrap();
assert_eq!(r.provider, "anthropic");
assert_eq!(r.credential_ref, None);
}
#[test]
fn defaults_match_spec_5_2() {
let l = AgentLimits::default();
assert_eq!(l.max_iter, 8);
assert_eq!(l.timeout, Duration::from_secs(60));
assert_eq!(l.max_history_turns, 20);
assert_eq!(l.llm_retry_attempts, 3);
assert_eq!(l.llm_retry_backoff, Duration::from_millis(250));
assert!(l.provider_failure_message.is_none());
assert!(l.daily_token_cap_per_tenant.is_none());
}
#[test]
fn agent_config_roundtrips_through_json() {
let original = AgentConfig {
agent_id: "a-1".into(),
system_prompt: "be helpful".into(),
tools: vec![ToolRef {
extension_id: "http".into(),
tool_name: "fetch".into(),
}],
guardrails: vec![],
llm: LlmProviderRef {
provider: "openai".into(),
model: "gpt-4o-mini".into(),
credential_ref: None,
},
limits: AgentLimits::default(),
memory: None,
knowledge: None,
};
let json = serde_json::to_string(&original).unwrap();
let round: AgentConfig = serde_json::from_str(&json).unwrap();
assert_eq!(round.agent_id, original.agent_id);
assert_eq!(round.limits.max_iter, 8);
assert_eq!(round.limits.timeout, Duration::from_secs(60));
assert_eq!(round.limits.llm_retry_backoff, Duration::from_millis(250));
}
#[test]
fn agent_config_defaults_guardrails_to_empty() {
let json = r#"{
"agent_id": "a1",
"system_prompt": "hi",
"tools": [],
"llm": { "provider": "openai", "model": "gpt-4o-mini" }
}"#;
let cfg: AgentConfig = serde_json::from_str(json).unwrap();
assert!(cfg.guardrails.is_empty());
}
#[test]
fn guardrail_ref_round_trips() {
let r = GuardrailRef {
cap_id: "greentic.cap.guardrail.v1".to_string(),
offer_id: Some("pii".to_string()),
config: serde_json::json!({ "mask": ["email"] }),
};
let s = serde_json::to_string(&r).unwrap();
let back: GuardrailRef = serde_json::from_str(&s).unwrap();
assert_eq!(back, r);
}
#[test]
fn agent_config_memory_defaults_to_none_when_omitted() {
let json = r#"{
"agent_id": "greeter",
"system_prompt": "hi",
"tools": [],
"llm": { "provider": "openai", "model": "gpt-4o-mini" }
}"#;
let cfg: AgentConfig = serde_json::from_str(json).unwrap();
assert!(cfg.memory.is_none());
}
#[test]
fn agent_config_memory_roundtrips_both_tiers() {
let json = r#"{
"agent_id": "greeter",
"system_prompt": "hi",
"tools": [],
"llm": { "provider": "openai", "model": "gpt-4o-mini" },
"memory": {
"short_term": { "provider": "redis", "capability": "cap://memory/short-term" },
"long_term": {
"provider": "chronicle",
"capability": "cap://memory/long-term",
"params": { "backend": "surrealdb" },
"credential_ref": "vault://acme/surreal"
}
}
}"#;
let cfg: AgentConfig = serde_json::from_str(json).unwrap();
let mem = cfg.memory.clone().expect("memory present");
let long = mem.long_term.expect("long_term present");
assert_eq!(long.provider, "chronicle");
assert_eq!(long.capability, "cap://memory/long-term");
assert_eq!(long.credential_ref.as_deref(), Some("vault://acme/surreal"));
assert_eq!(
mem.short_term.expect("short_term present").provider,
"redis"
);
let reser = serde_json::to_string(&cfg).unwrap();
let back: AgentConfig = serde_json::from_str(&reser).unwrap();
assert_eq!(back.memory, cfg.memory);
}
}