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.

Plugins are for behavior that must participate in the agent lifecycle, not just a single tool call. This is better than scattering hooks through tools because state keys, phase hooks, and effect handlers are registered once, validated at build time, and applied consistently across runs.

Use plugins to manage model context when the context depends on runtime state. Read inputs from PhaseContext, return a StateCommand, and schedule AddContextMessage instead of mutating prompts directly. The loop then owns context throttling, ordering, and cleanup.

  • 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.

with_plugin registers the plugin into the runtime’s plugin registry — the same registration model that backs tools, agents, models, and providers. See Agent Resolution.

When a plugin needs to change what the model sees, return commands instead of editing prompts or stores directly:

  • read the immutable snapshot through PhaseContext;
  • return a StateCommand;
  • add model-facing context with AddContextMessage;
  • narrow tool availability with IncludeOnlyTools or ExcludeTool;
  • adjust inference knobs with InferenceOverrideState when the plugin owns that policy.

This is better than mutating shared state from a hook because the loop owns ordering, throttling, conflict handling, and commit timing. See crates/awaken-runtime/src/agent/state/loop_actions.rs for the command types.

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-doctest/examples/plugin_registrar.rs — minimal Plugin trait implementation.
  • crates/awaken-doctest/examples/state_command.rsStateCommand with mutations, effects, and scheduled actions.
  • crates/awaken-doctest/examples/effect_spec.rs — typed effect spec shape.
  • crates/awaken-doctest/examples/scheduled_action.rs — scheduled action spec and handler shape.
  • crates/awaken-runtime/src/agent/state/loop_actions.rs — context message, tool filter, and inference override state commands.
  • crates/awaken-runtime/src/plugins/lifecycle.rsPlugin trait
  • crates/awaken-runtime/src/plugins/registry.rsPluginRegistrar
  • crates/awaken-runtime/src/hooks/phase_hook.rsPhaseHook trait