feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
257
crates/domain/src/agent.rs
Normal file
257
crates/domain/src/agent.rs
Normal file
@ -0,0 +1,257 @@
|
||||
//! Agent entity, its origin and the project agent manifest.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DomainError;
|
||||
use crate::ids::{AgentId, ProfileId, TemplateId};
|
||||
use crate::template::TemplateVersion;
|
||||
|
||||
/// Origin of an agent: created from scratch, or derived from a template.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum AgentOrigin {
|
||||
/// Created from scratch; no template link.
|
||||
Scratch,
|
||||
/// Derived from a template, tracking the last synced version.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
FromTemplate {
|
||||
/// Source template.
|
||||
template_id: TemplateId,
|
||||
/// Template version recorded at the last successful sync.
|
||||
synced_template_version: TemplateVersion,
|
||||
},
|
||||
}
|
||||
|
||||
impl AgentOrigin {
|
||||
/// Returns the source template id, if any.
|
||||
#[must_use]
|
||||
pub fn template_id(&self) -> Option<TemplateId> {
|
||||
match self {
|
||||
Self::Scratch => None,
|
||||
Self::FromTemplate { template_id, .. } => Some(*template_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this origin is a template.
|
||||
#[must_use]
|
||||
pub fn is_from_template(&self) -> bool {
|
||||
matches!(self, Self::FromTemplate { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// A project-scoped agent.
|
||||
///
|
||||
/// Invariants enforced here:
|
||||
/// - `name` non-empty,
|
||||
/// - `context_path` is a relative, safe path (the `.md` lives under `.ideai/`),
|
||||
/// - `synchronized == true` ⇒ `origin == FromTemplate { .. }`.
|
||||
///
|
||||
/// Note: "`context` must exist at activation" and "`profile_id` must reference a
|
||||
/// known profile" are *runtime/cross-aggregate* invariants checked by the
|
||||
/// application layer (they require I/O or the profile registry), not by this
|
||||
/// pure constructor.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Agent {
|
||||
/// Stable identifier.
|
||||
pub id: AgentId,
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Relative path of the agent's `.md` within `.ideai/` (e.g. `agents/foo.md`).
|
||||
pub context_path: String,
|
||||
/// Runtime profile reference.
|
||||
pub profile_id: ProfileId,
|
||||
/// Origin of the agent.
|
||||
pub origin: AgentOrigin,
|
||||
/// Whether the agent tracks its template (only valid for template origins).
|
||||
pub synchronized: bool,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
/// Builds a validated agent.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`DomainError::EmptyField`] if `name` is empty,
|
||||
/// - [`DomainError::PathNotRelativeSafe`] if `context_path` is absolute or
|
||||
/// contains `..`,
|
||||
/// - [`DomainError::SyncRequiresTemplate`] if `synchronized` is `true` while
|
||||
/// `origin` is [`AgentOrigin::Scratch`].
|
||||
pub fn new(
|
||||
id: AgentId,
|
||||
name: impl Into<String>,
|
||||
context_path: impl Into<String>,
|
||||
profile_id: ProfileId,
|
||||
origin: AgentOrigin,
|
||||
synchronized: bool,
|
||||
) -> Result<Self, DomainError> {
|
||||
let name = name.into();
|
||||
let context_path = context_path.into();
|
||||
crate::validation::non_empty(&name, "agent.name")?;
|
||||
crate::validation::relative_safe(&context_path)?;
|
||||
if synchronized && !origin.is_from_template() {
|
||||
return Err(DomainError::SyncRequiresTemplate);
|
||||
}
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
context_path,
|
||||
profile_id,
|
||||
origin,
|
||||
synchronized,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry in the project agent manifest (`.ideai/agents.json`).
|
||||
///
|
||||
/// This is the **persisted form of an [`Agent`]** (ARCHITECTURE §9.1): the
|
||||
/// manifest is the source of truth for a project's agents, so each entry carries
|
||||
/// everything needed to reconstruct the agent — its `name` and `profile_id`
|
||||
/// included (without them the IDE could not list agents or resolve the profile to
|
||||
/// launch). The template link is kept flat (`template_id` +
|
||||
/// `synced_template_version`) for a compact on-disk shape; [`to_agent`] folds it
|
||||
/// back into an [`AgentOrigin`].
|
||||
///
|
||||
/// Invariants:
|
||||
/// - `name` non-empty,
|
||||
/// - `md_path` relative and safe,
|
||||
/// - `synchronized == true` ⇒ `template_id.is_some() && synced_template_version.is_some()`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ManifestEntry {
|
||||
/// The agent this entry describes.
|
||||
pub agent_id: AgentId,
|
||||
/// Display name of the agent.
|
||||
pub name: String,
|
||||
/// Relative path of the agent's `.md`.
|
||||
pub md_path: String,
|
||||
/// Runtime profile reference.
|
||||
pub profile_id: ProfileId,
|
||||
/// Source template, if any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub template_id: Option<TemplateId>,
|
||||
/// Whether the agent tracks its template.
|
||||
pub synchronized: bool,
|
||||
/// Template version recorded at the last sync.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub synced_template_version: Option<TemplateVersion>,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
/// Builds a validated manifest entry.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`DomainError::EmptyField`] if `name` is empty,
|
||||
/// - [`DomainError::PathNotRelativeSafe`] if `md_path` is absolute or has `..`,
|
||||
/// - [`DomainError::InconsistentManifest`] if `synchronized` is `true` while
|
||||
/// `template_id` or `synced_template_version` is missing.
|
||||
pub fn new(
|
||||
agent_id: AgentId,
|
||||
name: impl Into<String>,
|
||||
md_path: impl Into<String>,
|
||||
profile_id: ProfileId,
|
||||
template_id: Option<TemplateId>,
|
||||
synchronized: bool,
|
||||
synced_template_version: Option<TemplateVersion>,
|
||||
) -> Result<Self, DomainError> {
|
||||
let name = name.into();
|
||||
let md_path = md_path.into();
|
||||
crate::validation::non_empty(&name, "manifestEntry.name")?;
|
||||
crate::validation::relative_safe(&md_path)?;
|
||||
if synchronized && (template_id.is_none() || synced_template_version.is_none()) {
|
||||
return Err(DomainError::InconsistentManifest {
|
||||
reason: "synchronized entry requires templateId and syncedTemplateVersion"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
agent_id,
|
||||
name,
|
||||
md_path,
|
||||
profile_id,
|
||||
template_id,
|
||||
synchronized,
|
||||
synced_template_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Projects an [`Agent`] into its manifest entry (flattening the origin).
|
||||
///
|
||||
/// Infallible: an [`Agent`] is already validated, and every entry invariant
|
||||
/// is implied by the agent's invariants.
|
||||
#[must_use]
|
||||
pub fn from_agent(agent: &Agent) -> Self {
|
||||
let (template_id, synced_template_version) = match &agent.origin {
|
||||
AgentOrigin::Scratch => (None, None),
|
||||
AgentOrigin::FromTemplate {
|
||||
template_id,
|
||||
synced_template_version,
|
||||
} => (Some(*template_id), Some(*synced_template_version)),
|
||||
};
|
||||
Self {
|
||||
agent_id: agent.id,
|
||||
name: agent.name.clone(),
|
||||
md_path: agent.context_path.clone(),
|
||||
profile_id: agent.profile_id,
|
||||
template_id,
|
||||
synchronized: agent.synchronized,
|
||||
synced_template_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs the validated [`Agent`] this entry persists, folding the flat
|
||||
/// template link back into an [`AgentOrigin`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns a [`DomainError`] if the persisted fields violate an [`Agent`]
|
||||
/// invariant (e.g. a synchronized entry whose origin is not a template).
|
||||
pub fn to_agent(&self) -> Result<Agent, DomainError> {
|
||||
let origin = match (self.template_id, self.synced_template_version) {
|
||||
(Some(template_id), Some(synced_template_version)) => AgentOrigin::FromTemplate {
|
||||
template_id,
|
||||
synced_template_version,
|
||||
},
|
||||
_ => AgentOrigin::Scratch,
|
||||
};
|
||||
Agent::new(
|
||||
self.agent_id,
|
||||
self.name.clone(),
|
||||
self.md_path.clone(),
|
||||
self.profile_id,
|
||||
origin,
|
||||
self.synchronized,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory image of `.ideai/agents.json`.
|
||||
///
|
||||
/// Invariant enforced here: `md_path` values are unique across entries.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentManifest {
|
||||
/// Schema version of the manifest file.
|
||||
pub version: u32,
|
||||
/// Entries (one per project agent).
|
||||
#[serde(rename = "agents")]
|
||||
pub entries: Vec<ManifestEntry>,
|
||||
}
|
||||
|
||||
impl AgentManifest {
|
||||
/// Builds a validated manifest.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`DomainError::InconsistentManifest`] if two entries share the
|
||||
/// same `md_path`.
|
||||
pub fn new(version: u32, entries: Vec<ManifestEntry>) -> Result<Self, DomainError> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for entry in &entries {
|
||||
if !seen.insert(entry.md_path.as_str()) {
|
||||
return Err(DomainError::InconsistentManifest {
|
||||
reason: format!("duplicate md_path `{}`", entry.md_path),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Self { version, entries })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user