feat(plugins): add harness tool result middleware (#71021)

This commit is contained in:
Vincent Koc
2026-04-24 12:39:13 -07:00
committed by GitHub
parent e471d40942
commit 47f6a98909
38 changed files with 738 additions and 86 deletions

View File

@@ -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);

View File

@@ -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,