mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:10:45 +00:00
feat: lazily load tool result middleware plugins
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
92
src/plugins/agent-tool-result-middleware-loader.ts
Normal file
92
src/plugins/agent-tool-result-middleware-loader.ts
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user