mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
perf(tests): avoid channel plugin imports in system prompt
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
39
src/agents/runtime-capabilities.ts
Normal file
39
src/agents/runtime-capabilities.ts
Normal 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) : [],
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
42
src/channels/plugins/native-approval-prompt.ts
Normal file
42
src/channels/plugins/native-approval-prompt.ts
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user