#![warn(missing_docs)]
use mlua_flow_ir::Node as FlowNode;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub const CURRENT_SCHEMA_VERSION: &str = "0.1.0";
fn default_schema_version() -> semver::Version {
current_schema_version()
}
pub fn current_schema_version() -> semver::Version {
semver::Version::parse(CURRENT_SCHEMA_VERSION)
.expect("CURRENT_SCHEMA_VERSION must be valid semver")
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Blueprint {
#[serde(default = "default_schema_version")]
#[schemars(with = "String")]
pub schema_version: semver::Version,
pub id: String,
#[schemars(with = "Value")]
pub flow: FlowNode,
#[serde(default)]
pub agents: Vec<AgentDef>,
#[serde(default)]
pub operators: Vec<OperatorDef>,
#[serde(default)]
pub hints: CompilerHints,
#[serde(default)]
pub strategy: CompilerStrategy,
#[serde(default)]
pub metadata: BlueprintMetadata,
#[serde(default)]
pub spawner_hints: SpawnerHints,
#[serde(default = "default_global_agent_kind")]
pub default_agent_kind: AgentKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_operator_kind: Option<OperatorKind>,
}
pub fn default_global_agent_kind() -> AgentKind {
AgentKind::Operator
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SpawnerHints {
#[serde(default)]
pub layers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentDef {
pub name: String,
pub kind: AgentKind,
#[serde(default)]
pub spec: Value,
#[serde(default)]
pub profile: Option<AgentProfile>,
#[serde(default)]
pub meta: Option<AgentMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentProfile {
#[serde(default)]
pub system_prompt: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub effort: Option<String>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub extras: Value,
#[serde(default)]
pub version_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_binding: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AgentKind {
Lua,
RustFn,
AgentBlock,
Subprocess,
Operator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OperatorKind {
MainAi,
#[default]
Automate,
Composite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct OperatorDef {
pub name: String,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub kind: Option<OperatorKind>,
#[serde(default)]
pub spec: Value,
#[serde(default)]
pub profile: Option<AgentProfile>,
#[serde(default)]
pub meta: Option<AgentMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentMeta {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CompilerHints {
#[serde(default)]
pub per_agent: HashMap<String, Value>,
#[serde(default)]
pub global: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CompilerStrategy {
#[serde(default = "default_true")]
pub strict_refs: bool,
#[serde(default = "default_true")]
pub strict_kind: bool,
}
fn default_true() -> bool {
true
}
impl Default for CompilerStrategy {
fn default() -> Self {
Self {
strict_refs: true,
strict_kind: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BlueprintMetadata {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub origin: BlueprintOrigin,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_name_alias: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_run_ttl_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BlueprintOrigin {
#[default]
Inline,
File {
path: String,
},
Algo {
session_id: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_version_default_parses() {
let v = default_schema_version();
assert_eq!(v.to_string(), "0.1.0");
}
#[test]
fn current_schema_version_const_matches() {
assert_eq!(CURRENT_SCHEMA_VERSION, "0.1.0");
}
#[test]
fn blueprint_json_schema_exports_key_properties() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
let props = v["properties"].as_object().expect("object schema");
for key in [
"schema_version",
"id",
"flow",
"agents",
"operators",
"hints",
"strategy",
"metadata",
"spawner_hints",
"default_agent_kind",
"default_operator_kind",
] {
assert!(props.contains_key(key), "missing property: {key}");
}
assert_eq!(v["properties"]["schema_version"]["type"], "string");
let dump = v.to_string();
assert!(dump.contains("agent_block"), "AgentKind variants in schema");
assert!(dump.contains("main_ai"), "OperatorKind variants in schema");
assert!(dump.contains("AgentDef"), "AgentDef definition in schema");
}
#[test]
fn agent_profile_worker_binding_roundtrips_when_some() {
let profile = AgentProfile {
worker_binding: Some("mse-worker-coder".to_string()),
..Default::default()
};
let json = serde_json::to_value(&profile).expect("serializes");
assert_eq!(json["worker_binding"], "mse-worker-coder");
let back: AgentProfile = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.worker_binding.as_deref(), Some("mse-worker-coder"));
}
#[test]
fn agent_profile_worker_binding_omitted_when_none() {
let profile = AgentProfile::default();
let json = serde_json::to_value(&profile).expect("serializes");
assert!(
json.as_object().unwrap().get("worker_binding").is_none(),
"worker_binding key must be absent when None: {json}"
);
let back: AgentProfile = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.worker_binding, None);
}
}