nexo-core 0.2.0

Agent runtime: event bus, sessions, plugin trait, heartbeat, A2A delegation.
Documentation
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};
use std::time::Duration;

use async_trait::async_trait;
use nexo_broker::AnyBroker;
use nexo_config::types::agents::{AgentConfig, AgentRuntimeConfig, HeartbeatConfig, ModelConfig};
use nexo_core::agent::{Agent, AgentBehavior, AgentContext, InboundMessage, NoOpAgent, RunTrigger};
use nexo_core::session::SessionManager;
use uuid::Uuid;

fn test_config(id: &str) -> AgentConfig {
    AgentConfig {
        id: id.to_string(),
        model: ModelConfig {
            provider: "minimax".to_string(),
            model: "MiniMax-M2.5".to_string(),
        },
        plugins: vec![],
        heartbeat: HeartbeatConfig::default(),
        config: AgentRuntimeConfig::default(),
        system_prompt: String::new(),
        workspace: String::new(),
        skills: vec![],
        skills_dir: "./skills".into(),
        skill_overrides: Default::default(),
        transcripts_dir: String::new(),
        dreaming: Default::default(),
        workspace_git: Default::default(),
        tool_rate_limits: None,
        tool_args_validation: None,
        extra_docs: Vec::new(),
        inbound_bindings: Vec::new(),
        allowed_tools: Vec::new(),
        sender_rate_limit: None,
        allowed_delegates: Vec::new(),
        accept_delegates_from: Vec::new(),
        description: String::new(),
        outbound_allowlist: Default::default(),
        google_auth: None,
        credentials: Default::default(),
        link_understanding: serde_json::Value::Null,
        web_search: serde_json::Value::Null,
        pairing_policy: serde_json::Value::Null,
        language: None,
        locale_prompts: Default::default(),
        context_optimization: None,
        dispatch_policy: Default::default(),
        plan_mode: Default::default(),
        remote_triggers: Vec::new(),
        lsp: nexo_config::types::lsp::LspPolicy::default(),
        config_tool: nexo_config::types::config_tool::ConfigToolPolicy::default(),
        team: nexo_config::types::team::TeamPolicy::default(),
        proactive: Default::default(),
        repl: Default::default(),
        auto_dream: None,
        assistant_mode: None,
        away_summary: None,
        brief: None,
        channels: None,
        auto_approve: false,
        extract_memories: None,
        event_subscribers: Vec::new(),
        tenant_id: None,
        extensions_config: std::collections::BTreeMap::new(),
        active: true,
    }
}

fn test_ctx(agent_id: &str) -> AgentContext {
    let config = Arc::new(test_config(agent_id));
    let broker = AnyBroker::local();
    let sessions = Arc::new(SessionManager::new(Duration::from_secs(3600), 50));
    AgentContext::new(agent_id, config, broker, sessions)
}

#[tokio::test]
async fn inbound_message_new() {
    let session_id = Uuid::new_v4();
    let msg = InboundMessage::new(session_id, "kate", "hello");

    assert_eq!(msg.session_id, session_id);
    assert_eq!(msg.agent_id, "kate");
    assert_eq!(msg.text, "hello");
    assert_eq!(msg.trigger, RunTrigger::User);
    assert!(msg.sender_id.is_none());
}

#[tokio::test]
async fn noop_agent_on_message() {
    let agent = NoOpAgent;
    let ctx = test_ctx("kate");
    let msg = InboundMessage::new(Uuid::new_v4(), "kate", "ping");

    let result = agent.on_message(&ctx, msg).await;
    assert!(result.is_ok());
}

#[tokio::test]
async fn noop_agent_default_methods() {
    use nexo_broker::Event;
    use serde_json::json;

    let agent = NoOpAgent;
    let ctx = test_ctx("kate");

    assert!(agent.on_heartbeat(&ctx).await.is_ok());
    let event = Event::new("test.topic", "test", json!({}));
    assert!(agent.on_event(&ctx, event).await.is_ok());
    let msg = InboundMessage::new(Uuid::new_v4(), "kate", "decide?");
    let result = agent.decide(&ctx, &msg).await.unwrap();
    assert!(result.is_empty());
}

#[tokio::test]
async fn agent_new() {
    let config = test_config("ventas");
    let agent = Agent::new(config, NoOpAgent);

    assert_eq!(agent.id, "ventas");
    assert_eq!(agent.config.id, "ventas");
}

#[tokio::test]
async fn custom_behavior_called() {
    struct TrackingAgent {
        called: Arc<AtomicBool>,
    }

    #[async_trait]
    impl AgentBehavior for TrackingAgent {
        async fn on_message(
            &self,
            _ctx: &AgentContext,
            _msg: InboundMessage,
        ) -> anyhow::Result<()> {
            self.called.store(true, Ordering::SeqCst);
            Ok(())
        }
    }

    let called = Arc::new(AtomicBool::new(false));
    let agent = TrackingAgent {
        called: Arc::clone(&called),
    };
    let ctx = test_ctx("kate");
    let msg = InboundMessage::new(Uuid::new_v4(), "kate", "test");

    agent.on_message(&ctx, msg).await.unwrap();
    assert!(called.load(Ordering::SeqCst));
}