From 2e1ddedc58f080d32101df98b42a1356a468c812 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Apr 2026 21:35:05 +0100 Subject: [PATCH] refactor: share chat content text coercion --- .../sanitize-user-facing-text.ts | 30 +--------- ...pi-embedded-subscribe.handlers.messages.ts | 38 +++---------- src/shared/chat-content.ts | 56 ++++++++++--------- 3 files changed, 40 insertions(+), 84 deletions(-) diff --git a/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts b/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts index f0e3ace5332..c45206851e8 100644 --- a/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts +++ b/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts @@ -5,6 +5,7 @@ import { parseApiErrorInfo, parseApiErrorPayload, } from "../../shared/assistant-error-format.js"; +import { coerceChatContentText } from "../../shared/chat-content.js"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, @@ -300,33 +301,8 @@ function shouldRewriteRawPayloadWithoutErrorContext(raw: string): boolean { return false; } -function coerceText(value: unknown): string { - if (typeof value === "string") { - return value; - } - if (value == null) { - return ""; - } - if ( - typeof value === "number" || - typeof value === "boolean" || - typeof value === "bigint" || - typeof value === "symbol" - ) { - return String(value); - } - if (typeof value === "object") { - try { - return JSON.stringify(value) ?? ""; - } catch { - return ""; - } - } - return ""; -} - function stripFinalTagsFromText(text: unknown): string { - const normalized = coerceText(text); + const normalized = coerceChatContentText(text); if (!normalized) { return normalized; } @@ -378,7 +354,7 @@ export function isLikelyHttpErrorText(raw: string): boolean { } export function sanitizeUserFacingText(text: unknown, opts?: { errorContext?: boolean }): string { - const raw = coerceText(text); + const raw = coerceChatContentText(text); if (!raw) { return raw; } diff --git a/src/agents/pi-embedded-subscribe.handlers.messages.ts b/src/agents/pi-embedded-subscribe.handlers.messages.ts index 15934558cd0..a6847b47d34 100644 --- a/src/agents/pi-embedded-subscribe.handlers.messages.ts +++ b/src/agents/pi-embedded-subscribe.handlers.messages.ts @@ -5,6 +5,7 @@ import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { emitAgentEvent } from "../infra/agent-events.js"; import { createInlineCodeState } from "../markdown/code-spans.js"; +import { coerceChatContentText } from "../shared/chat-content.js"; import { parseAssistantTextSignature, resolveAssistantMessagePhase, @@ -47,31 +48,6 @@ const stripTrailingDirective = (text: string): string => { return text.slice(0, openIndex); }; -const coerceText = (value: unknown): string => { - if (typeof value === "string") { - return value; - } - if (value == null) { - return ""; - } - if ( - typeof value === "number" || - typeof value === "boolean" || - typeof value === "bigint" || - typeof value === "symbol" - ) { - return String(value); - } - if (typeof value === "object") { - try { - return JSON.stringify(value) ?? ""; - } catch { - return ""; - } - } - return ""; -}; - function shouldSuppressAssistantVisibleOutput(message: AgentMessage | undefined): boolean { return resolveAssistantMessagePhase(message) === "commentary"; } @@ -189,12 +165,12 @@ export function resolveSilentReplyFallbackText(params: { text: unknown; messagingToolSentTexts: string[]; }): string { - const text = coerceText(params.text); + const text = coerceChatContentText(params.text); const trimmed = text.trim(); if (trimmed !== SILENT_REPLY_TOKEN) { return text; } - const fallback = coerceText(params.messagingToolSentTexts.at(-1)).trim(); + const fallback = coerceChatContentText(params.messagingToolSentTexts.at(-1)).trim(); if (!fallback) { return text; } @@ -397,7 +373,9 @@ export function handleMessageUpdate( if (deliveryPhase === "commentary") { return; } - const phaseAwareVisibleText = coerceText(extractAssistantVisibleText(partialAssistant)).trim(); + const phaseAwareVisibleText = coerceChatContentText( + extractAssistantVisibleText(partialAssistant), + ).trim(); const shouldUsePhaseAwareBlockReply = Boolean(deliveryPhase); if (chunk) { @@ -543,8 +521,8 @@ export function handleMessageEnd( } promoteThinkingTagsToBlocks(assistantMessage); - const rawText = coerceText(extractAssistantText(assistantMessage)); - const rawVisibleText = coerceText(extractAssistantVisibleText(assistantMessage)); + const rawText = coerceChatContentText(extractAssistantText(assistantMessage)); + const rawVisibleText = coerceChatContentText(extractAssistantVisibleText(assistantMessage)); appendRawStream({ ts: Date.now(), event: "assistant_message_end", diff --git a/src/shared/chat-content.ts b/src/shared/chat-content.ts index 30e32c8f34e..ec67a393323 100644 --- a/src/shared/chat-content.ts +++ b/src/shared/chat-content.ts @@ -1,3 +1,28 @@ +export function coerceChatContentText(value: unknown): string { + if (typeof value === "string") { + return value; + } + if (value == null) { + return ""; + } + if ( + typeof value === "number" || + typeof value === "boolean" || + typeof value === "bigint" || + typeof value === "symbol" + ) { + return String(value); + } + if (typeof value === "object") { + try { + return JSON.stringify(value) ?? ""; + } catch { + return ""; + } + } + return ""; +} + export function extractTextFromChatContent( content: unknown, opts?: { @@ -8,36 +33,13 @@ export function extractTextFromChatContent( ): string | null { const normalizeText = opts?.normalizeText ?? ((text: string) => text.replace(/\s+/g, " ").trim()); const joinWith = opts?.joinWith ?? " "; - const coerceText = (value: unknown): string => { - if (typeof value === "string") { - return value; - } - if (value == null) { - return ""; - } - if ( - typeof value === "number" || - typeof value === "boolean" || - typeof value === "bigint" || - typeof value === "symbol" - ) { - return String(value); - } - if (typeof value === "object") { - try { - return JSON.stringify(value) ?? ""; - } catch { - return ""; - } - } - return ""; - }; const sanitize = (text: unknown): string => { - const raw = coerceText(text); + const raw = coerceChatContentText(text); const sanitized = opts?.sanitizeText ? opts.sanitizeText(raw) : raw; - return coerceText(sanitized); + return coerceChatContentText(sanitized); }; - const normalize = (text: unknown): string => coerceText(normalizeText(coerceText(text))); + const normalize = (text: unknown): string => + coerceChatContentText(normalizeText(coerceChatContentText(text))); if (typeof content === "string") { const value = sanitize(content);