mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
4 Commits
v2026.4.25
...
codex/plug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd247bc20e | ||
|
|
49df1773fd | ||
|
|
7bb4105ac5 | ||
|
|
623cf440d1 |
557
experiments/plugin-sdk-namespaces-plan.md
Normal file
557
experiments/plugin-sdk-namespaces-plan.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# Plugin SDK Namespaces Plan
|
||||
|
||||
## TL;DR
|
||||
|
||||
OpenClaw should introduce a few clear SDK namespaces like `plugin`, `channel`,
|
||||
and `provider`, instead of keeping so much of the public surface flat.
|
||||
|
||||
The safe way to do that is:
|
||||
|
||||
- add thin ESM facade entrypoints, not TypeScript `namespace`
|
||||
- keep the root `openclaw/plugin-sdk` surface small
|
||||
- replace flat registration methods on `OpenClawPluginApi` with namespace groups
|
||||
- ship the cutover in one coordinated release instead of dragging old flat APIs
|
||||
along
|
||||
- forbid leaf modules from importing back through namespace facades
|
||||
|
||||
That gives plugin authors a cleaner SDK that feels closer to VS Code, without
|
||||
turning the SDK into a giant barrel or creating circular import problems.
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce public namespaces to the OpenClaw Plugin SDK so the surface feels
|
||||
closer to the VS Code extension API, while keeping the implementation tight,
|
||||
isolated, and resistant to circular imports.
|
||||
|
||||
This plan is about the public SDK shape. It is not a proposal to merge
|
||||
everything into one giant barrel.
|
||||
|
||||
## Why This Is Worth Doing
|
||||
|
||||
Today the Plugin SDK has three visible problems:
|
||||
|
||||
- The public package export surface is large and mostly flat.
|
||||
- `src/plugin-sdk/core.ts` and `src/plugin-sdk/index.ts` carry too many
|
||||
unrelated meanings.
|
||||
- `OpenClawPluginApi` is still a flat registration API even though
|
||||
`api.runtime` already proves grouped namespaces work well.
|
||||
|
||||
The result is harder docs, harder discovery, and too many helper names that
|
||||
look equally important even when they are not.
|
||||
|
||||
## Current Facts In The Repo
|
||||
|
||||
- Package exports are generated from a flat entrypoint list in
|
||||
`src/plugin-sdk/entrypoints.ts` and `scripts/lib/plugin-sdk-entrypoints.json`.
|
||||
- The root `openclaw/plugin-sdk` entry is intentionally tiny in
|
||||
`src/plugin-sdk/index.ts`.
|
||||
- `api.runtime` is already a successful namespace model. It groups behavior as
|
||||
`agent`, `subagent`, `media`, `imageGeneration`, `webSearch`, `tools`,
|
||||
`channel`, `events`, `logging`, `state`, `tts`, `mediaUnderstanding`, and
|
||||
`modelAuth` in `src/plugins/runtime/index.ts`.
|
||||
- The main plugin registration API is still flat in `OpenClawPluginApi` in
|
||||
`src/plugins/types.ts`.
|
||||
- The concrete API object is assembled in `src/plugins/registry.ts`, and a
|
||||
second partial copy exists in `src/plugins/captured-registration.ts`.
|
||||
|
||||
Those facts suggest a path that is low-risk:
|
||||
|
||||
- keep leaf modules as the source of truth
|
||||
- add namespace facades on top
|
||||
- cut docs, examples, and templates over in the same release as the namespace
|
||||
model
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Do Not Use TypeScript `namespace`
|
||||
|
||||
Use normal ESM modules and package exports.
|
||||
|
||||
The SDK already ships as package export subpaths. The namespace model should be
|
||||
implemented as public facade modules, not TypeScript `namespace` syntax.
|
||||
|
||||
### 2. Keep The Root Tiny
|
||||
|
||||
Do not turn `openclaw/plugin-sdk` into a giant VS Code-style monolith.
|
||||
|
||||
The closest safe equivalent is:
|
||||
|
||||
- a tiny root for shared types and a few universal values
|
||||
- a small number of explicit namespace entrypoints
|
||||
- optional ergonomic aggregation only after the namespace surfaces settle
|
||||
|
||||
### 3. Namespace Facades Must Be Thin
|
||||
|
||||
Namespace entrypoints should contain no real business logic.
|
||||
|
||||
They should only:
|
||||
|
||||
- re-export stable leaves
|
||||
- assemble small namespace objects
|
||||
|
||||
That keeps cycles and accidental coupling down.
|
||||
|
||||
### 4. Types Stay Direct And Easy To Import
|
||||
|
||||
Like VS Code, namespaces should mostly group behavior. Common types should stay
|
||||
directly importable from the root or the owning domain surface.
|
||||
|
||||
Examples:
|
||||
|
||||
- `ChannelPlugin`
|
||||
- `ProviderPlugin`
|
||||
- `OpenClawPluginApi`
|
||||
- `PluginRuntime`
|
||||
|
||||
### 5. Do Not Namespace Everything At Once
|
||||
|
||||
Only namespace areas that already have a clear public identity.
|
||||
|
||||
Phase 1 should focus on:
|
||||
|
||||
- `plugin`
|
||||
- `channel`
|
||||
- `provider`
|
||||
|
||||
`runtime` already has a good public namespace shape on `api.runtime` and should
|
||||
not be reopened as a giant package-export family in the first pass.
|
||||
|
||||
## Proposed Public Model
|
||||
|
||||
### Namespace Entry Points
|
||||
|
||||
Canonical public entrypoints:
|
||||
|
||||
- `openclaw/plugin-sdk/plugin`
|
||||
- `openclaw/plugin-sdk/channel`
|
||||
- `openclaw/plugin-sdk/provider`
|
||||
- `openclaw/plugin-sdk/runtime`
|
||||
- `openclaw/plugin-sdk/testing`
|
||||
|
||||
What each should mean:
|
||||
|
||||
- `plugin`
|
||||
- plugin entry helpers
|
||||
- shared plugin definition helpers
|
||||
- plugin-facing config schema helpers that are truly universal
|
||||
- `channel`
|
||||
- channel entry helpers
|
||||
- chat-channel builders
|
||||
- stable channel-facing contracts and helpers
|
||||
- `provider`
|
||||
- provider entry helpers
|
||||
- auth, catalog, models, onboard, stream, usage, and provider registration helpers
|
||||
- `runtime`
|
||||
- the existing `api.runtime` story and runtime-related public helpers that are
|
||||
truly stable
|
||||
- `testing`
|
||||
- plugin author testing helpers
|
||||
|
||||
### Nested Leaves
|
||||
|
||||
Under those namespaces, the long-term canonical leaves should become nested:
|
||||
|
||||
- `openclaw/plugin-sdk/channel/setup`
|
||||
- `openclaw/plugin-sdk/channel/pairing`
|
||||
- `openclaw/plugin-sdk/channel/reply-pipeline`
|
||||
- `openclaw/plugin-sdk/channel/contract`
|
||||
- `openclaw/plugin-sdk/channel/targets`
|
||||
- `openclaw/plugin-sdk/channel/actions`
|
||||
- `openclaw/plugin-sdk/channel/inbound`
|
||||
- `openclaw/plugin-sdk/channel/lifecycle`
|
||||
- `openclaw/plugin-sdk/channel/policy`
|
||||
- `openclaw/plugin-sdk/channel/feedback`
|
||||
- `openclaw/plugin-sdk/channel/config-schema`
|
||||
- `openclaw/plugin-sdk/channel/config-helpers`
|
||||
|
||||
- `openclaw/plugin-sdk/provider/auth`
|
||||
- `openclaw/plugin-sdk/provider/catalog`
|
||||
- `openclaw/plugin-sdk/provider/models`
|
||||
- `openclaw/plugin-sdk/provider/onboard`
|
||||
- `openclaw/plugin-sdk/provider/stream`
|
||||
- `openclaw/plugin-sdk/provider/usage`
|
||||
- `openclaw/plugin-sdk/provider/web-search`
|
||||
|
||||
Not every current flat subpath needs a namespaced replacement. The goal is to
|
||||
promote the stable public domains, not to preserve every current export forever.
|
||||
|
||||
## What Happens To `core`
|
||||
|
||||
`core` is overloaded today. In a namespace model it should shrink, not grow.
|
||||
|
||||
Target split:
|
||||
|
||||
- plugin-wide entry helpers move toward `plugin`
|
||||
- channel builders and channel-oriented shared helpers move toward `channel`
|
||||
- `core` stops being a first-class public destination and shrinks to the
|
||||
smallest possible remaining shared surface
|
||||
|
||||
Rule: no new public API should be added to `core` once namespace entrypoints
|
||||
exist.
|
||||
|
||||
## Proposed `OpenClawPluginApi` Shape
|
||||
|
||||
Keep context fields flat:
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `version`
|
||||
- `description`
|
||||
- `source`
|
||||
- `rootDir`
|
||||
- `registrationMode`
|
||||
- `config`
|
||||
- `pluginConfig`
|
||||
- `runtime`
|
||||
- `logger`
|
||||
- `resolvePath`
|
||||
|
||||
Move registration behavior behind namespaces:
|
||||
|
||||
| Current flat method | Proposed namespace location |
|
||||
| ------------------------------------ | ----------------------------------------- |
|
||||
| `registerTool` | `api.tool.register` |
|
||||
| `registerHook` | `api.hook.register` |
|
||||
| `on` | `api.hook.on` |
|
||||
| `registerHttpRoute` | `api.http.registerRoute` |
|
||||
| `registerChannel` | `api.channel.register` |
|
||||
| `registerProvider` | `api.provider.register` |
|
||||
| `registerSpeechProvider` | `api.provider.registerSpeech` |
|
||||
| `registerMediaUnderstandingProvider` | `api.provider.registerMediaUnderstanding` |
|
||||
| `registerImageGenerationProvider` | `api.provider.registerImageGeneration` |
|
||||
| `registerWebSearchProvider` | `api.provider.registerWebSearch` |
|
||||
| `registerGatewayMethod` | `api.gateway.registerMethod` |
|
||||
| `registerCli` | `api.cli.register` |
|
||||
| `registerService` | `api.service.register` |
|
||||
| `registerInteractiveHandler` | `api.interactive.register` |
|
||||
| `registerCommand` | `api.command.register` |
|
||||
| `registerContextEngine` | `api.contextEngine.register` |
|
||||
| `registerMemoryPromptSection` | `api.memory.registerPromptSection` |
|
||||
|
||||
The cutover should replace the flat methods in one coordinated change.
|
||||
|
||||
That gives plugin authors a clearer public shape and avoids carrying two public
|
||||
registration models at the same time.
|
||||
|
||||
## Example Public Usage
|
||||
|
||||
Proposed style:
|
||||
|
||||
```ts
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin";
|
||||
import { channel } from "openclaw/plugin-sdk/channel";
|
||||
import { provider } from "openclaw/plugin-sdk/provider";
|
||||
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
|
||||
const chatPlugin: ChannelPlugin = channel.createChatPlugin({
|
||||
id: "demo",
|
||||
/* ... */
|
||||
});
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "demo",
|
||||
register(api: OpenClawPluginApi) {
|
||||
api.channel.register(chatPlugin);
|
||||
api.command.register({
|
||||
name: "status",
|
||||
description: "Show plugin status",
|
||||
run: async () => ({ text: "ok" }),
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This is close to the VS Code mental model:
|
||||
|
||||
- grouped behavior
|
||||
- direct types
|
||||
- obvious public areas
|
||||
|
||||
without requiring a single monolithic root import.
|
||||
|
||||
## Optional Ergonomic Surface
|
||||
|
||||
If the project later wants the closest possible VS Code feel, add a dedicated
|
||||
opt-in facade such as `openclaw/plugin-sdk/sdk`.
|
||||
|
||||
That facade can assemble:
|
||||
|
||||
- `plugin`
|
||||
- `channel`
|
||||
- `provider`
|
||||
- `runtime`
|
||||
- `testing`
|
||||
|
||||
It should not be phase 1.
|
||||
|
||||
Why:
|
||||
|
||||
- it is the highest-risk barrel from a cycle and weight perspective
|
||||
- it is easier to add once the namespace surfaces already exist
|
||||
- it preserves the root `openclaw/plugin-sdk` entry as a small type-oriented
|
||||
surface
|
||||
|
||||
## Internal Implementation Rules
|
||||
|
||||
These rules are the important part. Without them, namespaces will rot into
|
||||
barrels and cycles.
|
||||
|
||||
### Rule 1: Namespace Facades Are One-Way
|
||||
|
||||
Namespace entrypoints may import leaf modules.
|
||||
|
||||
Leaf modules may not import their namespace entrypoint.
|
||||
|
||||
Examples:
|
||||
|
||||
- allowed: `src/plugin-sdk/channel.ts` importing `./channel-setup.ts`
|
||||
- forbidden: `src/plugin-sdk/channel-setup.ts` importing `./channel.ts`
|
||||
|
||||
### Rule 1A: Allowed Dependency Directions Must Be Explicit
|
||||
|
||||
The allowed directions should be:
|
||||
|
||||
- namespace facade -> leaves in the same namespace
|
||||
- leaf -> local implementation helpers
|
||||
- leaf -> dedicated shared internal leaf
|
||||
- leaf -> another leaf in the same namespace only by direct relative import,
|
||||
never through the namespace facade
|
||||
|
||||
The forbidden directions should be:
|
||||
|
||||
- leaf -> its own namespace facade
|
||||
- leaf -> another namespace facade
|
||||
- namespace facade -> another namespace facade
|
||||
- channel leaf -> provider leaf, or provider leaf -> channel leaf, unless the
|
||||
dependency is first extracted into a shared internal leaf
|
||||
|
||||
Short version:
|
||||
|
||||
- facades point downward
|
||||
- leaves never point back upward
|
||||
- cross-namespace sharing must go sideways through a shared internal leaf, not
|
||||
directly through another public namespace
|
||||
|
||||
### Rule 1B: If Two Namespaces Need Each Other, Extract A Shared Leaf
|
||||
|
||||
If `channel` and `provider` start needing each other directly, that is the sign
|
||||
that the seam is wrong.
|
||||
|
||||
Do not allow:
|
||||
|
||||
- `src/plugin-sdk/channel/*` importing from `src/plugin-sdk/provider/*`
|
||||
- `src/plugin-sdk/provider/*` importing from `src/plugin-sdk/channel/*`
|
||||
|
||||
Instead:
|
||||
|
||||
- extract the shared logic into a dedicated internal leaf
|
||||
- let both sides depend on that leaf
|
||||
- keep the public namespaces separate
|
||||
|
||||
This is the main cycle-prevention rule. Shared logic moves to a lower layer
|
||||
before it creates a back-edge.
|
||||
|
||||
### Rule 2: No Public-Specifier Self-Imports Inside The SDK
|
||||
|
||||
Files inside `src/plugin-sdk/**` should never import from
|
||||
`openclaw/plugin-sdk/...`.
|
||||
|
||||
They should import local source files directly.
|
||||
|
||||
### Rule 3: Shared Code Lives In Shared Leaves
|
||||
|
||||
If `channel` and `provider` need the same implementation detail, move that code
|
||||
to a shared leaf instead of importing one namespace from the other.
|
||||
|
||||
Good shared homes:
|
||||
|
||||
- a dedicated internal shared leaf
|
||||
- a very small shared core leaf only if it has a precise, stable reason to
|
||||
exist
|
||||
- existing domain-neutral helpers
|
||||
|
||||
Bad pattern:
|
||||
|
||||
- `provider/*` importing from `channel/index`
|
||||
- `channel/*` importing from `provider/index`
|
||||
|
||||
### Rule 4: Assemble The API Surface Once
|
||||
|
||||
`OpenClawPluginApi` should be built by one canonical factory.
|
||||
|
||||
`src/plugins/registry.ts` and `src/plugins/captured-registration.ts` should stop
|
||||
hand-building separate versions of the API object.
|
||||
|
||||
That factory can expose:
|
||||
|
||||
- the namespaced shape only
|
||||
|
||||
from the same underlying implementation.
|
||||
|
||||
### Rule 5: Namespace Entry Files Stay Small
|
||||
|
||||
Namespace facades should stay close to pure exports. If a namespace file grows
|
||||
real orchestration logic, split that logic back into leaf modules.
|
||||
|
||||
### Dependency Shape
|
||||
|
||||
The intended import graph is:
|
||||
|
||||
```text
|
||||
public facade
|
||||
-> same-namespace leaves
|
||||
-> local helpers
|
||||
-> shared internal leaves
|
||||
```
|
||||
|
||||
Not this:
|
||||
|
||||
```text
|
||||
channel facade -> provider facade
|
||||
channel leaf -> channel facade
|
||||
provider leaf -> channel leaf
|
||||
```
|
||||
|
||||
Concrete examples:
|
||||
|
||||
- allowed: `src/plugin-sdk/channel.ts` -> `./channel/setup.ts`
|
||||
- allowed: `src/plugin-sdk/channel/setup.ts` -> `./_internal/channel-shared.ts`
|
||||
- allowed: `src/plugin-sdk/provider/auth.ts` -> `../_internal/provider-shared.ts`
|
||||
- forbidden: `src/plugin-sdk/channel/setup.ts` -> `./channel.ts`
|
||||
- forbidden: `src/plugin-sdk/channel/setup.ts` -> `../provider/index.ts`
|
||||
- forbidden: `src/plugin-sdk/channel.ts` -> `./provider.ts`
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
This should be a cutover, not a long overlap period.
|
||||
|
||||
That means:
|
||||
|
||||
- one coordinated release
|
||||
- one migration guide
|
||||
- one docs/templates/test update
|
||||
- one public SDK shape after the release
|
||||
|
||||
## Phase 1: Extract The Canonical API Builder
|
||||
|
||||
Do this first, before changing the public surface.
|
||||
|
||||
Why:
|
||||
|
||||
- it removes duplicated API assembly
|
||||
- it gives one place to switch the public shape
|
||||
- it reduces cutover risk
|
||||
|
||||
Implementation:
|
||||
|
||||
- extract one canonical API builder from `src/plugins/registry.ts` and
|
||||
`src/plugins/captured-registration.ts`
|
||||
- make that builder assemble the new namespaced registration API
|
||||
|
||||
## Phase 2: Add Canonical Namespace Entrypoints
|
||||
|
||||
Add:
|
||||
|
||||
- `plugin`
|
||||
- `channel`
|
||||
- `provider`
|
||||
|
||||
as thin public facades over existing flat leaves.
|
||||
|
||||
Implementation detail:
|
||||
|
||||
- the first pass can re-export current flat files
|
||||
- do not move source layout and package exports in the same commit if it can be
|
||||
avoided
|
||||
|
||||
Examples:
|
||||
|
||||
- `src/plugin-sdk/channel/setup.ts` can initially re-export from
|
||||
`../channel-setup.js`
|
||||
- `src/plugin-sdk/provider/auth.ts` can initially re-export from
|
||||
`../provider-auth.js`
|
||||
|
||||
This lets the public namespace story land before the internal source move,
|
||||
without forcing all implementation files to move in the same commit.
|
||||
|
||||
## Phase 3: Cut Public API, Docs, And Templates Together
|
||||
|
||||
In the same release:
|
||||
|
||||
- docs prefer namespaced entrypoints
|
||||
- templates prefer namespaced imports
|
||||
- tests and examples switch to the namespaced shape
|
||||
- `OpenClawPluginApi` changes to the namespaced registration model
|
||||
- flat registration methods are removed instead of carried as aliases
|
||||
|
||||
## Phase 4: Remove The Old Public Story
|
||||
|
||||
After the cutover release lands:
|
||||
|
||||
- stop documenting superseded flat leaves as public API
|
||||
- keep only the namespace model in author-facing docs
|
||||
- remove any leftover flat registration surface that survived only as
|
||||
transitional scaffolding during implementation
|
||||
|
||||
## What Should Not Be Namespaced In Phase 1
|
||||
|
||||
To keep the refactor tight, do not force these into the first milestone:
|
||||
|
||||
- every `*-runtime` helper subpath
|
||||
- extension-branded public subpaths
|
||||
- one-off utilities that do not yet have a stable domain home
|
||||
- the root `openclaw/plugin-sdk` barrel
|
||||
|
||||
If a subpath is only public because history leaked it, namespace work should not
|
||||
promote it.
|
||||
|
||||
## Guardrails And Validation
|
||||
|
||||
The namespace rollout should ship with explicit checks.
|
||||
|
||||
### Existing Checks To Reuse
|
||||
|
||||
- `src/plugin-sdk/subpaths.test.ts`
|
||||
- `src/plugin-sdk/runtime-api-guardrails.test.ts`
|
||||
- `pnpm build` for `[CIRCULAR_REEXPORT]` warnings
|
||||
- `pnpm plugin-sdk:api:check`
|
||||
|
||||
### New Checks To Add
|
||||
|
||||
- namespace facade files may only re-export or compose approved leaves
|
||||
- leaf files under a namespace may not import their parent `index` facade
|
||||
- leaf files under one namespace may not import another namespace facade
|
||||
- cross-namespace leaf imports should fail unless the target is an approved
|
||||
shared internal leaf
|
||||
- namespace facades may not import other namespace facades
|
||||
- no new API should be added to `core` once namespace facades exist
|
||||
- `OpenClawPluginApi` must not expose both flat and namespaced registration
|
||||
methods after cutover
|
||||
|
||||
## Recommended End State
|
||||
|
||||
The elegant end state is:
|
||||
|
||||
- a tiny root
|
||||
- a few first-class namespaces
|
||||
- direct types
|
||||
- a grouped `api` registration surface
|
||||
- stable leaves under each namespace
|
||||
- no reverse imports from leaves back into namespace facades
|
||||
|
||||
That gives OpenClaw a VS Code-like feel where the public SDK has clear domains,
|
||||
but still respects the repo's existing build, lazy-loading, and package-boundary
|
||||
constraints.
|
||||
|
||||
## Short Recommendation
|
||||
|
||||
If this work starts soon, the first implementation step should be:
|
||||
|
||||
1. extract one canonical `OpenClawPluginApi` builder
|
||||
2. switch that builder to the namespaced registration shape
|
||||
3. add `plugin`, `channel`, and `provider` facade entrypoints
|
||||
4. cut docs, templates, and examples over in the same release
|
||||
5. remove the old flat registration story instead of maintaining dual public APIs
|
||||
|
||||
That sequence keeps the refactor elegant and minimizes the chance that
|
||||
namespaces become another layer of accidental coupling.
|
||||
Reference in New Issue
Block a user