awaken-runtime 0.4.0

Phase-based execution engine, plugin system, and agent loop for Awaken
Documentation
use crate::state::MutationBatch;
use awaken_contract::registry_spec::AgentSpec;
use awaken_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,
}

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(),
        }
    }
}

pub trait Plugin: Send + Sync + '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(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());
    }
}