Files
aryx/src/shared/utils/streamingText.ts
T
David KayaandCopilot c7910e6e4b fix: remove streaming text boundary heuristics that injected spurious spaces
The StreamingTextMerger (C#) and mergeStreamingText (TypeScript) contained
custom word-boundary heuristics that inserted spaces between streaming deltas
based on incorrect assumptions. Two bugs caused mid-word spaces:

1. Unconditional space insertion when incoming delta started with uppercase or
   digit characters, breaking acronyms (MDA -> M DA, UAG -> UA G)
2. LooksLikeWordBoundary using global token counts that fired on nearly every
   mid-word token split (writable -> wr itable, fragile -> frag ile)

Both upstream projects (Copilot SDK and Agent Framework) use simple string
concatenation for delta accumulation. LLM streaming tokens natively include
whitespace when needed, so no boundary detection is required.

Removed all boundary heuristic methods and replaced with plain concatenation.
Kept overlap detection and snapshot handling for event redelivery scenarios.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 18:55:34 +02:00

68 lines
1.6 KiB
TypeScript

function tokenize(value: string): string[] {
return value
.toLowerCase()
.split(/[^a-z0-9]+/i)
.filter((token) => token.length > 0);
}
function computeSuffixPrefixOverlap(current: string, incoming: string): number {
const maxOverlap = Math.min(current.length, incoming.length);
for (let length = maxOverlap; length > 0; length -= 1) {
if (current.slice(-length) === incoming.slice(0, length)) {
return length;
}
}
return 0;
}
function shouldReplaceWithSnapshot(current: string, incoming: string): boolean {
if (incoming.length < Math.floor(current.length * 0.6)) {
return false;
}
const currentTokens = new Set(tokenize(current));
const incomingTokens = new Set(tokenize(incoming));
if (currentTokens.size < 3 || incomingTokens.size < 3) {
return false;
}
let shared = 0;
for (const token of incomingTokens) {
if (currentTokens.has(token)) {
shared += 1;
}
}
return shared / Math.min(currentTokens.size, incomingTokens.size) >= 0.5;
}
export function mergeStreamingText(current: string, incoming: string): string {
if (!current) {
return incoming;
}
if (!incoming) {
return current;
}
if (incoming.startsWith(current) || incoming.includes(current)) {
return incoming;
}
if (current.includes(incoming)) {
return current;
}
const overlap = computeSuffixPrefixOverlap(current, incoming);
if (overlap > 0) {
return current + incoming.slice(overlap);
}
if (shouldReplaceWithSnapshot(current, incoming)) {
return incoming;
}
return current + incoming;
}