149 lines
4.9 KiB
Rust
149 lines
4.9 KiB
Rust
//! Agent templates (global IDE store) and their monotonic versioning.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::error::DomainError;
|
|
use crate::ids::{ProfileId, TemplateId};
|
|
use crate::markdown::MarkdownDoc;
|
|
use crate::mcp_tool_permissions::McpToolPolicy;
|
|
use crate::system_permissions::SystemPermissionSet;
|
|
|
|
/// Monotonically increasing template version.
|
|
///
|
|
/// Invariant (enforced via [`TemplateVersion::next`]): a version only ever
|
|
/// increases by one when the template content changes (ARCHITECTURE §8.1).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct TemplateVersion(pub u64);
|
|
|
|
impl TemplateVersion {
|
|
/// The initial version assigned to a freshly created template.
|
|
pub const INITIAL: Self = Self(1);
|
|
|
|
/// Returns the next version (current + 1).
|
|
#[must_use]
|
|
pub const fn next(self) -> Self {
|
|
Self(self.0 + 1)
|
|
}
|
|
|
|
/// Returns the raw version number.
|
|
#[must_use]
|
|
pub const fn get(self) -> u64 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Default for TemplateVersion {
|
|
fn default() -> Self {
|
|
Self::INITIAL
|
|
}
|
|
}
|
|
|
|
/// A reusable agent template stored in the global IDE store.
|
|
///
|
|
/// Invariants:
|
|
/// - `name` non-empty,
|
|
/// - `version` is monotonic; bumping is done via [`AgentTemplate::with_updated_content`].
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentTemplate {
|
|
/// Stable identifier.
|
|
pub id: TemplateId,
|
|
/// Display name.
|
|
pub name: String,
|
|
/// Markdown content.
|
|
pub content_md: MarkdownDoc,
|
|
/// Current version.
|
|
pub version: TemplateVersion,
|
|
/// Default runtime profile for agents created from this template.
|
|
pub default_profile_id: ProfileId,
|
|
/// Default agent name for agents created from this template. `None` falls
|
|
/// back to the template display name.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub default_agent_name: Option<String>,
|
|
/// Agent-level system permission override to apply at creation time.
|
|
/// `None` preserves project inheritance.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub default_system_permissions: Option<SystemPermissionSet>,
|
|
/// Agent-level MCP tool policy override to apply at creation time.
|
|
/// `None` preserves project inheritance.
|
|
#[serde(
|
|
default,
|
|
rename = "defaultMcpToolPermissions",
|
|
alias = "defaultMcpToolPolicy",
|
|
skip_serializing_if = "Option::is_none"
|
|
)]
|
|
pub default_mcp_tool_policy: Option<McpToolPolicy>,
|
|
/// Whether agents created from this template are cross-project contactable.
|
|
#[serde(default)]
|
|
pub default_contactable: bool,
|
|
}
|
|
|
|
impl AgentTemplate {
|
|
/// Builds a validated template at [`TemplateVersion::INITIAL`].
|
|
///
|
|
/// # Errors
|
|
/// Returns [`DomainError::EmptyField`] if `name` is empty.
|
|
pub fn new(
|
|
id: TemplateId,
|
|
name: impl Into<String>,
|
|
content_md: MarkdownDoc,
|
|
default_profile_id: ProfileId,
|
|
) -> Result<Self, DomainError> {
|
|
let name = name.into();
|
|
crate::validation::non_empty(&name, "template.name")?;
|
|
Ok(Self {
|
|
id,
|
|
name,
|
|
content_md,
|
|
version: TemplateVersion::INITIAL,
|
|
default_profile_id,
|
|
default_agent_name: None,
|
|
default_system_permissions: None,
|
|
default_mcp_tool_policy: None,
|
|
default_contactable: false,
|
|
})
|
|
}
|
|
|
|
/// Returns a copy of this template with new content and the version bumped
|
|
/// by one, preserving the monotonic-version invariant.
|
|
#[must_use]
|
|
pub fn with_updated_content(&self, content_md: MarkdownDoc) -> Self {
|
|
Self {
|
|
content_md,
|
|
version: self.version.next(),
|
|
..self.clone()
|
|
}
|
|
}
|
|
|
|
/// Returns a copy with template metadata changed. The template version is
|
|
/// left unchanged because these defaults affect future agent creation only.
|
|
///
|
|
/// # Errors
|
|
/// Returns [`DomainError::EmptyField`] if `name` or a provided default agent
|
|
/// name is empty.
|
|
pub fn with_metadata(
|
|
&self,
|
|
name: String,
|
|
default_profile_id: ProfileId,
|
|
default_agent_name: Option<String>,
|
|
default_system_permissions: Option<SystemPermissionSet>,
|
|
default_mcp_tool_policy: Option<McpToolPolicy>,
|
|
default_contactable: bool,
|
|
) -> Result<Self, DomainError> {
|
|
crate::validation::non_empty(&name, "template.name")?;
|
|
if let Some(default_agent_name) = &default_agent_name {
|
|
crate::validation::non_empty(default_agent_name, "template.defaultAgentName")?;
|
|
}
|
|
Ok(Self {
|
|
name,
|
|
default_profile_id,
|
|
default_agent_name,
|
|
default_system_permissions,
|
|
default_mcp_tool_policy,
|
|
default_contactable,
|
|
..self.clone()
|
|
})
|
|
}
|
|
}
|