Plugins¶
Maven is a plugin host. All integrations are plugins. The kernel (internal/kernel/) never imports a plugin. internal/gateway/wire.go is the only file that assembles them.
Base interface¶
Every plugin implements the minimal lifecycle (internal/kernel/plugin/plugin.go):
Start and Stop are called on every registered plugin in registration order, regardless of which axes it contributes.
Axis interfaces¶
A plugin contributes to one or more axes by implementing additional interfaces. None are required; nil contributions are fine.
| Interface | Method | Contributes |
|---|---|---|
ChannelPlugin |
Channels(cfg) []channels.Channel |
Chat transports. |
ToolPlugin |
Tools(cfg) []tool.Tool |
Agent tools registered with the runtime. |
SkillPlugin |
Skills(cfg) []api.SkillRegistration |
Prompt-time context injection. |
TTSPlugin |
TTSProvider(cfg) voice.TTS |
Text-to-speech provider. |
STTPlugin |
STTProvider(cfg) voice.STT |
Speech-to-text provider. |
SlashPlugin |
SlashCommands(cfg) []SlashCommand |
Pre-model /commands. |
TriggerPlugin |
Triggers(cfg) []Trigger |
Background execution. |
MemoryPlugin |
Read(ctx, cfg, q) ([]MemoryEntry, error) plus Primary() semantics |
Long-term memory. |
Registry aggregation¶
plugin.NewRegistry(plugins...) fixes registration order. At Apply time the gateway calls each axis method on plugins that implement it:
| Axis | Aggregation |
|---|---|
| Channels, Tools, Skills | Concatenated in registration order. |
| TTS, STT | First non-nil result wins. |
| SlashCommands | Concatenated; duplicate names error during registry build. |
| Triggers | Each trigger's Start(ctx, TurnExecutor, OutboundPublisher) is called; all execution flows through the same pipeline the chat path uses. |
| Memory | Read fans out concurrently with a 500 ms budget; Write routes to the single plugin where Primary() == true. Exactly one primary required. |
Trigger contract¶
type Trigger interface {
Name() string
Start(ctx context.Context, exec executor.TurnExecutor, pub OutboundPublisher) error
Stop() error
}
Triggers receive the pipeline as a TurnExecutor. Cron also receives the outbound publisher so it can deliver job output to channels.
Outbound publisher¶
type OutboundPublisher interface {
PublishOutbound(ctx context.Context, channel, chatID, content string) error
}
Narrow surface over bus.MessageBus.PublishOutbound. Triggers never see the bus directly.
Registered plugins¶
All plugins are wired in internal/gateway/wire.go:
| Package | Axes |
|---|---|
plugins/channel/telegram |
Channel |
plugins/channel/feishu |
Channel |
plugins/channel/wecom |
Channel |
plugins/channel/whatsapp |
Channel |
plugins/channel/matrix |
Channel |
plugins/channel/web |
Channel |
plugins/trigger/cron |
Trigger + Tool + Slash |
plugins/trigger/heartbeat |
Trigger |
plugins/trigger/memconsolidate |
Trigger |
plugins/skill/file |
Skill |
plugins/voice/deepgram |
STT + TTS |
plugins/voice/openai |
TTS |
plugins/voice/elevenlabs |
TTS |
plugins/voice/cartesia |
TTS |
plugins/tool/acp |
Tool |
plugins/memory/file |
Memory (primary) + Tool (remember, memory_search, memory_get) |
Adding a plugin¶
- Create
internal/plugins/<axis>/<name>/. - Define a type implementing
plugin.Pluginplus the axis interfaces you need. - Export
NewPlugin(...)returning the concrete type (or aplugin.AxisPlugininterface). - Add one line to
internal/gateway/wire.goinsideplugin.NewRegistry(...).
Zero changes to any kernel package.
Example: a Discord channel plugin
// internal/plugins/channel/discord/plugin.go
package discord
type Plugin struct{ /* … */ }
func NewPlugin(b *bus.MessageBus, lg *slog.Logger) plugin.ChannelPlugin { /* … */ }
func (p *Plugin) Name() string { return "discord" }
func (p *Plugin) Start(context.Context) error { return nil }
func (p *Plugin) Stop() error { return nil }
func (p *Plugin) Channels(cfg *config.Config) []channels.Channel { /* … */ }
Then in wire.go:
That is the entire change required.
Kernel wall enforcement¶
internal/kernel/ must never import internal/plugins/. Enforced by:
- Architectural rule: plugins depend on kernel, never the reverse.
depguardlinter rulekernel_no_pluginsin.golangci.yml:
depguard:
rules:
kernel_no_plugins:
files:
- "**/internal/kernel/**"
deny:
- pkg: "github.com/ageneralai/maven/internal/plugins/"
desc: "kernel must not import plugins"
Only internal/gateway/wire.go (and tests) cross the wall.