Skip to content

Commit 5644006

Browse files
author
Eva (agent)
committed
refactor(plugin-sdk): add grouped host-hook facades
1 parent 1f94f51 commit 5644006

12 files changed

Lines changed: 190 additions & 39 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
4d6c3407a896d12a453c5ba2069450db279e9cb9bed0722aa9e6f3deff69da8f plugin-sdk-api-baseline.json
2-
33753fed10bd182a0314e4678e6d8d896c225a07fd5a2d6980ee7abd46640e7d plugin-sdk-api-baseline.jsonl
1+
7cfa5421ebf3a7d9202ace45a2883bf132bb3ea827f48c169a4bdc3af584f3b0 plugin-sdk-api-baseline.json
2+
6a8dd879cddc41f722aa43cc4912b0cc67047435dfe782cfd2d73987cc9e9a92 plugin-sdk-api-baseline.jsonl

docs/plugins/sdk-overview.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,23 @@ plugins.
146146
| `api.registerSessionSchedulerJob(...)` | Plugin-owned session scheduler job records with deterministic cleanup |
147147
| `api.sendSessionAttachment(...)` | Bundled-only host-mediated file attachment delivery to the active direct-outbound session route |
148148

149+
Grouped aliases are also available for the host-hook-heavy parts of the SDK.
150+
They are additive aliases over the existing flat methods, not new runtime
151+
plumbing, so both styles remain valid:
152+
153+
- `api.session.state.registerSessionExtension(...)`
154+
- `api.session.workflow.enqueueNextTurnInjection(...)`
155+
- `api.session.workflow.registerSessionSchedulerJob(...)`
156+
- `api.session.workflow.sendSessionAttachment(...)`
157+
- `api.session.workflow.scheduleSessionTurn(...)`
158+
- `api.session.workflow.unscheduleSessionTurnsByTag(...)`
159+
- `api.session.controls.registerSessionAction(...)`
160+
- `api.session.controls.registerControlUiDescriptor(...)`
161+
- `api.agent.events.registerAgentEventSubscription(...)`
162+
- `api.agent.events.emitAgentEvent(...)`
163+
- `api.runContext.setRunContext(...)` / `getRunContext(...)` / `clearRunContext(...)`
164+
- `api.lifecycle.registerRuntimeLifecycle(...)`
165+
149166
The contracts intentionally split authority:
150167

151168
- External plugins can own session extensions, UI descriptors, commands, tool

src/plugin-sdk/plugin-test-api.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { attachPluginApiFacades } from "../plugins/api-facades.js";
12
import type { OpenClawPluginApi } from "./plugin-runtime.js";
23

34
export type TestPluginApiInput = Partial<OpenClawPluginApi>;
45

56
export function createTestPluginApi(api: TestPluginApiInput = {}): OpenClawPluginApi {
6-
return {
7+
return attachPluginApiFacades({
78
id: "test-plugin",
89
name: "test-plugin",
910
source: "test",
@@ -70,10 +71,6 @@ export function createTestPluginApi(api: TestPluginApiInput = {}): OpenClawPlugi
7071
sendSessionAttachment: async () => ({ ok: false, error: "test plugin api" }),
7172
scheduleSessionTurn: async () => undefined,
7273
unscheduleSessionTurnsByTag: async () => ({ removed: 0, failed: 0 }),
73-
registerSessionAction() {},
74-
sendSessionAttachment: async () => ({ ok: false, error: "test plugin api" }),
75-
scheduleSessionTurn: async () => undefined,
76-
unscheduleSessionTurnsByTag: async () => ({ removed: 0, failed: 0 }),
7774
registerMemoryCapability() {},
7875
registerMemoryPromptSection() {},
7976
registerMemoryPromptSupplement() {},
@@ -86,5 +83,5 @@ export function createTestPluginApi(api: TestPluginApiInput = {}): OpenClawPlugi
8683
},
8784
on() {},
8885
...api,
89-
};
86+
});
9087
}

src/plugins/api-builder.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import { attachPluginApiFacades } from "./api-facades.js";
23
import type { PluginRuntime } from "./runtime/types.js";
34
import type { OpenClawPluginApi, PluginLogger } from "./types.js";
45

@@ -176,7 +177,7 @@ const noopOn: OpenClawPluginApi["on"] = () => {};
176177
export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi {
177178
const handlers = params.handlers ?? {};
178179
const registerCli = handlers.registerCli ?? noopRegisterCli;
179-
return {
180+
return attachPluginApiFacades({
180181
id: params.id,
181182
name: params.name,
182183
version: params.version,
@@ -260,9 +261,6 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
260261
clearRunContext: handlers.clearRunContext ?? noopClearRunContext,
261262
registerSessionSchedulerJob:
262263
handlers.registerSessionSchedulerJob ?? noopRegisterSessionSchedulerJob,
263-
scheduleSessionTurn: handlers.scheduleSessionTurn ?? noopScheduleSessionTurn,
264-
unscheduleSessionTurnsByTag:
265-
handlers.unscheduleSessionTurnsByTag ?? noopUnscheduleSessionTurnsByTag,
266264
registerSessionAction: handlers.registerSessionAction ?? noopRegisterSessionAction,
267265
sendSessionAttachment: handlers.sendSessionAttachment ?? noopSendSessionAttachment,
268266
scheduleSessionTurn: handlers.scheduleSessionTurn ?? noopScheduleSessionTurn,
@@ -283,5 +281,5 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
283281
handlers.registerMemoryEmbeddingProvider ?? noopRegisterMemoryEmbeddingProvider,
284282
resolvePath: params.resolvePath,
285283
on: handlers.on ?? noopOn,
286-
};
284+
});
287285
}

src/plugins/api-facades.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { OpenClawPluginApi } from "./types.js";
2+
3+
export function attachPluginApiFacades<T extends OpenClawPluginApi>(api: T): T {
4+
api.session = {
5+
state: {
6+
registerSessionExtension: (...args) => api.registerSessionExtension(...args),
7+
},
8+
workflow: {
9+
enqueueNextTurnInjection: (...args) => api.enqueueNextTurnInjection(...args),
10+
registerSessionSchedulerJob: (...args) => api.registerSessionSchedulerJob(...args),
11+
sendSessionAttachment: (...args) => api.sendSessionAttachment(...args),
12+
scheduleSessionTurn: (...args) => api.scheduleSessionTurn(...args),
13+
unscheduleSessionTurnsByTag: (...args) => api.unscheduleSessionTurnsByTag(...args),
14+
},
15+
controls: {
16+
registerSessionAction: (...args) => api.registerSessionAction(...args),
17+
registerControlUiDescriptor: (...args) => api.registerControlUiDescriptor(...args),
18+
},
19+
};
20+
api.agent = {
21+
events: {
22+
registerAgentEventSubscription: (...args) =>
23+
api.registerAgentEventSubscription(...args),
24+
emitAgentEvent: (...args) => api.emitAgentEvent(...args),
25+
},
26+
};
27+
api.runContext = {
28+
setRunContext: (...args) => api.setRunContext(...args),
29+
getRunContext: (...args) => api.getRunContext(...args),
30+
clearRunContext: (...args) => api.clearRunContext(...args),
31+
};
32+
api.lifecycle = {
33+
registerRuntimeLifecycle: (...args) => api.registerRuntimeLifecycle(...args),
34+
};
35+
return api;
36+
}

src/plugins/api-lifecycle.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { OpenClawPluginApi } from "./types.js";
2+
3+
type FunctionPropertyNames<T> = Extract<
4+
{
5+
[K in keyof T]-?: Exclude<T[K], undefined> extends (
6+
...args: unknown[]
7+
) => unknown
8+
? K
9+
: never;
10+
}[keyof T],
11+
string
12+
>;
13+
14+
export type PluginApiMethodName = FunctionPropertyNames<OpenClawPluginApi>;
15+
16+
export type PluginApiLifecyclePolicy = {
17+
phase: "registration" | "runtime";
18+
lateCallable: boolean;
19+
};
20+
21+
const PLUGIN_API_METHOD_POLICIES: Partial<Record<PluginApiMethodName, PluginApiLifecyclePolicy>> =
22+
{
23+
sendSessionAttachment: { phase: "runtime", lateCallable: true },
24+
scheduleSessionTurn: { phase: "runtime", lateCallable: true },
25+
unscheduleSessionTurnsByTag: { phase: "runtime", lateCallable: true },
26+
};
27+
28+
export function getPluginApiMethodLifecyclePolicy(
29+
methodName: string,
30+
): PluginApiLifecyclePolicy | undefined {
31+
return PLUGIN_API_METHOD_POLICIES[methodName as PluginApiMethodName];
32+
}
33+
34+
export function isLateCallablePluginApiMethod(methodName: string): methodName is PluginApiMethodName {
35+
return getPluginApiMethodLifecyclePolicy(methodName)?.lateCallable === true;
36+
}

src/plugins/captured-registration.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ export function createCapturedPluginRegistration(params?: {
114114
const sessionSchedulerJobs: PluginSessionSchedulerJobRegistration[] = [];
115115
const sessionActions: PluginSessionActionRegistration[] = [];
116116
let capturedSessionTurnCount = 0;
117-
const sessionActions: PluginSessionActionRegistration[] = [];
118117
const tools: AnyAgentTool[] = [];
119118
const modelCatalogProviders: UnifiedModelCatalogProviderPlugin[] = [];
120119
const pluginId = params?.id ?? "captured-plugin-registration";

src/plugins/contracts/run-context-lifecycle.contract.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe("plugin run context lifecycle", () => {
5757
setActivePluginRegistry(createEmptyPluginRegistry());
5858

5959
expect(
60-
capturedApi?.setRunContext({
60+
capturedApi?.runContext?.setRunContext({
6161
runId: "stale-run",
6262
namespace: "state",
6363
value: { stale: true },
@@ -76,7 +76,7 @@ describe("plugin run context lifecycle", () => {
7676
patch: { runId: "stale-run", namespace: "state", value: { live: true } },
7777
}),
7878
).toBe(true);
79-
capturedApi?.clearRunContext({ runId: "stale-run", namespace: "state" });
79+
capturedApi?.runContext?.clearRunContext({ runId: "stale-run", namespace: "state" });
8080
expect(
8181
getPluginRunContext({
8282
pluginId: "stale-run-context-plugin",
@@ -104,16 +104,16 @@ describe("plugin run context lifecycle", () => {
104104
setActivePluginRegistry(registry.registry);
105105

106106
expect(
107-
capturedApi?.setRunContext({
107+
capturedApi?.runContext?.setRunContext({
108108
runId: "restored-run",
109109
namespace: "state",
110110
value: { restored: true },
111111
}),
112112
).toBe(true);
113113
expect(
114-
getPluginRunContext({
115-
pluginId: "restored-run-context-plugin",
116-
get: { runId: "restored-run", namespace: "state" },
114+
capturedApi?.runContext?.getRunContext({
115+
runId: "restored-run",
116+
namespace: "state",
117117
}),
118118
).toEqual({ restored: true });
119119
});

src/plugins/contracts/session-actions.contract.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ describe("plugin session actions", () => {
801801

802802
try {
803803
expect(
804-
bundledApi?.emitAgentEvent({
804+
bundledApi?.agent?.events.emitAgentEvent({
805805
runId: "run-emit",
806806
sessionKey: " agent:main:main ",
807807
stream: "approval",

src/plugins/loader.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,7 @@ describe("loadOpenClawPlugins", () => {
14761476
"keeps sendSessionAttachment callable after register closes while blocking registration-only APIs",
14771477
run: () => {
14781478
const registerGatewayMethod = vi.fn();
1479+
const registerSessionExtension = vi.fn();
14791480
const sendSessionAttachment = vi.fn(async () => ({
14801481
ok: true as const,
14811482
channel: "proofchat",
@@ -1498,6 +1499,7 @@ describe("loadOpenClawPlugins", () => {
14981499
resolvePath: (input) => input,
14991500
handlers: {
15001501
registerGatewayMethod,
1502+
registerSessionExtension,
15011503
sendSessionAttachment,
15021504
},
15031505
});
@@ -1521,14 +1523,23 @@ describe("loadOpenClawPlugins", () => {
15211523
text: "attachment ready",
15221524
};
15231525
const lateResult = capturedApi?.sendSessionAttachment(attachmentParams);
1526+
const lateWorkflowResult = capturedApi?.session?.workflow.sendSessionAttachment(
1527+
attachmentParams,
1528+
);
1529+
capturedApi?.session?.state.registerSessionExtension({
1530+
namespace: "late",
1531+
description: "late extension should stay blocked",
1532+
});
15241533

15251534
expect(lateResult).toBe(sendSessionAttachment.mock.results[0]?.value);
1535+
expect(lateWorkflowResult).toBe(sendSessionAttachment.mock.results[1]?.value);
15261536
expect(sendSessionAttachment).toHaveBeenCalledWith({
15271537
sessionKey: "agent:main:main",
15281538
files: [{ path: "./proof-report.txt" }],
15291539
text: "attachment ready",
15301540
});
1531-
expect(sendSessionAttachment).toHaveBeenCalledTimes(1);
1541+
expect(sendSessionAttachment).toHaveBeenCalledTimes(2);
1542+
expect(registerSessionExtension).not.toHaveBeenCalled();
15321543
},
15331544
},
15341545
{

0 commit comments

Comments
 (0)