use crate::types::TaskId;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ctx {
pub task_id: TaskId,
pub attempt: u32,
pub agent: String,
pub meta: CtxMeta,
#[serde(skip)]
pub operator: OperatorInfo,
}
impl Ctx {
pub fn new(task_id: TaskId, attempt: u32, agent: impl Into<String>) -> Self {
Self {
task_id,
attempt,
agent: agent.into(),
meta: CtxMeta::default(),
operator: OperatorInfo::default(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CtxMeta {
#[serde(default)]
pub runtime: HashMap<String, Value>,
#[serde(default)]
pub authz: HashMap<String, Value>,
#[serde(default)]
pub observer: HashMap<String, Value>,
#[serde(default)]
pub loop_ns: HashMap<String, Value>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperatorKind {
MainAi,
#[default]
Automate,
Composite,
}
impl From<mlua_swarm_schema::OperatorKind> for OperatorKind {
fn from(k: mlua_swarm_schema::OperatorKind) -> Self {
match k {
mlua_swarm_schema::OperatorKind::MainAi => OperatorKind::MainAi,
mlua_swarm_schema::OperatorKind::Automate => OperatorKind::Automate,
mlua_swarm_schema::OperatorKind::Composite => OperatorKind::Composite,
}
}
}
pub fn collapse_operator_kind(
runtime_agent: Option<OperatorKind>,
runtime_global: Option<OperatorKind>,
bp_agent: Option<OperatorKind>,
bp_global: Option<OperatorKind>,
) -> OperatorKind {
runtime_agent
.or(runtime_global)
.or(bp_agent)
.or(bp_global)
.unwrap_or_default()
}
#[cfg(test)]
mod collapse_operator_kind_tests {
use super::*;
#[test]
fn all_none_falls_back_to_automate() {
assert_eq!(
collapse_operator_kind(None, None, None, None),
OperatorKind::Automate
);
}
#[test]
fn bp_global_only_wins() {
assert_eq!(
collapse_operator_kind(None, None, None, Some(OperatorKind::MainAi)),
OperatorKind::MainAi
);
}
#[test]
fn bp_agent_only_wins() {
assert_eq!(
collapse_operator_kind(None, None, Some(OperatorKind::MainAi), None),
OperatorKind::MainAi
);
}
#[test]
fn runtime_global_only_wins() {
assert_eq!(
collapse_operator_kind(None, Some(OperatorKind::MainAi), None, None),
OperatorKind::MainAi
);
}
#[test]
fn runtime_agent_only_wins() {
assert_eq!(
collapse_operator_kind(Some(OperatorKind::MainAi), None, None, None),
OperatorKind::MainAi
);
}
#[test]
fn all_tiers_set_runtime_agent_wins() {
assert_eq!(
collapse_operator_kind(
Some(OperatorKind::MainAi),
Some(OperatorKind::Composite),
Some(OperatorKind::Automate),
Some(OperatorKind::Composite),
),
OperatorKind::MainAi
);
}
#[test]
fn runtime_global_beats_bp_agent() {
assert_eq!(
collapse_operator_kind(
None,
Some(OperatorKind::Composite),
Some(OperatorKind::MainAi),
None,
),
OperatorKind::Composite
);
}
#[test]
fn bp_agent_beats_bp_global_when_runtime_tiers_absent() {
assert_eq!(
collapse_operator_kind(
None,
None,
Some(OperatorKind::MainAi),
Some(OperatorKind::Composite),
),
OperatorKind::MainAi
);
}
#[test]
fn schema_operator_kind_converts_into_ctx_operator_kind() {
assert_eq!(
OperatorKind::from(mlua_swarm_schema::OperatorKind::MainAi),
OperatorKind::MainAi
);
assert_eq!(
OperatorKind::from(mlua_swarm_schema::OperatorKind::Automate),
OperatorKind::Automate
);
assert_eq!(
OperatorKind::from(mlua_swarm_schema::OperatorKind::Composite),
OperatorKind::Composite
);
}
}
#[derive(Clone)]
pub struct OperatorInfo {
pub kind: OperatorKind,
pub id: String,
pub senior_bridge: Option<Arc<dyn SeniorBridge>>,
pub spawn_hook: Option<Arc<dyn SpawnHook>>,
pub operator: Option<Arc<dyn crate::operator::Operator>>,
}
impl std::fmt::Debug for OperatorInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OperatorInfo")
.field("kind", &self.kind)
.field("id", &self.id)
.field("senior_bridge", &self.senior_bridge.is_some())
.field("spawn_hook", &self.spawn_hook.is_some())
.field("operator", &self.operator.is_some())
.finish()
}
}
impl Default for OperatorInfo {
fn default() -> Self {
Self {
kind: OperatorKind::Automate,
id: "default-automate".into(),
senior_bridge: None,
spawn_hook: None,
operator: None,
}
}
}
#[async_trait]
pub trait SeniorBridge: Send + Sync {
async fn ask(&self, task_id: &TaskId, question: Value) -> Result<Value, String>;
}
#[async_trait]
pub trait SpawnHook: Send + Sync {
async fn before(&self, ctx: &Ctx) -> Result<(), String>;
async fn after(&self, ctx: &Ctx, result: &Value) -> Result<(), String>;
}