mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-03 10:28:43 +02:00
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>
124 lines
3.8 KiB
C#
124 lines
3.8 KiB
C#
namespace Aryx.AgentHost.Services;
|
|
|
|
internal static partial class StreamingTextMerger
|
|
{
|
|
private const double SnapshotReplacementMinLengthRatio = 0.6;
|
|
private const int SnapshotReplacementMinTokenCount = 3;
|
|
private const double SnapshotReplacementSharedTokenRatio = 0.5;
|
|
|
|
public static string Merge(string current, string incoming)
|
|
{
|
|
if (string.IsNullOrEmpty(current))
|
|
{
|
|
return incoming;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(incoming))
|
|
{
|
|
return current;
|
|
}
|
|
|
|
if (TryMergeSnapshotVariants(current, incoming, out string merged)
|
|
|| TryMergeByOverlap(current, incoming, out merged))
|
|
{
|
|
return merged;
|
|
}
|
|
|
|
if (ShouldReplaceWithSnapshot(current, incoming))
|
|
{
|
|
return incoming;
|
|
}
|
|
|
|
return current + incoming;
|
|
}
|
|
|
|
private static int ComputeSuffixPrefixOverlap(string current, string incoming)
|
|
{
|
|
int maxOverlap = Math.Min(current.Length, incoming.Length);
|
|
for (int length = maxOverlap; length > 0; length--)
|
|
{
|
|
if (string.CompareOrdinal(current, current.Length - length, incoming, 0, length) == 0)
|
|
{
|
|
return length;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static bool ShouldReplaceWithSnapshot(string current, string incoming)
|
|
{
|
|
if (!HasViableSnapshotLength(current, incoming))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
HashSet<string> currentTokens = Tokenize(current).ToHashSet(StringComparer.Ordinal);
|
|
HashSet<string> incomingTokens = Tokenize(incoming).ToHashSet(StringComparer.Ordinal);
|
|
if (!HasEnoughTokensForSnapshotComparison(currentTokens, incomingTokens))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int sharedTokenCount = incomingTokens.Count(token => currentTokens.Contains(token));
|
|
double sharedTokenRatio = sharedTokenCount / (double)Math.Min(currentTokens.Count, incomingTokens.Count);
|
|
return sharedTokenRatio >= SnapshotReplacementSharedTokenRatio;
|
|
}
|
|
|
|
private static bool HasViableSnapshotLength(string current, string incoming)
|
|
{
|
|
return incoming.Length >= Math.Floor(current.Length * SnapshotReplacementMinLengthRatio);
|
|
}
|
|
|
|
private static bool HasEnoughTokensForSnapshotComparison(
|
|
HashSet<string> currentTokens,
|
|
HashSet<string> incomingTokens)
|
|
{
|
|
return currentTokens.Count >= SnapshotReplacementMinTokenCount
|
|
&& incomingTokens.Count >= SnapshotReplacementMinTokenCount;
|
|
}
|
|
|
|
private static IEnumerable<string> Tokenize(string value)
|
|
{
|
|
return TokenRegex()
|
|
.Matches(value.ToLowerInvariant())
|
|
.Select(match => match.Value)
|
|
.Where(token => token.Length > 0);
|
|
}
|
|
|
|
private static bool TryMergeSnapshotVariants(string current, string incoming, out string merged)
|
|
{
|
|
if (incoming.StartsWith(current, StringComparison.Ordinal)
|
|
|| incoming.Contains(current, StringComparison.Ordinal))
|
|
{
|
|
merged = incoming;
|
|
return true;
|
|
}
|
|
|
|
if (current.Contains(incoming, StringComparison.Ordinal))
|
|
{
|
|
merged = current;
|
|
return true;
|
|
}
|
|
|
|
merged = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryMergeByOverlap(string current, string incoming, out string merged)
|
|
{
|
|
int overlapLength = ComputeSuffixPrefixOverlap(current, incoming);
|
|
if (overlapLength == 0)
|
|
{
|
|
merged = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
merged = current + incoming[overlapLength..];
|
|
return true;
|
|
}
|
|
|
|
[System.Text.RegularExpressions.GeneratedRegex("[a-z0-9]+", System.Text.RegularExpressions.RegexOptions.IgnoreCase)]
|
|
private static partial System.Text.RegularExpressions.Regex TokenRegex();
|
|
}
|