Add a Plugin
Use this when you need to extend the agent lifecycle with state keys, phase hooks, scheduled actions, or effect handlers.
Purpose
Section titled “Purpose”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.
Prerequisites
Section titled “Prerequisites”awakencrate added toCargo.toml- Familiarity with
Phasevariants andStateKey
- 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; }}- 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) }}- 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(()) }}- 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.
Inject context and steer tools
Section titled “Inject context and steer tools”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
IncludeOnlyToolsorExcludeTool; - adjust inference knobs with
InferenceOverrideStatewhen 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.
Verify
Section titled “Verify”Run the agent and inspect the state snapshot. The audit_log key should contain entries added by the hook after each inference phase.
Common Errors
Section titled “Common Errors”| Error | Cause | Fix |
|---|---|---|
StateError::KeyAlreadyRegistered | Two plugins register the same StateKey | Use a unique KEY constant per state key |
StateError::UnknownKey | Accessing a key that was never registered | Ensure the plugin calling register_key is activated on the agent |
| Hook not firing | Plugin not loaded or hook filtered out | Add the plugin ID to plugin_ids; include it in with_hook_filter when using hook filters |
Related Example
Section titled “Related Example”crates/awaken-ext-observability/ — the built-in observability plugin registers phase hooks and state keys.
Code References
Section titled “Code References”crates/awaken-doctest/examples/plugin_registrar.rs— minimalPlugintrait implementation.crates/awaken-doctest/examples/state_command.rs—StateCommandwith 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.
Key Files
Section titled “Key Files”crates/awaken-runtime/src/plugins/lifecycle.rs—Plugintraitcrates/awaken-runtime/src/plugins/registry.rs—PluginRegistrarcrates/awaken-runtime/src/hooks/phase_hook.rs—PhaseHooktrait