graphlit-client 1.0.20250531005 → 1.0.20250610001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Get the actual model name for a given specification
3
+ * @param specification - The Graphlit specification object
4
+ * @returns The SDK-compatible model name
5
+ */
6
+ export declare function getModelName(specification: any): string | undefined;
7
+ /**
8
+ * Check if a service type supports streaming
9
+ * @param serviceType - The model service type
10
+ * @returns True if streaming is supported
11
+ */
12
+ export declare function isStreamingSupported(serviceType?: string): boolean;
13
+ /**
14
+ * Get the service type from specification
15
+ * @param specification - The specification object
16
+ * @returns The service type string
17
+ */
18
+ export declare function getServiceType(specification: any): string | undefined;
@@ -0,0 +1,95 @@
1
+ import * as Types from "./generated/graphql-types.js";
2
+ /**
3
+ * Model mapping utilities to convert Graphlit specification enums
4
+ * to actual model names used by LLM SDKs
5
+ */
6
+ // OpenAI model mappings
7
+ const OPENAI_MODEL_MAP = {
8
+ [Types.OpenAiModels.Gpt4]: "gpt-4",
9
+ [Types.OpenAiModels.Gpt4Turbo_128K]: "gpt-4-turbo",
10
+ [Types.OpenAiModels.Gpt4O_128K]: "gpt-4o",
11
+ [Types.OpenAiModels.Gpt4OMini_128K]: "gpt-4o-mini",
12
+ [Types.OpenAiModels.Gpt4OChat_128K]: "chatgpt-4o-latest",
13
+ [Types.OpenAiModels.Gpt35Turbo]: "gpt-3.5-turbo",
14
+ [Types.OpenAiModels.Gpt35Turbo_16K]: "gpt-3.5-turbo-16k",
15
+ [Types.OpenAiModels.Gpt41_1024K]: "gpt-4.1",
16
+ [Types.OpenAiModels.Gpt41Mini_1024K]: "gpt-4.1-mini",
17
+ [Types.OpenAiModels.Gpt41Nano_1024K]: "gpt-4.1-nano",
18
+ [Types.OpenAiModels.O3Mini_200K]: "o3-mini",
19
+ [Types.OpenAiModels.O3_200K]: "o3",
20
+ [Types.OpenAiModels.O4Mini_200K]: "o4-mini",
21
+ };
22
+ // Anthropic model mappings
23
+ const ANTHROPIC_MODEL_MAP = {
24
+ [Types.AnthropicModels.Claude_3Opus]: "claude-3-opus-20240229",
25
+ [Types.AnthropicModels.Claude_3Sonnet]: "claude-3-sonnet-20240229",
26
+ [Types.AnthropicModels.Claude_3Haiku]: "claude-3-haiku-20240307",
27
+ [Types.AnthropicModels.Claude_3_5Sonnet]: "claude-3-5-sonnet-20241022",
28
+ [Types.AnthropicModels.Claude_3_5Haiku]: "claude-3-5-haiku-20241022",
29
+ [Types.AnthropicModels.Claude_3_7Sonnet]: "claude-3-7-sonnet-20250219",
30
+ [Types.AnthropicModels.Claude_4Opus]: "claude-4-opus-20250514",
31
+ [Types.AnthropicModels.Claude_4Sonnet]: "claude-4-sonnet-20250514",
32
+ };
33
+ // Google model mappings
34
+ const GOOGLE_MODEL_MAP = {
35
+ [Types.GoogleModels.Gemini_1_5Pro]: "gemini-1.5-pro",
36
+ [Types.GoogleModels.Gemini_1_5Flash]: "gemini-1.5-flash",
37
+ [Types.GoogleModels.Gemini_1_5Flash_8B]: "gemini-1.5-flash-8b",
38
+ [Types.GoogleModels.Gemini_2_0Flash]: "gemini-2.0-flash-exp",
39
+ [Types.GoogleModels.Gemini_2_0FlashExperimental]: "gemini-2.0-flash-exp",
40
+ [Types.GoogleModels.Gemini_2_5FlashPreview]: "gemini-2.5-flash-preview-05-20",
41
+ [Types.GoogleModels.Gemini_2_5ProPreview]: "gemini-2.5-pro-preview-06-05",
42
+ };
43
+ /**
44
+ * Get the actual model name for a given specification
45
+ * @param specification - The Graphlit specification object
46
+ * @returns The SDK-compatible model name
47
+ */
48
+ export function getModelName(specification) {
49
+ const serviceType = specification?.serviceType;
50
+ // Check for custom model names first
51
+ if (specification?.openAI?.modelName) {
52
+ return specification.openAI.modelName;
53
+ }
54
+ if (specification?.anthropic?.modelName) {
55
+ return specification.anthropic.modelName;
56
+ }
57
+ if (specification?.google?.modelName) {
58
+ return specification.google.modelName;
59
+ }
60
+ // Map based on service type and model enum
61
+ switch (serviceType) {
62
+ case Types.ModelServiceTypes.OpenAi:
63
+ const openAIModel = specification?.openAI?.model;
64
+ return openAIModel ? OPENAI_MODEL_MAP[openAIModel] : undefined;
65
+ case Types.ModelServiceTypes.Anthropic:
66
+ const anthropicModel = specification?.anthropic?.model;
67
+ return anthropicModel ? ANTHROPIC_MODEL_MAP[anthropicModel] : undefined;
68
+ case Types.ModelServiceTypes.Google:
69
+ const googleModel = specification?.google?.model;
70
+ return googleModel ? GOOGLE_MODEL_MAP[googleModel] : undefined;
71
+ default:
72
+ return undefined;
73
+ }
74
+ }
75
+ /**
76
+ * Check if a service type supports streaming
77
+ * @param serviceType - The model service type
78
+ * @returns True if streaming is supported
79
+ */
80
+ export function isStreamingSupported(serviceType) {
81
+ const streamingServices = [
82
+ Types.ModelServiceTypes.OpenAi,
83
+ Types.ModelServiceTypes.Anthropic,
84
+ Types.ModelServiceTypes.Google,
85
+ ];
86
+ return streamingServices.includes(serviceType);
87
+ }
88
+ /**
89
+ * Get the service type from specification
90
+ * @param specification - The specification object
91
+ * @returns The service type string
92
+ */
93
+ export function getServiceType(specification) {
94
+ return specification?.serviceType;
95
+ }
@@ -0,0 +1,106 @@
1
+ import { StreamEvent } from "./client.js";
2
+ import { ConversationRoleTypes } from "./generated/graphql-types.js";
3
+ export declare class StreamEventAggregator {
4
+ private conversationId;
5
+ private messageBuffer;
6
+ private toolCallsBuffer;
7
+ private isFirstAssistantMessage;
8
+ private hasReceivedToolCalls;
9
+ private tokenBuffer;
10
+ /**
11
+ * Process a stream event and return any complete messages ready for the UI
12
+ */
13
+ processEvent(event: StreamEvent): AggregatedEvent | null;
14
+ /**
15
+ * Reset the aggregator for a new conversation
16
+ */
17
+ reset(): void;
18
+ /**
19
+ * Get current state (useful for debugging)
20
+ */
21
+ getState(): {
22
+ conversationId: string;
23
+ messageBuffer: string;
24
+ toolCallsCount: number;
25
+ hasReceivedToolCalls: boolean;
26
+ isFirstAssistantMessage: boolean;
27
+ tokenCount: number;
28
+ };
29
+ }
30
+ /**
31
+ * Aggregated event types that are ready for UI consumption
32
+ */
33
+ export type AggregatedEvent = {
34
+ type: "conversationStarted";
35
+ conversationId: string;
36
+ } | {
37
+ type: "token";
38
+ token: string;
39
+ accumulated: string;
40
+ } | {
41
+ type: "assistantMessage";
42
+ message: {
43
+ message?: string | null;
44
+ role?: ConversationRoleTypes | null;
45
+ toolCalls?: any[];
46
+ };
47
+ isFinal: boolean;
48
+ conversationId?: string;
49
+ } | {
50
+ type: "streamComplete";
51
+ conversationId?: string;
52
+ } | {
53
+ type: "error";
54
+ error: string;
55
+ };
56
+ /**
57
+ * Helper to create an SSE response with proper formatting
58
+ */
59
+ export declare function formatSSEEvent(data: any, eventName?: string): string;
60
+ /**
61
+ * Helper to create a TransformStream for SSE with built-in ping support
62
+ */
63
+ export declare function createSSEStream(options?: {
64
+ pingInterval?: number;
65
+ }): {
66
+ readable: ReadableStream<any>;
67
+ sendEvent: (data: any, eventName?: string) => Promise<void>;
68
+ close: () => Promise<void>;
69
+ writer: WritableStreamDefaultWriter<any>;
70
+ };
71
+ /**
72
+ * Helper to wrap tool handlers with result emission
73
+ */
74
+ export interface ToolResultEmitter {
75
+ (toolCallId: string, result: any, status: "complete" | "error" | "blocked", duration: number): void;
76
+ }
77
+ export declare function wrapToolHandlers(handlers: Record<string, (args: any) => Promise<any>>, emitResult: ToolResultEmitter): Record<string, (args: any) => Promise<any>>;
78
+ /**
79
+ * Helper to enhance tool calls with server information
80
+ */
81
+ export interface ServerMapping {
82
+ toolName: string;
83
+ serverName: string;
84
+ serverId: string;
85
+ }
86
+ export declare function enhanceToolCalls(toolCalls: any[], serverMappings: ServerMapping[]): any[];
87
+ /**
88
+ * Helper to track conversation metrics
89
+ */
90
+ export declare class ConversationMetrics {
91
+ private startTime;
92
+ private tokenCount;
93
+ private toolCallCount;
94
+ private errorCount;
95
+ recordToken(): void;
96
+ recordToolCall(): void;
97
+ recordError(): void;
98
+ getMetrics(): {
99
+ duration: number;
100
+ tokenCount: number;
101
+ toolCallCount: number;
102
+ errorCount: number;
103
+ tokensPerSecond: number;
104
+ };
105
+ reset(): void;
106
+ }
@@ -0,0 +1,237 @@
1
+ import { ConversationRoleTypes, } from "./generated/graphql-types.js";
2
+ export class StreamEventAggregator {
3
+ conversationId = "";
4
+ messageBuffer = "";
5
+ toolCallsBuffer = new Map();
6
+ isFirstAssistantMessage = true;
7
+ hasReceivedToolCalls = false;
8
+ tokenBuffer = [];
9
+ /**
10
+ * Process a stream event and return any complete messages ready for the UI
11
+ */
12
+ processEvent(event) {
13
+ switch (event.type) {
14
+ case "start":
15
+ this.conversationId = event.conversationId;
16
+ return {
17
+ type: "conversationStarted",
18
+ conversationId: event.conversationId,
19
+ };
20
+ case "token":
21
+ this.messageBuffer += event.token;
22
+ this.tokenBuffer.push(event.token);
23
+ return {
24
+ type: "token",
25
+ token: event.token,
26
+ accumulated: this.messageBuffer,
27
+ };
28
+ case "message":
29
+ // SDK provides accumulated message - we can use this instead of our buffer
30
+ this.messageBuffer = event.message;
31
+ return null; // Don't emit, wait for complete event
32
+ case "tool_call_start":
33
+ this.hasReceivedToolCalls = true;
34
+ this.toolCallsBuffer.set(event.toolCall.id, {
35
+ id: event.toolCall.id,
36
+ name: event.toolCall.name,
37
+ argumentsBuffer: "",
38
+ isComplete: false,
39
+ startTime: Date.now(),
40
+ });
41
+ return null; // Buffer until complete
42
+ case "tool_call_delta":
43
+ const toolCall = this.toolCallsBuffer.get(event.toolCallId);
44
+ if (toolCall) {
45
+ toolCall.argumentsBuffer += event.argumentDelta;
46
+ }
47
+ return null; // Buffer until complete
48
+ case "tool_call_complete":
49
+ const completeToolCall = this.toolCallsBuffer.get(event.toolCall.id);
50
+ if (completeToolCall) {
51
+ completeToolCall.argumentsBuffer = event.toolCall.arguments;
52
+ completeToolCall.isComplete = true;
53
+ }
54
+ // Check if all tool calls are complete
55
+ const allComplete = Array.from(this.toolCallsBuffer.values()).every((tc) => tc.isComplete);
56
+ if (allComplete &&
57
+ this.hasReceivedToolCalls &&
58
+ this.isFirstAssistantMessage) {
59
+ // Emit complete assistant message with all tool calls
60
+ const toolCalls = Array.from(this.toolCallsBuffer.values()).map((tc) => ({
61
+ id: tc.id,
62
+ name: tc.name,
63
+ arguments: tc.argumentsBuffer,
64
+ status: "pending",
65
+ }));
66
+ this.isFirstAssistantMessage = false;
67
+ return {
68
+ type: "assistantMessage",
69
+ message: {
70
+ message: this.messageBuffer,
71
+ role: ConversationRoleTypes.Assistant,
72
+ toolCalls,
73
+ },
74
+ isFinal: false,
75
+ };
76
+ }
77
+ return null;
78
+ case "complete":
79
+ // If we haven't sent a message yet (no tool calls), send it now
80
+ if (this.isFirstAssistantMessage && !this.hasReceivedToolCalls) {
81
+ return {
82
+ type: "assistantMessage",
83
+ message: {
84
+ message: this.messageBuffer,
85
+ role: ConversationRoleTypes.Assistant,
86
+ },
87
+ isFinal: true,
88
+ conversationId: event.conversationId,
89
+ };
90
+ }
91
+ return { type: "streamComplete", conversationId: event.conversationId };
92
+ case "error":
93
+ return { type: "error", error: event.error };
94
+ default:
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * Reset the aggregator for a new conversation
100
+ */
101
+ reset() {
102
+ this.conversationId = "";
103
+ this.messageBuffer = "";
104
+ this.toolCallsBuffer.clear();
105
+ this.isFirstAssistantMessage = true;
106
+ this.hasReceivedToolCalls = false;
107
+ this.tokenBuffer = [];
108
+ }
109
+ /**
110
+ * Get current state (useful for debugging)
111
+ */
112
+ getState() {
113
+ return {
114
+ conversationId: this.conversationId,
115
+ messageBuffer: this.messageBuffer,
116
+ toolCallsCount: this.toolCallsBuffer.size,
117
+ hasReceivedToolCalls: this.hasReceivedToolCalls,
118
+ isFirstAssistantMessage: this.isFirstAssistantMessage,
119
+ tokenCount: this.tokenBuffer.length,
120
+ };
121
+ }
122
+ }
123
+ /**
124
+ * Helper to create an SSE response with proper formatting
125
+ */
126
+ export function formatSSEEvent(data, eventName = "message") {
127
+ if (typeof data === "string") {
128
+ return `event: ${eventName}\ndata: ${data}\n\n`;
129
+ }
130
+ return `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
131
+ }
132
+ /**
133
+ * Helper to create a TransformStream for SSE with built-in ping support
134
+ */
135
+ export function createSSEStream(options) {
136
+ const encoder = new TextEncoder();
137
+ const { readable, writable } = new TransformStream();
138
+ const writer = writable.getWriter();
139
+ let pingInterval = null;
140
+ if (options?.pingInterval) {
141
+ pingInterval = globalThis.setInterval(() => {
142
+ writer.write(encoder.encode(":\n\n")).catch(() => {
143
+ // Ignore errors on ping
144
+ });
145
+ }, options.pingInterval);
146
+ }
147
+ const sendEvent = (data, eventName = "message") => {
148
+ const formatted = formatSSEEvent(data, eventName);
149
+ return writer.write(encoder.encode(formatted));
150
+ };
151
+ const close = async () => {
152
+ if (pingInterval) {
153
+ globalThis.clearInterval(pingInterval);
154
+ }
155
+ await writer.close();
156
+ };
157
+ return {
158
+ readable,
159
+ sendEvent,
160
+ close,
161
+ writer,
162
+ };
163
+ }
164
+ export function wrapToolHandlers(handlers, emitResult) {
165
+ const wrapped = {};
166
+ Object.entries(handlers).forEach(([name, handler]) => {
167
+ wrapped[name] = async (args) => {
168
+ const toolCallId = `tool_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
169
+ const startTime = Date.now();
170
+ try {
171
+ const result = await handler(args);
172
+ const duration = Date.now() - startTime;
173
+ // Emit success result
174
+ emitResult(toolCallId, { status: "success", result }, "complete", duration);
175
+ return result;
176
+ }
177
+ catch (error) {
178
+ const duration = Date.now() - startTime;
179
+ // Emit error result
180
+ emitResult(toolCallId, {
181
+ status: "error",
182
+ error: error instanceof Error ? error.message : String(error),
183
+ }, "error", duration);
184
+ throw error;
185
+ }
186
+ };
187
+ });
188
+ return wrapped;
189
+ }
190
+ export function enhanceToolCalls(toolCalls, serverMappings) {
191
+ const mappingDict = serverMappings.reduce((acc, mapping) => {
192
+ acc[mapping.toolName] = {
193
+ serverName: mapping.serverName,
194
+ serverId: mapping.serverId,
195
+ };
196
+ return acc;
197
+ }, {});
198
+ return toolCalls.map((toolCall) => ({
199
+ ...toolCall,
200
+ serverName: mappingDict[toolCall.name]?.serverName,
201
+ serverId: mappingDict[toolCall.name]?.serverId,
202
+ }));
203
+ }
204
+ /**
205
+ * Helper to track conversation metrics
206
+ */
207
+ export class ConversationMetrics {
208
+ startTime = Date.now();
209
+ tokenCount = 0;
210
+ toolCallCount = 0;
211
+ errorCount = 0;
212
+ recordToken() {
213
+ this.tokenCount++;
214
+ }
215
+ recordToolCall() {
216
+ this.toolCallCount++;
217
+ }
218
+ recordError() {
219
+ this.errorCount++;
220
+ }
221
+ getMetrics() {
222
+ const duration = Date.now() - this.startTime;
223
+ return {
224
+ duration,
225
+ tokenCount: this.tokenCount,
226
+ toolCallCount: this.toolCallCount,
227
+ errorCount: this.errorCount,
228
+ tokensPerSecond: this.tokenCount / (duration / 1000),
229
+ };
230
+ }
231
+ reset() {
232
+ this.startTime = Date.now();
233
+ this.tokenCount = 0;
234
+ this.toolCallCount = 0;
235
+ this.errorCount = 0;
236
+ }
237
+ }
@@ -0,0 +1,25 @@
1
+ export type ChunkingStrategy = "character" | "word" | "sentence" | ((text: string) => {
2
+ chunks: string[];
3
+ remainder: string;
4
+ });
5
+ export declare class ChunkBuffer {
6
+ private buffer;
7
+ private static readonly MAX_WORD_LEN;
8
+ private static readonly MAX_BUFFER_NO_BREAK;
9
+ private readonly graphemeSeg;
10
+ private readonly wordSeg;
11
+ private readonly sentenceSeg;
12
+ private readonly customChunker?;
13
+ private readonly strategy;
14
+ constructor(strategy: ChunkingStrategy);
15
+ /** Feed one LLM token, receive zero-or-more flushed chunks. */
16
+ addToken(token: string): string[];
17
+ /** Flush whatever is left in the buffer when the stream finishes. */
18
+ flush(): string[];
19
+ private flushGraphemes;
20
+ private flushWords;
21
+ private flushSentences;
22
+ /** Fallback guard to break up very long runs of text with no natural breaks. */
23
+ private flushLongRuns;
24
+ private flushCustom;
25
+ }
@@ -0,0 +1,170 @@
1
+ export class ChunkBuffer {
2
+ buffer = "";
3
+ // ----- Configurable Guards -----
4
+ static MAX_WORD_LEN = 50; // Breaks up extremely long "words" (e.g., URLs, code).
5
+ static MAX_BUFFER_NO_BREAK = 400; // Hard limit for any run without a natural break.
6
+ // --------------------------------
7
+ graphemeSeg;
8
+ wordSeg;
9
+ sentenceSeg;
10
+ customChunker;
11
+ strategy;
12
+ constructor(strategy) {
13
+ if (typeof strategy === "function") {
14
+ this.customChunker = strategy;
15
+ this.strategy = "custom";
16
+ }
17
+ else {
18
+ this.strategy = strategy;
19
+ }
20
+ this.graphemeSeg = new Intl.Segmenter(undefined, {
21
+ granularity: "grapheme",
22
+ });
23
+ this.wordSeg = new Intl.Segmenter(undefined, { granularity: "word" });
24
+ this.sentenceSeg = new Intl.Segmenter(undefined, {
25
+ granularity: "sentence",
26
+ });
27
+ }
28
+ /** Feed one LLM token, receive zero-or-more flushed chunks. */
29
+ addToken(token) {
30
+ this.buffer += token;
31
+ if (this.customChunker) {
32
+ return this.flushCustom();
33
+ }
34
+ // Pre-emptively flush any overly long runs of text that haven't found a natural break.
35
+ const longRunChunks = this.flushLongRuns();
36
+ let newChunks = [];
37
+ switch (this.strategy) {
38
+ case "character":
39
+ newChunks = this.flushGraphemes();
40
+ break;
41
+ case "word":
42
+ newChunks = this.flushWords();
43
+ break;
44
+ case "sentence":
45
+ newChunks = this.flushSentences();
46
+ break;
47
+ }
48
+ return [...longRunChunks, ...newChunks];
49
+ }
50
+ /** Flush whatever is left in the buffer when the stream finishes. */
51
+ flush() {
52
+ if (!this.buffer)
53
+ return [];
54
+ let finalChunks = [];
55
+ if (this.customChunker) {
56
+ // For custom chunkers, flush everything by treating the whole buffer as input.
57
+ const { chunks, remainder } = this.customChunker(this.buffer);
58
+ finalChunks.push(...chunks);
59
+ if (remainder) {
60
+ finalChunks.push(remainder);
61
+ }
62
+ }
63
+ else {
64
+ // For built-in strategies, the remaining buffer is the final chunk.
65
+ finalChunks.push(this.buffer);
66
+ }
67
+ this.buffer = "";
68
+ // Ensure no empty strings are returned.
69
+ return finalChunks.filter((c) => c.length > 0);
70
+ }
71
+ // ────────────────────────────────────────────────────────────────
72
+ // Internals
73
+ // ────────────────────────────────────────────────────────────────
74
+ flushGraphemes() {
75
+ const segments = Array.from(this.graphemeSeg.segment(this.buffer)).map((s) => s.segment);
76
+ // If there's only one segment, it might be incomplete. Wait for more.
77
+ if (segments.length <= 1) {
78
+ return [];
79
+ }
80
+ // Flush all but the last segment, which becomes the new buffer.
81
+ const chunksToFlush = segments.slice(0, -1);
82
+ this.buffer = segments[segments.length - 1];
83
+ return chunksToFlush;
84
+ }
85
+ flushWords() {
86
+ const chunks = [];
87
+ let currentWord = ""; // Accumulates the word part (e.g., "quick")
88
+ let currentNonWord = ""; // Accumulates trailing spaces/punctuation (e.g., " ")
89
+ // Iterate through all segments of the current buffer.
90
+ const segments = Array.from(this.wordSeg.segment(this.buffer));
91
+ // Process segments to form "word + non-word" chunks.
92
+ for (let i = 0; i < segments.length; i++) {
93
+ const part = segments[i];
94
+ if (part.isWordLike) {
95
+ // If we just finished a word and accumulated non-word characters,
96
+ // it means the previous "word + non-word" chunk is complete.
97
+ if (currentWord.length > 0 && currentNonWord.length > 0) {
98
+ chunks.push(currentWord + currentNonWord);
99
+ currentWord = "";
100
+ currentNonWord = "";
101
+ }
102
+ currentWord += part.segment;
103
+ }
104
+ else {
105
+ // This is a non-word segment (space, punctuation).
106
+ currentNonWord += part.segment;
107
+ }
108
+ // Guard against extremely long words (e.g., a URL) that don't have natural breaks.
109
+ // This flushes the accumulated word part even if it's not followed by a non-word yet.
110
+ if (currentWord.length > ChunkBuffer.MAX_WORD_LEN) {
111
+ chunks.push(currentWord + currentNonWord);
112
+ currentWord = "";
113
+ currentNonWord = "";
114
+ }
115
+ }
116
+ // After the loop, whatever remains in currentWord and currentNonWord
117
+ // is the incomplete part of the stream. This becomes the new buffer.
118
+ this.buffer = currentWord + currentNonWord;
119
+ // Filter out any empty strings that might result from edge cases.
120
+ return chunks.filter((c) => c.length > 0);
121
+ }
122
+ flushSentences() {
123
+ // This hybrid approach is more robust for sentence-ending punctuation.
124
+ // 1. Use a regex to find the last definitive sentence boundary.
125
+ // This is more reliable than Intl.Segmenter alone for partial streams.
126
+ const sentenceBoundaryRegex = /.*?[.?!](\s+|$)/g;
127
+ let lastMatchIndex = -1;
128
+ let match;
129
+ while ((match = sentenceBoundaryRegex.exec(this.buffer)) !== null) {
130
+ lastMatchIndex = match.index + match[0].length;
131
+ }
132
+ if (lastMatchIndex === -1) {
133
+ // No definitive sentence boundary found yet.
134
+ return [];
135
+ }
136
+ // 2. The text to be flushed is everything up to that boundary.
137
+ const textToFlush = this.buffer.substring(0, lastMatchIndex);
138
+ this.buffer = this.buffer.substring(lastMatchIndex);
139
+ // 3. Now, use Intl.Segmenter on the confirmed text to correctly split it.
140
+ // This handles cases where `textToFlush` contains multiple sentences.
141
+ return Array.from(this.sentenceSeg.segment(textToFlush))
142
+ .map((s) => s.segment)
143
+ .filter((c) => c.length > 0);
144
+ }
145
+ /** Fallback guard to break up very long runs of text with no natural breaks. */
146
+ flushLongRuns() {
147
+ const chunks = [];
148
+ // If the buffer is very long and contains no spaces (e.g., a single long word/URL),
149
+ // force a break to prevent excessive buffering.
150
+ if (this.buffer.length > ChunkBuffer.MAX_BUFFER_NO_BREAK &&
151
+ !/\s/.test(this.buffer)) {
152
+ chunks.push(this.buffer.slice(0, ChunkBuffer.MAX_BUFFER_NO_BREAK));
153
+ this.buffer = this.buffer.slice(ChunkBuffer.MAX_BUFFER_NO_BREAK);
154
+ }
155
+ return chunks;
156
+ }
157
+ flushCustom() {
158
+ try {
159
+ const { chunks, remainder } = this.customChunker(this.buffer);
160
+ this.buffer = remainder;
161
+ return chunks;
162
+ }
163
+ catch (err) {
164
+ console.error("Custom chunker failed. Flushing entire buffer to avoid data loss.", err);
165
+ const all = this.buffer;
166
+ this.buffer = "";
167
+ return [all];
168
+ }
169
+ }
170
+ }