mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
fix: stabilize streamed agent text
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { ChatMessageRecord, SessionRecord } from '@shared/domain/session';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
import { createId, nowIso } from '@shared/utils/ids';
|
||||
import { mergeStreamingText } from '@shared/utils/streamingText';
|
||||
|
||||
import { WorkspaceRepository } from '@main/persistence/workspaceRepository';
|
||||
import { SecretStore } from '@main/secrets/secretStore';
|
||||
@@ -278,7 +279,7 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
const existing = session.messages.find((message) => message.id === event.messageId);
|
||||
|
||||
if (existing) {
|
||||
existing.content += event.contentDelta;
|
||||
existing.content = mergeStreamingText(existing.content, event.contentDelta);
|
||||
existing.pending = true;
|
||||
} else {
|
||||
session.messages.push({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { ChatMessageRecord, SessionRecord } from '@shared/domain/session';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
import { mergeStreamingText } from '@shared/utils/streamingText';
|
||||
|
||||
export function applySessionEventWorkspace(
|
||||
current: WorkspaceState | undefined,
|
||||
@@ -86,7 +87,7 @@ function applyMessageDeltaEvent(session: SessionRecord, event: SessionEventRecor
|
||||
const nextMessage: ChatMessageRecord = {
|
||||
...existing,
|
||||
authorName: event.authorName ?? existing.authorName,
|
||||
content: `${existing.content}${event.contentDelta}`,
|
||||
content: mergeStreamingText(existing.content, event.contentDelta),
|
||||
pending: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user