mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
feat(plugins): add harness tool result middleware (#71021)
This commit is contained in:
@@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Breaking
|
||||
|
||||
- Plugin SDK/tool-result transforms: deprecate the Pi-only `api.registerEmbeddedExtensionFactory(...)` path for tool-result rewriting in favor of `api.registerAgentToolResultMiddleware(...)`, with `contracts.agentToolResultMiddleware` declaring the targeted harnesses. The legacy Pi hook remains wired as a bundled compatibility seam, but new plugins should use the harness-neutral middleware contract so transforms run consistently across Pi and Codex app-server dynamic tools. Thanks @vincentkoc.
|
||||
|
||||
### Changes
|
||||
|
||||
- Gradium: add a bundled text-to-speech provider with voice-note and telephony output support. (#64958) Thanks @LaurentMazare.
|
||||
@@ -3208,7 +3212,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/send retry classification: retry grammY `Network request ... failed after N attempts` envelopes in send flows without reclassifying plain `Network request ... failed!` wrappers as transient, restoring the intended retry path while keeping broad send-context message matching tight. (#38056) Thanks @0xlin2023.
|
||||
- Gateway/probes: keep `/health`, `/healthz`, `/ready`, and `/readyz` reachable when the Control UI is mounted at `/`, preserve plugin-owned route precedence on those paths, and make `/ready` and `/readyz` report channel-backed readiness with startup grace plus `503` on disconnected managed channels, while `/health` and `/healthz` stay shallow liveness probes. (#18446) Thanks @vibecodooor, @mahsumaktas, and @vincentkoc.
|
||||
- Feishu/media downloads: drop invalid timeout fields from SDK method calls now that client-level `httpTimeoutMs` applies to requests. (#38267) Thanks @ant1eicher and @thewilloftheshadow.
|
||||
- PI embedded runner/Feishu docs: propagate sender identity into embedded attempts so Feishu doc auto-grant restores requester access for embedded-runner executions. (#32915) thanks @cszhouwei.
|
||||
- Pi embedded runner/Feishu docs: propagate sender identity into embedded attempts so Feishu doc auto-grant restores requester access for embedded-runner executions. (#32915) thanks @cszhouwei.
|
||||
- Agents/usage normalization: normalize missing or partial assistant usage snapshots before compaction accounting so `openclaw agent --json` no longer crashes when provider payloads omit `totalTokens` or related usage fields. (#34977) thanks @sp-hk2ldn.
|
||||
- Venice/default model refresh: switch the built-in Venice default to `kimi-k2-5`, update onboarding aliasing, and refresh Venice provider docs/recommendations to match the current private and anonymized catalog. (from #12964) Fixes #20156. Thanks @sabrinaaquino and @vincentkoc.
|
||||
- Agents/skill API write pacing: add a global prompt guardrail that treats skill-driven external API writes as rate-limited by default, so runners prefer batched writes, avoid tight request loops, and respect `429`/`Retry-After`. Thanks @vincentkoc.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
c4a62f081d0b9fcfd5e76a843547411bba0fdc129c1c143e7f4c4f6294b040b9 plugin-sdk-api-baseline.json
|
||||
a62c9aea45d5694a851380ff6b35b7fb2ffd9fc4dfa3f0c567a8e1c97094475e plugin-sdk-api-baseline.jsonl
|
||||
b758a1c5503c08325113e0d6c9f1ac2db5a5fd9992a3902706ebe0f0dbbc1213 plugin-sdk-api-baseline.json
|
||||
2c9d0a00e526dcd47d131261b8ceddd8e59faa8530b129d108a3721a4cbcbea7 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -160,7 +160,7 @@ A single plugin can register any number of capabilities via the `api` object:
|
||||
| Video generation | `api.registerVideoGenerationProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
|
||||
| Web fetch | `api.registerWebFetchProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
|
||||
| Web search | `api.registerWebSearchProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) |
|
||||
| Embedded Pi extension | `api.registerEmbeddedExtensionFactory(...)` | [SDK Overview](/plugins/sdk-overview#registration-api) |
|
||||
| Tool-result middleware | `api.registerAgentToolResultMiddleware(...)` | [SDK Overview](/plugins/sdk-overview#registration-api) |
|
||||
| Agent tools | `api.registerTool(...)` | Below |
|
||||
| Custom commands | `api.registerCommand(...)` | [Entry Points](/plugins/sdk-entrypoints) |
|
||||
| Plugin hooks | `api.on(...)` | [Plugin hooks](/plugins/hooks) |
|
||||
@@ -170,10 +170,11 @@ A single plugin can register any number of capabilities via the `api` object:
|
||||
|
||||
For the full registration API, see [SDK Overview](/plugins/sdk-overview#registration-api).
|
||||
|
||||
Use `api.registerEmbeddedExtensionFactory(...)` when a plugin needs Pi-native
|
||||
embedded-runner hooks such as async `tool_result` rewriting before the final
|
||||
tool result message is emitted. Prefer regular OpenClaw plugin hooks when the
|
||||
work does not need Pi extension timing.
|
||||
Use `api.registerAgentToolResultMiddleware(...)` when a plugin needs async
|
||||
tool-result rewriting before the model sees the output. Declare the targeted
|
||||
harnesses in `contracts.agentToolResultMiddleware`, for example
|
||||
`["pi", "codex-app-server"]`. Prefer regular OpenClaw plugin hooks when the
|
||||
work does not need pre-model tool-result timing.
|
||||
|
||||
If your plugin registers custom gateway RPC methods, keep them on a
|
||||
plugin-specific prefix. Core admin namespaces (`config.*`,
|
||||
|
||||
@@ -25,11 +25,11 @@ These are in-process OpenClaw hooks, not Codex `hooks.json` command hooks:
|
||||
- `before_message_write` for mirrored transcript records
|
||||
- `agent_end`
|
||||
|
||||
Bundled plugins can also register a Codex app-server extension factory to add
|
||||
async `tool_result` middleware. That middleware runs for OpenClaw dynamic tools
|
||||
after OpenClaw executes the tool and before the result is returned to Codex. It
|
||||
is separate from the public `tool_result_persist` plugin hook, which transforms
|
||||
OpenClaw-owned transcript tool-result writes.
|
||||
Plugins can also register harness-neutral tool-result middleware to rewrite
|
||||
OpenClaw dynamic tool results after OpenClaw executes the tool and before the
|
||||
result is returned to Codex. This is separate from the public
|
||||
`tool_result_persist` plugin hook, which transforms OpenClaw-owned transcript
|
||||
tool-result writes.
|
||||
|
||||
The harness is off by default. New configs should keep OpenAI model refs
|
||||
canonical as `openai/gpt-*` and explicitly force
|
||||
|
||||
@@ -396,7 +396,7 @@ read without importing the plugin runtime.
|
||||
```json
|
||||
{
|
||||
"contracts": {
|
||||
"embeddedExtensionFactories": ["pi"],
|
||||
"agentToolResultMiddleware": ["pi", "codex-app-server"],
|
||||
"externalAuthProviders": ["acme-ai"],
|
||||
"speechProviders": ["openai"],
|
||||
"realtimeTranscriptionProviders": ["openai"],
|
||||
@@ -414,20 +414,26 @@ read without importing the plugin runtime.
|
||||
|
||||
Each list is optional:
|
||||
|
||||
| Field | Type | What it means |
|
||||
| -------------------------------- | ---------- | ----------------------------------------------------------------- |
|
||||
| `embeddedExtensionFactories` | `string[]` | Embedded runtime ids a bundled plugin may register factories for. |
|
||||
| `externalAuthProviders` | `string[]` | Provider ids whose external auth profile hook this plugin owns. |
|
||||
| `speechProviders` | `string[]` | Speech provider ids this plugin owns. |
|
||||
| `realtimeTranscriptionProviders` | `string[]` | Realtime-transcription provider ids this plugin owns. |
|
||||
| `realtimeVoiceProviders` | `string[]` | Realtime-voice provider ids this plugin owns. |
|
||||
| `memoryEmbeddingProviders` | `string[]` | Memory embedding provider ids this plugin owns. |
|
||||
| `mediaUnderstandingProviders` | `string[]` | Media-understanding provider ids this plugin owns. |
|
||||
| `imageGenerationProviders` | `string[]` | Image-generation provider ids this plugin owns. |
|
||||
| `videoGenerationProviders` | `string[]` | Video-generation provider ids this plugin owns. |
|
||||
| `webFetchProviders` | `string[]` | Web-fetch provider ids this plugin owns. |
|
||||
| `webSearchProviders` | `string[]` | Web-search provider ids this plugin owns. |
|
||||
| `tools` | `string[]` | Agent tool names this plugin owns for bundled contract checks. |
|
||||
| Field | Type | What it means |
|
||||
| -------------------------------- | ---------- | ---------------------------------------------------------------- |
|
||||
| `embeddedExtensionFactories` | `string[]` | Deprecated embedded extension factory ids. |
|
||||
| `agentToolResultMiddleware` | `string[]` | Harness ids this plugin may register tool-result middleware for. |
|
||||
| `externalAuthProviders` | `string[]` | Provider ids whose external auth profile hook this plugin owns. |
|
||||
| `speechProviders` | `string[]` | Speech provider ids this plugin owns. |
|
||||
| `realtimeTranscriptionProviders` | `string[]` | Realtime-transcription provider ids this plugin owns. |
|
||||
| `realtimeVoiceProviders` | `string[]` | Realtime-voice provider ids this plugin owns. |
|
||||
| `memoryEmbeddingProviders` | `string[]` | Memory embedding provider ids this plugin owns. |
|
||||
| `mediaUnderstandingProviders` | `string[]` | Media-understanding provider ids this plugin owns. |
|
||||
| `imageGenerationProviders` | `string[]` | Image-generation provider ids this plugin owns. |
|
||||
| `videoGenerationProviders` | `string[]` | Video-generation provider ids this plugin owns. |
|
||||
| `webFetchProviders` | `string[]` | Web-fetch provider ids this plugin owns. |
|
||||
| `webSearchProviders` | `string[]` | Web-search provider ids this plugin owns. |
|
||||
| `tools` | `string[]` | Agent tool names this plugin owns for bundled contract checks. |
|
||||
|
||||
`contracts.embeddedExtensionFactories` is retained for bundled compatibility
|
||||
code that still needs direct Pi embedded-runner events. New tool-result
|
||||
transforms should declare `contracts.agentToolResultMiddleware` and register
|
||||
with `api.registerAgentToolResultMiddleware(...)` instead.
|
||||
|
||||
Provider plugins that implement `resolveExternalAuthProfiles` should declare
|
||||
`contracts.externalAuthProviders`. Plugins without the declaration still run
|
||||
|
||||
@@ -144,14 +144,20 @@ OpenClaw requires Codex app-server `0.118.0` or newer. The Codex plugin checks
|
||||
the app-server initialize handshake and blocks older or unversioned servers so
|
||||
OpenClaw only runs against the protocol surface it has been tested with.
|
||||
|
||||
### Codex app-server tool-result middleware
|
||||
### Tool-result middleware
|
||||
|
||||
Bundled plugins can also attach Codex app-server-specific `tool_result`
|
||||
middleware through `api.registerCodexAppServerExtensionFactory(...)` when their
|
||||
manifest declares `contracts.embeddedExtensionFactories: ["codex-app-server"]`.
|
||||
This is the trusted-plugin seam for async tool-result transforms that need to
|
||||
run inside the native Codex harness before the tool output is projected back
|
||||
into the OpenClaw transcript.
|
||||
Plugins can attach harness-neutral tool-result middleware through
|
||||
`api.registerAgentToolResultMiddleware(...)` when their manifest declares the
|
||||
targeted harness ids in `contracts.agentToolResultMiddleware`. This is the seam
|
||||
for async tool-result transforms that must run before PI or Codex feeds tool
|
||||
output back into the model.
|
||||
|
||||
Legacy bundled plugins can still use
|
||||
`api.registerCodexAppServerExtensionFactory(...)` for Codex app-server-only
|
||||
middleware, but new result transforms should use the harness-neutral API.
|
||||
The Pi-only `api.registerEmbeddedExtensionFactory(...)` hook is deprecated for
|
||||
tool-result transforms; keep it only for bundled compatibility code that still
|
||||
needs direct Pi embedded-runner events.
|
||||
|
||||
### Native Codex harness mode
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ sidebarTitle: "Migrate to SDK"
|
||||
read_when:
|
||||
- You see the OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED warning
|
||||
- You see the OPENCLAW_EXTENSION_API_DEPRECATED warning
|
||||
- You use api.registerEmbeddedExtensionFactory
|
||||
- You are updating a plugin to the modern plugin architecture
|
||||
- You maintain an external OpenClaw plugin
|
||||
---
|
||||
@@ -23,8 +24,10 @@ anything they needed from a single entry point:
|
||||
new plugin architecture was being built.
|
||||
- **`openclaw/extension-api`** — a bridge that gave plugins direct access to
|
||||
host-side helpers like the embedded agent runner.
|
||||
- **`api.registerEmbeddedExtensionFactory(...)`** — a Pi-only bundled extension
|
||||
hook that could observe embedded-runner events such as `tool_result`.
|
||||
|
||||
Both surfaces are now **deprecated**. They still work at runtime, but new
|
||||
These surfaces are now **deprecated**. They still work at runtime, but new
|
||||
plugins must not use them, and existing plugins should migrate before the next
|
||||
major release removes them.
|
||||
|
||||
@@ -87,6 +90,41 @@ releases.
|
||||
## How to migrate
|
||||
|
||||
<Steps>
|
||||
<Step title="Migrate Pi tool-result extensions to middleware">
|
||||
Replace Pi-only `api.registerEmbeddedExtensionFactory(...)` tool-result
|
||||
handlers with harness-neutral middleware.
|
||||
|
||||
```typescript
|
||||
// Before: Pi-only compatibility hook
|
||||
api.registerEmbeddedExtensionFactory((pi) => {
|
||||
pi.on("tool_result", async (event) => {
|
||||
return compactToolResult(event);
|
||||
});
|
||||
});
|
||||
|
||||
// After: Pi and Codex app-server dynamic tools
|
||||
api.registerAgentToolResultMiddleware(async (event) => {
|
||||
return compactToolResult(event);
|
||||
}, {
|
||||
harnesses: ["pi", "codex-app-server"],
|
||||
});
|
||||
```
|
||||
|
||||
Update the plugin manifest at the same time:
|
||||
|
||||
```json
|
||||
{
|
||||
"contracts": {
|
||||
"agentToolResultMiddleware": ["pi", "codex-app-server"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep `contracts.embeddedExtensionFactories` only for bundled compatibility
|
||||
code that still needs direct Pi embedded-runner events.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Migrate approval-native handlers to capability facts">
|
||||
Approval-capable channel plugins now expose native approval behavior through
|
||||
`approvalCapability.nativeRuntime` plus the shared runtime-context registry.
|
||||
|
||||
@@ -99,7 +99,8 @@ methods:
|
||||
| `api.registerCli(registrar, opts?)` | CLI subcommand |
|
||||
| `api.registerService(service)` | Background service |
|
||||
| `api.registerInteractiveHandler(registration)` | Interactive handler |
|
||||
| `api.registerEmbeddedExtensionFactory(factory)` | Pi embedded-runner extension factory |
|
||||
| `api.registerAgentToolResultMiddleware(...)` | Harness tool-result middleware |
|
||||
| `api.registerEmbeddedExtensionFactory(factory)` | Deprecated PI extension factory |
|
||||
| `api.registerMemoryPromptSupplement(builder)` | Additive memory-adjacent prompt section |
|
||||
| `api.registerMemoryCorpusSupplement(adapter)` | Additive memory search/read corpus |
|
||||
|
||||
@@ -110,15 +111,22 @@ methods:
|
||||
plugin-owned methods.
|
||||
</Note>
|
||||
|
||||
<Accordion title="When to use registerEmbeddedExtensionFactory">
|
||||
Use `api.registerEmbeddedExtensionFactory(...)` when a plugin needs Pi-native
|
||||
event timing during OpenClaw embedded runs — for example async `tool_result`
|
||||
rewrites that must happen before the final tool-result message is emitted.
|
||||
<Accordion title="When to use tool-result middleware">
|
||||
Use `api.registerAgentToolResultMiddleware(...)` when a plugin needs to
|
||||
rewrite a tool result after execution and before the harness feeds that
|
||||
result back into the model. This is the harness-neutral seam for async output
|
||||
reducers such as tokenjuice.
|
||||
|
||||
This is a bundled-plugin seam today: only bundled plugins may register one,
|
||||
and they must declare `contracts.embeddedExtensionFactories: ["pi"]` in
|
||||
`openclaw.plugin.json`. Keep normal OpenClaw plugin hooks for everything that
|
||||
does not require that lower-level seam.
|
||||
Plugins must declare `contracts.agentToolResultMiddleware` for each targeted
|
||||
harness, for example `["pi", "codex-app-server"]`. Keep normal OpenClaw
|
||||
plugin hooks for work that does not need pre-model tool-result timing.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Legacy Pi extension factories">
|
||||
`api.registerEmbeddedExtensionFactory(...)` is deprecated. It remains a
|
||||
compatibility seam for bundled plugins that still need direct Pi
|
||||
embedded-runner events. New tool-result transforms should use
|
||||
`api.registerAgentToolResultMiddleware(...)` instead.
|
||||
</Accordion>
|
||||
|
||||
### Gateway discovery registration
|
||||
|
||||
@@ -13,8 +13,9 @@ tool results after the command has already run.
|
||||
It changes the returned `tool_result`, not the command itself. Tokenjuice does
|
||||
not rewrite shell input, rerun commands, or change exit codes.
|
||||
|
||||
Today this applies to Pi embedded runs, where tokenjuice hooks the embedded
|
||||
`tool_result` path and trims the output that goes back into the session.
|
||||
Today this applies to PI embedded runs and OpenClaw dynamic tools in the Codex
|
||||
app-server harness. Tokenjuice hooks OpenClaw's tool-result middleware and
|
||||
trims the output before it goes back into the active harness session.
|
||||
|
||||
## Enable the plugin
|
||||
|
||||
|
||||
@@ -210,7 +210,54 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("applies codex app-server tool_result extensions from the active plugin registry", async () => {
|
||||
it("applies agent tool result middleware from the active plugin registry", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const handler = vi.fn(
|
||||
async (event: { result: AgentToolResult<unknown>; toolName: string }) => ({
|
||||
result: {
|
||||
...event.result,
|
||||
content: [{ type: "text" as const, text: `${event.toolName} compacted` }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
registry.agentToolResultMiddlewares.push({
|
||||
pluginId: "tokenjuice",
|
||||
pluginName: "Tokenjuice",
|
||||
rawHandler: handler,
|
||||
handler,
|
||||
harnesses: ["codex-app-server"],
|
||||
source: "test",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const bridge = createBridgeWithToolResult("exec", {
|
||||
content: [{ type: "text", text: "raw output" }],
|
||||
details: {},
|
||||
});
|
||||
|
||||
const result = await bridge.handleToolCall({
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
callId: "call-1",
|
||||
namespace: null,
|
||||
tool: "exec",
|
||||
arguments: { command: "git status" },
|
||||
});
|
||||
|
||||
expect(result).toEqual(expectInputText("exec compacted"));
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
toolCallId: "call-1",
|
||||
toolName: "exec",
|
||||
args: { command: "git status" },
|
||||
}),
|
||||
expect.objectContaining({ harness: "codex-app-server" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("still applies legacy codex app-server extension factories after middleware", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const factory = async (codex: {
|
||||
on: (
|
||||
@@ -221,7 +268,7 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
codex.on("tool_result", async (event) => ({
|
||||
result: {
|
||||
...event.result,
|
||||
content: [{ type: "text", text: `${event.toolName} compacted` }],
|
||||
content: [{ type: "text", text: "legacy compacted" }],
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -248,7 +295,7 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
arguments: { command: "git status" },
|
||||
});
|
||||
|
||||
expect(result).toEqual(expectInputText("exec compacted"));
|
||||
expect(result).toEqual(expectInputText("legacy compacted"));
|
||||
});
|
||||
|
||||
it("fires after_tool_call for successful codex tool executions", async () => {
|
||||
@@ -441,29 +488,25 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
]),
|
||||
);
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const factory = async (codex: {
|
||||
on: (
|
||||
event: "tool_result",
|
||||
handler: (event: any) => Promise<{ result: AgentToolResult<unknown> }>,
|
||||
) => void;
|
||||
}) => {
|
||||
codex.on("tool_result", async (event) => {
|
||||
const handler = vi.fn(
|
||||
async (event: { args: Record<string, unknown>; result: AgentToolResult<unknown> }) => {
|
||||
events.push("middleware");
|
||||
expect(event.args).toEqual({ command: "status" });
|
||||
return {
|
||||
result: {
|
||||
...event.result,
|
||||
content: [{ type: "text", text: "compacted output" }],
|
||||
content: [{ type: "text" as const, text: "compacted output" }],
|
||||
details: { stage: "middleware" },
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
registry.codexAppServerExtensionFactories.push({
|
||||
},
|
||||
);
|
||||
registry.agentToolResultMiddlewares.push({
|
||||
pluginId: "tokenjuice",
|
||||
pluginName: "Tokenjuice",
|
||||
rawFactory: factory,
|
||||
factory,
|
||||
rawHandler: handler,
|
||||
handler,
|
||||
harnesses: ["codex-app-server"],
|
||||
source: "test",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
createAgentToolResultMiddlewareRunner,
|
||||
createCodexAppServerToolResultExtensionRunner,
|
||||
extractToolResultMediaArtifact,
|
||||
filterToolResultMediaUrls,
|
||||
@@ -58,7 +59,13 @@ export function createCodexDynamicToolBridge(params: {
|
||||
toolMediaUrls: [],
|
||||
toolAudioAsVoice: false,
|
||||
};
|
||||
const extensionRunner = createCodexAppServerToolResultExtensionRunner(params.hookContext ?? {});
|
||||
const middlewareRunner = createAgentToolResultMiddlewareRunner({
|
||||
harness: "codex-app-server",
|
||||
...params.hookContext,
|
||||
});
|
||||
const legacyExtensionRunner = createCodexAppServerToolResultExtensionRunner(
|
||||
params.hookContext ?? {},
|
||||
);
|
||||
|
||||
return {
|
||||
specs: tools.map((tool) => ({
|
||||
@@ -80,7 +87,7 @@ export function createCodexDynamicToolBridge(params: {
|
||||
try {
|
||||
const preparedArgs = tool.prepareArguments ? tool.prepareArguments(args) : args;
|
||||
const rawResult = await tool.execute(call.callId, preparedArgs, params.signal);
|
||||
const result = await extensionRunner.applyToolResultExtensions({
|
||||
const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
|
||||
threadId: call.threadId,
|
||||
turnId: call.turnId,
|
||||
toolCallId: call.callId,
|
||||
@@ -88,6 +95,14 @@ export function createCodexDynamicToolBridge(params: {
|
||||
args,
|
||||
result: rawResult,
|
||||
});
|
||||
const result = await legacyExtensionRunner.applyToolResultExtensions({
|
||||
threadId: call.threadId,
|
||||
turnId: call.turnId,
|
||||
toolCallId: call.callId,
|
||||
toolName: tool.name,
|
||||
args,
|
||||
result: middlewareResult,
|
||||
});
|
||||
collectToolTelemetry({
|
||||
toolName: tool.name,
|
||||
args,
|
||||
|
||||
@@ -31,8 +31,8 @@ describe("tokenjuice bundled plugin", () => {
|
||||
expect(manifest.enabledByDefault).toBeUndefined();
|
||||
});
|
||||
|
||||
it("registers the tokenjuice embedded extension factory", () => {
|
||||
const registerEmbeddedExtensionFactory = vi.fn();
|
||||
it("registers tokenjuice tool result middleware for Pi and Codex app-server", () => {
|
||||
const registerAgentToolResultMiddleware = vi.fn();
|
||||
|
||||
plugin.register(
|
||||
createTestPluginApi({
|
||||
@@ -42,11 +42,14 @@ describe("tokenjuice bundled plugin", () => {
|
||||
config: {},
|
||||
pluginConfig: {},
|
||||
runtime: {} as never,
|
||||
registerEmbeddedExtensionFactory,
|
||||
registerAgentToolResultMiddleware,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(createTokenjuiceOpenClawEmbeddedExtension).toHaveBeenCalledTimes(1);
|
||||
expect(registerEmbeddedExtensionFactory).toHaveBeenCalledWith(tokenjuiceFactory);
|
||||
expect(tokenjuiceFactory).toHaveBeenCalledTimes(1);
|
||||
expect(registerAgentToolResultMiddleware).toHaveBeenCalledWith(expect.any(Function), {
|
||||
harnesses: ["pi", "codex-app-server"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { createTokenjuiceOpenClawEmbeddedExtension } from "./runtime-api.js";
|
||||
import { createTokenjuiceAgentToolResultMiddleware } from "./tool-result-middleware.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "tokenjuice",
|
||||
name: "tokenjuice",
|
||||
description: "Compacts exec and bash tool results with tokenjuice reducers.",
|
||||
register(api) {
|
||||
api.registerEmbeddedExtensionFactory(createTokenjuiceOpenClawEmbeddedExtension());
|
||||
api.registerAgentToolResultMiddleware(createTokenjuiceAgentToolResultMiddleware(), {
|
||||
harnesses: ["pi", "codex-app-server"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ type TokenjuicePackageManifest = {
|
||||
|
||||
type TokenjuicePluginManifest = {
|
||||
contracts?: {
|
||||
embeddedExtensionFactories?: string[];
|
||||
agentToolResultMiddleware?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,11 +26,11 @@ describe("tokenjuice package manifest", () => {
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
|
||||
it("declares Pi embedded extension factory ownership in the manifest contract", () => {
|
||||
it("declares harness-neutral tool result middleware ownership in the manifest contract", () => {
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
|
||||
) as TokenjuicePluginManifest;
|
||||
|
||||
expect(manifest.contracts?.embeddedExtensionFactories).toEqual(["pi"]);
|
||||
expect(manifest.contracts?.agentToolResultMiddleware).toEqual(["pi", "codex-app-server"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "tokenjuice",
|
||||
"description": "Compacts exec and bash tool results with tokenjuice reducers.",
|
||||
"contracts": {
|
||||
"embeddedExtensionFactories": ["pi"]
|
||||
"agentToolResultMiddleware": ["pi", "codex-app-server"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
declare module "tokenjuice/openclaw" {
|
||||
export function createTokenjuiceOpenClawEmbeddedExtension(): Parameters<
|
||||
import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginApi["registerEmbeddedExtensionFactory"]
|
||||
>[0];
|
||||
type OpenClawPiRuntime = {
|
||||
on(event: string, handler: (event: unknown, ctx: { cwd: string }) => unknown): void;
|
||||
};
|
||||
|
||||
export function createTokenjuiceOpenClawEmbeddedExtension(): (pi: OpenClawPiRuntime) => void;
|
||||
}
|
||||
|
||||
63
extensions/tokenjuice/tool-result-middleware.ts
Normal file
63
extensions/tokenjuice/tool-result-middleware.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import process from "node:process";
|
||||
import type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareEvent,
|
||||
OpenClawAgentToolResult,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
import { createTokenjuiceOpenClawEmbeddedExtension } from "./runtime-api.js";
|
||||
|
||||
type TokenjuiceToolResultHandler = (
|
||||
event: {
|
||||
toolName: string;
|
||||
input: Record<string, unknown>;
|
||||
content: OpenClawAgentToolResult["content"];
|
||||
details: unknown;
|
||||
isError?: boolean;
|
||||
},
|
||||
ctx: { cwd: string },
|
||||
) => Promise<Partial<OpenClawAgentToolResult> | void> | Partial<OpenClawAgentToolResult> | void;
|
||||
|
||||
function readCwd(event: AgentToolResultMiddlewareEvent): string {
|
||||
if (event.cwd?.trim()) {
|
||||
return event.cwd;
|
||||
}
|
||||
const workdir = event.args.workdir;
|
||||
if (typeof workdir === "string" && workdir.trim()) {
|
||||
return workdir;
|
||||
}
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
export function createTokenjuiceAgentToolResultMiddleware(): AgentToolResultMiddleware {
|
||||
const handlers: TokenjuiceToolResultHandler[] = [];
|
||||
createTokenjuiceOpenClawEmbeddedExtension()({
|
||||
on(event, handler) {
|
||||
if (event === "tool_result") {
|
||||
handlers.push(handler as TokenjuiceToolResultHandler);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return async (event) => {
|
||||
let current = event.result;
|
||||
for (const handler of handlers) {
|
||||
const next = await handler(
|
||||
{
|
||||
toolName: event.toolName,
|
||||
input: event.args,
|
||||
content: current.content,
|
||||
details: current.details,
|
||||
isError: event.isError,
|
||||
},
|
||||
{ cwd: readCwd(event) },
|
||||
);
|
||||
if (next) {
|
||||
current = Object.assign({}, current, {
|
||||
content: next.content ?? current.content,
|
||||
details: next.details ?? current.details,
|
||||
});
|
||||
}
|
||||
}
|
||||
return current === event.result ? undefined : { result: current };
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createCodexAppServerToolResultExtensionRunner } from "../plugin-sdk/agent-harness.js";
|
||||
import {
|
||||
createAgentToolResultMiddlewareRunner,
|
||||
createCodexAppServerToolResultExtensionRunner,
|
||||
} from "../plugin-sdk/agent-harness.js";
|
||||
import { listAgentToolResultMiddlewares } from "../plugins/agent-tool-result-middleware.js";
|
||||
import { listCodexAppServerExtensionFactories } from "../plugins/codex-app-server-extension-factory.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import {
|
||||
@@ -20,6 +24,137 @@ afterEach(() => {
|
||||
cleanupTempPluginTestEnvironment(tempDirs, originalBundledPluginsDir);
|
||||
});
|
||||
|
||||
describe("agent tool result middleware", () => {
|
||||
it("includes plugin-registered middleware and restores it from cache", async () => {
|
||||
const tmp = createTempDir();
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
|
||||
|
||||
writeTempPlugin({
|
||||
dir: tmp,
|
||||
id: "tool-result-middleware",
|
||||
filename: "index.mjs",
|
||||
manifest: {
|
||||
contracts: {
|
||||
agentToolResultMiddleware: ["codex-app-server"],
|
||||
},
|
||||
},
|
||||
body: `export default { id: "tool-result-middleware", register(api) {
|
||||
api.registerAgentToolResultMiddleware(async (event) => ({
|
||||
result: { ...event.result, content: [{ type: "text", text: event.toolName + " compacted" }] }
|
||||
}), { harnesses: ["codex-app-server"] });
|
||||
} };`,
|
||||
});
|
||||
|
||||
const options = {
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"tool-result-middleware": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
loadOpenClawPlugins(options);
|
||||
expect(listAgentToolResultMiddlewares("codex-app-server")).toHaveLength(1);
|
||||
expect(listAgentToolResultMiddlewares("pi")).toHaveLength(0);
|
||||
|
||||
resetActivePluginRegistryForTest();
|
||||
expect(listAgentToolResultMiddlewares("codex-app-server")).toHaveLength(0);
|
||||
|
||||
loadOpenClawPlugins(options);
|
||||
const runner = createAgentToolResultMiddlewareRunner({ harness: "codex-app-server" });
|
||||
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 compacted" }]);
|
||||
});
|
||||
|
||||
it("rejects middleware when the manifest omits the harness contract", () => {
|
||||
const tmp = createTempDir();
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
|
||||
|
||||
writeTempPlugin({
|
||||
dir: tmp,
|
||||
id: "tool-result-middleware",
|
||||
filename: "index.mjs",
|
||||
manifest: {
|
||||
contracts: {
|
||||
agentToolResultMiddleware: ["pi"],
|
||||
},
|
||||
},
|
||||
body: `export default { id: "tool-result-middleware", register(api) {
|
||||
api.registerAgentToolResultMiddleware(() => undefined, { harnesses: ["codex-app-server"] });
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"tool-result-middleware": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.diagnostics).toContainEqual(
|
||||
expect.objectContaining({
|
||||
level: "error",
|
||||
pluginId: "tool-result-middleware",
|
||||
message: "plugin must declare contracts.agentToolResultMiddleware for: codex-app-server",
|
||||
}),
|
||||
);
|
||||
expect(listAgentToolResultMiddlewares("codex-app-server")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("merges harnesses when a plugin registers the same middleware function twice", () => {
|
||||
const tmp = createTempDir();
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
|
||||
|
||||
writeTempPlugin({
|
||||
dir: tmp,
|
||||
id: "tool-result-middleware",
|
||||
filename: "index.mjs",
|
||||
manifest: {
|
||||
contracts: {
|
||||
agentToolResultMiddleware: ["pi", "codex-app-server"],
|
||||
},
|
||||
},
|
||||
body: `const middleware = () => undefined;
|
||||
export default { id: "tool-result-middleware", register(api) {
|
||||
api.registerAgentToolResultMiddleware(middleware, { harnesses: ["pi"] });
|
||||
api.registerAgentToolResultMiddleware(middleware, { harnesses: ["codex-app-server"] });
|
||||
} };`,
|
||||
});
|
||||
|
||||
loadOpenClawPlugins({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"tool-result-middleware": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(listAgentToolResultMiddlewares("pi")).toHaveLength(1);
|
||||
expect(listAgentToolResultMiddlewares("codex-app-server")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Codex app-server extension factories", () => {
|
||||
it("includes plugin-registered Codex app-server extension factories and restores them from cache", async () => {
|
||||
const tmp = createTempDir();
|
||||
|
||||
37
src/agents/harness/tool-result-middleware.ts
Normal file
37
src/agents/harness/tool-result-middleware.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareContext,
|
||||
AgentToolResultMiddlewareEvent,
|
||||
OpenClawAgentToolResult,
|
||||
} from "../../plugins/agent-tool-result-middleware-types.js";
|
||||
import { listAgentToolResultMiddlewares } from "../../plugins/agent-tool-result-middleware.js";
|
||||
|
||||
const log = createSubsystemLogger("agents/harness");
|
||||
|
||||
export function createAgentToolResultMiddlewareRunner(
|
||||
ctx: AgentToolResultMiddlewareContext,
|
||||
handlers: AgentToolResultMiddleware[] = listAgentToolResultMiddlewares(ctx.harness),
|
||||
) {
|
||||
return {
|
||||
async applyToolResultMiddleware(
|
||||
event: AgentToolResultMiddlewareEvent,
|
||||
): Promise<OpenClawAgentToolResult> {
|
||||
let current = event.result;
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
const next = await handler({ ...event, result: current }, ctx);
|
||||
if (next?.result) {
|
||||
current = next.result;
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
log.warn(
|
||||
`[${ctx.harness}] tool result middleware failed for ${event.toolName}: ${detail}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return current;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -63,7 +63,7 @@ describe("buildEmbeddedExtensionFactories", () => {
|
||||
modelId: "gpt-5.4",
|
||||
model: undefined,
|
||||
});
|
||||
expect(firstFactories).toHaveLength(1);
|
||||
expect(firstFactories).toHaveLength(2);
|
||||
expect(listEmbeddedExtensionFactories()).toHaveLength(1);
|
||||
|
||||
resetActivePluginRegistryForTest();
|
||||
@@ -78,10 +78,10 @@ describe("buildEmbeddedExtensionFactories", () => {
|
||||
modelId: "gpt-5.4",
|
||||
model: undefined,
|
||||
});
|
||||
expect(cachedFactories).toHaveLength(1);
|
||||
expect(cachedFactories).toHaveLength(2);
|
||||
|
||||
const handlers = new Map<string, Function>();
|
||||
await cachedFactories[0]?.({
|
||||
await cachedFactories[1]?.({
|
||||
on(event: string, handler: Function) {
|
||||
handlers.set(event, handler);
|
||||
},
|
||||
@@ -134,7 +134,7 @@ describe("buildEmbeddedExtensionFactories", () => {
|
||||
modelId: "gpt-5.4",
|
||||
model: undefined,
|
||||
}),
|
||||
).toHaveLength(0);
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects bundled plugins that omit the Pi embedded extension manifest contract", () => {
|
||||
@@ -254,10 +254,10 @@ describe("buildEmbeddedExtensionFactories", () => {
|
||||
modelId: "gpt-5.4",
|
||||
model: undefined,
|
||||
});
|
||||
expect(factories).toHaveLength(1);
|
||||
expect(factories).toHaveLength(2);
|
||||
|
||||
await expect(
|
||||
factories[0]?.({
|
||||
factories[1]?.({
|
||||
on() {},
|
||||
} as never),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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 { listEmbeddedExtensionFactories } from "../../plugins/embedded-extension-factory.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";
|
||||
import { createAgentToolResultMiddlewareRunner } from "../harness/tool-result-middleware.js";
|
||||
import { setCompactionSafeguardRuntime } from "../pi-hooks/compaction-safeguard-runtime.js";
|
||||
import compactionSafeguardExtension from "../pi-hooks/compaction-safeguard.js";
|
||||
import contextPruningExtension from "../pi-hooks/context-pruning.js";
|
||||
@@ -14,6 +16,57 @@ import { ensurePiCompactionReserveTokens } from "../pi-settings.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { isCacheTtlEligibleProvider, readLastCacheTtlTimestamp } from "./cache-ttl.js";
|
||||
|
||||
type PiToolResultEvent = {
|
||||
threadId?: string;
|
||||
turnId?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
input?: unknown;
|
||||
content?: AgentToolResult<unknown>["content"];
|
||||
details?: unknown;
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
function recordFromUnknown(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function buildAgentToolResultMiddlewareFactory(): ExtensionFactory {
|
||||
const runner = createAgentToolResultMiddlewareRunner({ harness: "pi" });
|
||||
return (pi) => {
|
||||
pi.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => {
|
||||
const event = recordFromUnknown(rawEvent) as PiToolResultEvent;
|
||||
if (!event.toolName) {
|
||||
return undefined;
|
||||
}
|
||||
const content = Array.isArray(event.content) ? event.content : [];
|
||||
const current = {
|
||||
content,
|
||||
details: event.details,
|
||||
} satisfies AgentToolResult<unknown>;
|
||||
const result = await runner.applyToolResultMiddleware({
|
||||
threadId: event.threadId,
|
||||
turnId: event.turnId,
|
||||
toolCallId: event.toolCallId ?? event.toolName,
|
||||
toolName: event.toolName,
|
||||
args: recordFromUnknown(event.input),
|
||||
cwd: ctx.cwd,
|
||||
isError: event.isError,
|
||||
result: current,
|
||||
});
|
||||
if (result === current) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
content: result.content,
|
||||
details: result.details,
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function resolveContextWindowTokens(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
provider: string;
|
||||
@@ -115,6 +168,7 @@ export function buildEmbeddedExtensionFactories(params: {
|
||||
if (pruningFactory) {
|
||||
factories.push(pruningFactory);
|
||||
}
|
||||
factories.push(buildAgentToolResultMiddlewareFactory());
|
||||
factories.push(...listEmbeddedExtensionFactories());
|
||||
return factories;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({
|
||||
memoryEmbeddingProviders: [],
|
||||
embeddedExtensionFactories: [],
|
||||
codexAppServerExtensionFactories: [],
|
||||
agentToolResultMiddlewares: [],
|
||||
textTransforms: [],
|
||||
agentHarnesses: [],
|
||||
gatewayHandlers: {},
|
||||
|
||||
@@ -24,6 +24,7 @@ function createStubPluginRegistry(): PluginRegistry {
|
||||
webSearchProviders: [],
|
||||
embeddedExtensionFactories: [],
|
||||
codexAppServerExtensionFactories: [],
|
||||
agentToolResultMiddlewares: [],
|
||||
memoryEmbeddingProviders: [],
|
||||
textTransforms: [],
|
||||
agentHarnesses: [],
|
||||
|
||||
@@ -30,6 +30,15 @@ export type { MessagingToolSend } from "../agents/pi-embedded-messaging.types.js
|
||||
export type { AgentApprovalEventData } from "../infra/agent-events.js";
|
||||
export type { ExecApprovalDecision } from "../infra/exec-approvals.js";
|
||||
export type { NormalizedUsage } from "../agents/usage.js";
|
||||
export type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareContext,
|
||||
AgentToolResultMiddlewareEvent,
|
||||
AgentToolResultMiddlewareHarness,
|
||||
AgentToolResultMiddlewareOptions,
|
||||
AgentToolResultMiddlewareResult,
|
||||
OpenClawAgentToolResult,
|
||||
} from "../plugins/agent-tool-result-middleware-types.js";
|
||||
export type {
|
||||
CodexAppServerExtensionContext,
|
||||
CodexAppServerExtensionFactory,
|
||||
@@ -84,6 +93,7 @@ export {
|
||||
runAgentHarnessBeforeCompactionHook,
|
||||
} from "../agents/harness/prompt-compaction-hook-helpers.js";
|
||||
export { createCodexAppServerToolResultExtensionRunner } from "../agents/harness/codex-app-server-extensions.js";
|
||||
export { createAgentToolResultMiddlewareRunner } from "../agents/harness/tool-result-middleware.js";
|
||||
export {
|
||||
assembleHarnessContextEngine,
|
||||
bootstrapHarnessContextEngine,
|
||||
|
||||
37
src/plugins/agent-tool-result-middleware-types.ts
Normal file
37
src/plugins/agent-tool-result-middleware-types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { AgentToolResult as PiAgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
|
||||
export type OpenClawAgentToolResult<TResult = unknown> = PiAgentToolResult<TResult>;
|
||||
|
||||
export type AgentToolResultMiddlewareHarness = "pi" | "codex-app-server";
|
||||
|
||||
export type AgentToolResultMiddlewareEvent = {
|
||||
threadId?: string;
|
||||
turnId?: string;
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
cwd?: string;
|
||||
isError?: boolean;
|
||||
result: OpenClawAgentToolResult;
|
||||
};
|
||||
|
||||
export type AgentToolResultMiddlewareContext = {
|
||||
harness: AgentToolResultMiddlewareHarness;
|
||||
agentId?: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
};
|
||||
|
||||
export type AgentToolResultMiddlewareResult = {
|
||||
result: OpenClawAgentToolResult;
|
||||
};
|
||||
|
||||
export type AgentToolResultMiddleware = (
|
||||
event: AgentToolResultMiddlewareEvent,
|
||||
ctx: AgentToolResultMiddlewareContext,
|
||||
) => Promise<AgentToolResultMiddlewareResult | void> | AgentToolResultMiddlewareResult | void;
|
||||
|
||||
export type AgentToolResultMiddlewareOptions = {
|
||||
harnesses?: AgentToolResultMiddlewareHarness[];
|
||||
};
|
||||
44
src/plugins/agent-tool-result-middleware.ts
Normal file
44
src/plugins/agent-tool-result-middleware.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareHarness,
|
||||
AgentToolResultMiddlewareOptions,
|
||||
} from "./agent-tool-result-middleware-types.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
export const AGENT_TOOL_RESULT_MIDDLEWARE_HARNESSES = [
|
||||
"pi",
|
||||
"codex-app-server",
|
||||
] as const satisfies AgentToolResultMiddlewareHarness[];
|
||||
|
||||
const AGENT_TOOL_RESULT_MIDDLEWARE_HARNESS_SET = new Set<string>(
|
||||
AGENT_TOOL_RESULT_MIDDLEWARE_HARNESSES,
|
||||
);
|
||||
|
||||
export function normalizeAgentToolResultMiddlewareHarnesses(
|
||||
options?: AgentToolResultMiddlewareOptions,
|
||||
): AgentToolResultMiddlewareHarness[] {
|
||||
const requested = options?.harnesses;
|
||||
if (!requested || requested.length === 0) {
|
||||
return [...AGENT_TOOL_RESULT_MIDDLEWARE_HARNESSES];
|
||||
}
|
||||
const normalized: AgentToolResultMiddlewareHarness[] = [];
|
||||
for (const harness of requested) {
|
||||
if (!AGENT_TOOL_RESULT_MIDDLEWARE_HARNESS_SET.has(harness)) {
|
||||
continue;
|
||||
}
|
||||
if (!normalized.includes(harness)) {
|
||||
normalized.push(harness);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function listAgentToolResultMiddlewares(
|
||||
harness: AgentToolResultMiddlewareHarness,
|
||||
): AgentToolResultMiddleware[] {
|
||||
return (
|
||||
getActivePluginRegistry()
|
||||
?.agentToolResultMiddlewares?.filter((entry) => entry.harnesses.includes(harness))
|
||||
.map((entry) => entry.handler) ?? []
|
||||
);
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export type BuildPluginApiParams = {
|
||||
| "registerAgentHarness"
|
||||
| "registerEmbeddedExtensionFactory"
|
||||
| "registerCodexAppServerExtensionFactory"
|
||||
| "registerAgentToolResultMiddleware"
|
||||
| "registerDetachedTaskRuntime"
|
||||
| "registerMemoryCapability"
|
||||
| "registerMemoryPromptSection"
|
||||
@@ -108,6 +109,8 @@ const noopRegisterEmbeddedExtensionFactory: OpenClawPluginApi["registerEmbeddedE
|
||||
() => {};
|
||||
const noopRegisterCodexAppServerExtensionFactory: OpenClawPluginApi["registerCodexAppServerExtensionFactory"] =
|
||||
() => {};
|
||||
const noopRegisterAgentToolResultMiddleware: OpenClawPluginApi["registerAgentToolResultMiddleware"] =
|
||||
() => {};
|
||||
const noopRegisterDetachedTaskRuntime: OpenClawPluginApi["registerDetachedTaskRuntime"] = () => {};
|
||||
const noopRegisterMemoryCapability: OpenClawPluginApi["registerMemoryCapability"] = () => {};
|
||||
const noopRegisterMemoryPromptSection: OpenClawPluginApi["registerMemoryPromptSection"] = () => {};
|
||||
@@ -181,6 +184,8 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
|
||||
handlers.registerEmbeddedExtensionFactory ?? noopRegisterEmbeddedExtensionFactory,
|
||||
registerCodexAppServerExtensionFactory:
|
||||
handlers.registerCodexAppServerExtensionFactory ?? noopRegisterCodexAppServerExtensionFactory,
|
||||
registerAgentToolResultMiddleware:
|
||||
handlers.registerAgentToolResultMiddleware ?? noopRegisterAgentToolResultMiddleware,
|
||||
registerDetachedTaskRuntime:
|
||||
handlers.registerDetachedTaskRuntime ?? noopRegisterDetachedTaskRuntime,
|
||||
registerMemoryCapability: handlers.registerMemoryCapability ?? noopRegisterMemoryCapability,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { AgentToolResultMiddleware } from "./agent-tool-result-middleware-types.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js";
|
||||
import type { MemoryEmbeddingProviderAdapter } from "./memory-embedding-providers.js";
|
||||
@@ -39,6 +40,7 @@ export type CapturedPluginRegistration = {
|
||||
textTransforms: PluginTextTransformRegistration[];
|
||||
embeddedExtensionFactories: ExtensionFactory[];
|
||||
codexAppServerExtensionFactories: CodexAppServerExtensionFactory[];
|
||||
agentToolResultMiddlewares: AgentToolResultMiddleware[];
|
||||
speechProviders: SpeechProviderPlugin[];
|
||||
realtimeTranscriptionProviders: RealtimeTranscriptionProviderPlugin[];
|
||||
realtimeVoiceProviders: RealtimeVoiceProviderPlugin[];
|
||||
@@ -63,6 +65,7 @@ export function createCapturedPluginRegistration(params?: {
|
||||
const textTransforms: PluginTextTransformRegistration[] = [];
|
||||
const embeddedExtensionFactories: ExtensionFactory[] = [];
|
||||
const codexAppServerExtensionFactories: CodexAppServerExtensionFactory[] = [];
|
||||
const agentToolResultMiddlewares: AgentToolResultMiddleware[] = [];
|
||||
const speechProviders: SpeechProviderPlugin[] = [];
|
||||
const realtimeTranscriptionProviders: RealtimeTranscriptionProviderPlugin[] = [];
|
||||
const realtimeVoiceProviders: RealtimeVoiceProviderPlugin[] = [];
|
||||
@@ -89,6 +92,7 @@ export function createCapturedPluginRegistration(params?: {
|
||||
textTransforms,
|
||||
embeddedExtensionFactories,
|
||||
codexAppServerExtensionFactories,
|
||||
agentToolResultMiddlewares,
|
||||
speechProviders,
|
||||
realtimeTranscriptionProviders,
|
||||
realtimeVoiceProviders,
|
||||
@@ -145,6 +149,9 @@ export function createCapturedPluginRegistration(params?: {
|
||||
registerCodexAppServerExtensionFactory(factory: CodexAppServerExtensionFactory) {
|
||||
codexAppServerExtensionFactories.push(factory);
|
||||
},
|
||||
registerAgentToolResultMiddleware(handler: AgentToolResultMiddleware) {
|
||||
agentToolResultMiddlewares.push(handler);
|
||||
},
|
||||
registerCliBackend(backend: CliBackendPlugin) {
|
||||
cliBackends.push(backend);
|
||||
},
|
||||
|
||||
@@ -40,12 +40,24 @@ export function createMockPluginRegistry(
|
||||
imageGenerationProviders: [],
|
||||
videoGenerationProviders: [],
|
||||
musicGenerationProviders: [],
|
||||
webFetchProviders: [],
|
||||
webSearchProviders: [],
|
||||
embeddedExtensionFactories: [],
|
||||
codexAppServerExtensionFactories: [],
|
||||
agentToolResultMiddlewares: [],
|
||||
memoryEmbeddingProviders: [],
|
||||
agentHarnesses: [],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
gatewayMethodScopes: {},
|
||||
cliRegistrars: [],
|
||||
textTransforms: [],
|
||||
reloads: [],
|
||||
nodeHostCommands: [],
|
||||
securityAuditCollectors: [],
|
||||
services: [],
|
||||
gatewayDiscoveryServices: [],
|
||||
conversationBindingResolvedHandlers: [],
|
||||
commands: [],
|
||||
diagnostics: [],
|
||||
} as unknown as PluginRegistry;
|
||||
|
||||
@@ -280,6 +280,7 @@ type PluginRegistrySnapshot = {
|
||||
webSearchProviders: PluginRegistry["webSearchProviders"];
|
||||
embeddedExtensionFactories: PluginRegistry["embeddedExtensionFactories"];
|
||||
codexAppServerExtensionFactories: PluginRegistry["codexAppServerExtensionFactories"];
|
||||
agentToolResultMiddlewares: PluginRegistry["agentToolResultMiddlewares"];
|
||||
memoryEmbeddingProviders: PluginRegistry["memoryEmbeddingProviders"];
|
||||
agentHarnesses: PluginRegistry["agentHarnesses"];
|
||||
httpRoutes: PluginRegistry["httpRoutes"];
|
||||
@@ -318,6 +319,7 @@ function snapshotPluginRegistry(registry: PluginRegistry): PluginRegistrySnapsho
|
||||
webSearchProviders: [...registry.webSearchProviders],
|
||||
embeddedExtensionFactories: [...registry.embeddedExtensionFactories],
|
||||
codexAppServerExtensionFactories: [...registry.codexAppServerExtensionFactories],
|
||||
agentToolResultMiddlewares: [...registry.agentToolResultMiddlewares],
|
||||
memoryEmbeddingProviders: [...registry.memoryEmbeddingProviders],
|
||||
agentHarnesses: [...registry.agentHarnesses],
|
||||
httpRoutes: [...registry.httpRoutes],
|
||||
@@ -355,6 +357,7 @@ function restorePluginRegistry(registry: PluginRegistry, snapshot: PluginRegistr
|
||||
registry.webSearchProviders = snapshot.arrays.webSearchProviders;
|
||||
registry.embeddedExtensionFactories = snapshot.arrays.embeddedExtensionFactories;
|
||||
registry.codexAppServerExtensionFactories = snapshot.arrays.codexAppServerExtensionFactories;
|
||||
registry.agentToolResultMiddlewares = snapshot.arrays.agentToolResultMiddlewares;
|
||||
registry.memoryEmbeddingProviders = snapshot.arrays.memoryEmbeddingProviders;
|
||||
registry.agentHarnesses = snapshot.arrays.agentHarnesses;
|
||||
registry.httpRoutes = snapshot.arrays.httpRoutes;
|
||||
|
||||
@@ -233,6 +233,7 @@ export type PluginManifest = {
|
||||
|
||||
export type PluginManifestContracts = {
|
||||
embeddedExtensionFactories?: string[];
|
||||
agentToolResultMiddleware?: string[];
|
||||
/**
|
||||
* Provider ids whose external auth profile hook can contribute runtime-only
|
||||
* credentials. Declaring this lets auth-store overlays load only the owning
|
||||
@@ -426,6 +427,7 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
|
||||
}
|
||||
|
||||
const embeddedExtensionFactories = normalizeTrimmedStringList(value.embeddedExtensionFactories);
|
||||
const agentToolResultMiddleware = normalizeTrimmedStringList(value.agentToolResultMiddleware);
|
||||
const externalAuthProviders = normalizeTrimmedStringList(value.externalAuthProviders);
|
||||
const memoryEmbeddingProviders = normalizeTrimmedStringList(value.memoryEmbeddingProviders);
|
||||
const speechProviders = normalizeTrimmedStringList(value.speechProviders);
|
||||
@@ -442,6 +444,7 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
|
||||
const tools = normalizeTrimmedStringList(value.tools);
|
||||
const contracts = {
|
||||
...(embeddedExtensionFactories.length > 0 ? { embeddedExtensionFactories } : {}),
|
||||
...(agentToolResultMiddleware.length > 0 ? { agentToolResultMiddleware } : {}),
|
||||
...(externalAuthProviders.length > 0 ? { externalAuthProviders } : {}),
|
||||
...(memoryEmbeddingProviders.length > 0 ? { memoryEmbeddingProviders } : {}),
|
||||
...(speechProviders.length > 0 ? { speechProviders } : {}),
|
||||
|
||||
@@ -22,6 +22,7 @@ export function createEmptyPluginRegistry(): PluginRegistry {
|
||||
webSearchProviders: [],
|
||||
embeddedExtensionFactories: [],
|
||||
codexAppServerExtensionFactories: [],
|
||||
agentToolResultMiddlewares: [],
|
||||
memoryEmbeddingProviders: [],
|
||||
agentHarnesses: [],
|
||||
gatewayHandlers: {},
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { OperatorScope } from "../gateway/operator-scopes.js";
|
||||
import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js";
|
||||
import type { HookEntry } from "../hooks/types.js";
|
||||
import type { JsonSchemaObject } from "../shared/json-schema.types.js";
|
||||
import type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareHarness,
|
||||
} from "./agent-tool-result-middleware-types.js";
|
||||
import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js";
|
||||
import type { PluginActivationSource } from "./config-state.js";
|
||||
import type {
|
||||
@@ -164,6 +168,15 @@ export type PluginCodexAppServerExtensionFactoryRegistration = {
|
||||
source: string;
|
||||
rootDir?: string;
|
||||
};
|
||||
export type PluginAgentToolResultMiddlewareRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
rawHandler: AgentToolResultMiddleware;
|
||||
handler: AgentToolResultMiddleware;
|
||||
harnesses: AgentToolResultMiddlewareHarness[];
|
||||
source: string;
|
||||
rootDir?: string;
|
||||
};
|
||||
export type PluginAgentHarnessRegistration = {
|
||||
pluginId: string;
|
||||
pluginName?: string;
|
||||
@@ -312,6 +325,7 @@ export type PluginRegistry = {
|
||||
webSearchProviders: PluginWebSearchProviderRegistration[];
|
||||
embeddedExtensionFactories: PluginEmbeddedExtensionFactoryRegistration[];
|
||||
codexAppServerExtensionFactories: PluginCodexAppServerExtensionFactoryRegistration[];
|
||||
agentToolResultMiddlewares: PluginAgentToolResultMiddlewareRegistration[];
|
||||
memoryEmbeddingProviders: PluginMemoryEmbeddingProviderRegistration[];
|
||||
agentHarnesses: PluginAgentHarnessRegistration[];
|
||||
gatewayHandlers: GatewayRequestHandlers;
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
registerDetachedTaskLifecycleRuntime,
|
||||
} from "../tasks/detached-task-runtime-state.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import type { AgentToolResultMiddleware } from "./agent-tool-result-middleware-types.js";
|
||||
import { normalizeAgentToolResultMiddlewareHarnesses } from "./agent-tool-result-middleware.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { normalizeRegisteredChannelPlugin } from "./channel-validation.js";
|
||||
import { CODEX_APP_SERVER_EXTENSION_RUNTIME_ID } from "./codex-app-server-extension-factory.js";
|
||||
@@ -329,6 +331,70 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
};
|
||||
|
||||
const registerAgentToolResultMiddleware = (
|
||||
record: PluginRecord,
|
||||
handler: Parameters<OpenClawPluginApi["registerAgentToolResultMiddleware"]>[0],
|
||||
options: Parameters<OpenClawPluginApi["registerAgentToolResultMiddleware"]>[1],
|
||||
) => {
|
||||
if (typeof (handler as unknown) !== "function") {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "agent tool result middleware must be a function",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const harnesses = normalizeAgentToolResultMiddlewareHarnesses(options);
|
||||
if (harnesses.length === 0) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "agent tool result middleware must target at least one supported harness",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const declared = record.contracts?.agentToolResultMiddleware ?? [];
|
||||
const missing = harnesses.filter((harness) => !declared.includes(harness));
|
||||
if (missing.length > 0) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `plugin must declare contracts.agentToolResultMiddleware for: ${missing.join(", ")}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existing = registry.agentToolResultMiddlewares.find(
|
||||
(entry) => entry.pluginId === record.id && entry.rawHandler === handler,
|
||||
);
|
||||
if (existing) {
|
||||
existing.harnesses = [...new Set([...existing.harnesses, ...harnesses])];
|
||||
return;
|
||||
}
|
||||
const safeHandler: AgentToolResultMiddleware = async (event, ctx) => {
|
||||
try {
|
||||
return await handler(event, ctx);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
registryParams.logger.warn(
|
||||
`[plugins] agent tool result middleware failed for ${record.id}: ${detail}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
registry.agentToolResultMiddlewares.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
rawHandler: handler,
|
||||
handler: safeHandler,
|
||||
harnesses,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
};
|
||||
|
||||
const registerTool = (
|
||||
record: PluginRecord,
|
||||
tool: AnyAgentTool | OpenClawPluginToolFactory,
|
||||
@@ -1466,6 +1532,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registerCodexAppServerExtensionFactory: (factory) => {
|
||||
registerCodexAppServerExtensionFactory(record, factory);
|
||||
},
|
||||
registerAgentToolResultMiddleware: (handler, options) => {
|
||||
registerAgentToolResultMiddleware(record, handler, options);
|
||||
},
|
||||
registerMemoryCapability: (capability) => {
|
||||
if (!hasKind(record.kind, "memory")) {
|
||||
pushDiagnostic({
|
||||
|
||||
@@ -131,6 +131,7 @@ export function createPluginLoadResult(
|
||||
webSearchProviders: [],
|
||||
embeddedExtensionFactories: [],
|
||||
codexAppServerExtensionFactories: [],
|
||||
agentToolResultMiddlewares: [],
|
||||
memoryEmbeddingProviders: [],
|
||||
textTransforms: [],
|
||||
agentHarnesses: [],
|
||||
|
||||
@@ -69,6 +69,10 @@ import type {
|
||||
} from "../tts/provider-types.js";
|
||||
import type { VideoGenerationProvider } from "../video-generation/types.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareOptions,
|
||||
} from "./agent-tool-result-middleware-types.js";
|
||||
import type {
|
||||
CliBackendAuthEpochMode,
|
||||
CliBackendNormalizeConfigContext,
|
||||
@@ -142,6 +146,15 @@ export type {
|
||||
} from "./tool-types.js";
|
||||
export type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
export type { AgentHarness } from "../agents/harness/types.js";
|
||||
export type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareContext,
|
||||
AgentToolResultMiddlewareEvent,
|
||||
AgentToolResultMiddlewareHarness,
|
||||
AgentToolResultMiddlewareOptions,
|
||||
AgentToolResultMiddlewareResult,
|
||||
OpenClawAgentToolResult,
|
||||
} from "./agent-tool-result-middleware-types.js";
|
||||
export type {
|
||||
PluginConversationBinding,
|
||||
PluginConversationBindingRequestParams,
|
||||
@@ -2119,10 +2132,21 @@ export type OpenClawPluginApi = {
|
||||
) => void;
|
||||
/** Register an agent harness implementation. */
|
||||
registerAgentHarness: (harness: AgentHarness) => void;
|
||||
/** Register a Pi embedded extension factory for OpenClaw embedded runs. Only bundled plugins may use this seam, and `contracts.embeddedExtensionFactories` must include `"pi"`. */
|
||||
/**
|
||||
* Register a Pi embedded extension factory for OpenClaw embedded runs.
|
||||
*
|
||||
* @deprecated This is a bundled compatibility seam. New tool-result transforms
|
||||
* should use `registerAgentToolResultMiddleware(...)` and declare
|
||||
* `contracts.agentToolResultMiddleware` for the targeted harnesses.
|
||||
*/
|
||||
registerEmbeddedExtensionFactory: (factory: ExtensionFactory) => void;
|
||||
/** Register a Codex app-server extension factory for Codex harness tool-result middleware. Only bundled plugins may use this seam, and `contracts.embeddedExtensionFactories` must include `"codex-app-server"`. */
|
||||
registerCodexAppServerExtensionFactory: (factory: CodexAppServerExtensionFactory) => void;
|
||||
/** Register harness-neutral tool-result middleware. Declare `contracts.agentToolResultMiddleware` for every targeted harness. */
|
||||
registerAgentToolResultMiddleware: (
|
||||
handler: AgentToolResultMiddleware,
|
||||
options?: AgentToolResultMiddlewareOptions,
|
||||
) => void;
|
||||
/** Register the active detached task runtime for this plugin (exclusive slot). */
|
||||
registerDetachedTaskRuntime: (
|
||||
runtime: import("./runtime/runtime-tasks.types.js").DetachedTaskLifecycleRuntime,
|
||||
|
||||
@@ -37,6 +37,7 @@ export const createTestRegistry = (channels: TestChannelRegistration[] = []): Pl
|
||||
webSearchProviders: [],
|
||||
embeddedExtensionFactories: [],
|
||||
codexAppServerExtensionFactories: [],
|
||||
agentToolResultMiddlewares: [],
|
||||
memoryEmbeddingProviders: [],
|
||||
textTransforms: [],
|
||||
agentHarnesses: [],
|
||||
|
||||
@@ -44,6 +44,7 @@ export function createTestPluginApi(api: TestPluginApiInput = {}): OpenClawPlugi
|
||||
registerAgentHarness() {},
|
||||
registerEmbeddedExtensionFactory() {},
|
||||
registerCodexAppServerExtensionFactory() {},
|
||||
registerAgentToolResultMiddleware() {},
|
||||
registerDetachedTaskRuntime() {},
|
||||
registerMemoryCapability() {},
|
||||
registerMemoryPromptSection() {},
|
||||
|
||||
Reference in New Issue
Block a user