mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 22:10:51 +00:00
fix(plugins): late-binding subagent runtime for non-gateway load paths (#46648)
Merged via squash.
Prepared head SHA: 44742652c9
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -10,11 +10,16 @@ vi.mock("../../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||
}));
|
||||
|
||||
import { createPluginRuntime } from "./index.js";
|
||||
import {
|
||||
clearGatewaySubagentRuntime,
|
||||
createPluginRuntime,
|
||||
setGatewaySubagentRuntime,
|
||||
} from "./index.js";
|
||||
|
||||
describe("plugin runtime command execution", () => {
|
||||
beforeEach(() => {
|
||||
runCommandWithTimeoutMock.mockClear();
|
||||
clearGatewaySubagentRuntime();
|
||||
});
|
||||
|
||||
it("exposes runtime.system.runCommandWithTimeout by default", async () => {
|
||||
@@ -82,4 +87,37 @@ describe("plugin runtime command execution", () => {
|
||||
// Wrappers should NOT be the same reference as the raw functions
|
||||
expect(runtime.modelAuth.getApiKeyForModel).not.toBe(rawGetApiKey);
|
||||
});
|
||||
|
||||
it("keeps subagent unavailable by default even after gateway initialization", async () => {
|
||||
const runtime = createPluginRuntime();
|
||||
setGatewaySubagentRuntime({
|
||||
run: vi.fn(),
|
||||
waitForRun: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
deleteSession: vi.fn(),
|
||||
});
|
||||
|
||||
expect(() => runtime.subagent.run({ sessionKey: "s-1", message: "hello" })).toThrow(
|
||||
"Plugin runtime subagent methods are only available during a gateway request.",
|
||||
);
|
||||
});
|
||||
|
||||
it("late-binds to the gateway subagent when explicitly enabled", async () => {
|
||||
const run = vi.fn().mockResolvedValue({ runId: "run-1" });
|
||||
const runtime = createPluginRuntime({ allowGatewaySubagentBinding: true });
|
||||
|
||||
setGatewaySubagentRuntime({
|
||||
run,
|
||||
waitForRun: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
deleteSession: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(runtime.subagent.run({ sessionKey: "s-2", message: "hello" })).resolves.toEqual({
|
||||
runId: "run-1",
|
||||
});
|
||||
expect(run).toHaveBeenCalledWith({ sessionKey: "s-2", message: "hello" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,8 +46,82 @@ function createUnavailableSubagentRuntime(): PluginRuntime["subagent"] {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Process-global gateway subagent runtime ─────────────────────────
|
||||
// The gateway creates a real subagent runtime during startup, but gateway-owned
|
||||
// plugin registries may be loaded (and cached) before the gateway path runs.
|
||||
// A process-global holder lets explicitly gateway-bindable runtimes resolve the
|
||||
// active gateway subagent dynamically without changing the default behavior for
|
||||
// ordinary plugin runtimes.
|
||||
|
||||
const GATEWAY_SUBAGENT_SYMBOL: unique symbol = Symbol.for(
|
||||
"openclaw.plugin.gatewaySubagentRuntime",
|
||||
) as unknown as typeof GATEWAY_SUBAGENT_SYMBOL;
|
||||
|
||||
type GatewaySubagentState = {
|
||||
subagent: PluginRuntime["subagent"] | undefined;
|
||||
};
|
||||
|
||||
const gatewaySubagentState: GatewaySubagentState = (() => {
|
||||
const g = globalThis as typeof globalThis & {
|
||||
[GATEWAY_SUBAGENT_SYMBOL]?: GatewaySubagentState;
|
||||
};
|
||||
const existing = g[GATEWAY_SUBAGENT_SYMBOL];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const created: GatewaySubagentState = { subagent: undefined };
|
||||
g[GATEWAY_SUBAGENT_SYMBOL] = created;
|
||||
return created;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Set the process-global gateway subagent runtime.
|
||||
* Called during gateway startup so that gateway-bindable plugin runtimes can
|
||||
* resolve subagent methods dynamically even when their registry was cached
|
||||
* before the gateway finished loading plugins.
|
||||
*/
|
||||
export function setGatewaySubagentRuntime(subagent: PluginRuntime["subagent"]): void {
|
||||
gatewaySubagentState.subagent = subagent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the process-global gateway subagent runtime.
|
||||
* Used by tests to avoid leaking gateway state across module reloads.
|
||||
*/
|
||||
export function clearGatewaySubagentRuntime(): void {
|
||||
gatewaySubagentState.subagent = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a late-binding subagent that resolves to:
|
||||
* 1. An explicitly provided subagent (from runtimeOptions), OR
|
||||
* 2. The process-global gateway subagent when the caller explicitly opts in, OR
|
||||
* 3. The unavailable fallback (throws with a clear error message).
|
||||
*/
|
||||
function createLateBindingSubagent(
|
||||
explicit?: PluginRuntime["subagent"],
|
||||
allowGatewaySubagentBinding = false,
|
||||
): PluginRuntime["subagent"] {
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const unavailable = createUnavailableSubagentRuntime();
|
||||
if (!allowGatewaySubagentBinding) {
|
||||
return unavailable;
|
||||
}
|
||||
|
||||
return new Proxy(unavailable, {
|
||||
get(_target, prop, _receiver) {
|
||||
const resolved = gatewaySubagentState.subagent ?? unavailable;
|
||||
return Reflect.get(resolved, prop, resolved);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type CreatePluginRuntimeOptions = {
|
||||
subagent?: PluginRuntime["subagent"];
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
};
|
||||
|
||||
export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}): PluginRuntime {
|
||||
@@ -55,7 +129,10 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
|
||||
version: resolveVersion(),
|
||||
config: createRuntimeConfig(),
|
||||
agent: createRuntimeAgent(),
|
||||
subagent: _options.subagent ?? createUnavailableSubagentRuntime(),
|
||||
subagent: createLateBindingSubagent(
|
||||
_options.subagent,
|
||||
_options.allowGatewaySubagentBinding === true,
|
||||
),
|
||||
system: createRuntimeSystem(),
|
||||
media: createRuntimeMedia(),
|
||||
tts: { textToSpeechTelephony },
|
||||
|
||||
Reference in New Issue
Block a user