@ruska/cli 0.1.4 → 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.
- package/README.md +23 -0
- package/dist/cli.js +10 -4
- package/dist/commands/chat.d.ts +1 -0
- package/dist/commands/chat.js +21 -15
- package/dist/components/model-select.d.ts +2 -2
- package/dist/components/model-select.js +2 -2
- package/dist/hooks/use-stream.d.ts +2 -1
- package/dist/hooks/use-stream.js +24 -5
- package/dist/lib/output/error-handler.d.ts +2 -0
- package/dist/lib/output/error-handler.js +30 -0
- package/dist/lib/services/stream-service.d.ts +48 -4
- package/dist/lib/services/stream-service.js +255 -7
- package/dist/lib/tools.d.ts +23 -0
- package/dist/lib/tools.js +42 -0
- package/dist/types/output.d.ts +1 -1
- package/dist/types/stream.d.ts +42 -1
- package/dist/types/stream.js +37 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -176,11 +176,34 @@ $ ruska chat "Hello" -a <assistant-id> | jq '.type'
|
|
|
176
176
|
| `-a, --assistant` | Assistant ID (optional, uses default chat if omitted) |
|
|
177
177
|
| `-t, --thread` | Thread ID to continue an existing conversation |
|
|
178
178
|
| `-m, --message` | Message to send (alternative to positional arg) |
|
|
179
|
+
| `--tools` | Tools for the chat session (see below for modes) |
|
|
179
180
|
| `--json` | Output as newline-delimited JSON (NDJSON) |
|
|
180
181
|
| `--truncate <n>` | Max characters for tool output (default: 500) |
|
|
181
182
|
| `--truncate-lines` | Max lines for tool output (default: 10) |
|
|
182
183
|
| `--full-output` | Disable truncation (show full output) |
|
|
183
184
|
|
|
185
|
+
**Tool options:**
|
|
186
|
+
|
|
187
|
+
| Value | Behavior |
|
|
188
|
+
| --------------------- | --------------------------------------------------------------------------------------- |
|
|
189
|
+
| (not provided) | Uses default tools: web_search, web_scrape, math_calculator, think_tool, python_sandbox |
|
|
190
|
+
| `--tools=disabled` | Disables all tools |
|
|
191
|
+
| `--tools=tool1,tool2` | Uses only the specified tools |
|
|
192
|
+
|
|
193
|
+
**Examples with tools:**
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Chat with default tools (web search, scrape, calculator, think, python)
|
|
197
|
+
$ ruska chat "What's the weather in Dallas?"
|
|
198
|
+
|
|
199
|
+
# Chat without any tools
|
|
200
|
+
$ ruska chat "Tell me a joke" --tools=disabled
|
|
201
|
+
|
|
202
|
+
# Chat with specific tools only
|
|
203
|
+
$ ruska chat "Calculate 2+2" --tools=math_calculator
|
|
204
|
+
$ ruska chat "Search and analyze" --tools=web_search,think_tool
|
|
205
|
+
```
|
|
206
|
+
|
|
184
207
|
**Exit codes:**
|
|
185
208
|
|
|
186
209
|
| Code | Meaning |
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,9 @@ const cli = meow(`
|
|
|
33
33
|
-a, --assistant Assistant ID (optional, uses default chat if omitted)
|
|
34
34
|
-t, --thread Thread ID to continue a conversation
|
|
35
35
|
-m, --message Message (alternative to positional arg)
|
|
36
|
+
--tools Tools to enable (default: web_search,web_scrape,math_calculator,think_tool,python_sandbox)
|
|
37
|
+
Use --tools=disabled to disable all tools
|
|
38
|
+
Use --tools=tool1,tool2 for specific tools
|
|
36
39
|
--json Output as newline-delimited JSON (auto-enabled when piped)
|
|
37
40
|
--truncate <n> Max characters for tool output (default: 500)
|
|
38
41
|
--truncate-lines Max lines for tool output (default: 10)
|
|
@@ -50,7 +53,9 @@ const cli = meow(`
|
|
|
50
53
|
$ ruska auth # Configure API key and host
|
|
51
54
|
$ ruska assistants # List your assistants
|
|
52
55
|
$ ruska assistant abc-123 # Get assistant details
|
|
53
|
-
$ ruska chat "Hello" #
|
|
56
|
+
$ ruska chat "Hello" # Chat with default tools enabled
|
|
57
|
+
$ ruska chat "Hello" --tools=disabled # Chat without any tools
|
|
58
|
+
$ ruska chat "Search X" --tools=web_search # Chat with specific tools
|
|
54
59
|
$ ruska chat "Hello" -a <assistant-id> # Chat with specific assistant
|
|
55
60
|
$ ruska chat "Follow up" -t <thread-id> # Continue existing thread
|
|
56
61
|
$ ruska chat "Hello" -a <id> --json # Output as NDJSON
|
|
@@ -116,7 +121,7 @@ const cli = meow(`
|
|
|
116
121
|
},
|
|
117
122
|
},
|
|
118
123
|
});
|
|
119
|
-
const [command, ...
|
|
124
|
+
const [command, ...arguments_] = cli.input;
|
|
120
125
|
// Route to appropriate command
|
|
121
126
|
async function main() {
|
|
122
127
|
if (cli.flags.ui) {
|
|
@@ -134,7 +139,7 @@ async function main() {
|
|
|
134
139
|
break;
|
|
135
140
|
}
|
|
136
141
|
case 'assistant': {
|
|
137
|
-
const assistantId =
|
|
142
|
+
const assistantId = arguments_[0];
|
|
138
143
|
if (!assistantId) {
|
|
139
144
|
console.error('Usage: ruska assistant <id>');
|
|
140
145
|
console.log('Run `ruska assistants` to list available assistants');
|
|
@@ -153,7 +158,7 @@ async function main() {
|
|
|
153
158
|
cli.flags['a']?.toString();
|
|
154
159
|
const threadId = cli.flags.thread ??
|
|
155
160
|
cli.flags['t']?.toString();
|
|
156
|
-
const message =
|
|
161
|
+
const message = arguments_.join(' ') || cli.flags.message;
|
|
157
162
|
if (!message) {
|
|
158
163
|
console.error('Error: Message is required');
|
|
159
164
|
console.error('Usage: ruska chat "<message>" [-a <assistant-id>]');
|
|
@@ -163,6 +168,7 @@ async function main() {
|
|
|
163
168
|
json: cli.flags.json,
|
|
164
169
|
assistantId,
|
|
165
170
|
threadId,
|
|
171
|
+
tools: cli.flags.tools,
|
|
166
172
|
truncateOptions: cli.flags.fullOutput
|
|
167
173
|
? undefined
|
|
168
174
|
: {
|
package/dist/commands/chat.d.ts
CHANGED
package/dist/commands/chat.js
CHANGED
|
@@ -14,6 +14,7 @@ import { classifyError, exitCodes } from '../lib/output/error-handler.js';
|
|
|
14
14
|
import { writeJson, checkIsTty } from '../lib/output/writers.js';
|
|
15
15
|
import { StreamService, StreamConnectionError, } from '../lib/services/stream-service.js';
|
|
16
16
|
import { truncate } from '../lib/output/truncate.js';
|
|
17
|
+
import { parseToolsFlag } from '../lib/tools.js';
|
|
17
18
|
/**
|
|
18
19
|
* Group messages into blocks by type + name boundaries
|
|
19
20
|
*/
|
|
@@ -83,7 +84,7 @@ function StatusIndicator({ status }) {
|
|
|
83
84
|
/**
|
|
84
85
|
* TUI mode chat command using React hook
|
|
85
86
|
*/
|
|
86
|
-
function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
|
|
87
|
+
function ChatCommandTui({ message, assistantId, threadId, tools, truncateOptions, }) {
|
|
87
88
|
const { exit } = useApp();
|
|
88
89
|
const [config, setConfig] = useState();
|
|
89
90
|
const [authError, setAuthError] = useState(false);
|
|
@@ -106,15 +107,16 @@ function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
|
|
|
106
107
|
const request = useMemo(() => config
|
|
107
108
|
? {
|
|
108
109
|
input: { messages: [{ role: 'user', content: message }] },
|
|
110
|
+
tools,
|
|
109
111
|
metadata: {
|
|
110
112
|
...(assistantId && { assistant_id: assistantId }),
|
|
111
113
|
...(threadId && { thread_id: threadId }),
|
|
112
114
|
},
|
|
113
115
|
}
|
|
114
|
-
: undefined, [config, assistantId, message, threadId]);
|
|
116
|
+
: undefined, [config, assistantId, message, threadId, tools]);
|
|
115
117
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
116
118
|
// Stream
|
|
117
|
-
const { status, messages, events, error } = useStream(config, request);
|
|
119
|
+
const { status, messages, events, streamMetadata, error } = useStream(config, request);
|
|
118
120
|
// Group messages into blocks by type + name boundaries
|
|
119
121
|
const messageBlocks = useMemo(() => groupMessagesIntoBlocks(messages), [messages]);
|
|
120
122
|
// Exit on completion
|
|
@@ -159,22 +161,23 @@ function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
|
|
|
159
161
|
})()))) : (React.createElement(Text, null, block.content))))),
|
|
160
162
|
status === 'done' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
161
163
|
React.createElement(Text, { color: "green" }, "Done"),
|
|
162
|
-
request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
|
|
163
|
-
"Thread: ",
|
|
164
|
-
request.metadata.thread_id)),
|
|
165
164
|
extractModelFromEvents(events) && (React.createElement(Text, { dimColor: true },
|
|
166
165
|
"Model: ",
|
|
167
166
|
extractModelFromEvents(events))),
|
|
168
|
-
|
|
169
|
-
"
|
|
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,
|
|
170
173
|
' ',
|
|
171
|
-
|
|
174
|
+
"\"message\""))))));
|
|
172
175
|
}
|
|
173
176
|
/**
|
|
174
177
|
* JSON mode chat command - direct streaming without React hooks
|
|
175
178
|
* Outputs NDJSON for downstream consumption
|
|
176
179
|
*/
|
|
177
|
-
async function runJsonMode(message, assistantId, threadId) {
|
|
180
|
+
async function runJsonMode(message, assistantId, threadId, tools) {
|
|
178
181
|
const config = await loadConfig();
|
|
179
182
|
if (!config) {
|
|
180
183
|
const formatter = new OutputFormatter();
|
|
@@ -190,6 +193,7 @@ async function runJsonMode(message, assistantId, threadId) {
|
|
|
190
193
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
191
194
|
const request = {
|
|
192
195
|
input: { messages: [{ role: 'user', content: message }] },
|
|
196
|
+
tools,
|
|
193
197
|
metadata: {
|
|
194
198
|
...(assistantId && { assistant_id: assistantId }),
|
|
195
199
|
...(threadId && { thread_id: threadId }),
|
|
@@ -244,22 +248,22 @@ async function runJsonMode(message, assistantId, threadId) {
|
|
|
244
248
|
/**
|
|
245
249
|
* Main chat command component - handles JSON mode branching
|
|
246
250
|
*/
|
|
247
|
-
function ChatCommand({ message, isJsonMode, assistantId, threadId, truncateOptions, }) {
|
|
251
|
+
function ChatCommand({ message, isJsonMode, assistantId, threadId, tools, truncateOptions, }) {
|
|
248
252
|
const { exit } = useApp();
|
|
249
253
|
useEffect(() => {
|
|
250
254
|
if (isJsonMode) {
|
|
251
255
|
// JSON mode runs outside React, just exit immediately
|
|
252
|
-
void runJsonMode(message, assistantId, threadId).finally(() => {
|
|
256
|
+
void runJsonMode(message, assistantId, threadId, tools).finally(() => {
|
|
253
257
|
exit();
|
|
254
258
|
});
|
|
255
259
|
}
|
|
256
|
-
}, [message, isJsonMode, assistantId, threadId, exit]);
|
|
260
|
+
}, [message, isJsonMode, assistantId, threadId, tools, exit]);
|
|
257
261
|
// JSON mode: no UI (handled in useEffect)
|
|
258
262
|
if (isJsonMode) {
|
|
259
263
|
return null;
|
|
260
264
|
}
|
|
261
265
|
// TUI mode
|
|
262
|
-
return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId, truncateOptions: truncateOptions }));
|
|
266
|
+
return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId, tools: tools, truncateOptions: truncateOptions }));
|
|
263
267
|
}
|
|
264
268
|
/**
|
|
265
269
|
* Run the chat command
|
|
@@ -267,6 +271,8 @@ function ChatCommand({ message, isJsonMode, assistantId, threadId, truncateOptio
|
|
|
267
271
|
export async function runChatCommand(message, options = {}) {
|
|
268
272
|
// Auto-detect: use JSON mode if not TTY (piped) or explicitly requested
|
|
269
273
|
const isJsonMode = options.json ?? !checkIsTty();
|
|
270
|
-
|
|
274
|
+
// Parse tools flag (undefined = defaults, 'disabled' = [], 'a,b' = ['a', 'b'])
|
|
275
|
+
const parsedTools = parseToolsFlag(options.tools);
|
|
276
|
+
const { waitUntilExit } = render(React.createElement(ChatCommand, { message: message, isJsonMode: isJsonMode, assistantId: options.assistantId, threadId: options.threadId, tools: parsedTools, truncateOptions: options.truncateOptions }));
|
|
271
277
|
await waitUntilExit();
|
|
272
278
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
type
|
|
2
|
+
type ModelSelectProperties = {
|
|
3
3
|
readonly models: string[];
|
|
4
4
|
readonly value: string;
|
|
5
5
|
readonly onChange: (value: string) => void;
|
|
6
6
|
readonly onSubmit: (value: string) => void;
|
|
7
7
|
readonly onEscape?: () => void;
|
|
8
8
|
};
|
|
9
|
-
export declare function ModelSelect({ models, value, onChange, onSubmit, onEscape, }:
|
|
9
|
+
export declare function ModelSelect({ models, value, onChange, onSubmit, onEscape, }: ModelSelectProperties): React.JSX.Element;
|
|
10
10
|
export {};
|
|
@@ -61,8 +61,8 @@ export function ModelSelect({ models, value, onChange, onSubmit, onEscape, }) {
|
|
|
61
61
|
" \u2191 ",
|
|
62
62
|
startIndex,
|
|
63
63
|
" more"),
|
|
64
|
-
visibleModels.map((model,
|
|
65
|
-
const actualIndex = startIndex +
|
|
64
|
+
visibleModels.map((model, index) => {
|
|
65
|
+
const actualIndex = startIndex + index;
|
|
66
66
|
const isSelected = actualIndex === selectedIndex;
|
|
67
67
|
return (React.createElement(Box, { key: model },
|
|
68
68
|
React.createElement(Text, { color: isSelected ? 'cyan' : undefined }, isSelected ? '❯ ' : ' '),
|
|
@@ -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;
|
package/dist/hooks/use-stream.js
CHANGED
|
@@ -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,11 +13,12 @@ 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
|
-
const
|
|
19
|
+
const handleReference = useRef();
|
|
19
20
|
const abort = useCallback(() => {
|
|
20
|
-
|
|
21
|
+
handleReference.current?.abort();
|
|
21
22
|
}, []);
|
|
22
23
|
useEffect(() => {
|
|
23
24
|
if (!config || !request)
|
|
@@ -29,11 +30,12 @@ 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 {
|
|
35
37
|
const handle = await service.connect(request);
|
|
36
|
-
|
|
38
|
+
handleReference.current = handle;
|
|
37
39
|
if (cancelled) {
|
|
38
40
|
handle.abort();
|
|
39
41
|
return;
|
|
@@ -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
|
}
|
|
@@ -90,7 +108,7 @@ export function useStream(config, request) {
|
|
|
90
108
|
void run();
|
|
91
109
|
return () => {
|
|
92
110
|
cancelled = true;
|
|
93
|
-
|
|
111
|
+
handleReference.current?.abort();
|
|
94
112
|
};
|
|
95
113
|
}, [config, request]);
|
|
96
114
|
return {
|
|
@@ -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,
|
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
22
|
-
|
|
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
|
-
*
|
|
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 { 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,
|
|
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, "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default tools and parsing for chat command
|
|
3
|
+
* @see frontend/src/components/menus/BaseToolMenu.tsx for frontend equivalent
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Default tools enabled for chat command
|
|
7
|
+
* Mirrors frontend DEFAULT_AGENT_TOOLS in BaseToolMenu.tsx
|
|
8
|
+
*/
|
|
9
|
+
export declare const defaultAgentTools: readonly ["web_search", "web_scrape", "math_calculator", "think_tool", "python_sandbox"];
|
|
10
|
+
export type DefaultAgentTool = (typeof defaultAgentTools)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Parse --tools flag value into array of tool names
|
|
13
|
+
*
|
|
14
|
+
* @param value - Raw flag value (undefined, 'disabled', or comma-separated)
|
|
15
|
+
* @returns Array of tool names to send to API
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* parseToolsFlag(undefined) // returns DEFAULT_AGENT_TOOLS
|
|
19
|
+
* parseToolsFlag('') // returns DEFAULT_AGENT_TOOLS
|
|
20
|
+
* parseToolsFlag('disabled') // returns []
|
|
21
|
+
* parseToolsFlag('web_search,think_tool') // returns ['web_search', 'think_tool']
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseToolsFlag(value: string | undefined): string[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default tools and parsing for chat command
|
|
3
|
+
* @see frontend/src/components/menus/BaseToolMenu.tsx for frontend equivalent
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Default tools enabled for chat command
|
|
7
|
+
* Mirrors frontend DEFAULT_AGENT_TOOLS in BaseToolMenu.tsx
|
|
8
|
+
*/
|
|
9
|
+
export const defaultAgentTools = [
|
|
10
|
+
'web_search',
|
|
11
|
+
'web_scrape',
|
|
12
|
+
'math_calculator',
|
|
13
|
+
'think_tool',
|
|
14
|
+
'python_sandbox',
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Parse --tools flag value into array of tool names
|
|
18
|
+
*
|
|
19
|
+
* @param value - Raw flag value (undefined, 'disabled', or comma-separated)
|
|
20
|
+
* @returns Array of tool names to send to API
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* parseToolsFlag(undefined) // returns DEFAULT_AGENT_TOOLS
|
|
24
|
+
* parseToolsFlag('') // returns DEFAULT_AGENT_TOOLS
|
|
25
|
+
* parseToolsFlag('disabled') // returns []
|
|
26
|
+
* parseToolsFlag('web_search,think_tool') // returns ['web_search', 'think_tool']
|
|
27
|
+
*/
|
|
28
|
+
export function parseToolsFlag(value) {
|
|
29
|
+
// No flag or empty string: use defaults
|
|
30
|
+
if (value === undefined || value.trim() === '') {
|
|
31
|
+
return [...defaultAgentTools];
|
|
32
|
+
}
|
|
33
|
+
// Explicit disable
|
|
34
|
+
if (value.toLowerCase() === 'disabled') {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
// Parse comma-separated list
|
|
38
|
+
return value
|
|
39
|
+
.split(',')
|
|
40
|
+
.map(t => t.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
package/dist/types/output.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -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
|
package/dist/types/stream.js
CHANGED
|
@@ -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.
|
|
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>",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dev": "tsc --watch",
|
|
48
48
|
"build": "tsc",
|
|
49
49
|
"build:clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npm run build",
|
|
50
|
-
"format": "prettier --write \"**/*.{ts,tsx,json,md}\"
|
|
50
|
+
"format": "xo --fix && prettier --write \"**/*.{ts,tsx,json,md}\"",
|
|
51
51
|
"format:check": "prettier --check .",
|
|
52
52
|
"lint": "xo",
|
|
53
53
|
"test": "npm run lint && npm run build && ava",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"ts-node": "^10.9.1",
|
|
88
88
|
"tsx": "^4.21.0",
|
|
89
89
|
"typescript": "^5.0.3",
|
|
90
|
-
"xo": "^0.
|
|
90
|
+
"xo": "^0.60.0"
|
|
91
91
|
},
|
|
92
92
|
"ava": {
|
|
93
93
|
"files": [
|
|
@@ -103,7 +103,16 @@
|
|
|
103
103
|
"rules": {
|
|
104
104
|
"react/prop-types": "off",
|
|
105
105
|
"unicorn/expiring-todo-comments": "off",
|
|
106
|
-
"ava/no-ignored-test-files": "off"
|
|
106
|
+
"ava/no-ignored-test-files": "off",
|
|
107
|
+
"unicorn/prevent-abbreviations": "off",
|
|
108
|
+
"@typescript-eslint/switch-exhaustiveness-check": "off",
|
|
109
|
+
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
110
|
+
"@typescript-eslint/no-unsafe-call": "off",
|
|
111
|
+
"@typescript-eslint/restrict-plus-operands": "off",
|
|
112
|
+
"promise/prefer-await-to-then": "off",
|
|
113
|
+
"complexity": "off",
|
|
114
|
+
"unicorn/prefer-at": "off",
|
|
115
|
+
"prettier/prettier": "off"
|
|
107
116
|
}
|
|
108
117
|
},
|
|
109
118
|
"prettier": "@vdemedes/prettier-config"
|