use std::path::{Path, PathBuf};
use crate::model::Prompty;
use crate::model::context::LoadContext;
mod error;
mod frontmatter;
mod resolve;
pub use error::LoadError;
pub fn load(path: impl AsRef<Path>) -> Result<Prompty, LoadError> {
let resolved = path
.as_ref()
.canonicalize()
.map_err(|e| LoadError::FileNotFound(path.as_ref().to_path_buf(), e.to_string()))?;
let span = crate::tracing::Tracer::start("load");
span.emit("signature", &serde_json::json!("prompty.load"));
span.emit(
"inputs",
&serde_json::json!({ "path": resolved.display().to_string() }),
);
let raw = std::fs::read_to_string(&resolved)
.map_err(|e| LoadError::FileNotFound(resolved.clone(), e.to_string()))?;
let raw = raw.replace("\r\n", "\n");
match build_agent(&raw, &resolved) {
Ok(agent) => {
span.emit("result", &serde_json::json!({ "name": agent.name }));
span.end();
Ok(agent)
}
Err(e) => {
span.emit("error", &serde_json::json!(e.to_string()));
span.end();
Err(e)
}
}
}
pub async fn load_async(path: impl AsRef<Path>) -> Result<Prompty, LoadError> {
let path_buf = path.as_ref().to_path_buf();
let resolved = tokio::fs::canonicalize(&path_buf)
.await
.map_err(|e| LoadError::FileNotFound(path_buf.clone(), e.to_string()))?;
let span = crate::tracing::Tracer::start("load");
span.emit("signature", &serde_json::json!("prompty.load"));
span.emit(
"inputs",
&serde_json::json!({ "path": resolved.display().to_string() }),
);
let raw = tokio::fs::read_to_string(&resolved)
.await
.map_err(|e| LoadError::FileNotFound(resolved.clone(), e.to_string()))?;
let raw = raw.replace("\r\n", "\n");
let resolved_clone = resolved.clone();
let result = tokio::task::spawn_blocking(move || build_agent(&raw, &resolved_clone))
.await
.map_err(|e| LoadError::Other(format!("spawn_blocking panicked: {e}")))?;
match result {
Ok(agent) => {
span.emit("result", &serde_json::json!({ "name": agent.name }));
span.end();
Ok(agent)
}
Err(e) => {
span.emit("error", &serde_json::json!(e.to_string()));
span.end();
Err(e)
}
}
}
pub fn load_from_string(raw: &str, base_path: impl AsRef<Path>) -> Result<Prompty, LoadError> {
build_agent(raw, base_path.as_ref())
}
fn build_agent(raw: &str, file_path: &Path) -> Result<Prompty, LoadError> {
let (mut data, body) = frontmatter::split(raw)?;
let body = body.trim_end_matches('\n').trim_end_matches('\r');
if !body.is_empty() {
data.insert(
"instructions".to_string(),
serde_json::Value::String(body.to_string()),
);
}
data.insert(
"kind".to_string(),
serde_json::Value::String("prompt".to_string()),
);
let agent_dir = file_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let ctx = make_load_context(agent_dir);
let mut value = serde_json::Value::Object(data);
resolve::resolve_references(&mut value, file_path.parent().unwrap_or(Path::new(".")))?;
let agent = Prompty::load_from_value(&value, &ctx);
let mut result = agent;
let meta = ensure_metadata_object(&mut result);
meta.insert(
"__source_path".to_string(),
serde_json::Value::String(file_path.to_string_lossy().to_string()),
);
Ok(result)
}
fn make_load_context(agent_dir: PathBuf) -> LoadContext {
LoadContext {
pre_process: Some(Box::new(move |mut value| {
if let Some(obj) = value.as_object_mut() {
for val in obj.values_mut() {
if let Some(s) = val.as_str() {
if let Some(resolved) = resolve::resolve_single_ref(s, &agent_dir) {
*val = resolved;
}
}
}
}
value
})),
post_process: None,
}
}
fn ensure_metadata_object(agent: &mut Prompty) -> &mut serde_json::Map<String, serde_json::Value> {
if !agent.metadata.is_object() {
agent.metadata = serde_json::Value::Object(serde_json::Map::new());
}
agent.metadata.as_object_mut().unwrap()
}