feat: lazily load tool result middleware plugins

This commit is contained in:
Shakker
2026-04-28 03:47:04 +01:00
parent fc3b8ad3ee
commit 08cc44b57d
5 changed files with 163 additions and 9 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (`openclaw-plugin-yuanbao`) in the official channel catalog, contract suites, and community plugin docs, with a new `docs/channels/yuanbao.md` quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.
- Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C `stream_messages` streaming with a `StreamingController` lifecycle manager, unified `sendMedia` with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via `createEngineAdapters()`. (#70624) Thanks @cxyhhhhh.
- Plugins/startup: migrate bundled plugin manifests to explicit `activation.onStartup` declarations so Gateway startup imports only the bundled plugins that intentionally register startup-time runtime surfaces. Thanks @shakkernerd.
- Plugins/runtime: load bundled agent tool-result middleware from manifest contracts on demand so tokenjuice stays startup-lazy without losing Pi/Codex tool-output compaction. Thanks @shakkernerd.
- Plugins/startup: add explicit `activation.onStartup` metadata so plugins can declare Gateway startup import behavior while the deprecated implicit sidecar fallback remains for legacy plugins. Thanks @shakkernerd.
- Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.
- CLI/models: declare fixed Qianfan, Xiaomi, NVIDIA, Cerebras, and Mistral model catalogs in plugin manifests so provider-filtered model listing can use the manifest fast path without loading provider runtime catalog code. Thanks @shakkernerd.

View File

@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import {
createAgentToolResultMiddlewareRunner,
createCodexAppServerToolResultExtensionRunner,
@@ -21,6 +22,7 @@ function createTempDir(): string {
}
afterEach(() => {
clearRuntimeConfigSnapshot();
cleanupTempPluginTestEnvironment(tempDirs, originalBundledPluginsDir);
});
@@ -190,6 +192,56 @@ export default { id: "tool-result-middleware", register(api) {
expect(listAgentToolResultMiddlewares("pi")).toHaveLength(1);
expect(listAgentToolResultMiddlewares("codex")).toHaveLength(1);
});
it("lazily loads bundled middleware owners from manifest contracts", async () => {
const tmp = createTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
dir: tmp,
id: "tool-result-middleware",
filename: "index.mjs",
manifest: {
activation: {
onStartup: false,
},
contracts: {
agentToolResultMiddleware: ["codex"],
},
},
body: `export default { id: "tool-result-middleware", register(api) {
api.registerAgentToolResultMiddleware(async (event) => ({
result: { ...event.result, content: [{ type: "text", text: event.toolName + " lazily compacted" }] }
}), { runtimes: ["codex"] });
} };`,
});
setRuntimeConfigSnapshot({
plugins: {
entries: {
"tool-result-middleware": {
enabled: true,
},
},
},
});
resetActivePluginRegistryForTest();
expect(listAgentToolResultMiddlewares("codex")).toHaveLength(0);
const runner = createAgentToolResultMiddlewareRunner({ runtime: "codex" });
const result = await runner.applyToolResultMiddleware({
threadId: "thread-1",
turnId: "turn-1",
toolCallId: "call-1",
toolName: "exec",
args: { command: "git status" },
result: { content: [{ type: "text", text: "raw" }], details: {} },
});
expect(result.content).toEqual([{ type: "text", text: "exec lazily compacted" }]);
expect(listAgentToolResultMiddlewares("codex")).toHaveLength(0);
});
});
describe("Codex app-server extension factories", () => {

View File

@@ -5,7 +5,6 @@ import type {
AgentToolResultMiddlewareEvent,
OpenClawAgentToolResult,
} from "../../plugins/agent-tool-result-middleware-types.js";
import { listAgentToolResultMiddlewares } from "../../plugins/agent-tool-result-middleware.js";
import { truncateUtf16Safe } from "../../utils.js";
const log = createSubsystemLogger("agents/harness");
@@ -122,15 +121,30 @@ function buildMiddlewareFailureResult(): OpenClawAgentToolResult {
export function createAgentToolResultMiddlewareRunner(
ctx: AgentToolResultMiddlewareContext,
handlers: AgentToolResultMiddleware[] = listAgentToolResultMiddlewares(ctx.runtime),
handlers?: AgentToolResultMiddleware[],
) {
const middlewareContext = { ...ctx, harness: ctx.harness ?? ctx.runtime };
let resolvedHandlers = handlers;
let resolvedHandlersPromise: Promise<AgentToolResultMiddleware[]> | undefined;
const resolveHandlers = async (): Promise<AgentToolResultMiddleware[]> => {
if (resolvedHandlers) {
return resolvedHandlers;
}
resolvedHandlersPromise ??= import("../../plugins/agent-tool-result-middleware-loader.js").then(
({ loadAgentToolResultMiddlewaresForRuntime }) =>
loadAgentToolResultMiddlewaresForRuntime({
runtime: ctx.runtime,
}),
);
resolvedHandlers = await resolvedHandlersPromise;
return resolvedHandlers;
};
return {
async applyToolResultMiddleware(
event: AgentToolResultMiddlewareEvent,
): Promise<OpenClawAgentToolResult> {
let current = event.result;
for (const handler of handlers) {
for (const handler of await resolveHandlers()) {
try {
const next = await handler({ ...event, result: current }, middlewareContext);
// Middleware may mutate event.result in place for legacy Pi parity.

View File

@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ExtensionFactory, SessionManager } from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { listAgentToolResultMiddlewares } from "../../plugins/agent-tool-result-middleware.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
@@ -35,13 +34,9 @@ function recordFromUnknown(value: unknown): Record<string, unknown> {
}
function buildAgentToolResultMiddlewareFactory(): ExtensionFactory {
const handlers = listAgentToolResultMiddlewares("pi");
const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }, handlers);
const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" });
return (pi) => {
pi.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => {
if (handlers.length === 0) {
return undefined;
}
const event = recordFromUnknown(rawEvent) as PiToolResultEvent;
if (!event.toolName) {
return undefined;

View File

@@ -0,0 +1,92 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type {
AgentToolResultMiddleware,
AgentToolResultMiddlewareRuntime,
} from "./agent-tool-result-middleware-types.js";
import {
listAgentToolResultMiddlewares,
normalizeAgentToolResultMiddlewareRuntimeIds,
} from "./agent-tool-result-middleware.js";
import { loadOpenClawPlugins } from "./loader.js";
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
const log = createSubsystemLogger("plugins/agent-tool-result-middleware");
async function resolveRuntimeConfig(): Promise<OpenClawConfig> {
const { getRuntimeConfig } = await import("../config/config.js");
return getRuntimeConfig();
}
function listMiddlewareOwnerPluginIds(params: {
manifestRegistry: PluginManifestRegistry;
runtime: AgentToolResultMiddlewareRuntime;
}): string[] {
const pluginIds: string[] = [];
for (const record of params.manifestRegistry.plugins) {
if (record.origin !== "bundled") {
continue;
}
const runtimes = normalizeAgentToolResultMiddlewareRuntimeIds(
record.contracts?.agentToolResultMiddleware,
);
if (runtimes.includes(params.runtime) && !pluginIds.includes(record.id)) {
pluginIds.push(record.id);
}
}
return pluginIds;
}
export async function loadAgentToolResultMiddlewaresForRuntime(params: {
runtime: AgentToolResultMiddlewareRuntime;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
manifestRegistry?: PluginManifestRegistry;
}): Promise<AgentToolResultMiddleware[]> {
const activeHandlers = listAgentToolResultMiddlewares(params.runtime);
if (activeHandlers.length > 0) {
return activeHandlers;
}
try {
const config = params.config ?? (await resolveRuntimeConfig());
const env = params.env ?? process.env;
const manifestRegistry =
params.manifestRegistry ??
loadPluginManifestRegistry({
config,
workspaceDir: params.workspaceDir,
env,
});
const pluginIds = listMiddlewareOwnerPluginIds({
manifestRegistry,
runtime: params.runtime,
});
if (pluginIds.length === 0) {
return [];
}
const registry = loadOpenClawPlugins({
config,
workspaceDir: params.workspaceDir,
env,
manifestRegistry,
onlyPluginIds: pluginIds,
activate: false,
throwOnLoadError: false,
});
return registry.agentToolResultMiddlewares
.filter((entry) => entry.runtimes.includes(params.runtime))
.map((entry) => entry.handler);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
log.warn(`[${params.runtime}] failed to load tool result middleware plugins: ${detail}`);
return listAgentToolResultMiddlewares(params.runtime);
}
}
export const __testing = {
listMiddlewareOwnerPluginIds,
};