refactor: share chat content text coercion

This commit is contained in:
Peter Steinberger
2026-04-18 21:35:05 +01:00
parent dc30298b29
commit 2e1ddedc58
3 changed files with 40 additions and 84 deletions

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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);