@ruska/cli 0.1.5 → 0.1.6

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.
@@ -116,7 +116,7 @@ function ChatCommandTui({ message, assistantId, threadId, tools, truncateOptions
116
116
  : undefined, [config, assistantId, message, threadId, tools]);
117
117
  /* eslint-enable @typescript-eslint/naming-convention */
118
118
  // Stream
119
- const { status, messages, events, error } = useStream(config, request);
119
+ const { status, messages, events, streamMetadata, error } = useStream(config, request);
120
120
  // Group messages into blocks by type + name boundaries
121
121
  const messageBlocks = useMemo(() => groupMessagesIntoBlocks(messages), [messages]);
122
122
  // Exit on completion
@@ -161,16 +161,17 @@ function ChatCommandTui({ message, assistantId, threadId, tools, truncateOptions
161
161
  })()))) : (React.createElement(Text, null, block.content))))),
162
162
  status === 'done' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
163
163
  React.createElement(Text, { color: "green" }, "Done"),
164
- request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
165
- "Thread: ",
166
- request.metadata.thread_id)),
167
164
  extractModelFromEvents(events) && (React.createElement(Text, { dimColor: true },
168
165
  "Model: ",
169
166
  extractModelFromEvents(events))),
170
- request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
171
- "Continue: ruska chat \"message\" -t",
167
+ streamMetadata?.thread_id && (React.createElement(Text, { dimColor: true },
168
+ "Thread: ",
169
+ streamMetadata.thread_id)),
170
+ streamMetadata?.thread_id && (React.createElement(Text, { dimColor: true },
171
+ "Continue: ruska chat -t ",
172
+ streamMetadata.thread_id,
172
173
  ' ',
173
- request.metadata.thread_id))))));
174
+ "\"message\""))))));
174
175
  }
175
176
  /**
176
177
  * JSON mode chat command - direct streaming without React hooks
@@ -3,13 +3,14 @@
3
3
  * From Beta proposal with critical values event handling
4
4
  */
5
5
  import type { Config } from '../types/index.js';
6
- import { type MessagePayload, type StreamRequest, type StreamEvent, type ValuesPayload } from '../types/stream.js';
6
+ import { type MessagePayload, type MetadataPayload, type StreamRequest, type StreamEvent, type ValuesPayload } from '../types/stream.js';
7
7
  export type StreamStatus = 'idle' | 'connecting' | 'streaming' | 'done' | 'error';
8
8
  export type UseStreamResult = {
9
9
  status: StreamStatus;
10
10
  events: StreamEvent[];
11
11
  messages: MessagePayload[];
12
12
  finalResponse: ValuesPayload | undefined;
13
+ streamMetadata: MetadataPayload | undefined;
13
14
  error: string | undefined;
14
15
  errorCode: number | undefined;
15
16
  abort: () => void;
@@ -3,7 +3,7 @@
3
3
  * From Beta proposal with critical values event handling
4
4
  */
5
5
  import { useState, useEffect, useRef, useCallback } from 'react';
6
- import { StreamService, StreamConnectionError, } from '../lib/services/stream-service.js';
6
+ import { StreamService, StreamConnectionError, DistributedStreamError, MalformedDistributedResponse, } from '../lib/services/stream-service.js';
7
7
  /**
8
8
  * React hook for consuming LLM streams.
9
9
  * CRITICAL: Captures "values" event payload as finalResponse
@@ -13,6 +13,7 @@ export function useStream(config, request) {
13
13
  const [events, setEvents] = useState([]);
14
14
  const [messages, setMessages] = useState([]);
15
15
  const [finalResponse, setFinalResponse] = useState();
16
+ const [streamMetadata, setStreamMetadata] = useState();
16
17
  const [error, setError] = useState();
17
18
  const [errorCode, setErrorCode] = useState();
18
19
  const handleReference = useRef();
@@ -29,6 +30,7 @@ export function useStream(config, request) {
29
30
  setEvents([]);
30
31
  setMessages([]);
31
32
  setFinalResponse(undefined);
33
+ setStreamMetadata(undefined);
32
34
  setError(undefined);
33
35
  setErrorCode(undefined);
34
36
  try {
@@ -58,11 +60,21 @@ export function useStream(config, request) {
58
60
  setFinalResponse(event.payload);
59
61
  break;
60
62
  }
63
+ case 'metadata': {
64
+ // Capture stream metadata (thread_id, etc.)
65
+ setStreamMetadata(event.payload);
66
+ break;
67
+ }
61
68
  case 'error': {
62
69
  setError(event.payload.message);
63
70
  setStatus('error');
64
71
  return;
65
72
  }
73
+ case 'done': {
74
+ // Terminal event from distributed mode
75
+ // Stream will end naturally after this
76
+ break;
77
+ }
66
78
  default: {
67
79
  // Unknown event type - ignore
68
80
  break;
@@ -77,6 +89,12 @@ export function useStream(config, request) {
77
89
  setError(error_.message);
78
90
  setErrorCode(error_.statusCode);
79
91
  }
92
+ else if (error_ instanceof DistributedStreamError) {
93
+ setError(`Distributed error (${error_.phase}): ${error_.message}`);
94
+ }
95
+ else if (error_ instanceof MalformedDistributedResponse) {
96
+ setError(`Invalid distributed response: ${error_.message}`);
97
+ }
80
98
  else if (error_ instanceof Error) {
81
99
  setError(error_.message);
82
100
  }
@@ -98,6 +116,7 @@ export function useStream(config, request) {
98
116
  events,
99
117
  messages,
100
118
  finalResponse,
119
+ streamMetadata,
101
120
  error,
102
121
  errorCode,
103
122
  abort,
@@ -19,6 +19,8 @@ export declare const exitCodes: {
19
19
  readonly rateLimited: 3;
20
20
  readonly timeout: 4;
21
21
  readonly serverError: 5;
22
+ readonly distributedError: 6;
23
+ readonly distributedTimeout: 7;
22
24
  };
23
25
  /**
24
26
  * Map raw errors to structured error responses
@@ -2,6 +2,7 @@
2
2
  * Error handler with taxonomy for CLI output
3
3
  * From Gamma proposal with structured error codes
4
4
  */
5
+ import { DistributedStreamError, MalformedDistributedResponse, } from '../services/stream-service.js';
5
6
  /**
6
7
  * Exit codes for scripting
7
8
  */
@@ -12,6 +13,8 @@ export const exitCodes = {
12
13
  rateLimited: 3,
13
14
  timeout: 4,
14
15
  serverError: 5,
16
+ distributedError: 6,
17
+ distributedTimeout: 7,
15
18
  };
16
19
  /**
17
20
  * Map raw errors to structured error responses
@@ -45,6 +48,33 @@ export function classifyError(error, statusCode) {
45
48
  }
46
49
  }
47
50
  if (error instanceof Error) {
51
+ // Handle distributed mode specific errors
52
+ if (error instanceof DistributedStreamError) {
53
+ // Check if it's a timeout during distributed streaming
54
+ if (error.message.includes('timeout') ||
55
+ error.message.includes('aborted')) {
56
+ return {
57
+ code: 'DISTRIBUTED_TIMEOUT',
58
+ message: `Distributed stream timed out during ${error.phase} phase.`,
59
+ recoverable: true,
60
+ exitCode: exitCodes.distributedTimeout,
61
+ };
62
+ }
63
+ return {
64
+ code: 'DISTRIBUTED_ERROR',
65
+ message: `Distributed stream error (${error.phase}): ${error.message}`,
66
+ recoverable: error.phase === 'handshake',
67
+ exitCode: exitCodes.distributedError,
68
+ };
69
+ }
70
+ if (error instanceof MalformedDistributedResponse) {
71
+ return {
72
+ code: 'DISTRIBUTED_ERROR',
73
+ message: `Invalid distributed response: ${error.message}`,
74
+ recoverable: false,
75
+ exitCode: exitCodes.distributedError,
76
+ };
77
+ }
48
78
  const errorMessage = error.message.toLowerCase();
49
79
  // Network errors
50
80
  if (errorMessage.includes('fetch failed') ||
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Stream service implementation for SSE consumption
3
- * Combines Beta's architecture with Alpha's timeout handling
3
+ * Supports both sync mode (HTTP 200) and distributed worker mode (HTTP 202)
4
+ * @see backend/src/routes/v0/llm.py - distributed mode detection
5
+ * @see backend/src/routes/v0/thread.py - thread stream endpoint
4
6
  */
5
7
  import type { Config } from '../../types/index.js';
6
- import type { StreamRequest, StreamHandle } from '../../types/stream.js';
8
+ import { type StreamRequest, type StreamHandle } from '../../types/stream.js';
7
9
  import type { StreamServiceInterface } from './stream-service.interface.js';
8
10
  /**
9
11
  * Custom error for stream connection failures
@@ -12,16 +14,58 @@ export declare class StreamConnectionError extends Error {
12
14
  readonly statusCode: number;
13
15
  constructor(message: string, statusCode: number);
14
16
  }
17
+ /**
18
+ * Error specific to distributed streaming mode
19
+ * Includes phase information for debugging
20
+ */
21
+ export declare class DistributedStreamError extends Error {
22
+ readonly phase: 'handshake' | 'streaming';
23
+ readonly rawResponse?: string | undefined;
24
+ constructor(message: string, phase: 'handshake' | 'streaming', rawResponse?: string | undefined);
25
+ }
26
+ /**
27
+ * Error for malformed distributed response from initial POST
28
+ */
29
+ export declare class MalformedDistributedResponse extends Error {
30
+ readonly rawResponse?: string | undefined;
31
+ constructor(message: string, rawResponse?: string | undefined);
32
+ }
15
33
  /**
16
34
  * Production stream service using SSE
17
35
  */
18
36
  export declare class StreamService implements StreamServiceInterface {
19
37
  private readonly host;
20
38
  private readonly apiKey;
21
- private readonly timeoutMs;
22
- constructor(config: Config, timeoutMs?: number);
39
+ private readonly connectionTimeoutMs;
40
+ private readonly streamIdleTimeoutMs;
41
+ constructor(config: Config, connectionTimeoutMs?: number, streamIdleTimeoutMs?: number);
23
42
  connect(request: StreamRequest): Promise<StreamHandle>;
24
43
  private fetchStream;
44
+ /**
45
+ * Handle distributed worker mode (HTTP 202 response)
46
+ * Parses the initial response and connects to thread stream endpoint
47
+ */
48
+ private handleDistributedMode;
49
+ /**
50
+ * Connect to GET /api/threads/{thread_id}/stream for distributed mode
51
+ * URL-encodes thread_id to prevent path injection
52
+ */
53
+ private connectToThreadStream;
54
+ /**
55
+ * Parse SSE events from distributed worker stream
56
+ * Handles [DONE] marker, keep-alive comments, and error JSON
57
+ */
58
+ private parseDistributedEventStream;
59
+ /**
60
+ * Extract events from distributed stream buffer
61
+ * Handles keep-alive comments, [DONE] marker, and error JSON
62
+ */
63
+ private extractDistributedEvents;
64
+ /**
65
+ * Parse a single distributed event data payload
66
+ * Handles error JSON format and regular event format
67
+ */
68
+ private parseDistributedEvent;
25
69
  private parseEventStream;
26
70
  private extractEvents;
27
71
  private parseEvent;
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Stream service implementation for SSE consumption
3
- * Combines Beta's architecture with Alpha's timeout handling
3
+ * Supports both sync mode (HTTP 200) and distributed worker mode (HTTP 202)
4
+ * @see backend/src/routes/v0/llm.py - distributed mode detection
5
+ * @see backend/src/routes/v0/thread.py - thread stream endpoint
4
6
  */
5
- const defaultTimeoutMs = 30000;
7
+ import { isDistributedResponse, isDistributedError, STREAM_DONE_MARKER, } from '../../types/stream.js';
8
+ const defaultConnectionTimeoutMs = 30000;
9
+ const defaultStreamIdleTimeoutMs = 60000;
6
10
  /**
7
11
  * Custom error for stream connection failures
8
12
  */
@@ -18,11 +22,48 @@ export class StreamConnectionError extends Error {
18
22
  this.name = 'StreamConnectionError';
19
23
  }
20
24
  }
25
+ /**
26
+ * Error specific to distributed streaming mode
27
+ * Includes phase information for debugging
28
+ */
29
+ export class DistributedStreamError extends Error {
30
+ constructor(message, phase, rawResponse) {
31
+ super(message);
32
+ Object.defineProperty(this, "phase", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: phase
37
+ });
38
+ Object.defineProperty(this, "rawResponse", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: rawResponse
43
+ });
44
+ this.name = 'DistributedStreamError';
45
+ }
46
+ }
47
+ /**
48
+ * Error for malformed distributed response from initial POST
49
+ */
50
+ export class MalformedDistributedResponse extends Error {
51
+ constructor(message, rawResponse) {
52
+ super(message);
53
+ Object.defineProperty(this, "rawResponse", {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: rawResponse
58
+ });
59
+ this.name = 'MalformedDistributedResponse';
60
+ }
61
+ }
21
62
  /**
22
63
  * Production stream service using SSE
23
64
  */
24
65
  export class StreamService {
25
- constructor(config, timeoutMs = defaultTimeoutMs) {
66
+ constructor(config, connectionTimeoutMs = defaultConnectionTimeoutMs, streamIdleTimeoutMs = defaultStreamIdleTimeoutMs) {
26
67
  Object.defineProperty(this, "host", {
27
68
  enumerable: true,
28
69
  configurable: true,
@@ -35,7 +76,13 @@ export class StreamService {
35
76
  writable: true,
36
77
  value: void 0
37
78
  });
38
- Object.defineProperty(this, "timeoutMs", {
79
+ Object.defineProperty(this, "connectionTimeoutMs", {
80
+ enumerable: true,
81
+ configurable: true,
82
+ writable: true,
83
+ value: void 0
84
+ });
85
+ Object.defineProperty(this, "streamIdleTimeoutMs", {
39
86
  enumerable: true,
40
87
  configurable: true,
41
88
  writable: true,
@@ -43,17 +90,23 @@ export class StreamService {
43
90
  });
44
91
  this.host = config.host.replace(/\/$/, '');
45
92
  this.apiKey = config.apiKey;
46
- this.timeoutMs = timeoutMs;
93
+ this.connectionTimeoutMs = connectionTimeoutMs;
94
+ this.streamIdleTimeoutMs = streamIdleTimeoutMs;
47
95
  }
48
96
  async connect(request) {
49
97
  const controller = new AbortController();
50
98
  // Set timeout for connection
51
99
  const timeoutId = setTimeout(() => {
52
100
  controller.abort();
53
- }, this.timeoutMs);
101
+ }, this.connectionTimeoutMs);
54
102
  try {
55
103
  const response = await this.fetchStream(request, controller.signal);
56
104
  clearTimeout(timeoutId);
105
+ // Check for distributed mode (HTTP 202)
106
+ if (response.status === 202) {
107
+ return await this.handleDistributedMode(response, controller);
108
+ }
109
+ // Sync mode (HTTP 200)
57
110
  if (!response.body) {
58
111
  throw new StreamConnectionError('No response body', 0);
59
112
  }
@@ -83,12 +136,207 @@ export class StreamService {
83
136
  body: JSON.stringify(request),
84
137
  signal,
85
138
  });
86
- if (!response.ok) {
139
+ // Accept both 200 (sync) and 202 (distributed) as valid
140
+ if (!response.ok && response.status !== 202) {
87
141
  const error = await this.parseError(response);
88
142
  throw new StreamConnectionError(error, response.status);
89
143
  }
90
144
  return response;
91
145
  }
146
+ /**
147
+ * Handle distributed worker mode (HTTP 202 response)
148
+ * Parses the initial response and connects to thread stream endpoint
149
+ */
150
+ async handleDistributedMode(initialResponse, controller) {
151
+ // Parse the distributed response body
152
+ const rawText = await initialResponse.text();
153
+ let data;
154
+ try {
155
+ data = JSON.parse(rawText);
156
+ }
157
+ catch {
158
+ throw new MalformedDistributedResponse('Invalid JSON in distributed response', rawText);
159
+ }
160
+ // Validate the response shape
161
+ if (!isDistributedResponse(data)) {
162
+ throw new MalformedDistributedResponse('Invalid distributed response: missing or invalid thread_id', rawText);
163
+ }
164
+ // Check if already aborted before making second request
165
+ if (controller.signal.aborted) {
166
+ throw new DOMException('Aborted', 'AbortError');
167
+ }
168
+ // Connect to the thread stream endpoint
169
+ const streamHandle = await this.connectToThreadStream(data.thread_id, controller);
170
+ return streamHandle;
171
+ }
172
+ /**
173
+ * Connect to GET /api/threads/{thread_id}/stream for distributed mode
174
+ * URL-encodes thread_id to prevent path injection
175
+ */
176
+ async connectToThreadStream(threadId, controller) {
177
+ // URL-encode thread_id to prevent path injection
178
+ const encodedThreadId = encodeURIComponent(threadId);
179
+ const streamUrl = `${this.host}/api/threads/${encodedThreadId}/stream`;
180
+ // Set connection timeout for the GET request
181
+ const connectionTimeoutId = setTimeout(() => {
182
+ controller.abort();
183
+ }, this.connectionTimeoutMs);
184
+ try {
185
+ const streamResponse = await fetch(streamUrl, {
186
+ method: 'GET',
187
+ headers: {
188
+ 'x-api-key': this.apiKey,
189
+ // eslint-disable-next-line @typescript-eslint/naming-convention
190
+ Accept: 'text/event-stream',
191
+ },
192
+ signal: controller.signal,
193
+ });
194
+ clearTimeout(connectionTimeoutId);
195
+ if (!streamResponse.ok) {
196
+ const error = await this.parseError(streamResponse);
197
+ throw new DistributedStreamError(`Failed to connect to thread stream: ${error}`, 'handshake');
198
+ }
199
+ if (!streamResponse.body) {
200
+ throw new DistributedStreamError('No response body from thread stream', 'handshake');
201
+ }
202
+ const events = this.parseDistributedEventStream(streamResponse.body, controller);
203
+ return {
204
+ events,
205
+ abort() {
206
+ controller.abort();
207
+ },
208
+ };
209
+ }
210
+ catch (error) {
211
+ clearTimeout(connectionTimeoutId);
212
+ throw error;
213
+ }
214
+ }
215
+ /**
216
+ * Parse SSE events from distributed worker stream
217
+ * Handles [DONE] marker, keep-alive comments, and error JSON
218
+ */
219
+ async *parseDistributedEventStream(body, controller) {
220
+ const reader = body.getReader();
221
+ const decoder = new TextDecoder();
222
+ let buffer = '';
223
+ let idleTimeoutId;
224
+ // Reset idle timeout on data
225
+ const resetIdleTimeout = () => {
226
+ if (idleTimeoutId) {
227
+ clearTimeout(idleTimeoutId);
228
+ }
229
+ idleTimeoutId = setTimeout(() => {
230
+ controller.abort();
231
+ }, this.streamIdleTimeoutMs);
232
+ };
233
+ try {
234
+ resetIdleTimeout();
235
+ while (true) {
236
+ // eslint-disable-next-line no-await-in-loop
237
+ const { done, value } = await reader.read();
238
+ if (done)
239
+ break;
240
+ // Reset idle timeout on any data received
241
+ resetIdleTimeout();
242
+ buffer += decoder.decode(value, { stream: true });
243
+ const result = this.extractDistributedEvents(buffer);
244
+ buffer = result.remaining;
245
+ for (const event of result.parsed) {
246
+ // Check for terminal [DONE] marker
247
+ if (event.type === 'done') {
248
+ return;
249
+ }
250
+ yield event;
251
+ }
252
+ }
253
+ // Process any remaining buffer
254
+ if (buffer.trim()) {
255
+ const result = this.extractDistributedEvents(buffer + '\n');
256
+ for (const event of result.parsed) {
257
+ if (event.type === 'done') {
258
+ return;
259
+ }
260
+ yield event;
261
+ }
262
+ }
263
+ }
264
+ finally {
265
+ if (idleTimeoutId) {
266
+ clearTimeout(idleTimeoutId);
267
+ }
268
+ reader.releaseLock();
269
+ }
270
+ }
271
+ /**
272
+ * Extract events from distributed stream buffer
273
+ * Handles keep-alive comments, [DONE] marker, and error JSON
274
+ */
275
+ extractDistributedEvents(buffer) {
276
+ const parsed = [];
277
+ const lines = buffer.split('\n');
278
+ let remaining = '';
279
+ for (let i = 0; i < lines.length; i++) {
280
+ const line = lines[i];
281
+ // Incomplete line at end (no newline after it)
282
+ if (i === lines.length - 1 && !buffer.endsWith('\n')) {
283
+ remaining = line;
284
+ break;
285
+ }
286
+ // Skip empty lines (SSE event separator)
287
+ if (line.trim() === '') {
288
+ continue;
289
+ }
290
+ // Skip keep-alive comments (lines starting with :)
291
+ if (line.startsWith(':')) {
292
+ continue;
293
+ }
294
+ // Parse data lines
295
+ if (line.startsWith('data: ')) {
296
+ const data = line.slice(6);
297
+ // Check for [DONE] marker
298
+ if (data === STREAM_DONE_MARKER) {
299
+ parsed.push({ type: 'done', payload: undefined });
300
+ continue;
301
+ }
302
+ // Try to parse as JSON
303
+ const event = this.parseDistributedEvent(data);
304
+ if (event) {
305
+ parsed.push(event);
306
+ }
307
+ }
308
+ }
309
+ return { parsed, remaining };
310
+ }
311
+ /**
312
+ * Parse a single distributed event data payload
313
+ * Handles error JSON format and regular event format
314
+ */
315
+ parseDistributedEvent(data) {
316
+ try {
317
+ const parsed = JSON.parse(data);
318
+ // Check for distributed error format: {"error": "..."}
319
+ if (isDistributedError(parsed)) {
320
+ return {
321
+ type: 'error',
322
+ payload: { message: parsed.error },
323
+ };
324
+ }
325
+ // Regular event format: [type, payload]
326
+ if (Array.isArray(parsed) && parsed.length >= 2) {
327
+ const [type, payload] = parsed;
328
+ // Type assertion needed because we're parsing dynamic SSE data
329
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
330
+ return { type, payload };
331
+ }
332
+ // Unknown format - skip
333
+ return undefined;
334
+ }
335
+ catch {
336
+ // Parse error - skip this event
337
+ return undefined;
338
+ }
339
+ }
92
340
  async *parseEventStream(body) {
93
341
  const reader = body.getReader();
94
342
  const decoder = new TextDecoder();
@@ -37,7 +37,7 @@ export type ErrorOutput = {
37
37
  /**
38
38
  * Typed error codes for programmatic handling
39
39
  */
40
- export type ErrorCode = 'AUTH_FAILED' | 'NETWORK_ERROR' | 'RATE_LIMITED' | 'INVALID_REQUEST' | 'STREAM_INTERRUPTED' | 'PARSE_ERROR' | 'SERVER_ERROR' | 'TIMEOUT';
40
+ export type ErrorCode = 'AUTH_FAILED' | 'NETWORK_ERROR' | 'RATE_LIMITED' | 'INVALID_REQUEST' | 'STREAM_INTERRUPTED' | 'PARSE_ERROR' | 'SERVER_ERROR' | 'TIMEOUT' | 'DISTRIBUTED_ERROR' | 'DISTRIBUTED_TIMEOUT';
41
41
  /**
42
42
  * Union of all output types
43
43
  */
@@ -5,7 +5,33 @@
5
5
  /**
6
6
  * Stream event types matching backend SSE format
7
7
  */
8
- export type StreamEventType = 'messages' | 'values' | 'error';
8
+ export type StreamEventType = 'messages' | 'values' | 'error' | 'metadata' | 'done';
9
+ /**
10
+ * Special SSE markers from distributed stream
11
+ */
12
+ export declare const STREAM_DONE_MARKER = "[DONE]";
13
+ /**
14
+ * Response from POST /llm/stream when DISTRIBUTED_WORKERS=true
15
+ */
16
+ export type DistributedResponse = {
17
+ thread_id: string;
18
+ distributed: true;
19
+ };
20
+ /**
21
+ * Type guard for distributed response with strict validation
22
+ * Validates thread_id exists, is non-empty, and within bounds
23
+ */
24
+ export declare function isDistributedResponse(data: unknown): data is DistributedResponse;
25
+ /**
26
+ * Error response format from distributed stream
27
+ */
28
+ export type DistributedErrorResponse = {
29
+ error: string;
30
+ };
31
+ /**
32
+ * Type guard for distributed error format
33
+ */
34
+ export declare function isDistributedError(data: unknown): data is DistributedErrorResponse;
9
35
  /**
10
36
  * Content block for multi-modal messages
11
37
  */
@@ -49,6 +75,15 @@ export type ValuesPayload = {
49
75
  export type ErrorPayload = {
50
76
  message: string;
51
77
  };
78
+ /**
79
+ * Metadata payload from 'metadata' events
80
+ * Sent at the start of stream with thread/session info
81
+ */
82
+ export type MetadataPayload = {
83
+ thread_id?: string;
84
+ assistant_id?: string;
85
+ project_id?: string;
86
+ };
52
87
  /**
53
88
  * Discriminated union for stream events
54
89
  */
@@ -62,6 +97,12 @@ export type StreamEvent = {
62
97
  } | {
63
98
  type: 'error';
64
99
  payload: ErrorPayload;
100
+ } | {
101
+ type: 'metadata';
102
+ payload: MetadataPayload;
103
+ } | {
104
+ type: 'done';
105
+ payload: undefined;
65
106
  };
66
107
  /**
67
108
  * Request body for /llm/stream endpoint
@@ -2,6 +2,43 @@
2
2
  * Stream types for /api/llm/stream endpoint
3
3
  * @see backend/src/utils/stream.py:handle_multi_mode
4
4
  */
5
+ /**
6
+ * Special SSE markers from distributed stream
7
+ */
8
+ // eslint-disable-next-line @typescript-eslint/naming-convention
9
+ export const STREAM_DONE_MARKER = '[DONE]';
10
+ /**
11
+ * Type guard for distributed response with strict validation
12
+ * Validates thread_id exists, is non-empty, and within bounds
13
+ */
14
+ export function isDistributedResponse(data) {
15
+ if (typeof data !== 'object' || data === null) {
16
+ return false;
17
+ }
18
+ const obj = data;
19
+ if (obj['distributed'] !== true) {
20
+ return false;
21
+ }
22
+ const threadId = obj['thread_id'];
23
+ if (typeof threadId !== 'string') {
24
+ return false;
25
+ }
26
+ // Strict validation: non-empty and bounded length (prevent DoS)
27
+ if (threadId.length === 0 || threadId.length > 256) {
28
+ return false;
29
+ }
30
+ return true;
31
+ }
32
+ /**
33
+ * Type guard for distributed error format
34
+ */
35
+ export function isDistributedError(data) {
36
+ if (typeof data !== 'object' || data === null) {
37
+ return false;
38
+ }
39
+ const obj = data;
40
+ return typeof obj['error'] === 'string';
41
+ }
5
42
  /**
6
43
  * Extract text content from a MessagePayload.
7
44
  * Handles both string content and multi-modal content blocks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruska/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for Orchestra - AI Agent Orchestration Platform",
5
5
  "license": "Apache-2.0",
6
6
  "author": "RUSKA <reggleston@ruska.ai>",