Create A Plugin
Step-by-step guide for adding a new built-in plugin to Anna.
Start With The Plugin ID
Choose a stable plugin ID first.
Typical patterns:
tool/<name>channel/<name>hook/<name>provider/<name>memory/<name>- standalone IDs like
reflectwhen the feature does not fit the kind/name pattern
The plugin ID is the ownership key for config, runtime state, status, and capability registration. Treat it as a long-lived identifier.
Create The Package
Add a package under plugins/ that matches the feature you are introducing.
Examples:
plugins/tools/mytool/plugins/providers/gemini/plugins/channels/slack/
Most plugins expose a PluginID constant and register in init().
Register The Plugin
The minimal shape looks like this:
package mytool
import (
pkgplugins "github.com/vaayne/anna/pkg/plugins"
)
const PluginID = "tool/mytool"
func init() {
pkgplugins.Register(PluginID, pkgplugins.PluginFunc(func(host pkgplugins.Host) {
host.SetInfo(pkgplugins.PluginInfo{
ID: PluginID,
Kind: "tool",
Name: "mytool",
DisplayName: "My Tool",
Description: "Example tool plugin.",
Capabilities: []string{
pkgplugins.CapabilityTool,
},
})
}))
}SetInfo describes the plugin as a whole. It does not register a capability by itself.
Add The Capability
For a tool plugin, add ToolSpec:
host.AddTool(pkgplugins.ToolSpec{
PluginID: PluginID,
Name: "mytool",
Description: "Run the mytool operation.",
Build: func(ctx pkgplugins.ToolContext) (tools.Tool, error) {
return NewTool(ctx.Platform.Logger()), nil
},
})For a managed runtime plugin, add AdminSpec and RuntimeSpec:
host.AddAdmin(pkgplugins.AdminSpec{
PluginID: PluginID,
DefaultConfig: DefaultConfig,
Schema: ConfigSchema(),
Validate: ValidateConfig,
Redact: RedactConfig,
})
host.AddRuntime(pkgplugins.RuntimeSpec{
PluginID: PluginID,
Name: "main",
Build: func(ctx pkgplugins.RuntimeContext) (pkgplugins.Runtime, error) {
return NewManagedRuntime(ctx.Platform), nil
},
})
host.AddAdmin(pkgplugins.AdminSpec{
PluginID: PluginID,
Status: func(ctx context.Context, build pkgplugins.AdminContext) (any, error) {
handle, ok := build.Platform.RuntimeLookup().Lookup(PluginID, "main")
if !ok {
return map[string]any{"state": "stopped"}, nil
}
snap, err := handle.Snapshot(ctx)
if err != nil {
return nil, err
}
return map[string]any{"state": snap.State, "metadata": snap.Metadata}, nil
},
})Import The Plugin
Built-in plugins are registered through blank imports. Add your package to the existing startup imports so the init() function runs.
Look at cmd/anna/plugins_imports.go and follow the established pattern.
Without that import, the plugin package compiles but never registers itself.
Keep Ownership Local
A plugin package should own:
- plugin metadata
- config decode/validate/redact logic
- capability registration
- runtime construction logic
- tests for registration and behavior
Do not split registration into one package and runtime/config logic into unrelated packages unless there is a clear boundary.
Recommended Shape By Plugin Type
Tool plugin:
SetInfoAddTool- optional
AddAdminif the tool needs config - optional
AddPromptInventoryif the tool should contribute prompt-visible inventory
Provider plugin:
SetInfoAddProvider
Hook plugin:
SetInfoAddHook
Memory plugin:
SetInfoAddMemory
Managed runtime plugin:
SetInfoAddAdminfor configAddRuntimeAddAdminfor status
CLI-backed tool plugin:
SetInfoAddBinary- optional
AddAdminfor OAuth or other config - optional
AddSessionEnvwhen the runner must inject session auth - optional
AddBundledSkillwhen the plugin ships a builtin system skill - optional
AddSystemPromptfor usage guidance
Managed channel plugin:
- usually use
RegisterManagedChannelPlugin(...)
A Real Tool Example
The notify plugin is a good minimal reference because it uses the clean surface directly:
host.SetInfo(pkgplugins.PluginInfo{
ID: PluginID,
Kind: "tool",
Name: "notify",
DisplayName: "Notify",
Description: "Send notifications through Anna's configured notification routes.",
Capabilities: []string{
pkgplugins.CapabilityTool,
},
})
host.AddTool(pkgplugins.ToolSpec{
PluginID: PluginID,
Name: "notify",
Description: "Send a notification message to the user.",
Required: true,
Build: func(ctx pkgplugins.ToolContext) (tools.Tool, error) {
service := ctx.Platform.Notifier()
if service == nil {
return nil, nil
}
return &Tool{service: service}, nil
},
})The important part is not the feature itself. The important part is the pattern:
- metadata first
- capability registration second
- all host-owned services come from
ctx.Platform
A Real Managed Runtime Example
The reflect plugin is a good managed-runtime reference because it owns config, runtime, and status in one package:
host.AddRuntime(pkgplugins.RuntimeSpec{
PluginID: PluginID,
Name: RuntimeName,
Build: func(ctx pkgplugins.RuntimeContext) (pkgplugins.Runtime, error) {
return newRuntime(ctx.Platform)
},
})
host.AddAdmin(pkgplugins.AdminSpec{
PluginID: PluginID,
Status: func(ctx context.Context, build pkgplugins.AdminContext) (any, error) {
return runtimeStatusFromLookup(ctx, build.Platform.RuntimeLookup())
},
})This is the standard pattern for host-managed background services.
Testing A New Plugin
At minimum, add tests for:
- registration completeness
- config validation
- runtime apply or capability build behavior
- status behavior if the plugin exposes status
Good examples already exist in:
plugins/channels/telegram/plugin_test.goplugins/reflect/plugin_runtime_test.goplugins/tools/mcp/plugin_test.goplugins/tools/lark-cli/plugin.go
Common Mistakes
- registering a capability without calling
SetInfo - using the wrong
PluginIDin a capability spec - reaching for global services instead of
ctx.Platform - mixing plugin ID and capability name
- adding a runtime without adding status or config when operators need them
- forgetting the blank import that triggers registration