use crate::error::DispatchError;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const SUPPORTED_IR_VERSION: &str = "0.9";
pub fn validate_ir_version(ir: &WarbleIr) -> Result<(), DispatchError> {
if ir.warble_ir_version != SUPPORTED_IR_VERSION {
return Err(DispatchError::new(format!(
"unsupported warble_ir_version '{}' (this back-end understands: {SUPPORTED_IR_VERSION})",
ir.warble_ir_version
)));
}
if ir
.components
.iter()
.any(|node| node.context_binding.absent != ir.context_binding.absent)
{
return Err(DispatchError::new(
"root and component context_binding must agree on context absence",
));
}
Ok(())
}
pub fn reject_unsupported_component_composition(
ir: &WarbleIr,
target: &str,
) -> Result<(), DispatchError> {
for node in ir.components.iter().filter(|node| node.entrypoint) {
for call in &node.llm_calls {
if let Some(component_call) = call.component_calls.first() {
return Err(DispatchError::new(format!(
"step '{}' on component '{}' authorizes component call alias '{}' to '{}', \
but component_invocation resolves fail on target '{target}' because no trusted \
invocation handler is installed (wall-hit)",
call.name, node.id, component_call.alias, component_call.component
)));
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RealizationKind {
Skill,
Tool,
GatedTool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComponentType {
Analytical,
Assertive,
Mutating,
Constitutive,
Orchestrating,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TriggerKind {
OneShot,
Scheduled,
Event,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutcomeKind {
None,
Assertion,
Mutation,
Dispatch,
}
impl RealizationKind {
pub fn as_str(&self) -> &'static str {
match self {
RealizationKind::Skill => "skill",
RealizationKind::Tool => "tool",
RealizationKind::GatedTool => "gated-tool",
}
}
}
impl TriggerKind {
pub fn as_str(&self) -> &'static str {
match self {
TriggerKind::OneShot => "one_shot",
TriggerKind::Scheduled => "scheduled",
TriggerKind::Event => "event",
}
}
}
impl OutcomeKind {
pub fn as_str(&self) -> &'static str {
match self {
OutcomeKind::None => "none",
OutcomeKind::Assertion => "assertion",
OutcomeKind::Mutation => "mutation",
OutcomeKind::Dispatch => "dispatch",
}
}
}
impl ComponentType {
pub fn as_str(&self) -> &'static str {
match self {
ComponentType::Analytical => "analytical",
ComponentType::Assertive => "assertive",
ComponentType::Mutating => "mutating",
ComponentType::Constitutive => "constitutive",
ComponentType::Orchestrating => "orchestrating",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ContextBinding {
pub absent: bool,
pub project: String,
pub binding_mode: String,
pub resolved: Option<serde_json::Value>,
}
impl<'de> Deserialize<'de> for ContextBinding {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Bound {
project: String,
binding_mode: String,
#[serde(default)]
resolved: Option<serde_json::Value>,
}
Ok(match Option::<Bound>::deserialize(deserializer)? {
Some(value) => Self {
absent: false,
project: value.project,
binding_mode: value.binding_mode,
resolved: value.resolved,
},
None => Self {
absent: true,
project: String::new(),
binding_mode: String::new(),
resolved: None,
},
})
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct IrConfig {}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct LlmCall {
pub name: String,
pub tier: String,
#[serde(default)]
pub consumes: Vec<String>,
#[serde(default)]
pub produces: Option<String>,
pub prompt: String,
#[serde(default)]
pub conditional: bool,
#[serde(default)]
pub when: Option<WhenGuard>,
#[serde(default)]
pub component_calls: Vec<ComponentCall>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ComponentCall {
pub alias: String,
pub component: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct WhenGuard {
pub guard: String,
pub target: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Guardrail {
pub name: String,
pub locked: bool,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub threshold: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Trigger {
pub kind: TriggerKind,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct RenderBlock {
#[serde(rename = "type")]
pub block_type: String,
#[serde(default)]
pub fields: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Outcome {
pub kind: OutcomeKind,
#[serde(default)]
pub verdict_type: Option<String>,
#[serde(default)]
pub emits: Option<Vec<String>>,
#[serde(default)]
pub target: Option<String>,
#[serde(default)]
pub change_type: Option<String>,
#[serde(default)]
pub routable_scope: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Effect {
#[serde(default)]
pub render_blocks: Vec<RenderBlock>,
pub outcome: Outcome,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct PreconditionResult {
pub status: String,
#[serde(default)]
pub checks: Vec<PreconditionCheck>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct PreconditionCheck {
pub predicate: String,
pub outcome: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Precondition {
pub predicate: String,
#[serde(default)]
pub args: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Param {
pub name: String,
#[serde(default)]
pub bind: Option<String>,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub default: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct EvalSpec {
pub template_ref: String,
#[serde(default)]
pub metrics: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ComponentNode {
pub id: String,
pub entrypoint: bool,
pub verb: String,
#[serde(rename = "type")]
pub component_type: ComponentType,
pub realization_kind: RealizationKind,
#[serde(deserialize_with = "ContextBinding::deserialize")]
pub context_binding: ContextBinding,
pub precondition_result: PreconditionResult,
pub prompt_fragment: String,
pub llm_calls: Vec<LlmCall>,
pub guardrails: Vec<Guardrail>,
pub trigger: Trigger,
#[serde(default)]
pub required_capabilities: Vec<String>,
#[serde(default)]
pub borrowed_actions: Vec<String>,
pub eval_ref: String,
pub effect: Effect,
#[serde(default)]
pub context_requirements: Vec<String>,
#[serde(default)]
pub context_precondition: Vec<Precondition>,
#[serde(default)]
pub params: Vec<Param>,
#[serde(default)]
pub eval: Option<EvalSpec>,
#[serde(default)]
pub brief: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub examples: Vec<String>,
#[serde(default)]
pub slots: Vec<SlotDecl>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, serde::Serialize)]
pub struct SlotDecl {
pub name: String,
pub default: String,
pub variants: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub present_when: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct WarbleIr {
pub warble_ir_version: String,
pub profile: String,
#[serde(deserialize_with = "ContextBinding::deserialize")]
pub context_binding: ContextBinding,
pub config: IrConfig,
pub components: Vec<ComponentNode>,
#[serde(default)]
pub slots: Vec<SlotDecl>,
}