refactor: add provider replay runtime hook surfaces (#59143)

Merged via squash.

Prepared head SHA: 56b41e87a5
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Josh Lehman
2026-04-01 13:45:41 -07:00
committed by GitHub
parent ca76e2fedc
commit 71346940ad
15 changed files with 771 additions and 102 deletions

View File

@@ -37,6 +37,9 @@ vi.mock("../plugins/provider-runtime.js", () => ({
dropThinkingBlockModelHints: ["claude"],
}
: undefined,
sanitizeProviderReplayHistoryWithPlugin: vi.fn(async ({ messages }) => messages),
resolveProviderReplayPolicyWithPlugin: vi.fn(() => undefined),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
}));
let sanitizeSessionHistory: SanitizeSessionHistoryFn;

View File

@@ -54,11 +54,7 @@ import { ensureOpenClawModelsJson } from "../models-config.js";
import { resolveOwnerDisplaySetting } from "../owner-display.js";
import { createBundleLspToolRuntime } from "../pi-bundle-lsp-runtime.js";
import { createBundleMcpToolRuntime } from "../pi-bundle-mcp-tools.js";
import {
ensureSessionHeader,
validateAnthropicTurns,
validateGeminiTurns,
} from "../pi-embedded-helpers.js";
import { ensureSessionHeader } from "../pi-embedded-helpers.js";
import {
consumeCompactionSafeguardCancelReason,
setCompactionSafeguardCancelReason,
@@ -106,6 +102,7 @@ import {
logToolSchemasForGoogle,
sanitizeSessionHistory,
sanitizeToolsForGoogle,
validateReplayTurns,
} from "./google.js";
import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js";
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
@@ -488,6 +485,12 @@ export async function compactEmbeddedPiSessionDirect(
const tools = sanitizeToolsForGoogle({
tools: toolsEnabled ? toolsRaw : [],
provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId,
modelApi: model.api,
model,
});
const bundleMcpRuntime = toolsEnabled
? await createBundleMcpToolRuntime({
@@ -512,7 +515,16 @@ export async function compactEmbeddedPiSessionDirect(
...(bundleLspRuntime?.tools ?? []),
];
const allowedToolNames = collectAllowedToolNames({ tools: effectiveTools });
logToolSchemasForGoogle({ tools: effectiveTools, provider });
logToolSchemasForGoogle({
tools: effectiveTools,
provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId,
modelApi: model.api,
model,
});
const machineName = await getMachineDisplayName();
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
let runtimeCapabilities = runtimeChannel
@@ -593,7 +605,14 @@ export async function compactEmbeddedPiSessionDirect(
channelActions,
};
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
const reasoningTagHint = isReasoningTagProvider(provider);
const reasoningTagHint = isReasoningTagProvider(provider, {
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId,
modelApi: model.api,
model,
});
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
@@ -658,6 +677,10 @@ export async function compactEmbeddedPiSessionDirect(
modelApi: model.api,
provider,
modelId,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model,
});
const sessionManager = guardSessionManager(SessionManager.open(params.sessionFile), {
agentId: sessionAgentId,
@@ -731,16 +754,25 @@ export async function compactEmbeddedPiSessionDirect(
provider,
allowedToolNames,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model,
sessionManager,
sessionId: params.sessionId,
policy: transcriptPolicy,
});
const validatedGemini = transcriptPolicy.validateGeminiTurns
? validateGeminiTurns(prior)
: prior;
const validated = transcriptPolicy.validateAnthropicTurns
? validateAnthropicTurns(validatedGemini)
: validatedGemini;
const validated = await validateReplayTurns({
messages: prior,
modelApi: model.api,
modelId,
provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model,
sessionId: params.sessionId,
policy: transcriptPolicy,
});
// Apply validated transcript to the live session even when no history limit is configured,
// so compaction and hook metrics are based on the same message set.
session.agent.replaceMessages(validated);

View File

@@ -4,6 +4,12 @@ import type { SessionManager } from "@mariozechner/pi-coding-agent";
import type { TSchema } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
import {
normalizeProviderToolSchemasWithPlugin,
sanitizeProviderReplayHistoryWithPlugin,
validateProviderReplayTurnsWithPlugin,
} from "../../plugins/provider-runtime.js";
import type { ProviderRuntimeModel } from "../../plugins/types.js";
import {
hasInterSessionUserProvenance,
normalizeInputProvenance,
@@ -16,6 +22,8 @@ import {
isGoogleModelApi,
sanitizeGoogleTurnOrdering,
sanitizeSessionMessagesImages,
validateAnthropicTurns,
validateGeminiTurns,
} from "../pi-embedded-helpers.js";
import { cleanToolSchemaForGemini } from "../pi-tools.schema.js";
import {
@@ -23,6 +31,7 @@ import {
stripToolResultDetails,
sanitizeToolUseResultPairing,
} from "../session-transcript-repair.js";
import type { AnyAgentTool } from "../tools/common.js";
import type { TranscriptPolicy } from "../transcript-policy.js";
import { resolveTranscriptPolicy } from "../transcript-policy.js";
import {
@@ -407,12 +416,39 @@ export function sanitizeToolsForGoogle<
>(params: {
tools: AgentTool<TSchemaType, TResult>[];
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
modelId?: string;
modelApi?: string | null;
model?: ProviderRuntimeModel;
}): AgentTool<TSchemaType, TResult>[] {
const provider = params.provider.trim();
const pluginNormalized = normalizeProviderToolSchemasWithPlugin({
provider,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
context: {
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
provider,
modelId: params.modelId,
modelApi: params.modelApi,
model: params.model,
tools: params.tools as unknown as AnyAgentTool[],
},
});
if (Array.isArray(pluginNormalized)) {
return pluginNormalized as AgentTool<TSchemaType, TResult>[];
}
// Cloud Code Assist uses the OpenAPI 3.03 `parameters` field for both Gemini
// AND Claude models. This field does not support JSON Schema keywords such as
// patternProperties, additionalProperties, $ref, etc. We must clean schemas
// for every provider that routes through this path.
if (params.provider !== "google-gemini-cli") {
if (provider !== "google-gemini-cli") {
return params.tools;
}
return params.tools.map((tool) => {
@@ -428,8 +464,17 @@ export function sanitizeToolsForGoogle<
});
}
export function logToolSchemasForGoogle(params: { tools: AgentTool[]; provider: string }) {
if (params.provider !== "google-gemini-cli") {
export function logToolSchemasForGoogle(params: {
tools: AgentTool[];
provider: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
modelId?: string;
modelApi?: string | null;
model?: ProviderRuntimeModel;
}) {
if (params.provider.trim() !== "google-gemini-cli") {
return;
}
const toolNames = params.tools.map((tool, index) => `${index}:${tool.name}`);
@@ -581,6 +626,9 @@ export async function sanitizeSessionHistory(params: {
provider?: string;
allowedToolNames?: Iterable<string>;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
model?: ProviderRuntimeModel;
sessionManager: SessionManager;
sessionId: string;
policy?: TranscriptPolicy;
@@ -592,6 +640,10 @@ export async function sanitizeSessionHistory(params: {
modelApi: params.modelApi,
provider: params.provider,
modelId: params.modelId,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
model: params.model,
});
const withInterSessionMarkers = annotateInterSessionUserMessages(params.messages);
const canonicalizedAssistantHistory = canonicalizeAssistantHistoryMessages({
@@ -645,6 +697,29 @@ export async function sanitizeSessionHistory(params: {
downgradeOpenAIReasoningBlocks(sanitizedCompactionUsage),
)
: sanitizedCompactionUsage;
const provider = params.provider?.trim();
const providerSanitized =
provider && provider.length > 0
? await sanitizeProviderReplayHistoryWithPlugin({
provider,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
context: {
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
provider,
modelId: params.modelId,
modelApi: params.modelApi,
model: params.model,
sessionId: params.sessionId,
messages: sanitizedOpenAI,
allowedToolNames: params.allowedToolNames,
},
})
: undefined;
const sanitizedWithProvider = providerSanitized ?? sanitizedOpenAI;
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
appendModelSnapshot(params.sessionManager, {
@@ -656,13 +731,13 @@ export async function sanitizeSessionHistory(params: {
}
if (!policy.applyGoogleTurnOrdering) {
return sanitizedOpenAI;
return sanitizedWithProvider;
}
// Google models use the full wrapper with logging and session markers.
if (isGoogleModelApi(params.modelApi)) {
return applyGoogleTurnOrderingFix({
messages: sanitizedOpenAI,
messages: sanitizedWithProvider,
modelApi: params.modelApi,
sessionManager: params.sessionManager,
sessionId: params.sessionId,
@@ -673,5 +748,58 @@ export async function sanitizeSessionHistory(params: {
// conversations that start with an assistant turn (e.g. delivery-mirror
// messages after /new). Apply the same ordering fix without the
// Google-specific session markers. See #38962.
return sanitizeGoogleTurnOrdering(sanitizedOpenAI);
return sanitizeGoogleTurnOrdering(sanitizedWithProvider);
}
export async function validateReplayTurns(params: {
messages: AgentMessage[];
modelApi?: string | null;
modelId?: string;
provider?: string;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
model?: ProviderRuntimeModel;
sessionId?: string;
policy?: TranscriptPolicy;
}): Promise<AgentMessage[]> {
const policy =
params.policy ??
resolveTranscriptPolicy({
modelApi: params.modelApi,
provider: params.provider,
modelId: params.modelId,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
model: params.model,
});
const provider = params.provider?.trim();
if (provider) {
const providerValidated = await validateProviderReplayTurnsWithPlugin({
provider,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
context: {
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
provider,
modelId: params.modelId,
modelApi: params.modelApi,
model: params.model,
sessionId: params.sessionId,
messages: params.messages,
},
});
if (providerValidated) {
return providerValidated;
}
}
const validatedGemini = policy.validateGeminiTurns
? validateGeminiTurns(params.messages)
: params.messages;
return policy.validateAnthropicTurns ? validateAnthropicTurns(validatedGemini) : validatedGemini;
}

View File

@@ -68,8 +68,6 @@ import {
resolveBootstrapMaxChars,
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
validateAnthropicTurns,
validateGeminiTurns,
} from "../../pi-embedded-helpers.js";
import { subscribeEmbeddedPiSession } from "../../pi-embedded-subscribe.js";
import { createPreparedEmbeddedPiSettingsManager } from "../../pi-project-settings.js";
@@ -107,6 +105,7 @@ import {
logToolSchemasForGoogle,
sanitizeSessionHistory,
sanitizeToolsForGoogle,
validateReplayTurns,
} from "../google.js";
import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "../history.js";
import { log } from "../logger.js";
@@ -419,64 +418,64 @@ export async function runEmbeddedAttempt(
? []
: (() => {
const allTools = createOpenClawCodingTools({
agentId: sessionAgentId,
trigger: params.trigger,
memoryFlushWritePath: params.memoryFlushWritePath,
exec: {
...params.execOverrides,
elevated: params.bashElevated,
},
sandbox,
messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
senderIsOwner: params.senderIsOwner,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
runId: params.runId,
agentDir,
workspaceDir: effectiveWorkspace,
// When sandboxing uses a copied workspace (`ro` or `none`), effectiveWorkspace points
// at the sandbox copy. Spawned subagents should inherit the real workspace instead.
spawnWorkspaceDir: resolveAttemptSpawnWorkspaceDir({
agentId: sessionAgentId,
trigger: params.trigger,
memoryFlushWritePath: params.memoryFlushWritePath,
exec: {
...params.execOverrides,
elevated: params.bashElevated,
},
sandbox,
resolvedWorkspace,
}),
config: params.config,
abortSignal: runAbortController.signal,
modelProvider: params.model.provider,
modelId: params.modelId,
modelCompat: params.model.compat,
modelApi: params.model.api,
modelContextWindowTokens: params.model.contextWindow,
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
modelHasVision,
requireExplicitMessageTarget:
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
disableMessageTool: params.disableMessageTool,
onYield: (message) => {
yieldDetected = true;
yieldMessage = message;
queueYieldInterruptForSession?.();
runAbortController.abort("sessions_yield");
abortSessionForYield?.();
},
});
messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
senderIsOwner: params.senderIsOwner,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
runId: params.runId,
agentDir,
workspaceDir: effectiveWorkspace,
// When sandboxing uses a copied workspace (`ro` or `none`), effectiveWorkspace points
// at the sandbox copy. Spawned subagents should inherit the real workspace instead.
spawnWorkspaceDir: resolveAttemptSpawnWorkspaceDir({
sandbox,
resolvedWorkspace,
}),
config: params.config,
abortSignal: runAbortController.signal,
modelProvider: params.model.provider,
modelId: params.modelId,
modelCompat: params.model.compat,
modelApi: params.model.api,
modelContextWindowTokens: params.model.contextWindow,
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
modelHasVision,
requireExplicitMessageTarget:
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
disableMessageTool: params.disableMessageTool,
onYield: (message) => {
yieldDetected = true;
yieldMessage = message;
queueYieldInterruptForSession?.();
runAbortController.abort("sessions_yield");
abortSessionForYield?.();
},
});
if (params.toolsAllow && params.toolsAllow.length > 0) {
const allowSet = new Set(params.toolsAllow);
return allTools.filter((tool) => allowSet.has(tool.name));
@@ -487,6 +486,12 @@ export async function runEmbeddedAttempt(
const tools = sanitizeToolsForGoogle({
tools: toolsEnabled ? toolsRaw : [],
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
});
const clientTools = toolsEnabled ? params.clientTools : undefined;
const bundleMcpSessionRuntime = toolsEnabled
@@ -526,7 +531,16 @@ export async function runEmbeddedAttempt(
tools: effectiveTools,
clientTools,
});
logToolSchemasForGoogle({ tools: effectiveTools, provider: params.provider });
logToolSchemasForGoogle({
tools: effectiveTools,
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
});
const machineName = await getMachineDisplayName();
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
@@ -568,7 +582,14 @@ export async function runEmbeddedAttempt(
})
: undefined;
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
const reasoningTagHint = isReasoningTagProvider(params.provider);
const reasoningTagHint = isReasoningTagProvider(params.provider, {
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
});
// Resolve channel-specific message actions for system prompt
const channelActions = runtimeChannel
? listChannelSupportedActions(
@@ -621,7 +642,7 @@ export async function runEmbeddedAttempt(
const promptMode = resolvePromptModeForSession(params.sessionKey);
// When toolsAllow is set, use minimal prompt and strip skills catalog
const effectivePromptMode = params.toolsAllow?.length ? "minimal" as const : promptMode;
const effectivePromptMode = params.toolsAllow?.length ? ("minimal" as const) : promptMode;
const effectiveSkillsPrompt = params.toolsAllow?.length ? undefined : skillsPrompt;
const docsPath = await resolveOpenClawDocsPath({
workspaceDir: effectiveWorkspace,
@@ -724,6 +745,10 @@ export async function runEmbeddedAttempt(
modelApi: params.model?.api,
provider: params.provider,
modelId: params.modelId,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model: params.model,
});
await prewarmSessionFile(params.sessionFile);
@@ -1098,17 +1123,26 @@ export async function runEmbeddedAttempt(
provider: params.provider,
allowedToolNames,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model: params.model,
sessionManager,
sessionId: params.sessionId,
policy: transcriptPolicy,
});
cacheTrace?.recordStage("session:sanitized", { messages: prior });
const validatedGemini = transcriptPolicy.validateGeminiTurns
? validateGeminiTurns(prior)
: prior;
const validated = transcriptPolicy.validateAnthropicTurns
? validateAnthropicTurns(validatedGemini)
: validatedGemini;
const validated = await validateReplayTurns({
messages: prior,
modelApi: params.model.api,
modelId: params.modelId,
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model: params.model,
sessionId: params.sessionId,
policy: transcriptPolicy,
});
const truncated = limitHistoryTurns(
validated,
getDmHistoryLimitFromSessionKey(params.sessionKey, params.config),

View File

@@ -24,6 +24,7 @@ vi.mock("../plugins/provider-runtime.js", () => ({
return undefined;
}
}),
resolveProviderReplayPolicyWithPlugin: vi.fn(() => undefined),
resetProviderRuntimeHookCacheForTest: vi.fn(),
}));

View File

@@ -1,3 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveProviderReplayPolicyWithPlugin } from "../plugins/provider-runtime.js";
import type { ProviderRuntimeModel } from "../plugins/types.js";
import { normalizeProviderId } from "./model-selection.js";
import { isGoogleModelApi } from "./pi-embedded-helpers/google.js";
import {
@@ -61,6 +64,10 @@ export function resolveTranscriptPolicy(params: {
modelApi?: string | null;
provider?: string | null;
modelId?: string | null;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
model?: ProviderRuntimeModel;
}): TranscriptPolicy {
const provider = normalizeProviderId(params.provider ?? "");
const modelId = params.modelId ?? "";
@@ -111,7 +118,7 @@ export function resolveTranscriptPolicy(params: {
? { allowBase64Only: true, includeCamelCase: true }
: undefined;
return {
const basePolicy: TranscriptPolicy = {
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
sanitizeToolCallIds:
(!isOpenAi && sanitizeToolCallIds) || requiresOpenAiCompatibleToolIdSanitization,
@@ -126,4 +133,52 @@ export function resolveTranscriptPolicy(params: {
validateAnthropicTurns: !isOpenAi && (isAnthropic || isStrictOpenAiCompatible),
allowSyntheticToolResults: !isOpenAi && (isGoogle || isAnthropic),
};
const pluginPolicy = provider
? resolveProviderReplayPolicyWithPlugin({
provider,
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
context: {
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
provider,
modelId,
modelApi: params.modelApi,
model: params.model,
},
})
: undefined;
if (!pluginPolicy) {
return basePolicy;
}
return {
...basePolicy,
...(pluginPolicy.sanitizeMode != null ? { sanitizeMode: pluginPolicy.sanitizeMode } : {}),
...(typeof pluginPolicy.sanitizeToolCallIds === "boolean"
? { sanitizeToolCallIds: pluginPolicy.sanitizeToolCallIds }
: {}),
...(pluginPolicy.toolCallIdMode ? { toolCallIdMode: pluginPolicy.toolCallIdMode } : {}),
...(typeof pluginPolicy.repairToolUseResultPairing === "boolean"
? { repairToolUseResultPairing: pluginPolicy.repairToolUseResultPairing }
: {}),
...(typeof pluginPolicy.preserveSignatures === "boolean"
? { preserveSignatures: pluginPolicy.preserveSignatures }
: {}),
...(pluginPolicy.sanitizeThoughtSignatures
? { sanitizeThoughtSignatures: pluginPolicy.sanitizeThoughtSignatures }
: {}),
...(typeof pluginPolicy.dropThinkingBlocks === "boolean"
? { dropThinkingBlocks: pluginPolicy.dropThinkingBlocks }
: {}),
...(typeof pluginPolicy.applyAssistantFirstOrderingFix === "boolean"
? { applyGoogleTurnOrdering: pluginPolicy.applyAssistantFirstOrderingFix }
: {}),
...(typeof pluginPolicy.allowSyntheticToolResults === "boolean"
? { allowSyntheticToolResults: pluginPolicy.allowSyntheticToolResults }
: {}),
};
}