Build your own plugin
This walkthrough uses @alphacro/agent-planning as the worked example — a
real plugin that contributes tools, hooks, state, and RPC methods. By the
end you’ll have a complete picture of how to build, wire, and test a plugin.
Step 1 — Define the factory function
Section titled “Step 1 — Define the factory function”import type { Plugin } from "@alphacro/agent-core";
export function createPlanningPlugin( options: CreatePlanningPluginOptions = {},): Plugin<PlanningState> { // ... return { name: "@alphacro/agent-planning", version: "1.0.0", description: "Task DAG plugin: tasks, dependencies, and task-scoped tool decoration.", // contributions follow };}Plugins are created via factory functions (not classes) so the host can inject
dependencies. The options object carries injected ports — stores, ID
generators, clocks — that keep the plugin testable without mocks.
Step 2 — Contribute tools
Section titled “Step 2 — Contribute tools”tools: () => [ createListTasksTool(store), createGetNextTasksTool(store), createUpsertTaskTool({ store, now }), createCompleteTaskTool({ store, now }), createCancelTaskTool({ store, now }), createUpsertTaskDependencyTool({ store, newDependencyId, now }), createDeleteTaskDependencyTool({ store, now }),],Each tool is a standalone ToolDefinition with its own input schema (Zod),
execute function, and description. The composer collects them into a single
ToolRegistry.
Step 3 — Contribute hooks
Section titled “Step 3 — Contribute hooks”hooks: () => [ decorateToolSchemaHook(store, { excludeToolNames: exempt }), preToolUseHook(store, { exemptToolNames: exempt, now }), postToolUseHook(store, now), decorateSystemPromptHook(store),],The planning plugin uses four hooks:
DecorateToolSchema— adds atask_idsfield to every non-planning tool’s input schema while tasks are active, so the model scopes each action to a task.PreToolUse— validatestask_idsand auto-starts pending tasks on first reference.PostToolUse— rolls uptask.progressfrom tool results.DecorateSystemPrompt— surfaces the active task list to the model.
Step 4 — Add RPC methods (optional)
Section titled “Step 4 — Add RPC methods (optional)”rpcMethods: () => planningRpcMethods({ store, newTaskId, newDependencyId, now }),RPC methods let the SPA drive the same store the model uses — the viewer can create, reorder, or delete tasks through the same interface.
Step 5 — Add state (optional)
Section titled “Step 5 — Add state (optional)”initialState: () => initialPlanningState(),reduce: (state) => state,When a plugin owns durable state, it provides an initial state factory and a
reducer. The runner hydrates state from the event log via reducePluginState.
Step 6 — Wire it into a host
Section titled “Step 6 — Wire it into a host”In editor-web, the host calls composeAgentSessionPlugins:
composeAgentSessionPlugins({ prePlugins: [createCloudflareRuntimePlugin(), createTranspilePlugin()], planning: planningHost.composed.plugins, extraPlugins: [skillsPlugin, editorWebPlugin],});In the CLI, the same planning plugin is composed with a Node runtime adapter instead of the Cloudflare one — same plugin, different host.
Step 7 — Test in isolation
Section titled “Step 7 — Test in isolation”Plugins are pure functions of their inputs. Test the factory, the tools, and the
hooks independently with a mock HookContext:
const plugin = createPlanningPlugin({ store: new PlanningStore() });const tools = plugin.tools!();expect(tools.map((t) => t.name)).toContain("upsert_task");No platform pool, no Durable Object, no edge runtime needed.
Summary
Section titled “Summary”A plugin is a plain object with a factory function. It declares what it
contributes (tools, hooks, state, RPC, etc.) and the runtime composes
everything via composePlugins. The key principles:
- Factory functions — not classes. Inject dependencies via options.
- Typed contributions — tools, hooks, state slices are all typed interfaces.
- Collision-safe composition — the composer validates uniqueness at startup.
- Layer-2 portable — no Cloudflare types in plugin packages.
- Testable in isolation — no platform runtime needed for unit tests.