Agent plugin system
The agent runtime is deliberately feature-free. Planning, skills, MCP, sub-agents, permissions, telemetry, annotations — every advanced capability ships as a self-contained plugin that contributes tools, hooks, state, HTTP routes, RPC methods, slash commands, workflows, and UI cards.
This page explains the Plugin contract, how composePlugins merges
contributions, and the portability seam. For a hands-on tutorial, see
Build your own plugin.
What is a Plugin?
Section titled “What is a Plugin?”A plugin is any object satisfying the Plugin<TState> interface exported from
@alphacro/agent-core (defined in packages/agent-core/src/plugins/types.ts).
It never reaches into the runtime — it declares contributions and the runtime
composes them.
import type { Plugin } from "@alphacro/agent-core";
export function createMyPlugin(): Plugin { return { name: "@alphacro/agent-my-feature", version: "1.0.0", description: "One-line summary of what composing this plugin adds.", tools: () => [/* ToolDefinition[] */], hooks: () => [/* AnyHookMatcher[] */], // ...other contributions };}Identity fields
Section titled “Identity fields”| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier. Convention: @alphacro/agent-<name>. |
version | string | Yes | SemVer version string. |
description | string | No | Human-readable summary of what composing the plugin adds. |
dependsOn | string[] | No | Other plugin names that must appear in the composition. The composer validates at startup. |
Contribution types
Section titled “Contribution types”A plugin can declare any combination of the following contributions.
All are optional — a minimal plugin only needs name and version.
tools?(): ReadonlyArray<ToolDefinition>Tool definitions contributed to the shared ToolRegistry. Names must be unique
across the entire composition — composePlugins throws on collisions.
hooks?(): ReadonlyArray<AnyHookMatcher>The runtime exposes a 17-hook surface. Plugins register typed handlers via
defineHook:
import { defineHook } from "@alphacro/agent-core";
defineHook("PreToolUse", { priority: 100, // higher runs first matcher: /^Write$/, // optional: only fire for matching tool names handler: async (input, ctx) => { // return a decision or void },});Hook reference
Section titled “Hook reference”| Hook | Category | Fires when | Can return |
|---|---|---|---|
SessionStart | Lifecycle | Session opens or resumes | additionalContext |
SessionEnd | Lifecycle | Session ends | void |
BeforeTurn | Lifecycle | Before each model step | void |
Stop | Lifecycle | Run stops (success, abort, or error) | void |
PreCompact | Lifecycle | Before context compaction | additionalContext |
DecorateToolSchema | Registry | Before model sees tools | Transformed tools[] |
DecorateSystemPrompt | Registry | Per-turn prompt assembly | SystemPromptFragment[] |
DecoratePromptMessages | Prompt | After buildPrompt, before model.doStream | Transformed prompt |
UserPromptSubmit | Per-turn | User sends a message | Block, rewrite, or add context |
PreToolUse | Per-tool | Before tool execution | allow / deny / ask / defer + updatedInput |
PostToolUse | Per-tool | After tool succeeds | additionalContext or updatedToolOutput |
PostToolUseFailure | Per-tool | After tool fails | additionalContext |
PostToolBatch | Per-tool | After a batch of tool calls | additionalContext |
SubagentStart | Sub-agents | Child agent spawned | void |
SubagentStop | Sub-agents | Child agent finished | void |
Notification | Misc | Notification emitted | void |
OnEvent | Misc | Every event appended to log | void |
Of these, 10 are Claude-Code-compatible (the compat adapter maps 1:1) and 7 are
AlphaCRO-only additions: BeforeTurn, DecorateToolSchema,
DecorateSystemPrompt, DecoratePromptMessages, PostToolUseFailure,
PostToolBatch, and OnEvent.
Hook ordering is controlled by priority (higher first) and plugin registration
order (tiebreak). Convention: permissions at +1000, telemetry at -100.
Sub-agents
Section titled “Sub-agents”subagents?(): ReadonlyArray<SubagentDefinition>Sub-agent definitions that become entries in the synthesised Agent meta-tool.
Each sub-agent specifies its own systemPrompt, allowedTools, isolation
mode ("shared" or "isolated"), and optional skills and plugins.
Slash commands
Section titled “Slash commands”slashCommands?(): ReadonlyArray<SlashCommandDefinition>User-facing / commands. The viewer surfaces them in the composer’s
autocomplete. Each command has a name (without the /), description,
optional argumentHint, and a handler.
HTTP routes
Section titled “HTTP routes”httpRoutes?(): ReadonlyArray<HttpRouteDefinition>Routes the plugin owns. The host (CF Worker or Node) registers them on the fetch handler. The composer detects collisions (same method + path).
RPC methods
Section titled “RPC methods”rpcMethods?(): ReadonlyArray<RpcMethodDefinition>Callable methods on the Durable Object (or JSON-RPC bridge on Node). Inputs can be validated with a Zod schema.
Workflows
Section titled “Workflows”workflows?(): ReadonlyArray<WorkflowDefinition>Durable workflows. The Cloudflare adapter mounts each as an AgentWorkflow
class; the Node adapter runs the body in-process via InProcessWorkflowExecutor.
Permissions
Section titled “Permissions”permissions?(): PluginPermissionsPermission policy contributions. Composed with deny-wins: if any plugin’s
canUseTool returns deny, the tool is blocked regardless of other plugins’
allow decisions. Evaluation order:
disallowedTools→ denyallowedTools→ allowcanUseTool(call, ctx)→ allow / deny / deferpermissionModedefault
Tool cards (UI)
Section titled “Tool cards (UI)”cards?(): ReadonlyArray<ToolCardEntry>React tool-card entries consumed by <ToolCardRouter />. The runtime keeps
agent-core free of React by type-erasing the render function.
Composable state
Section titled “Composable state”initialState?(): TStatereduce?(state: TState, event: AgentEvent): TStateState slices are keyed by plugin name and composed via type intersection.
Reducers run per-event in topological order (dependencies first). Plugins that
don’t own durable state omit these methods.
Lifecycle shortcuts
Section titled “Lifecycle shortcuts”onSessionStart?(ctx: HookContext): Promise<void> | voidonSessionEnd?(ctx: HookContext): Promise<void> | voidstartupPhase?: "A" | "B" | "C"onSessionStart / onSessionEnd are shortcuts for registering SessionStart /
SessionEnd hooks. startupPhase declares which startup phase the plugin
participates in (used by SessionStartupWorkflow for sequencing).
How composePlugins works
Section titled “How composePlugins works”import { composePlugins } from "@alphacro/agent-core";
const composed = composePlugins({ plugins: [p1, p2, p3] });composePlugins validates and merges an array of plugins into a
ComposedPlugins bundle:
- Duplicate check — throws if two plugins share the same
name. - Dependency validation — throws if any
dependsOnentry is missing. - Topological sort — plugins are ordered so dependencies come first. Stable on unrelated plugins (input order is preserved for tiebreaks).
- Collision detection — tool names, HTTP routes, RPC methods, sub-agent names, slash commands, and workflow names are checked for uniqueness.
- Aggregation — all contributions are collected into flat arrays. Sub-agents and slash commands are sorted alphabetically for stable model output.
The result exposes:
interface ComposedPlugins { plugins: Plugin[]; // topologically sorted tools: ToolDefinition[]; toolRegistry: ToolRegistry; hooks: AnyHookMatcher[]; subagents: SubagentDefinition[]; slashCommands: SlashCommandDefinition[]; httpRoutes: HttpRouteDefinition[]; rpcMethods: RpcMethodDefinition[]; workflows: WorkflowDefinition[]; permissions: { pluginName; permissions }[]; cards: ToolCardEntry[]; initialState: Record<string, unknown>; reducers: { pluginName; reduce }[]; ownership: { tools: Map<string, string>; // ... per contribution type };}Plugin ordering convention
Section titled “Plugin ordering convention”Order in the plugins array controls hook tiebreaks and reducer execution
order. The convention is:
- Runtime adapter (e.g.
@alphacro/agent-runtime-cloudflare) — first - Permissions — early, so the permission gate fires before other hooks
- Package plugins (planning, requirements) — middle
- Host userland plugins (editor-web, skills) — last, can read package state
This is encoded by the shared recipe in @alphacro/agent-session:
composeAgentSessionPlugins({ prePlugins: [runtimeAdapter, transpile], planning: planningPlugins, requirements: requirementsPlugins, extraPlugins: [delegation, skills, editorWebPlugin],});How the company agent uses this
Section titled “How the company agent uses this”You rarely call composeAgentSessionPlugins directly.
defineCoframeAgent (Layer 2) wraps it for
you: it calls buildSessionBundle (the @alphacro/agent-session recipe that
seeds the understand_goals root task and runs composeAgentSessionPlugins
under the hood) and slots the result into Layer 1’s compose seam. So the
ordering above is exactly what one defineCoframeAgent({}) produces:
| Slot | What the company agent puts there |
|---|---|
prePlugins | the host’s runtime adapter (e.g. transpile, permissions) |
planning / requirements | the seeded planning + requirements spine |
extraPlugins | common tools, delegation, alias evidence, then your extraPlugins / extraTools |
defineCoframeAgent’s onCompose(composition) hook hands back the
{ composed, toolRegistry, planningStore, excludeAliasToolNames, getRequirements }
this recipe produced, which is how the CLI and editor-web register
post-composition MCP tools and persist planning/requirements. Reach for this
recipe directly only when you are building a host that can’t use Layer 2.
The portability seam
Section titled “The portability seam”From AGENTS.md:
The model loop, tool registry, workspace contract, and state reducers compile and run without Cloudflare-specific symbols. The Cloudflare runtime is one adapter; a Node CLI is another.
Hard rule: anything that prevents the Node CLI from compiling must not
appear in agent-core, tool, workspace, or state packages.
Cloudflare-only types (DurableObject, R2Bucket, WorkflowEntrypoint,
container handles) belong in the runtime adapter or the app’s worker
entrypoint — never in the portable layer.
Every plugin package should be portable: no Cloudflare types, no SDK
coupling beyond @alphacro/agent-core / @alphacro/agent-tools. That is what
lets the same plugins compose under both the Node CLI and the Cloudflare
runtime.
Existing plugin packages
Section titled “Existing plugin packages”For generated per-package reference documentation, see the Plugin reference section.
| Package | What it contributes |
|---|---|
agent-planning | 7 task tools, 4 hooks (schema decoration, pre/post tool use, system prompt), 4 RPC methods |
agent-delegation | 4 delegation tools (spawn, wait, check status, fan-out), parallelism prompt hook |
agent-requirements | Requirement read + write tools, system prompt hook |
agent-skills-plugin | 4 skill tools (list, load, unload, install), prompt message rewriter hook |
agent-permissions | Permission triad PreToolUse hook (4-layer: disallowed → allowed → canUseTool → mode default) |
agent-claude-code-compat | Claude Code plugin loader: slash commands, sub-agents, hooks, skills from .claude-plugin/ directories |