use crate::horde_graph::HordeEdge;
use crate::horde_trigger::HordeTrigger;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::OnceLock;
pub const MANIFEST_SCHEMA_VERSION: &str = "1.0";
pub const MANIFEST_SCHEMA_MAJOR: u32 = 1;
pub const BUILTIN_TOOL_PROVIDER: &str = "builtin";
pub const WORKFLOW_MANIFEST_SCHEMA_JSON: &str =
include_str!("../../resources/schemas/workflow-manifest.schema.json");
pub fn workflow_manifest_schema() -> &'static Value {
static SCHEMA: OnceLock<Value> = OnceLock::new();
SCHEMA.get_or_init(|| {
serde_json::from_str(WORKFLOW_MANIFEST_SCHEMA_JSON)
.expect("embedded workflow-manifest schema must be valid JSON")
})
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct WorkflowManifest {
pub schema_version: String,
pub identifier: String,
pub name: String,
pub version: String,
#[serde(default)]
pub description: String,
pub steps: Vec<ManifestStep>,
pub pipeline: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edges: Vec<HordeEdge>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kowalski: Option<ManifestExtension>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ManifestStep {
pub id: String,
pub kind: String,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<StepAgent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_bindings: Vec<ToolBinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<StepInput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kowalski: Option<StepExtension>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StepAgent {
pub system_prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ToolBinding {
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overrides: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StepInput {
pub prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ManifestExtension {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capability_prefix: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_question: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_topic: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workdir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivery_title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivery_note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivery_root_rel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivery_summary_note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_tip: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub triggers: Vec<HordeTrigger>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StepExtension {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capability: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub isolation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verify_command: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verify_cwd: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub apply_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub normalize_doc_title: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub normalize_sections: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub normalize_fallback: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub normalize_fallback_sections: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_body: Option<String>,
}
impl StepExtension {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
impl ManifestExtension {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}