Skip to content

Add a Plugin

Use this when you need to extend the agent lifecycle with state keys, phase hooks, scheduled actions, or effect handlers.

  • awaken crate added to Cargo.toml
  • Familiarity with Phase variants and StateKey
  1. Define a state key.
use serde::{Serialize, Deserialize};
use awaken::{MergeStrategy, StateKey};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditLog {
pub entries: Vec<String>,
}
pub struct AuditLogKey;
impl StateKey for AuditLogKey {
type Value = AuditLog;
const KEY: &'static str = "audit_log";
const MERGE: MergeStrategy = MergeStrategy::Exclusive;
type Update = AuditLog;
fn apply(value: &mut Self::Value, update: Self::Update) {
*value = update;
}
}
  1. Implement a phase hook.
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use awaken::{MergeStrategy, PhaseContext, PhaseHook, StateCommand, StateError, StateKey};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditLog {
pub entries: Vec<String>,
}
pub struct AuditLogKey;
impl StateKey for AuditLogKey {
const KEY: &'static str = "audit_log";
const MERGE: MergeStrategy = MergeStrategy::Exclusive;
type Value = AuditLog;
type Update = AuditLog;
fn apply(value: &mut Self::Value, update: Self::Update) {
*value = update;
}
}
pub struct AuditHook;
#[async_trait]
impl PhaseHook for AuditHook {
async fn run(&self, ctx: &PhaseContext) -> Result<StateCommand, StateError> {
let mut log = ctx.state::<AuditLogKey>().cloned().unwrap_or_default();
log.entries.push(format!("Phase executed at {:?}", ctx.phase));
let mut cmd = StateCommand::new();
cmd.update::<AuditLogKey>(log);
Ok(cmd)
}
}
  1. Implement the Plugin trait.
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use awaken::{
KeyScope, MergeStrategy, Phase, PhaseContext, PhaseHook, Plugin, PluginDescriptor,
PluginRegistrar, StateCommand, StateError, StateKey, StateKeyOptions,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditLog {
pub entries: Vec<String>,
}
pub struct AuditLogKey;
impl StateKey for AuditLogKey {
const KEY: &'static str = "audit_log";
const MERGE: MergeStrategy = MergeStrategy::Exclusive;
type Value = AuditLog;
type Update = AuditLog;
fn apply(value: &mut Self::Value, update: Self::Update) {
*value = update;
}
}
pub struct AuditHook;
#[async_trait]
impl PhaseHook for AuditHook {
async fn run(&self, _ctx: &PhaseContext) -> Result<StateCommand, StateError> {
Ok(StateCommand::new())
}
}
pub struct AuditPlugin;
impl Plugin for AuditPlugin {
fn descriptor(&self) -> PluginDescriptor {
PluginDescriptor { name: "audit" }
}
fn register(&self, registrar: &mut PluginRegistrar) -> Result<(), StateError> {
registrar.register_key::<AuditLogKey>(StateKeyOptions {
scope: KeyScope::Run,
..Default::default()
})?;
registrar.register_phase_hook(
"audit",
Phase::AfterInference,
AuditHook,
)?;
Ok(())
}
}
  1. Register the plugin and activate it on an agent.
use std::sync::Arc;
use awaken::engine::GenaiExecutor;
use awaken::registry_spec::ModelSpec;
use awaken::{AgentSpec, AgentRuntimeBuilder, Plugin, PluginDescriptor};
pub struct AuditPlugin;
impl Plugin for AuditPlugin {
fn descriptor(&self) -> PluginDescriptor {
PluginDescriptor { name: "audit" }
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut spec = AgentSpec::new("assistant")
.with_model_id("claude-sonnet")
.with_system_prompt("You are a helpful assistant.")
.with_hook_filter("audit");
spec.plugin_ids.push("audit".into());
let runtime = AgentRuntimeBuilder::new()
.with_plugin("audit", Arc::new(AuditPlugin))
.with_agent_spec(spec)
.with_provider("anthropic", Arc::new(GenaiExecutor::new()))
.with_model(ModelSpec::new("claude-sonnet", "anthropic", "claude-sonnet-4-20250514"))
.build()?;
let _runtime = runtime;
Ok(())
}

plugin_ids loads the plugin for the agent. with_hook_filter only filters the hooks, tools, and request transforms from plugins that have already been loaded.

Run the agent and inspect the state snapshot. The audit_log key should contain entries added by the hook after each inference phase.

ErrorCauseFix
StateError::KeyAlreadyRegisteredTwo plugins register the same StateKeyUse a unique KEY constant per state key
StateError::UnknownKeyAccessing a key that was never registeredEnsure the plugin calling register_key is activated on the agent
Hook not firingPlugin not loaded or hook filtered outAdd the plugin ID to plugin_ids; include it in with_hook_filter when using hook filters

crates/awaken-ext-observability/ — the built-in observability plugin registers phase hooks and state keys.

  • crates/awaken-runtime/src/plugins/lifecycle.rsPlugin trait
  • crates/awaken-runtime/src/plugins/registry.rsPluginRegistrar
  • crates/awaken-runtime/src/hooks/phase_hook.rsPhaseHook trait