#[cfg(feature = "ai")]
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct AgentPack {
pub schema_version: String,
pub crate_name: String,
pub crate_version: String,
pub ai_context: super::ai_context::AIContext,
pub guidelines: GuidelinesSnapshot,
pub docs: DocBundle,
}
#[cfg(feature = "ai")]
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct GuidelinesSnapshot {
pub unsafe_forbidden: bool,
pub use_statements_forbidden: bool,
pub function_length_max: u32,
pub testing_mandate: bool,
pub error_guidelines: Vec<String>,
pub revision_history_required: bool,
}
#[cfg(feature = "ai")]
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct DocBundle {
pub entries: Vec<DocEntry>,
}
#[cfg(feature = "ai")]
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct DocEntry {
pub path: String,
pub title: String,
pub content: String,
pub bytes: usize,
}
#[cfg(feature = "ai")]
impl AgentPack {
pub fn from_graph_with_defaults(
graph: &crate::graph::hex_graph::HexGraph,
) -> crate::result::hex_result::HexResult<Self> {
let context = super::context_builder::ContextBuilder::new(graph).build()?;
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let default_paths = vec![
manifest_dir.join("README.md"),
manifest_dir
.join("src")
.join("error")
.join("ERROR_GUIDE.md"),
manifest_dir.join("..").join(".junie").join("guidelines.md"),
manifest_dir
.join("..")
.join(".aiassistant")
.join("rules")
.join("SYS_PROMPT.md"),
];
let docs = Self::load_docs(default_paths);
let guidelines = Self::default_guidelines();
let pack = AgentPack {
schema_version: String::from("1.0.0"),
crate_name: String::from(env!("CARGO_PKG_NAME")),
crate_version: String::from(env!("CARGO_PKG_VERSION")),
ai_context: context,
guidelines,
docs,
};
Result::Ok(pack)
}
pub fn to_json(&self) -> crate::result::hex_result::HexResult<String> {
serde_json::to_string(self).map_err(|e| {
crate::error::hex_error::Hexserror::adapter(
crate::error::codes::adapter::MAPPING_FAILURE,
&format!("Failed to serialize AgentPack to JSON: {e}"),
)
})
}
pub fn builder() -> AgentPackBuilder {
AgentPackBuilder::new()
}
fn default_guidelines() -> GuidelinesSnapshot {
GuidelinesSnapshot {
unsafe_forbidden: true,
use_statements_forbidden: true,
function_length_max: 50,
testing_mandate: true,
error_guidelines: vec![
String::from("C-QUESTION-MARK"),
String::from("C-GOOD-ERR"),
String::from("C-CTOR"),
String::from("C-STRUCT-PRIVATE"),
],
revision_history_required: true,
}
}
fn load_docs(paths: Vec<std::path::PathBuf>) -> DocBundle {
let mut entries: Vec<DocEntry> = Vec::new();
for p in paths {
if p.exists() {
let content_result = std::fs::read_to_string(&p);
match content_result {
std::result::Result::Ok(content) => {
let title = Self::derive_title(&content, &p);
let entry = DocEntry {
path: p.to_string_lossy().to_string(),
title,
bytes: content.len(),
content,
};
entries.push(entry);
}
std::result::Result::Err(_e) => {
}
}
}
}
DocBundle { entries }
}
fn derive_title(content: &str, path: &std::path::Path) -> String {
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
return String::from(trimmed);
}
}
match path.file_name() {
Some(os) => os.to_string_lossy().to_string(),
None => String::from("document"),
}
}
}
#[cfg(feature = "ai")]
pub struct AgentPackBuilder {
docs: std::option::Option<DocBundle>,
guidelines: std::option::Option<GuidelinesSnapshot>,
}
#[cfg(feature = "ai")]
impl AgentPackBuilder {
fn new() -> Self {
AgentPackBuilder {
docs: std::option::Option::None,
guidelines: std::option::Option::None,
}
}
pub fn with_docs(mut self, docs: DocBundle) -> Self {
self.docs = std::option::Option::Some(docs);
self
}
pub fn with_guidelines(mut self, guidelines: GuidelinesSnapshot) -> Self {
self.guidelines = std::option::Option::Some(guidelines);
self
}
pub fn build(
self,
graph: &crate::graph::hex_graph::HexGraph,
) -> crate::result::hex_result::HexResult<AgentPack> {
let mut pack = AgentPack::from_graph_with_defaults(graph)?;
if let std::option::Option::Some(docs) = self.docs {
pack.docs = docs;
}
if let std::option::Option::Some(guidelines) = self.guidelines {
pack.guidelines = guidelines;
}
Result::Ok(pack)
}
}
#[cfg(all(test, feature = "ai"))]
mod tests_agent_pack {
#[test]
fn test_build_and_serialize_agent_pack() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let pack = super::AgentPack::from_graph_with_defaults(&graph).unwrap();
let json = pack.to_json().unwrap();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"crate_name\""));
assert!(json.contains("\"ai_context\""));
}
#[test]
fn test_builder_with_guidelines_overrides_hexser_defaults() {
let graph = crate::graph::builder::GraphBuilder::new().build();
let custom_guidelines = super::GuidelinesSnapshot {
unsafe_forbidden: false,
use_statements_forbidden: false,
function_length_max: 999,
testing_mandate: false,
error_guidelines: std::vec![std::string::String::from("CUSTOM-RULE")],
revision_history_required: false,
};
let pack = super::AgentPack::builder()
.with_guidelines(custom_guidelines)
.build(&graph)
.unwrap();
std::assert_eq!(pack.guidelines.function_length_max, 999);
std::assert_eq!(
pack.guidelines.error_guidelines,
std::vec![std::string::String::from("CUSTOM-RULE")]
);
std::assert!(!pack.guidelines.unsafe_forbidden);
std::assert!(!pack.guidelines.use_statements_forbidden);
std::assert!(!pack.guidelines.testing_mandate);
std::assert!(!pack.guidelines.revision_history_required);
std::assert_eq!(pack.crate_name, String::from(env!("CARGO_PKG_NAME")));
}
}