refactor: dedupe agent tool lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 12:31:36 +01:00
parent da6ca1c094
commit 0cbf99ab42
14 changed files with 55 additions and 41 deletions

View File

@@ -11,7 +11,11 @@ import {
parseAgentSessionKey,
resolveAgentIdFromSessionKey,
} from "../routing/session-key.js";
import { readStringValue, resolvePrimaryStringValue } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
readStringValue,
resolvePrimaryStringValue,
} from "../shared/string-coerce.js";
import { resolveUserPath } from "../utils.js";
import { resolveEffectiveAgentSkillFilter } from "./skills/agent-filter.js";
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
@@ -105,11 +109,10 @@ export function resolveSessionAgentIds(params: {
sessionAgentId: string;
} {
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
const explicitAgentIdRaw =
typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : "";
const explicitAgentIdRaw = normalizeLowercaseStringOrEmpty(params.agentId);
const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null;
const sessionKey = params.sessionKey?.trim();
const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined;
const normalizedSessionKey = sessionKey ? normalizeLowercaseStringOrEmpty(sessionKey) : undefined;
const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;
const sessionAgentId =
explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId);

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { DEFAULT_IDENTITY_FILENAME } from "./workspace.js";
export type AgentIdentityFile = {
@@ -26,8 +27,7 @@ function normalizeIdentityValue(value: string): string {
normalized = normalized.slice(1, -1).trim();
}
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
normalized = normalized.replace(/\s+/g, " ").toLowerCase();
return normalized;
return normalizeLowercaseStringOrEmpty(normalized.replace(/\s+/g, " "));
}
function isIdentityPlaceholder(value: string): boolean {
@@ -44,7 +44,9 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
if (colonIndex === -1) {
continue;
}
const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase();
const label = normalizeLowercaseStringOrEmpty(
cleaned.slice(0, colonIndex).replace(/[*_]/g, ""),
);
const value = cleaned
.slice(colonIndex + 1)
.replace(/^[*_]+|[*_]+$/g, "")

View File

@@ -20,7 +20,7 @@ import type { ExecApprovalDecision } from "../infra/exec-approvals.js";
import { splitMediaFromOutput } from "../media/parse.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import type { PluginHookAfterToolCallEvent } from "../plugins/types.js";
import { readStringValue } from "../shared/string-coerce.js";
import { normalizeOptionalLowercaseString, readStringValue } from "../shared/string-coerce.js";
import type { ApplyPatchSummary } from "./apply-patch.js";
import type { ExecToolDetails } from "./bash-tools.exec-types.js";
import { parseExecApprovalResultText } from "./exec-approval-result.js";
@@ -63,7 +63,7 @@ function isCronAddAction(args: unknown): boolean {
return false;
}
const action = (args as Record<string, unknown>).action;
return typeof action === "string" && action.trim().toLowerCase() === "add";
return normalizeOptionalLowercaseString(action) === "add";
}
function buildToolCallSummary(toolName: string, args: unknown, meta?: string): ToolCallSummary {
@@ -180,7 +180,7 @@ function buildPatchSummaryText(summary: ApplyPatchSummary): string {
}
function extendExecMeta(toolName: string, args: unknown, meta?: string): string | undefined {
const normalized = toolName.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(toolName);
if (normalized !== "exec" && normalized !== "bash") {
return meta;
}

View File

@@ -37,7 +37,7 @@ function normalizeToolErrorText(text: string): string | undefined {
}
function isErrorLikeStatus(status: string): boolean {
const normalized = status.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(status);
if (!normalized) {
return false;
}

View File

@@ -8,6 +8,10 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
import type { AgentToolsConfig } from "../config/types.tools.js";
import { normalizeAgentId } from "../routing/session-key.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
@@ -128,7 +132,7 @@ type ToolPolicyConfig = {
};
function normalizeProviderKey(value: string): string {
return value.trim().toLowerCase();
return normalizeLowercaseStringOrEmpty(value);
}
function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
@@ -167,7 +171,7 @@ function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
if (!groupId) {
return {};
}
return { channel: channel.trim().toLowerCase(), groupId };
return { channel: normalizeLowercaseStringOrEmpty(channel), groupId };
}
function resolveProviderToolPolicy(params: {
@@ -195,7 +199,7 @@ function resolveProviderToolPolicy(params: {
}
const normalizedProvider = normalizeProviderKey(provider);
const rawModelId = params.modelId?.trim().toLowerCase();
const rawModelId = normalizeOptionalLowercaseString(params.modelId);
const fullModelId =
rawModelId && !rawModelId.includes("/") ? `${normalizedProvider}/${rawModelId}` : rawModelId;

View File

@@ -2,6 +2,7 @@ import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
import type { OpenClawConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import { normalizeSubagentSessionKey } from "./subagent-session-key.js";
@@ -19,18 +20,12 @@ type SessionCapabilityEntry = {
};
function normalizeSubagentRole(value: unknown): SubagentSessionRole | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim().toLowerCase();
const trimmed = normalizeOptionalLowercaseString(value);
return SUBAGENT_SESSION_ROLES.find((entry) => entry === trimmed);
}
function normalizeSubagentControlScope(value: unknown): SubagentControlScope | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim().toLowerCase();
const trimmed = normalizeOptionalLowercaseString(value);
return SUBAGENT_CONTROL_SCOPES.find((entry) => entry === trimmed);
}

View File

@@ -1,4 +1,7 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { resolveExecDetail } from "./tool-display-exec.js";
import { asRecord } from "./tool-display-record.js";
@@ -165,7 +168,7 @@ export function formatDetailKey(raw: string, overrides: Record<string, string> =
}
const cleaned = last.replace(/_/g, " ").replace(/-/g, " ");
const spaced = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
return spaced.trim().toLowerCase() || last.toLowerCase();
return normalizeLowercaseStringOrEmpty(spaced) || normalizeLowercaseStringOrEmpty(last);
}
export function resolvePathArg(args: unknown): string | undefined {

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
type PreambleResult = {
command: string;
chdirPath?: string;
@@ -82,7 +84,7 @@ export function binaryName(token: string | undefined): string | undefined {
}
const cleaned = stripOuterQuotes(token) ?? token;
const segment = cleaned.split(/[/]/).at(-1) ?? cleaned;
return segment.trim().toLowerCase();
return normalizeLowercaseStringOrEmpty(segment);
}
export function optionValue(words: string[], names: string[]): string | undefined {

View File

@@ -16,6 +16,7 @@ import { getImageMetadata } from "../../media/image-ops.js";
import { saveMediaBuffer } from "../../media/store.js";
import { loadWebMedia } from "../../media/web-media.js";
import { getProviderEnvVars } from "../../secrets/provider-env-vars.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeProviderId } from "../provider-id.js";
import { ToolInputError, readNumberParam, readStringParam } from "./common.js";
@@ -213,7 +214,7 @@ function resolveAction(args: Record<string, unknown>): "generate" | "list" {
if (!raw) {
return "generate";
}
const normalized = raw.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(raw);
if (normalized === "generate" || normalized === "list") {
return normalized;
}

View File

@@ -17,6 +17,7 @@ import type {
MusicGenerationSourceImage,
} from "../../music-generation/types.js";
import { readSnakeCaseParamRaw } from "../../param-key.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { resolveUserPath } from "../../utils.js";
import type { DeliveryContext } from "../../utils/delivery-context.js";
import {
@@ -144,7 +145,7 @@ function resolveAction(args: Record<string, unknown>): "generate" | "list" | "st
if (!raw) {
return "generate";
}
const normalized = raw.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(raw);
if (normalized === "generate" || normalized === "list" || normalized === "status") {
return normalized;
}
@@ -157,7 +158,7 @@ function readBooleanParam(params: Record<string, unknown>, key: string): boolean
return raw;
}
if (typeof raw === "string") {
const normalized = raw.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(raw);
if (normalized === "true") {
return true;
}
@@ -169,7 +170,9 @@ function readBooleanParam(params: Record<string, unknown>, key: string): boolean
}
function normalizeOutputFormat(raw: string | undefined): MusicGenerationOutputFormat | undefined {
const normalized = raw?.trim().toLowerCase() as MusicGenerationOutputFormat | undefined;
const normalized = normalizeOptionalLowercaseString(raw) as
| MusicGenerationOutputFormat
| undefined;
if (!normalized) {
return undefined;
}

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import { parseTimeoutMs } from "../../cli/parse-timeout.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { jsonResult, readStringParam } from "./common.js";
import type { GatewayCallOptions } from "./gateway.js";
import { callGatewayTool } from "./gateway.js";
@@ -53,10 +54,7 @@ export async function executeNodeCommandAction(params: {
case "notifications_action": {
const node = readStringParam(params.input, "node", { required: true });
const notificationKey = readStringParam(params.input, "notificationKey", { required: true });
const notificationAction =
typeof params.input.notificationAction === "string"
? params.input.notificationAction.trim().toLowerCase()
: "";
const notificationAction = normalizeLowercaseStringOrEmpty(params.input.notificationAction);
if (
notificationAction !== "open" &&
notificationAction !== "dismiss" &&
@@ -118,7 +116,7 @@ export async function executeNodeCommandAction(params: {
const node = readStringParam(params.input, "node", { required: true });
const nodeId = await resolveNodeId(params.gatewayOpts, node);
const invokeCommand = readStringParam(params.input, "invokeCommand", { required: true });
const invokeCommandNormalized = invokeCommand.trim().toLowerCase();
const invokeCommandNormalized = normalizeLowercaseStringOrEmpty(invokeCommand);
if (BLOCKED_INVOKE_COMMANDS.has(invokeCommandNormalized)) {
throw new Error(
`invokeCommand "${invokeCommand}" is reserved for shell execution; use exec with host=node instead`,

View File

@@ -8,7 +8,7 @@ import {
} from "../../config/sessions.js";
import { callGateway } from "../../gateway/call.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { readStringValue } from "../../shared/string-coerce.js";
import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js";
import {
describeSessionsListTool,
SESSIONS_LIST_TOOL_DISPLAY_SUMMARY,
@@ -75,9 +75,9 @@ export function createSessionsListTool(opts?: {
sandboxed: opts?.sandboxed === true,
});
const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) =>
value.trim().toLowerCase(),
);
const kindsRaw = readStringArrayParam(params, "kinds")
?.map((value) => normalizeOptionalLowercaseString(value))
.filter(Boolean);
const allowedKindsList = (kindsRaw ?? []).filter((value) =>
["main", "group", "cron", "hook", "node", "other"].includes(value),
);

View File

@@ -6,6 +6,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
import { saveMediaBuffer } from "../../media/store.js";
import { loadWebMedia } from "../../media/web-media.js";
import { readSnakeCaseParamRaw } from "../../param-key.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { resolveUserPath } from "../../utils.js";
import type { DeliveryContext } from "../../utils/delivery-context.js";
import {
@@ -163,7 +164,7 @@ function resolveAction(args: Record<string, unknown>): "generate" | "list" | "st
if (!raw) {
return "generate";
}
const normalized = raw.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(raw);
if (normalized === "generate" || normalized === "list" || normalized === "status") {
return normalized;
}
@@ -205,7 +206,7 @@ function readBooleanParam(params: Record<string, unknown>, key: string): boolean
return raw;
}
if (typeof raw === "string") {
const normalized = raw.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(raw);
if (normalized === "true") {
return true;
}

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
export type CacheEntry<T> = {
value: T;
expiresAt: number;
@@ -20,7 +22,7 @@ export function resolveCacheTtlMs(value: unknown, fallbackMinutes: number): numb
}
export function normalizeCacheKey(value: string): string {
return value.trim().toLowerCase();
return normalizeLowercaseStringOrEmpty(value);
}
export function readCache<T>(