refactor(agents): centralize delivery fallback

This commit is contained in:
Peter Steinberger
2026-06-19 16:53:01 -04:00
parent ec8f0dcd5d
commit e5f948cf31
6 changed files with 124 additions and 66 deletions

View File

@@ -25,7 +25,6 @@ import {
isMessagingToolSendAction,
normalizeHeartbeatToolResponse,
projectRuntimeToolInputSchema,
createDeliveredMessagingResultReconciler,
runAgentHarnessAfterToolCallHook,
sanitizeToolResult,
setBeforeToolCallDiagnosticsEnabled,
@@ -289,12 +288,6 @@ export function createCodexDynamicToolBridge(params: {
);
const telemetryRawResult = sanitizeToolResult(rawResult);
const rawIsError = isCodexToolResultError(rawResult);
const reconcileMiddlewareResult = createDeliveredMessagingResultReconciler({
toolName,
args: executedArgs,
rawResult,
rawIsError,
});
const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
threadId: call.threadId,
turnId: call.turnId,
@@ -304,14 +297,13 @@ export function createCodexDynamicToolBridge(params: {
isError: rawIsError,
result: rawResult,
});
const reconciledMiddlewareResult = reconcileMiddlewareResult(middlewareResult);
const result = await legacyExtensionRunner.applyToolResultExtensions({
threadId: call.threadId,
turnId: call.turnId,
toolCallId: call.callId,
toolName,
args: structuredClone(executedArgs),
result: reconciledMiddlewareResult,
result: middlewareResult,
});
const resultIsError = rawIsError || isCodexToolResultError(result);
notifyAgentToolResult(options?.onAgentToolResult, toolName, result, resultIsError);

View File

@@ -18,7 +18,6 @@ import {
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { createAgentToolResultMiddlewareRunner } from "../harness/tool-result-middleware.js";
import { createDeliveredMessagingResultReconciler } from "../messaging-tool-result-fallback.js";
import type { AgentToolResult } from "../runtime/index.js";
import type { ExtensionFactory, SessionManager } from "../sessions/index.js";
import { isToolResultError } from "../tool-result-error.js";
@@ -80,24 +79,16 @@ function buildAgentToolResultMiddlewareFactory(
const adjustedInput = eventToolCallId
? peekAdjustedParamsForToolCall(eventToolCallId, runId)
: undefined;
const args = recordFromUnknown(adjustedInput ?? event.input);
const reconcileMiddlewareResult = createDeliveredMessagingResultReconciler({
toolName: event.toolName,
args,
rawResult: current,
rawIsError: event.isError === true || inputHadErrorStatus,
});
const middlewareResult = await runner.applyToolResultMiddleware({
const result = await runner.applyToolResultMiddleware({
threadId: event.threadId,
turnId: event.turnId,
toolCallId,
toolName: event.toolName,
args,
args: recordFromUnknown(adjustedInput ?? event.input),
cwd: ctx.cwd,
isError: event.isError,
result: current,
});
const result = reconcileMiddlewareResult(middlewareResult);
const isError = event.isError === true || inputHadErrorStatus || isToolResultError(result);
if (eventToolCallId) {
finalizeToolTerminalPresentation({

View File

@@ -550,6 +550,70 @@ describe("createAgentToolResultMiddlewareRunner", () => {
expect(sanitized.originalSizeBytes ?? 0).toBeGreaterThan(100_000);
});
it("snapshots confirmed delivery before oversized details are collapsed", async () => {
const runner = createAgentToolResultMiddlewareRunner({ runtime: "codex" }, [
() => {
throw new Error("post-processing failed");
},
]);
const result = await runner.applyToolResultMiddleware({
toolCallId: "call-1",
toolName: "message",
args: { action: "send", target: "C123" },
result: {
content: [{ type: "text", text: "raw result must stay private" }],
details: {
ok: true,
result: { messageId: "1700000000.000100", channelId: "C123" },
raw: "x".repeat(200_000),
},
},
});
expect(result).toEqual({
content: [{ type: "text", text: "Message delivered, but result post-processing failed." }],
details: {
ok: true,
deliveryStatus: "sent",
middlewareWarning: "post-processing failed",
},
});
});
it("preserves confirmed delivery when middleware returns an explicit failure", async () => {
const runner = createAgentToolResultMiddlewareRunner({ runtime: "codex" }, [
() => ({
result: {
content: [{ type: "text", text: "post-processing failed" }],
details: { status: "error", middlewareError: true },
},
}),
]);
const result = await runner.applyToolResultMiddleware({
toolCallId: "call-1",
toolName: "message",
args: { action: "send", target: "C123" },
result: {
content: [{ type: "text", text: "raw result must stay private" }],
details: {
ok: true,
result: { messageId: "1700000000.000100", channelId: "C123" },
},
},
});
expect(result).toEqual({
content: [{ type: "text", text: "Message delivered, but result post-processing failed." }],
details: {
ok: true,
deliveryStatus: "sent",
middlewareWarning: "post-processing failed",
},
});
});
it("accepts well-formed middleware results", async () => {
const runner = createAgentToolResultMiddlewareRunner({ runtime: "codex" }, [
(eventValue, ctx) => ({

View File

@@ -11,6 +11,12 @@ import type {
} from "../../plugins/agent-tool-result-middleware-types.js";
import { createLazyPromiseLoader } from "../../shared/lazy-promise.js";
import { truncateUtf16Safe } from "../../utils.js";
import {
hasMessagingDeliveryReceipt,
isDeliveredMessagingToolResult,
} from "../embedded-agent-message-tool-source-reply.js";
import { isMessagingToolSendAction } from "../embedded-agent-messaging.js";
import { isToolResultError } from "../tool-result-error.js";
const log = createSubsystemLogger("agents/harness");
const MAX_MIDDLEWARE_CONTENT_BLOCKS = 200;
@@ -429,6 +435,42 @@ function buildMiddlewareFailureResult(): OpenClawAgentToolResult {
};
}
function buildDeliveredMessagingFailureFallback(
event: AgentToolResultMiddlewareEvent,
result: OpenClawAgentToolResult,
): OpenClawAgentToolResult | undefined {
if (
event.isError === true ||
isToolResultError(result) ||
!isMessagingToolSendAction(event.toolName, event.args) ||
!isDeliveredMessagingToolResult({
toolName: event.toolName,
args: event.args,
result,
}) ||
!hasMessagingDeliveryReceipt(result)
) {
return undefined;
}
return {
content: [{ type: "text", text: "Message delivered, but result post-processing failed." }],
details: {
ok: true,
deliveryStatus: "sent",
middlewareWarning: "post-processing failed",
},
};
}
function reconcileDeliveredMessagingFailure(
result: OpenClawAgentToolResult,
fallback: OpenClawAgentToolResult | undefined,
): OpenClawAgentToolResult {
return fallback && isRecord(result.details) && result.details.middlewareError === true
? fallback
: result;
}
export function createAgentToolResultMiddlewareRunner(
ctx: AgentToolResultMiddlewareContext,
handlers?: AgentToolResultMiddleware[],
@@ -461,6 +503,12 @@ export function createAgentToolResultMiddlewareRunner(
if (handlersForRun.length === 0) {
return event.result;
}
// Snapshot the confirmed side effect before legacy middleware can mutate
// or sanitization can collapse the receipt; never expose the raw result.
const deliveredMessagingFallback = buildDeliveredMessagingFailureFallback(
event,
event.result,
);
let current = sanitizeToolResultForMiddleware(event.result);
for (const handler of handlersForRun) {
try {
@@ -479,7 +527,10 @@ export function createAgentToolResultMiddlewareRunner(
120,
)}`,
);
return buildMiddlewareFailureResult();
return reconcileDeliveredMessagingFailure(
buildMiddlewareFailureResult(),
deliveredMessagingFallback,
);
}
} catch {
log.warn(
@@ -488,10 +539,13 @@ export function createAgentToolResultMiddlewareRunner(
120,
)}`,
);
return buildMiddlewareFailureResult();
return reconcileDeliveredMessagingFailure(
buildMiddlewareFailureResult(),
deliveredMessagingFallback,
);
}
}
return current;
return reconcileDeliveredMessagingFailure(current, deliveredMessagingFallback);
},
};
}

View File

@@ -1,42 +0,0 @@
import type { OpenClawAgentToolResult } from "../plugins/agent-tool-result-middleware-types.js";
import {
hasMessagingDeliveryReceipt,
isDeliveredMessagingToolResult,
} from "./embedded-agent-message-tool-source-reply.js";
import { isMessagingToolSendAction } from "./embedded-agent-messaging.js";
import { readToolResultDetails } from "./tool-result-error.js";
/** Snapshots confirmed delivery before middleware can mutate the raw result in place. */
export function createDeliveredMessagingResultReconciler(params: {
toolName: string;
args: Record<string, unknown>;
rawResult: OpenClawAgentToolResult;
rawIsError: boolean;
}): (middlewareResult: OpenClawAgentToolResult) => OpenClawAgentToolResult {
const confirmedDelivery =
!params.rawIsError &&
isMessagingToolSendAction(params.toolName, params.args) &&
isDeliveredMessagingToolResult({
toolName: params.toolName,
args: params.args,
result: params.rawResult,
}) &&
hasMessagingDeliveryReceipt(params.rawResult);
const fallback: OpenClawAgentToolResult = {
content: [
{
type: "text",
text: "Message delivered, but result post-processing failed.",
},
],
details: {
ok: true,
deliveryStatus: "sent",
middlewareWarning: "post-processing failed",
},
};
return (middlewareResult) =>
confirmedDelivery && readToolResultDetails(middlewareResult)?.middlewareError === true
? fallback
: middlewareResult;
}

View File

@@ -289,7 +289,6 @@ export {
} 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 { createDeliveredMessagingResultReconciler } from "../agents/messaging-tool-result-fallback.js";
export {
assertContextEngineHostSupport,
CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,