awaken-runtime 0.6.0

Phase-based execution engine, plugin system, and agent loop for Awaken
Documentation
use std::any::Any;

use crate::state::MutationBatch;
use awaken_runtime_contract::registry_spec::AgentSpec;
use awaken_runtime_contract::{PluginConfigKey, StateError};

use super::{PluginDescriptor, PluginRegistrar};

/// Schema declaration for a plugin's config section.
///
/// Returned by [`Plugin::config_schemas`] to enable eager validation
/// during resolve — before hooks ever run.
///
/// Contains a JSON Schema generated via `schemars`, validated at resolve
/// time with `jsonschema`.
pub struct ConfigSchema {
    /// Section key in `AgentSpec.sections` (must match `PluginConfigKey::KEY`).
    pub key: &'static str,
    /// JSON Schema for this section, generated by `schemars::schema_for!`.
    pub json_schema: serde_json::Value,
    /// Human-readable label for admin and API clients.
    pub display_name: Option<&'static str>,
    /// Short explanation of what the section controls.
    pub description: Option<&'static str>,
    /// Stable grouping hint for config UIs.
    pub category: Option<&'static str>,
    /// Preferred editor implementation. Unknown values fall back to schema rendering.
    pub editor: Option<&'static str>,
    /// Default section value serialized from the typed config default.
    pub default_value: serde_json::Value,
    /// JSON Schema Form compatible UI hints.
    pub ui_schema: serde_json::Value,
}

impl ConfigSchema {
    /// Build a schema declaration directly from a typed plugin config key.
    pub fn for_key<K: PluginConfigKey>() -> Self {
        Self {
            key: K::KEY,
            json_schema: serde_json::to_value(schemars::schema_for!(K::Config)).unwrap_or_default(),
            display_name: None,
            description: None,
            category: None,
            editor: None,
            default_value: serde_json::to_value(K::Config::default()).unwrap_or_default(),
            ui_schema: serde_json::Value::Object(Default::default()),
        }
    }

    #[must_use]
    pub fn with_display_name(mut self, display_name: &'static str) -> Self {
        self.display_name = Some(display_name);
        self
    }

    #[must_use]
    pub fn with_description(mut self, description: &'static str) -> Self {
        self.description = Some(description);
        self
    }

    #[must_use]
    pub fn with_category(mut self, category: &'static str) -> Self {
        self.category = Some(category);
        self
    }

    #[must_use]
    pub fn with_editor(mut self, editor: &'static str) -> Self {
        self.editor = Some(editor);
        self
    }

    #[must_use]
    pub fn with_ui_schema(mut self, ui_schema: serde_json::Value) -> Self {
        self.ui_schema = ui_schema;
        self
    }
}

pub trait Plugin: Send + Sync + Any + 'static {
    fn descriptor(&self) -> PluginDescriptor;

    /// Bind per-run runtime context to the plugin instance.
    ///
    /// This is invoked at run startup and on agent handoff so plugins that
    /// keep runtime-owned handles can bind to the current run's store or inbox.
    fn bind_runtime_context(
        &self,
        _store: &crate::state::StateStore,
        _owner_inbox: Option<&crate::inbox::InboxSender>,
    ) {
    }

    /// Declare capabilities: state keys, hooks, action handlers, effect handlers, permission checkers.
    /// Called once per resolve to build the ExecutionEnv.
    fn register(&self, _registrar: &mut PluginRegistrar) -> Result<(), StateError> {
        Ok(())
    }

    /// Declare config section schemas for eager validation during resolve.
    ///
    /// Override this to declare which `AgentSpec.sections` keys this plugin
    /// owns and how to validate them. The resolve pipeline calls this to
    /// catch malformed config before hooks run.
    fn config_schemas(&self) -> Vec<ConfigSchema> {
        Vec::new()
    }

    /// Agent activated: read spec config, write initial state.
    /// Called when this plugin becomes active for a specific agent.
    fn on_activate(
        &self,
        _agent_spec: &AgentSpec,
        _patch: &mut MutationBatch,
    ) -> Result<(), StateError> {
        Ok(())
    }

    /// Agent deactivated: clean up agent-scoped state.
    /// Called when switching away from an agent that uses this plugin.
    fn on_deactivate(&self, _patch: &mut MutationBatch) -> Result<(), StateError> {
        Ok(())
    }
}

#[cfg(any(feature = "background", test))]
impl dyn Plugin {
    /// View this plugin as `&dyn Any` for downcasting.
    ///
    /// Available under the `background` feature (used by the background
    /// scheduler) and in tests (used by resolve-pipeline introspection).
    pub(crate) fn as_any(&self) -> &dyn Any {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::{Deserialize, Serialize};

    #[derive(Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
    struct TestConfig {
        value: String,
    }

    struct TestConfigKey;

    impl PluginConfigKey for TestConfigKey {
        const KEY: &'static str = "test";
        type Config = TestConfig;
    }

    #[test]
    fn config_schema_for_key_uses_typed_key_and_config() {
        let schema = ConfigSchema::for_key::<TestConfigKey>();

        assert_eq!(schema.key, TestConfigKey::KEY);
        assert_eq!(schema.json_schema["type"], "object");
        assert!(schema.json_schema["properties"].get("value").is_some());
        assert_eq!(schema.default_value, serde_json::json!({"value": ""}));
    }
}