@axiom-lattice/gateway 1.0.11 → 1.0.13

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,388 @@
1
+ # Resume Stream Feature
2
+
3
+ ## Overview
4
+
5
+ The `resume_stream` feature allows you to continue receiving streaming chunks from a known position. This is particularly useful for scenarios where the client connection is interrupted (e.g., page refresh, network issues) and you want to seamlessly continue from where you left off.
6
+
7
+ ## Architecture
8
+
9
+ ### ChunkBuffer Integration
10
+
11
+ The feature leverages the `ChunkBuffer` module from `@axiom-lattice/core` to:
12
+
13
+ - Store streaming chunks in memory
14
+ - Track thread status (active/completed/aborted)
15
+ - Provide TTL-based automatic cleanup
16
+ - Calculate and return only new chunks since a known position
17
+
18
+ ### How It Works
19
+
20
+ ```
21
+ ┌─────────────────┐
22
+ │ Original │
23
+ │ Streaming │ ──► Chunks stored in ChunkBuffer
24
+ │ (agent_stream) │ (thread_id, message_id, content)
25
+ └─────────────────┘
26
+
27
+ │ Connection lost / Page refresh
28
+
29
+
30
+ ┌─────────────────┐
31
+ │ Resume Stream │
32
+ │ (from known │ ──► Polls for new chunks
33
+ │ position) │ Returns only new content
34
+ └─────────────────┘ Ends when thread completes
35
+ ```
36
+
37
+ ## API Reference
38
+
39
+ ### `resume_stream(options)`
40
+
41
+ Creates an async iterator that yields new chunks as they arrive.
42
+
43
+ #### Parameters
44
+
45
+ ```typescript
46
+ {
47
+ thread_id: string; // Thread identifier
48
+ message_id: string; // Message identifier (usually run_id)
49
+ known_content: string; // Content already received (used to find resume position)
50
+ poll_interval?: number; // Polling interval in ms (default: 100)
51
+ }
52
+ ```
53
+
54
+ #### Returns
55
+
56
+ An async iterable object with `Symbol.asyncIterator`:
57
+
58
+ ```typescript
59
+ {
60
+ content: string; // Chunk content
61
+ timestamp: number; // When chunk was added
62
+ messageId: string; // Message identifier
63
+ }
64
+ ```
65
+
66
+ #### Behavior
67
+
68
+ - **Polling**: Checks for new chunks every `poll_interval` milliseconds
69
+ - **Status Check**: Monitors thread status (active/completed/aborted)
70
+ - **Timeout**: Automatically stops after 30 seconds of no new data
71
+ - **Completion**: Exits when thread is no longer active
72
+
73
+ ## Usage Examples
74
+
75
+ ### Example 1: Basic Resume After Page Refresh
76
+
77
+ ```typescript
78
+ import {
79
+ resume_stream,
80
+ get_accumulated_content,
81
+ } from "./services/agent_service";
82
+
83
+ // Client already received content before refresh (e.g., from localStorage or state)
84
+ const knownContent = "Hello world! This is content I already received...";
85
+
86
+ const stream = await resume_stream({
87
+ thread_id: "thread-123",
88
+ message_id: "msg-abc-456",
89
+ known_content: knownContent,
90
+ });
91
+
92
+ // Consume new chunks
93
+ for await (const chunk of stream) {
94
+ console.log("New content:", chunk.content);
95
+ displayInUI(chunk.content); // Update your UI
96
+ }
97
+
98
+ console.log("Stream completed!");
99
+ ```
100
+
101
+ ### Example 2: Check Status Before Resuming
102
+
103
+ ```typescript
104
+ import { get_thread_status, resume_stream } from "./services/agent_service";
105
+
106
+ // Check thread status first
107
+ const status = await get_thread_status("thread-123");
108
+
109
+ if (!status.exists) {
110
+ console.log("Thread not found or expired");
111
+ } else if (status.status === "completed") {
112
+ // Thread already finished, get full content
113
+ const content = await get_accumulated_content("thread-123");
114
+ displayFullContent(content);
115
+ } else if (status.status === "active") {
116
+ // Thread still streaming, resume from current position
117
+ const currentContent = await get_accumulated_content("thread-123");
118
+
119
+ const stream = await resume_stream({
120
+ thread_id: "thread-123",
121
+ message_id: "msg-123",
122
+ known_content: currentContent,
123
+ });
124
+
125
+ for await (const chunk of stream) {
126
+ appendToUI(chunk.content);
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Example 3: Express/Fastify Endpoint (SSE)
132
+
133
+ ```typescript
134
+ import express from "express";
135
+ import { resume_stream } from "./services/agent_service";
136
+
137
+ const app = express();
138
+
139
+ app.post("/api/resume-stream", async (req, res) => {
140
+ const { thread_id, message_id, known_content } = req.body;
141
+
142
+ // Set up Server-Sent Events
143
+ res.setHeader("Content-Type", "text/event-stream");
144
+ res.setHeader("Cache-Control", "no-cache");
145
+ res.setHeader("Connection", "keep-alive");
146
+
147
+ try {
148
+ const stream = await resume_stream({
149
+ thread_id,
150
+ message_id,
151
+ known_content,
152
+ });
153
+
154
+ for await (const chunk of stream) {
155
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
156
+ }
157
+
158
+ res.write("event: complete\ndata: {}\n\n");
159
+ res.end();
160
+ } catch (error) {
161
+ res.write(
162
+ `event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`
163
+ );
164
+ res.end();
165
+ }
166
+ });
167
+ ```
168
+
169
+ ### Example 4: React Hook
170
+
171
+ ```typescript
172
+ import { useState, useEffect } from 'react';
173
+
174
+ function useResumeStream(threadId: string, messageId: string, knownContent: string) {
175
+ const [content, setContent] = useState('');
176
+ const [isStreaming, setIsStreaming] = useState(false);
177
+ const [error, setError] = useState<Error | null>(null);
178
+
179
+ useEffect(() => {
180
+ let isCancelled = false;
181
+
182
+ async function startStream() {
183
+ setIsStreaming(true);
184
+
185
+ try {
186
+ const stream = await resume_stream({
187
+ thread_id: threadId,
188
+ message_id: messageId,
189
+ known_content: knownContent,
190
+ });
191
+
192
+ for await (const chunk of stream) {
193
+ if (isCancelled) break;
194
+ setContent(prev => prev + chunk.content);
195
+ }
196
+ } catch (err) {
197
+ if (!isCancelled) {
198
+ setError(err as Error);
199
+ }
200
+ } finally {
201
+ if (!isCancelled) {
202
+ setIsStreaming(false);
203
+ }
204
+ }
205
+ }
206
+
207
+ startStream();
208
+
209
+ return () => {
210
+ isCancelled = true;
211
+ };
212
+ }, [threadId, messageId, knownContent]);
213
+
214
+ return { content, isStreaming, error };
215
+ }
216
+
217
+ // Usage in component
218
+ function ChatMessage({ threadId, messageId, initialContent }) {
219
+ const { content, isStreaming } = useResumeStream(
220
+ threadId,
221
+ messageId,
222
+ initialContent
223
+ );
224
+
225
+ return (
226
+ <div>
227
+ {initialContent + content}
228
+ {isStreaming && <span className="cursor">▋</span>}
229
+ </div>
230
+ );
231
+ }
232
+ ```
233
+
234
+ ## Related Functions
235
+
236
+ ### `get_accumulated_content(thread_id)`
237
+
238
+ Get all accumulated content for a thread.
239
+
240
+ ```typescript
241
+ const content = await get_accumulated_content("thread-123");
242
+ ```
243
+
244
+ ### `get_thread_status(thread_id)`
245
+
246
+ Get thread status and metadata.
247
+
248
+ ```typescript
249
+ const status = await get_thread_status("thread-123");
250
+ // {
251
+ // exists: true,
252
+ // status: 'active' | 'completed' | 'aborted',
253
+ // chunkCount: 42,
254
+ // createdAt: 1234567890,
255
+ // updatedAt: 1234567900
256
+ // }
257
+ ```
258
+
259
+ ### `get_active_threads()`
260
+
261
+ Get all currently active threads.
262
+
263
+ ```typescript
264
+ const activeThreads = await get_active_threads();
265
+ // ['thread-1', 'thread-2', 'thread-3']
266
+ ```
267
+
268
+ ### `clear_thread_buffer(thread_id)`
269
+
270
+ Manually clear a thread's buffer.
271
+
272
+ ```typescript
273
+ await clear_thread_buffer("thread-123");
274
+ ```
275
+
276
+ ## Configuration
277
+
278
+ ### TTL (Time-To-Live)
279
+
280
+ Threads are automatically cleaned up after 1 hour of inactivity:
281
+
282
+ ```typescript
283
+ // In agent_service.ts
284
+ const buffer = new InMemoryChunkBuffer({
285
+ ttl: 60 * 60 * 1000, // 1 hour
286
+ cleanupInterval: 5 * 60 * 1000, // Clean every 5 minutes
287
+ });
288
+ ```
289
+
290
+ ### Polling Interval
291
+
292
+ Adjust polling frequency based on your needs:
293
+
294
+ ```typescript
295
+ // Fast polling for real-time updates
296
+ const stream = await resume_stream({
297
+ thread_id: "thread-123",
298
+ message_id: "msg-123",
299
+ known_content: knownContent,
300
+ poll_interval: 50, // Check every 50ms
301
+ });
302
+
303
+ // Slower polling to reduce server load
304
+ const stream = await resume_stream({
305
+ thread_id: "thread-123",
306
+ message_id: "msg-123",
307
+ known_content: knownContent,
308
+ poll_interval: 500, // Check every 500ms
309
+ });
310
+ ```
311
+
312
+ ### Timeout
313
+
314
+ The resume stream automatically times out after 30 seconds of no new data:
315
+
316
+ ```typescript
317
+ // In agent_service.ts - resume_stream function
318
+ const maxIdleTime = 30000; // 30 seconds
319
+ ```
320
+
321
+ ## Error Handling
322
+
323
+ ### Thread Not Found
324
+
325
+ ```typescript
326
+ const stream = await resume_stream({
327
+ thread_id: "non-existent",
328
+ message_id: "msg-123",
329
+ known_content: "",
330
+ });
331
+
332
+ // Stream will exit immediately if thread doesn't exist
333
+ for await (const chunk of stream) {
334
+ // Won't execute
335
+ }
336
+ ```
337
+
338
+ ### Network Errors
339
+
340
+ ```typescript
341
+ try {
342
+ const stream = await resume_stream({
343
+ thread_id: "thread-123",
344
+ message_id: "msg-123",
345
+ known_content: previousContent,
346
+ });
347
+
348
+ for await (const chunk of stream) {
349
+ displayChunk(chunk);
350
+ }
351
+ } catch (error) {
352
+ console.error("Stream error:", error);
353
+ showErrorToUser("Failed to resume stream");
354
+ }
355
+ ```
356
+
357
+ ## Best Practices
358
+
359
+ 1. **Always Check Status First**: Check thread status before attempting to resume
360
+ 2. **Store Message ID**: Keep track of the `message_id` (usually `run_id`) with your content
361
+ 3. **Preserve Content Exactly**: Store the exact content as received (no formatting/processing)
362
+ 4. **Handle Completion**: Be prepared for the stream to end at any time
363
+ 5. **Implement Retry Logic**: Add retry mechanism for transient failures
364
+ 6. **Set Appropriate Poll Interval**: Balance between responsiveness and server load
365
+ 7. **Content Matching**: The algorithm matches content intelligently - exact match, prefix, or suffix
366
+
367
+ ## Performance Considerations
368
+
369
+ - **Memory Usage**: Chunks are stored in memory (consider TTL for long-running applications)
370
+ - **Polling Overhead**: Lower `poll_interval` increases server load but improves responsiveness
371
+ - **Concurrent Streams**: Multiple resume streams can run concurrently
372
+ - **Cleanup**: Old threads are automatically cleaned up based on TTL
373
+
374
+ ## Limitations
375
+
376
+ - **In-Memory Only**: Current implementation stores chunks in memory (lost on server restart)
377
+ - **Single Message Per Thread**: Designed for one active message per thread
378
+ - **Content Matching**: Relies on exact content matching (whitespace, encoding must match)
379
+ - **No Persistence**: Chunks are not persisted to disk or database
380
+ - **Performance**: Content matching is O(n) where n is number of chunks
381
+
382
+ ## Future Enhancements
383
+
384
+ - **Persistent Storage**: Add Redis or database backend for persistence across restarts
385
+ - **WebSocket Support**: Direct WebSocket integration for lower latency
386
+ - **Event-Based Notifications**: Use event emitters instead of polling
387
+ - **Compression**: Compress stored chunks to reduce memory usage
388
+ - **Multi-Message Support**: Handle multiple concurrent messages per thread
package/dist/index.d.mts CHANGED
@@ -1,10 +1,56 @@
1
- import * as fastify from 'fastify';
2
1
  import * as http from 'http';
2
+ import * as fastify from 'fastify';
3
+
4
+ declare const defaultSwaggerConfig: {
5
+ openapi: {
6
+ openapi: string;
7
+ info: {
8
+ title: string;
9
+ description: string;
10
+ version: string;
11
+ contact: {
12
+ name: string;
13
+ email: string;
14
+ };
15
+ };
16
+ servers: {
17
+ url: string;
18
+ description: string;
19
+ }[];
20
+ components: {
21
+ securitySchemes: {
22
+ bearerAuth: {
23
+ type: "http";
24
+ scheme: "bearer";
25
+ bearerFormat: string;
26
+ };
27
+ };
28
+ };
29
+ security: {
30
+ bearerAuth: never[];
31
+ }[];
32
+ tags: {
33
+ name: string;
34
+ description: string;
35
+ }[];
36
+ };
37
+ };
38
+ declare const defaultSwaggerUiConfig: {
39
+ routePrefix: string;
40
+ uiConfig: {
41
+ docExpansion: "full";
42
+ deepLinking: boolean;
43
+ };
44
+ staticCSP: boolean;
45
+ transformStaticCSP: (header: string) => string;
46
+ };
3
47
 
4
48
  declare const LatticeGateway: {
5
49
  startAsHttpEndpoint: ({ port }: {
6
50
  port: number;
7
51
  }) => Promise<void>;
52
+ configureSwagger: (app: fastify.FastifyInstance, customSwaggerConfig?: Partial<typeof defaultSwaggerConfig>, customSwaggerUiConfig?: Partial<typeof defaultSwaggerUiConfig>) => Promise<void>;
53
+ registerLatticeRoutes: (app: fastify.FastifyInstance) => void;
8
54
  app: fastify.FastifyInstance<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault> & PromiseLike<fastify.FastifyInstance<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault>> & {
9
55
  __linterBrands: "SafePromiseLike";
10
56
  };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,56 @@
1
- import * as fastify from 'fastify';
2
1
  import * as http from 'http';
2
+ import * as fastify from 'fastify';
3
+
4
+ declare const defaultSwaggerConfig: {
5
+ openapi: {
6
+ openapi: string;
7
+ info: {
8
+ title: string;
9
+ description: string;
10
+ version: string;
11
+ contact: {
12
+ name: string;
13
+ email: string;
14
+ };
15
+ };
16
+ servers: {
17
+ url: string;
18
+ description: string;
19
+ }[];
20
+ components: {
21
+ securitySchemes: {
22
+ bearerAuth: {
23
+ type: "http";
24
+ scheme: "bearer";
25
+ bearerFormat: string;
26
+ };
27
+ };
28
+ };
29
+ security: {
30
+ bearerAuth: never[];
31
+ }[];
32
+ tags: {
33
+ name: string;
34
+ description: string;
35
+ }[];
36
+ };
37
+ };
38
+ declare const defaultSwaggerUiConfig: {
39
+ routePrefix: string;
40
+ uiConfig: {
41
+ docExpansion: "full";
42
+ deepLinking: boolean;
43
+ };
44
+ staticCSP: boolean;
45
+ transformStaticCSP: (header: string) => string;
46
+ };
3
47
 
4
48
  declare const LatticeGateway: {
5
49
  startAsHttpEndpoint: ({ port }: {
6
50
  port: number;
7
51
  }) => Promise<void>;
52
+ configureSwagger: (app: fastify.FastifyInstance, customSwaggerConfig?: Partial<typeof defaultSwaggerConfig>, customSwaggerUiConfig?: Partial<typeof defaultSwaggerUiConfig>) => Promise<void>;
53
+ registerLatticeRoutes: (app: fastify.FastifyInstance) => void;
8
54
  app: fastify.FastifyInstance<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault> & PromiseLike<fastify.FastifyInstance<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault>> & {
9
55
  __linterBrands: "SafePromiseLike";
10
56
  };