perf(tests): avoid channel plugin imports in system prompt

This commit is contained in:
Peter Steinberger
2026-04-29 20:06:03 +01:00
parent b0ae867034
commit 4b4e0c82e4
8 changed files with 144 additions and 74 deletions

View File

@@ -6,6 +6,10 @@ import {
resolveCurrentChannelMessageToolDiscoveryAdapter,
__testing as messageActionTesting,
} from "../channels/plugins/message-action-discovery.js";
import {
channelPluginHasNativeApprovalPromptUi,
NATIVE_APPROVAL_PROMPT_RUNTIME_CAPABILITY,
} from "../channels/plugins/native-approval-prompt.js";
import type {
ChannelAgentTool,
ChannelMessageActionName,
@@ -144,9 +148,31 @@ export function resolveChannelMessageToolCapabilities(params: {
return [];
}
const cfg = params.cfg ?? ({} as OpenClawConfig);
return (resolve({ cfg, accountId: params.accountId }) ?? [])
.map((entry) => entry.trim())
.filter(Boolean);
return normalizePromptCapabilities(resolve({ cfg, accountId: params.accountId }));
}
export function resolveChannelPromptCapabilities(params: {
cfg?: OpenClawConfig;
channel?: string | null;
accountId?: string | null;
}): string[] {
const channelId = normalizeAnyChannelId(params.channel);
if (!channelId) {
return [];
}
const plugin = getChannelPlugin(channelId);
const cfg = params.cfg ?? ({} as OpenClawConfig);
const capabilities = normalizePromptCapabilities(
plugin?.agentPrompt?.messageToolCapabilities?.({ cfg, accountId: params.accountId }),
);
if (channelPluginHasNativeApprovalPromptUi(plugin)) {
capabilities.push(NATIVE_APPROVAL_PROMPT_RUNTIME_CAPABILITY);
}
return capabilities;
}
function normalizePromptCapabilities(capabilities?: readonly string[] | null): string[] {
return (capabilities ?? []).map((entry) => entry.trim()).filter(Boolean);
}
export function resolveChannelReactionGuidance(params: {

View File

@@ -9,7 +9,6 @@ import {
} from "@mariozechner/pi-coding-agent";
import { isAcpRuntimeSpawnAvailable } from "../../acp/runtime/availability.js";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
captureCompactionCheckpointSnapshot,
@@ -30,7 +29,6 @@ import {
transformProviderSystemPrompt,
} from "../../plugins/provider-runtime.js";
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeMessageChannel } from "../../utils/message-channel.js";
@@ -44,7 +42,6 @@ import {
} from "../bootstrap-files.js";
import {
listChannelSupportedActions,
resolveChannelMessageToolCapabilities,
resolveChannelMessageToolHints,
resolveChannelReactionGuidance,
} from "../channel-tools.js";
@@ -79,6 +76,7 @@ import { applyPiCompactionSettingsFromConfig } from "../pi-settings.js";
import { createOpenClawCodingTools } from "../pi-tools.js";
import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js";
import { registerProviderStreamForModel } from "../provider-stream.js";
import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js";
import { buildAgentRuntimePlan } from "../runtime-plan/build.js";
import type { AgentRuntimePlan } from "../runtime-plan/types.js";
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
@@ -634,35 +632,11 @@ export async function compactEmbeddedPiSessionDirect(
runtimePlan.tools.logDiagnostics(effectiveTools, runtimePlanModelContext);
const machineName = await getMachineDisplayName();
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
let runtimeCapabilities = runtimeChannel
? (resolveChannelCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
}) ?? [])
: undefined;
const promptCapabilities =
runtimeChannel && params.config
? resolveChannelMessageToolCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
})
: [];
if (promptCapabilities.length > 0) {
runtimeCapabilities ??= [];
const seenCapabilities = new Set(
runtimeCapabilities.map((cap) => normalizeOptionalLowercaseString(cap)).filter(Boolean),
);
for (const capability of promptCapabilities) {
const normalizedCapability = normalizeOptionalLowercaseString(capability);
if (!normalizedCapability || seenCapabilities.has(normalizedCapability)) {
continue;
}
seenCapabilities.add(normalizedCapability);
runtimeCapabilities.push(capability);
}
}
const runtimeCapabilities = collectRuntimeChannelCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
});
const reactionGuidance =
runtimeChannel && params.config
? resolveChannelReactionGuidance({

View File

@@ -9,7 +9,6 @@ import {
} from "@mariozechner/pi-coding-agent";
import { isAcpRuntimeSpawnAvailable } from "../../../acp/runtime/availability.js";
import { filterHeartbeatPairs } from "../../../auto-reply/heartbeat-filter.js";
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
import { getRuntimeConfig } from "../../../config/config.js";
import { emitTrustedDiagnosticEvent } from "../../../infra/diagnostic-events.js";
import {
@@ -36,7 +35,6 @@ import {
import { getPluginToolMeta } from "../../../plugins/tools.js";
import { isAcpSessionKey, isSubagentSessionKey } from "../../../routing/session-key.js";
import { annotateInterSessionPromptText } from "../../../sessions/input-provenance.js";
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
import {
buildTrajectoryArtifacts,
@@ -71,7 +69,6 @@ import {
import { createCacheTrace } from "../../cache-trace.js";
import {
listChannelSupportedActions,
resolveChannelMessageToolCapabilities,
resolveChannelMessageToolHints,
resolveChannelReactionGuidance,
} from "../../channel-tools.js";
@@ -122,6 +119,7 @@ import {
import { wrapStreamFnTextTransforms } from "../../plugin-text-transforms.js";
import { describeProviderRequestRoutingSummary } from "../../provider-attribution.js";
import { registerProviderStreamForModel } from "../../provider-stream.js";
import { collectRuntimeChannelCapabilities } from "../../runtime-capabilities.js";
import {
logAgentRuntimeToolDiagnostics,
normalizeAgentRuntimeTools,
@@ -1006,35 +1004,11 @@ export async function runEmbeddedAttempt(
const machineName = await getMachineDisplayName();
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
let runtimeCapabilities = runtimeChannel
? (resolveChannelCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
}) ?? [])
: undefined;
const promptCapabilities =
runtimeChannel && params.config
? resolveChannelMessageToolCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
})
: [];
if (promptCapabilities.length > 0) {
runtimeCapabilities ??= [];
const seenCapabilities = new Set(
runtimeCapabilities.map((cap) => normalizeOptionalLowercaseString(cap)).filter(Boolean),
);
for (const capability of promptCapabilities) {
const normalizedCapability = normalizeOptionalLowercaseString(capability);
if (!normalizedCapability || seenCapabilities.has(normalizedCapability)) {
continue;
}
seenCapabilities.add(normalizedCapability);
runtimeCapabilities.push(capability);
}
}
const runtimeCapabilities = collectRuntimeChannelCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
});
const reactionGuidance =
runtimeChannel && params.config
? resolveChannelReactionGuidance({

View File

@@ -0,0 +1,39 @@
import { resolveChannelCapabilities } from "../config/channel-capabilities.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { resolveChannelPromptCapabilities } from "./channel-tools.js";
export function mergeRuntimeCapabilities(
base?: readonly string[] | null,
additions: readonly string[] = [],
): string[] | undefined {
const merged = [...(base ?? [])];
const seen = new Set(
merged.map((capability) => normalizeOptionalLowercaseString(capability)).filter(Boolean),
);
for (const capability of additions) {
const normalizedCapability = normalizeOptionalLowercaseString(capability);
if (!normalizedCapability || seen.has(normalizedCapability)) {
continue;
}
seen.add(normalizedCapability);
merged.push(capability);
}
return merged.length > 0 ? merged : undefined;
}
export function collectRuntimeChannelCapabilities(params: {
cfg?: OpenClawConfig;
channel?: string | null;
accountId?: string | null;
}): string[] | undefined {
if (!params.channel) {
return undefined;
}
return mergeRuntimeCapabilities(
resolveChannelCapabilities(params),
params.cfg ? resolveChannelPromptCapabilities(params) : [],
);
}

View File

@@ -812,6 +812,18 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("do not also send plain chat /approve instructions");
});
it("suppresses plain chat approval commands for native approval channels", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
runtimeInfo: {
channel: "slack",
},
});
expect(prompt).toContain("rely on native approval card/buttons when they appear");
expect(prompt).toContain("do not also send plain chat /approve instructions");
});
it("keeps approval slug guidance separate from command previews", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",

View File

@@ -2,8 +2,10 @@ import { createHmac, createHash } from "node:crypto";
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { resolveChannelApprovalCapability } from "../channels/plugins/approvals.js";
import { getChannelPlugin } from "../channels/plugins/index.js";
import {
hasNativeApprovalPromptRuntimeCapability,
isKnownNativeApprovalPromptChannel,
} from "../channels/plugins/native-approval-prompt.js";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import { buildMemoryPromptSection } from "../plugins/memory-state.js";
import {
@@ -141,13 +143,13 @@ function buildHeartbeatSection(params: { isMinimal: boolean; heartbeatPrompt?: s
function buildExecApprovalPromptGuidance(params: {
runtimeChannel?: string;
inlineButtonsEnabled?: boolean;
runtimeCapabilities?: readonly string[];
}) {
const runtimeChannel = normalizeOptionalLowercaseString(params.runtimeChannel);
const usesNativeApprovalUi =
params.inlineButtonsEnabled ||
(runtimeChannel
? Boolean(resolveChannelApprovalCapability(getChannelPlugin(runtimeChannel))?.native)
: false);
hasNativeApprovalPromptRuntimeCapability(params.runtimeCapabilities) ||
isKnownNativeApprovalPromptChannel(runtimeChannel);
if (usesNativeApprovalUi) {
return 'When exec returns approval-pending on this channel, rely on native approval card/buttons when they appear and do not also send plain chat /approve instructions. Only include the concrete /approve command if the tool result says chat approvals are unavailable or only manual approval is possible; when needed, copy the exact /approve command from the tool output\'s "Reply with:" line.';
}
@@ -769,6 +771,7 @@ export function buildAgentSystemPrompt(params: {
buildExecApprovalPromptGuidance({
runtimeChannel: params.runtimeInfo?.channel,
inlineButtonsEnabled,
runtimeCapabilities,
}),
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
"Treat allow-once as single-command only: if another elevated command needs approval, request a fresh /approve and do not claim prior approval covered it.",

View File

@@ -0,0 +1,42 @@
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { resolveChannelApprovalCapability } from "./approvals.js";
import type { ChannelPlugin } from "./types.plugin.js";
export const NATIVE_APPROVAL_PROMPT_RUNTIME_CAPABILITY = "nativeApprovals";
const NATIVE_APPROVAL_PROMPT_RUNTIME_CAPABILITY_NORMALIZED = "nativeapprovals";
// Keep prompt construction lightweight. Full plugin loading is too expensive on
// prompt-only import paths; plugin-backed checks still cover loaded native
// channels at runtime.
const KNOWN_NATIVE_APPROVAL_PROMPT_CHANNELS = new Set([
"discord",
"matrix",
"qqbot",
"slack",
"telegram",
]);
export function channelPluginHasNativeApprovalPromptUi(
plugin?: Pick<ChannelPlugin, "approvalCapability"> | null,
): boolean {
const capability = resolveChannelApprovalCapability(plugin);
return Boolean(capability?.native || capability?.nativeRuntime);
}
export function isKnownNativeApprovalPromptChannel(channel?: string | null): boolean {
const normalized = normalizeOptionalLowercaseString(channel);
return Boolean(normalized && KNOWN_NATIVE_APPROVAL_PROMPT_CHANNELS.has(normalized));
}
export function hasNativeApprovalPromptRuntimeCapability(
capabilities?: readonly string[] | null,
): boolean {
return Boolean(
capabilities?.some(
(capability) =>
normalizeOptionalLowercaseString(capability) ===
NATIVE_APPROVAL_PROMPT_RUNTIME_CAPABILITY_NORMALIZED,
),
);
}

View File

@@ -1,4 +1,4 @@
import { normalizeChannelId } from "../channels/plugins/index.js";
import { normalizeAnyChannelId } from "../channels/registry.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import type { OpenClawConfig } from "./config.js";
@@ -49,7 +49,7 @@ export function resolveChannelCapabilities(params: {
accountId?: string | null;
}): string[] | undefined {
const cfg = params.cfg;
const channel = normalizeChannelId(params.channel);
const channel = normalizeAnyChannelId(params.channel);
if (!cfg || !channel) {
return undefined;
}