Skip to content

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.

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
};
}
FieldTypeRequiredDescription
namestringYesUnique identifier. Convention: @alphacro/agent-<name>.
versionstringYesSemVer version string.
descriptionstringNoHuman-readable summary of what composing the plugin adds.
dependsOnstring[]NoOther plugin names that must appear in the composition. The composer validates at startup.

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
},
});
HookCategoryFires whenCan return
SessionStartLifecycleSession opens or resumesadditionalContext
SessionEndLifecycleSession endsvoid
BeforeTurnLifecycleBefore each model stepvoid
StopLifecycleRun stops (success, abort, or error)void
PreCompactLifecycleBefore context compactionadditionalContext
DecorateToolSchemaRegistryBefore model sees toolsTransformed tools[]
DecorateSystemPromptRegistryPer-turn prompt assemblySystemPromptFragment[]
DecoratePromptMessagesPromptAfter buildPrompt, before model.doStreamTransformed prompt
UserPromptSubmitPer-turnUser sends a messageBlock, rewrite, or add context
PreToolUsePer-toolBefore tool executionallow / deny / ask / defer + updatedInput
PostToolUsePer-toolAfter tool succeedsadditionalContext or updatedToolOutput
PostToolUseFailurePer-toolAfter tool failsadditionalContext
PostToolBatchPer-toolAfter a batch of tool callsadditionalContext
SubagentStartSub-agentsChild agent spawnedvoid
SubagentStopSub-agentsChild agent finishedvoid
NotificationMiscNotification emittedvoid
OnEventMiscEvery event appended to logvoid

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.

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.

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.

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

rpcMethods?(): ReadonlyArray<RpcMethodDefinition>

Callable methods on the Durable Object (or JSON-RPC bridge on Node). Inputs can be validated with a Zod schema.

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?(): PluginPermissions

Permission 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:

  1. disallowedTools → deny
  2. allowedTools → allow
  3. canUseTool(call, ctx) → allow / deny / defer
  4. permissionMode default
cards?(): ReadonlyArray<ToolCardEntry>

React tool-card entries consumed by <ToolCardRouter />. The runtime keeps agent-core free of React by type-erasing the render function.

initialState?(): TState
reduce?(state: TState, event: AgentEvent): TState

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

onSessionStart?(ctx: HookContext): Promise<void> | void
onSessionEnd?(ctx: HookContext): Promise<void> | void
startupPhase?: "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).

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:

  1. Duplicate check — throws if two plugins share the same name.
  2. Dependency validation — throws if any dependsOn entry is missing.
  3. Topological sort — plugins are ordered so dependencies come first. Stable on unrelated plugins (input order is preserved for tiebreaks).
  4. Collision detection — tool names, HTTP routes, RPC methods, sub-agent names, slash commands, and workflow names are checked for uniqueness.
  5. 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
};
}

Order in the plugins array controls hook tiebreaks and reducer execution order. The convention is:

  1. Runtime adapter (e.g. @alphacro/agent-runtime-cloudflare) — first
  2. Permissions — early, so the permission gate fires before other hooks
  3. Package plugins (planning, requirements) — middle
  4. 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],
});

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:

SlotWhat the company agent puts there
prePluginsthe host’s runtime adapter (e.g. transpile, permissions)
planning / requirementsthe seeded planning + requirements spine
extraPluginscommon 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.

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.

For generated per-package reference documentation, see the Plugin reference section.

PackageWhat it contributes
agent-planning7 task tools, 4 hooks (schema decoration, pre/post tool use, system prompt), 4 RPC methods
agent-delegation4 delegation tools (spawn, wait, check status, fan-out), parallelism prompt hook
agent-requirementsRequirement read + write tools, system prompt hook
agent-skills-plugin4 skill tools (list, load, unload, install), prompt message rewriter hook
agent-permissionsPermission triad PreToolUse hook (4-layer: disallowed → allowed → canUseTool → mode default)
agent-claude-code-compatClaude Code plugin loader: slash commands, sub-agents, hooks, skills from .claude-plugin/ directories