meerkat-mob 0.6.21

Multi-agent orchestration runtime for Meerkat
Documentation
use crate::error::MobError;
use crate::event::{MobEvent, MobEventKind, NewMobEvent};
use crate::ids::{
    AgentIdentity, AgentRuntimeId, FlowId, MeerkatId, MobId, ProfileName, RunId, StepId,
};
use crate::store::MobEventStore;
use meerkat_core::event::{AgentErrorReport, TurnErrorMetadata};
use std::sync::Arc;

#[derive(Clone)]
pub struct MobEventEmitter {
    store: Arc<dyn MobEventStore>,
    mob_id: MobId,
}

impl MobEventEmitter {
    pub fn new(store: Arc<dyn MobEventStore>, mob_id: MobId) -> Self {
        Self { store, mob_id }
    }

    async fn append(&self, kind: MobEventKind) -> Result<MobEvent, MobError> {
        self.store
            .append(NewMobEvent {
                mob_id: self.mob_id.clone(),
                timestamp: None,
                kind,
            })
            .await
            .map_err(MobError::from)
    }

    pub async fn flow_started(
        &self,
        run_id: RunId,
        flow_id: FlowId,
        params: serde_json::Value,
    ) -> Result<MobEvent, MobError> {
        self.append(MobEventKind::FlowStarted {
            run_id,
            flow_id,
            params,
        })
        .await
    }

    pub async fn step_dispatched(
        &self,
        run_id: RunId,
        step_id: StepId,
        agent_identity: MeerkatId,
    ) -> Result<MobEvent, MobError> {
        let target = AgentRuntimeId::initial(AgentIdentity::from(agent_identity.as_str()));
        self.append(MobEventKind::StepDispatched {
            run_id,
            step_id,
            target,
        })
        .await
    }

    pub async fn step_target_completed(
        &self,
        run_id: RunId,
        step_id: StepId,
        agent_identity: MeerkatId,
    ) -> Result<MobEvent, MobError> {
        let target = AgentRuntimeId::initial(AgentIdentity::from(agent_identity.as_str()));
        self.append(MobEventKind::StepTargetCompleted {
            run_id,
            step_id,
            target,
        })
        .await
    }

    pub async fn step_target_failed(
        &self,
        run_id: RunId,
        step_id: StepId,
        agent_identity: MeerkatId,
        reason: String,
    ) -> Result<MobEvent, MobError> {
        self.step_target_failed_with_error(run_id, step_id, agent_identity, reason, None, None)
            .await
    }

    pub async fn step_target_failed_with_error(
        &self,
        run_id: RunId,
        step_id: StepId,
        agent_identity: MeerkatId,
        reason: String,
        error_report: Option<AgentErrorReport>,
        error: Option<TurnErrorMetadata>,
    ) -> Result<MobEvent, MobError> {
        let target = AgentRuntimeId::initial(AgentIdentity::from(agent_identity.as_str()));
        self.append(MobEventKind::StepTargetFailed {
            run_id,
            step_id,
            target,
            reason,
            error_report,
            error,
        })
        .await
    }

    pub async fn step_completed(
        &self,
        run_id: RunId,
        step_id: StepId,
    ) -> Result<MobEvent, MobError> {
        self.append(MobEventKind::StepCompleted { run_id, step_id })
            .await
    }

    pub async fn step_failed(
        &self,
        run_id: RunId,
        step_id: StepId,
        reason: String,
    ) -> Result<MobEvent, MobError> {
        self.append(MobEventKind::StepFailed {
            run_id,
            step_id,
            reason,
        })
        .await
    }

    pub async fn step_skipped(
        &self,
        run_id: RunId,
        step_id: StepId,
        reason: String,
    ) -> Result<MobEvent, MobError> {
        self.append(MobEventKind::StepSkipped {
            run_id,
            step_id,
            reason,
        })
        .await
    }

    pub async fn topology_violation(
        &self,
        from_role: ProfileName,
        to_role: ProfileName,
    ) -> Result<MobEvent, MobError> {
        self.append(MobEventKind::TopologyViolation { from_role, to_role })
            .await
    }

    pub async fn supervisor_escalation(
        &self,
        run_id: RunId,
        step_id: StepId,
        escalated_to: MeerkatId,
    ) -> Result<MobEvent, MobError> {
        self.append(MobEventKind::SupervisorEscalation {
            run_id,
            step_id,
            escalated_to: AgentIdentity::from(escalated_to.as_str()),
        })
        .await
    }
}

#[cfg(test)]
mod tests {
    use super::MobEventEmitter;
    use crate::event::MobEventKind;
    use crate::ids::{MeerkatId, MobId, RunId, StepId};
    use crate::store::{InMemoryMobEventStore, MobEventStore};
    use meerkat_core::event::{AgentErrorClass, AgentErrorReason, AgentErrorReport};
    use std::sync::Arc;

    #[tokio::test]
    async fn step_target_failed_persists_typed_error_metadata() {
        let store: Arc<dyn MobEventStore> = Arc::new(InMemoryMobEventStore::new());
        let emitter = MobEventEmitter::new(Arc::clone(&store), MobId::from("mob-typed-error"));
        let report = AgentErrorReport {
            class: AgentErrorClass::Terminal,
            reason: Some(AgentErrorReason::TurnTerminalCause {
                outcome: meerkat_core::TurnTerminalOutcome::Failed,
                cause_kind: meerkat_core::TurnTerminalCauseKind::LlmFailure,
            }),
            message: "LLM failure terminal turn".to_string(),
        };
        let error = meerkat_core::TurnErrorMetadata::terminal(
            meerkat_core::TurnTerminalCauseKind::LlmFailure,
            meerkat_core::TurnTerminalOutcome::Failed,
            "LLM failure terminal turn",
        );

        emitter
            .step_target_failed_with_error(
                RunId::new(),
                StepId::from("review"),
                MeerkatId::from("reviewer"),
                "LLM failure terminal turn".to_string(),
                Some(report.clone()),
                Some(error.clone()),
            )
            .await
            .expect("event append should succeed");

        let events = store
            .replay_all()
            .await
            .expect("event replay should succeed");
        match &events
            .first()
            .expect("step target failed event should persist")
            .kind
        {
            MobEventKind::StepTargetFailed {
                reason,
                error_report,
                error: persisted_error,
                ..
            } => {
                assert_eq!(reason, "LLM failure terminal turn");
                assert_eq!(error_report.as_ref(), Some(&report));
                assert_eq!(persisted_error.as_ref(), Some(&error));
            }
            other => panic!("expected StepTargetFailed, got {other:?}"),
        }
    }
}