use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::fs;
use uuid::Uuid;
pub fn create_minimal_agentstate(
state_type: &str,
name: &str,
description: Option<&str>,
) -> Result<Value, String> {
let allowed_types = ["memory", "skill", "plan", "config", "hook", "other"];
if !allowed_types.contains(&state_type) {
return Err(format!(
"Invalid agent state type: '{}'. Must be one of: {:?}",
state_type, allowed_types
));
}
let mut doc = json!({
"$schema": "https://hai.ai/schemas/agentstate/v1/agentstate.schema.json",
"jacsAgentStateType": state_type,
"jacsAgentStateName": name,
});
if let Some(desc) = description {
doc["jacsAgentStateDescription"] = json!(desc);
}
doc["id"] = json!(Uuid::new_v4().to_string());
doc["jacsType"] = json!("agentstate");
doc["jacsLevel"] = json!("config");
Ok(doc)
}
pub fn create_agentstate_with_file(
state_type: &str,
name: &str,
file_path: &str,
embed: bool,
) -> Result<Value, String> {
let mut doc = create_minimal_agentstate(state_type, name, None)?;
let content = fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read file '{}': {}", file_path, e))?;
let hash = compute_sha256(&content);
let mimetype = mime_guess::from_path(file_path)
.first_or_octet_stream()
.to_string();
let should_embed = embed || state_type == "hook";
let mut file_entry = json!({
"mimetype": mimetype,
"path": file_path,
"embed": should_embed,
"sha256": hash,
});
if should_embed {
file_entry["contents"] = json!(content);
doc["jacsAgentStateContent"] = json!(content);
}
doc["jacsAgentStateContentType"] = json!(mimetype);
doc["jacsFiles"] = json!([file_entry]);
Ok(doc)
}
pub fn create_agentstate_with_content(
state_type: &str,
name: &str,
content: &str,
content_type: &str,
) -> Result<Value, String> {
let mut doc = create_minimal_agentstate(state_type, name, None)?;
doc["jacsAgentStateContent"] = json!(content);
doc["jacsAgentStateContentType"] = json!(content_type);
Ok(doc)
}
pub fn set_agentstate_framework(doc: &mut Value, framework: &str) -> Result<(), String> {
doc["jacsAgentStateFramework"] = json!(framework);
Ok(())
}
pub fn set_agentstate_origin(
doc: &mut Value,
origin: &str,
source_url: Option<&str>,
) -> Result<(), String> {
let allowed_origins = ["authored", "adopted", "generated", "imported"];
if !allowed_origins.contains(&origin) {
return Err(format!(
"Invalid origin: '{}'. Must be one of: {:?}",
origin, allowed_origins
));
}
doc["jacsAgentStateOrigin"] = json!(origin);
if let Some(url) = source_url {
doc["jacsAgentStateSourceUrl"] = json!(url);
}
Ok(())
}
pub fn set_agentstate_tags(doc: &mut Value, tags: Vec<&str>) -> Result<(), String> {
doc["jacsAgentStateTags"] = json!(tags);
Ok(())
}
pub fn verify_agentstate_file_hash(doc: &Value) -> Result<bool, String> {
let files = doc
.get("jacsFiles")
.and_then(|f| f.as_array())
.ok_or_else(|| "No jacsFiles array in document".to_string())?;
if files.is_empty() {
return Err("jacsFiles array is empty".to_string());
}
for file_entry in files {
let path = file_entry
.get("path")
.and_then(|p| p.as_str())
.ok_or_else(|| "File entry missing 'path' field".to_string())?;
let expected_hash = file_entry
.get("sha256")
.and_then(|h| h.as_str())
.ok_or_else(|| "File entry missing 'sha256' field".to_string())?;
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read file '{}': {}", path, e))?;
let actual_hash = compute_sha256(&content);
if actual_hash != expected_hash {
return Ok(false);
}
}
Ok(true)
}
pub fn set_agentstate_version(doc: &mut Value, version: &str) -> Result<(), String> {
doc["jacsAgentStateVersion"] = json!(version);
Ok(())
}
fn compute_sha256(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("{:x}", hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_sha256() {
let hash = compute_sha256("hello world");
assert_eq!(
hash,
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn test_create_minimal_agentstate_valid() {
let doc = create_minimal_agentstate("memory", "Test Memory", Some("A test memory"))
.expect("Should create valid agentstate");
assert_eq!(doc["jacsAgentStateType"], "memory");
assert_eq!(doc["jacsAgentStateName"], "Test Memory");
assert_eq!(doc["jacsAgentStateDescription"], "A test memory");
assert_eq!(doc["jacsType"], "agentstate");
assert_eq!(doc["jacsLevel"], "config");
}
#[test]
fn test_create_minimal_agentstate_invalid_type() {
let result = create_minimal_agentstate("invalid", "Test", None);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid agent state type"));
}
#[test]
fn test_set_agentstate_origin_valid() {
let mut doc = create_minimal_agentstate("skill", "Test Skill", None).unwrap();
set_agentstate_origin(&mut doc, "adopted", Some("https://example.com/skill")).unwrap();
assert_eq!(doc["jacsAgentStateOrigin"], "adopted");
assert_eq!(doc["jacsAgentStateSourceUrl"], "https://example.com/skill");
}
#[test]
fn test_set_agentstate_origin_invalid() {
let mut doc = create_minimal_agentstate("skill", "Test Skill", None).unwrap();
let result = set_agentstate_origin(&mut doc, "bogus", None);
assert!(result.is_err());
}
#[test]
fn test_set_agentstate_tags() {
let mut doc = create_minimal_agentstate("memory", "Test", None).unwrap();
set_agentstate_tags(&mut doc, vec!["crypto", "signing"]).unwrap();
let tags = doc["jacsAgentStateTags"].as_array().unwrap();
assert_eq!(tags.len(), 2);
assert_eq!(tags[0], "crypto");
}
}