refactor: dedupe acp reader helpers

This commit is contained in:
Peter Steinberger
2026-04-07 06:15:45 +01:00
parent ea7297b344
commit e42f11ed62
10 changed files with 51 additions and 43 deletions

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import { isKnownCoreToolId } from "../agents/tool-catalog.js";
import { isMutatingToolCall } from "../agents/tool-mutation.js";
import { resolveOwnerOnlyToolApprovalClass } from "../agents/tool-policy.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { asRecord } from "./record-shared.js";
const SAFE_SEARCH_TOOL_IDS = new Set(["search", "web_search", "memory_search"]);
@@ -41,9 +42,9 @@ function readFirstStringValue(
return undefined;
}
for (const key of keys) {
const value = source[key];
if (typeof value === "string" && value.trim()) {
return value.trim();
const value = normalizeOptionalString(source[key]);
if (value) {
return value;
}
}
return undefined;
@@ -61,7 +62,7 @@ function parseToolNameFromTitle(title: string | undefined | null): string | unde
if (!title) {
return undefined;
}
const head = title.split(":", 1)[0]?.trim();
const head = normalizeOptionalString(title.split(":", 1)[0]);
return head ? normalizeToolName(head) : undefined;
}

View File

@@ -21,6 +21,7 @@ import {
listKnownProviderAuthEnvVarNames,
omitEnvKeysCaseInsensitive,
} from "../secrets/provider-env-vars.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { sanitizeTerminalText } from "../terminal/safe-text.js";
import { classifyAcpToolApproval, type AcpApprovalClass } from "./approval-classifier.js";
@@ -209,11 +210,11 @@ export function shouldStripProviderAuthEnvVarsForAcpServer(
defaultServerArgs?: string[];
} = {},
): boolean {
const serverCommand = params.serverCommand?.trim();
const serverCommand = normalizeOptionalString(params.serverCommand);
if (!serverCommand) {
return true;
}
const defaultServerCommand = params.defaultServerCommand?.trim();
const defaultServerCommand = normalizeOptionalString(params.defaultServerCommand);
if (!defaultServerCommand || serverCommand !== defaultServerCommand) {
return false;
}
@@ -282,7 +283,7 @@ function resolveSelfEntryPath(): string | null {
// ignore
}
const argv1 = process.argv[1]?.trim();
const argv1 = normalizeOptionalString(process.argv[1]);
if (argv1) {
return path.isAbsolute(argv1) ? argv1 : path.resolve(process.cwd(), argv1);
}

View File

@@ -5,6 +5,11 @@ import type {
ToolCallLocation,
ToolKind,
} from "@agentclientprotocol/sdk";
import {
hasNonEmptyString,
normalizeOptionalString,
readStringValue,
} from "../shared/string-coerce.js";
import { asRecord } from "./record-shared.js";
export type GatewayAttachment = {
@@ -171,7 +176,7 @@ function collectLocationsFromTextMarkers(
locations: Map<string, ToolCallLocation>,
): void {
for (const match of text.matchAll(TOOL_RESULT_PATH_MARKER_RE)) {
const candidate = match[1]?.trim();
const candidate = normalizeOptionalString(match[1]);
if (candidate) {
addToolLocation(locations, candidate);
}
@@ -336,7 +341,7 @@ export function inferToolKind(name?: string): ToolKind {
}
export function extractToolCallContent(value: unknown): ToolCallContent[] | undefined {
if (typeof value === "string") {
if (hasNonEmptyString(value)) {
return value.trim()
? [
{
@@ -359,7 +364,7 @@ export function extractToolCallContent(value: unknown): ToolCallContent[] | unde
const blocks = Array.isArray(record.content) ? record.content : [];
for (const block of blocks) {
const entry = asRecord(block);
if (entry?.type === "text" && typeof entry.text === "string" && entry.text.trim()) {
if (entry?.type === "text" && hasNonEmptyString(entry.text)) {
contents.push({
type: "content",
content: {
@@ -375,15 +380,11 @@ export function extractToolCallContent(value: unknown): ToolCallContent[] | unde
}
const fallbackText =
typeof record.text === "string"
? record.text
: typeof record.message === "string"
? record.message
: typeof record.error === "string"
? record.error
: undefined;
readStringValue(record.text) ??
readStringValue(record.message) ??
readStringValue(record.error);
if (!fallbackText?.trim()) {
if (!hasNonEmptyString(fallbackText)) {
return undefined;
}

View File

@@ -31,7 +31,8 @@ function sessionMatchesConfiguredBinding(params: {
return false;
}
const desiredBackend = params.spec.backend?.trim() || params.cfg.acp?.backend?.trim() || "";
const desiredBackend =
normalizeText(params.spec.backend) ?? normalizeText(params.cfg.acp?.backend) ?? "";
if (desiredBackend) {
const currentBackend = (params.meta.backend ?? "").trim();
if (!currentBackend || currentBackend !== desiredBackend) {
@@ -39,7 +40,7 @@ function sessionMatchesConfiguredBinding(params: {
}
}
const desiredCwd = params.spec.cwd?.trim();
const desiredCwd = normalizeText(params.spec.cwd);
if (desiredCwd !== undefined) {
const currentCwd = (params.meta.runtimeOptions?.cwd ?? params.meta.cwd ?? "").trim();
if (desiredCwd !== currentCwd) {

View File

@@ -117,7 +117,7 @@ export function parseConfiguredAcpSessionKey(
if (tokens.length !== 5 || tokens[0] !== "acp" || tokens[1] !== "binding") {
return null;
}
const channel = tokens[2]?.trim().toLowerCase();
const channel = normalizeText(tokens[2])?.toLowerCase();
if (!channel) {
return null;
}

View File

@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { expect } from "vitest";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { toAcpRuntimeError } from "./errors.js";
import type { AcpRuntime, AcpRuntimeEvent } from "./types.js";
@@ -75,7 +76,7 @@ export async function runAcpRuntimeAdapterContract(
let errorThrown: unknown = null;
const errorEvents: AcpRuntimeEvent[] = [];
const errorPrompt = params.errorPrompt?.trim();
const errorPrompt = normalizeOptionalString(params.errorPrompt);
if (errorPrompt) {
try {
for await (const event of runtime.runTurn({

View File

@@ -1,4 +1,5 @@
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { AcpRuntimeError } from "./errors.js";
import type { AcpRuntime } from "./types.js";
@@ -26,7 +27,7 @@ function resolveAcpRuntimeRegistryGlobalState(): AcpRuntimeRegistryGlobalState {
const ACP_BACKENDS_BY_ID = resolveAcpRuntimeRegistryGlobalState().backendsById;
function normalizeBackendId(id: string | undefined): string {
return id?.trim().toLowerCase() || "";
return normalizeOptionalString(id)?.toLowerCase() || "";
}
function isBackendHealthy(backend: AcpRuntimeBackend): boolean {

View File

@@ -6,6 +6,7 @@ import { loadConfig } from "../config/config.js";
import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js";
import { GatewayClient } from "../gateway/client.js";
import { isMainModule } from "../infra/is-main.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { readSecretFromFile } from "./secret-file.js";
import { AcpGatewayAgent } from "./translator.js";
@@ -192,17 +193,21 @@ function parseArgs(args: string[]): AcpServerOptions {
process.exit(0);
}
}
if (opts.gatewayToken?.trim() && tokenFile?.trim()) {
const gatewayToken = normalizeOptionalString(opts.gatewayToken);
const gatewayPassword = normalizeOptionalString(opts.gatewayPassword);
const normalizedTokenFile = normalizeOptionalString(tokenFile);
const normalizedPasswordFile = normalizeOptionalString(passwordFile);
if (gatewayToken && normalizedTokenFile) {
throw new Error("Use either --token or --token-file.");
}
if (opts.gatewayPassword?.trim() && passwordFile?.trim()) {
if (gatewayPassword && normalizedPasswordFile) {
throw new Error("Use either --password or --password-file.");
}
if (tokenFile?.trim()) {
opts.gatewayToken = readSecretFromFile(tokenFile, "Gateway token");
if (normalizedTokenFile) {
opts.gatewayToken = readSecretFromFile(normalizedTokenFile, "Gateway token");
}
if (passwordFile?.trim()) {
opts.gatewayPassword = readSecretFromFile(passwordFile, "Gateway password");
if (normalizedPasswordFile) {
opts.gatewayPassword = readSecretFromFile(normalizedPasswordFile, "Gateway password");
}
return opts;
}

View File

@@ -1,14 +1,10 @@
import type { SessionEntry } from "../config/sessions/types.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type AcpSessionInteractionMode = "interactive" | "parent-owned-background";
type SessionInteractionEntry = Pick<SessionEntry, "spawnedBy" | "parentSessionKey" | "acp">;
function normalizeText(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
export function resolveAcpSessionInteractionMode(
entry?: SessionInteractionEntry | null,
): AcpSessionInteractionMode {
@@ -18,7 +14,7 @@ export function resolveAcpSessionInteractionMode(
if (entry?.acp?.mode !== "oneshot") {
return "interactive";
}
if (normalizeText(entry.spawnedBy) || normalizeText(entry.parentSessionKey)) {
if (normalizeOptionalString(entry.spawnedBy) || normalizeOptionalString(entry.parentSessionKey)) {
return "parent-owned-background";
}
return "interactive";

View File

@@ -35,6 +35,7 @@ import {
createFixedWindowRateLimiter,
type FixedWindowRateLimiter,
} from "../infra/fixed-window-rate-limit.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { shortenHomePath } from "../utils.js";
import { getAvailableCommands } from "./commands.js";
import {
@@ -232,7 +233,7 @@ function buildSessionPresentation(params: {
...params.overrides,
};
const availableLevelIds: string[] = [...listThinkingLevels(row.modelProvider, row.model)];
const currentModeId = row.thinkingLevel?.trim() || "adaptive";
const currentModeId = normalizeOptionalString(row.thinkingLevel) || "adaptive";
if (!availableLevelIds.includes(currentModeId)) {
availableLevelIds.push(currentModeId);
}
@@ -268,14 +269,14 @@ function buildSessionPresentation(params: {
name: "Tool verbosity",
description:
"Controls how much tool progress and output detail OpenClaw keeps enabled for the session.",
currentValue: row.verboseLevel?.trim() || "off",
currentValue: normalizeOptionalString(row.verboseLevel) || "off",
values: ["off", "on", "full"],
}),
buildSelectConfigOption({
id: ACP_REASONING_LEVEL_CONFIG_ID,
name: "Reasoning stream",
description: "Controls whether reasoning-capable models emit reasoning text for the session.",
currentValue: row.reasoningLevel?.trim() || "off",
currentValue: normalizeOptionalString(row.reasoningLevel) || "off",
values: ["off", "on", "stream"],
}),
buildSelectConfigOption({
@@ -283,14 +284,14 @@ function buildSessionPresentation(params: {
name: "Usage detail",
description:
"Controls how much usage information OpenClaw attaches to responses for the session.",
currentValue: row.responseUsage?.trim() || "off",
currentValue: normalizeOptionalString(row.responseUsage) || "off",
values: ["off", "tokens", "full"],
}),
buildSelectConfigOption({
id: ACP_ELEVATED_LEVEL_CONFIG_ID,
name: "Elevated actions",
description: "Controls how aggressively the session allows elevated execution behavior.",
currentValue: row.elevatedLevel?.trim() || "off",
currentValue: normalizeOptionalString(row.elevatedLevel) || "off",
values: ["off", "on", "ask", "full"],
}),
];
@@ -350,9 +351,9 @@ function buildSessionMetadata(params: {
sessionKey: string;
}): SessionMetadata {
const title =
params.row?.derivedTitle?.trim() ||
params.row?.displayName?.trim() ||
params.row?.label?.trim() ||
normalizeOptionalString(params.row?.derivedTitle) ||
normalizeOptionalString(params.row?.displayName) ||
normalizeOptionalString(params.row?.label) ||
params.sessionKey;
const updatedAt =
typeof params.row?.updatedAt === "number" && Number.isFinite(params.row.updatedAt)