@chaoslabs/ai-sdk 0.0.2 → 0.0.4

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,230 @@
1
+ // Chaos AI SDK - Conversation Management
2
+ //
3
+ // A class for managing multi-turn conversations with history tracking.
4
+ import { extractText } from './types.js';
5
+ import { WALLET_MODEL } from './request.js';
6
+ /**
7
+ * Manages a multi-turn conversation with history tracking.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Chaos, Conversation } from '@chaoslabs/ai-sdk';
12
+ *
13
+ * const chaos = new Chaos({ apiKey: 'ck-...' });
14
+ * const conversation = new Conversation(chaos, {
15
+ * userId: 'user-123',
16
+ * walletId: '0x...',
17
+ * });
18
+ *
19
+ * // Send messages - history is tracked automatically
20
+ * const response1 = await conversation.send("What's my portfolio value?");
21
+ * const response2 = await conversation.send("Which asset has the highest allocation?");
22
+ *
23
+ * // Get conversation stats
24
+ * console.log(conversation.stats);
25
+ *
26
+ * // Reset for a new conversation
27
+ * conversation.reset();
28
+ * ```
29
+ */
30
+ export class Conversation {
31
+ client;
32
+ model;
33
+ maxHistoryLength;
34
+ history = [];
35
+ metadata;
36
+ userTurns = 0;
37
+ assistantTurns = 0;
38
+ startedAt;
39
+ lastMessageAt = null;
40
+ constructor(client, options) {
41
+ this.client = client;
42
+ this.model = options.model || WALLET_MODEL;
43
+ this.maxHistoryLength = options.maxHistoryLength || 50;
44
+ this.startedAt = new Date();
45
+ this.metadata = {
46
+ user_id: options.userId,
47
+ wallet_id: options.walletId,
48
+ session_id: options.sessionId || `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
49
+ };
50
+ }
51
+ /**
52
+ * Get the current session ID.
53
+ */
54
+ get sessionId() {
55
+ return this.metadata.session_id;
56
+ }
57
+ /**
58
+ * Get the conversation history.
59
+ */
60
+ get messages() {
61
+ return [...this.history];
62
+ }
63
+ /**
64
+ * Get conversation statistics.
65
+ */
66
+ get stats() {
67
+ return {
68
+ userTurns: this.userTurns,
69
+ assistantTurns: this.assistantTurns,
70
+ totalMessages: this.history.length,
71
+ sessionId: this.metadata.session_id,
72
+ startedAt: this.startedAt,
73
+ lastMessageAt: this.lastMessageAt,
74
+ };
75
+ }
76
+ /**
77
+ * Send a message and get a response.
78
+ * Automatically manages conversation history.
79
+ */
80
+ async send(message) {
81
+ // Add user message to history
82
+ this.addUserMessage(message);
83
+ // Send request with full history
84
+ const response = await this.client.chat.responses.create({
85
+ model: this.model,
86
+ input: [...this.history],
87
+ metadata: this.metadata,
88
+ });
89
+ // Add assistant response to history if successful
90
+ if (response.status === 'completed') {
91
+ const text = extractText(response);
92
+ if (text) {
93
+ this.addAssistantMessage(text);
94
+ }
95
+ }
96
+ this.lastMessageAt = new Date();
97
+ return response;
98
+ }
99
+ /**
100
+ * Add a user message to the history without sending.
101
+ * Useful for restoring conversation state.
102
+ */
103
+ addUserMessage(content) {
104
+ this.history.push({
105
+ type: 'message',
106
+ role: 'user',
107
+ content,
108
+ });
109
+ this.userTurns++;
110
+ this.trimHistory();
111
+ }
112
+ /**
113
+ * Add an assistant message to the history.
114
+ * Useful for restoring conversation state.
115
+ */
116
+ addAssistantMessage(content) {
117
+ // Store assistant responses as user messages with prefix for context
118
+ // This matches the pattern used in the multi-turn example
119
+ this.history.push({
120
+ type: 'message',
121
+ role: 'user',
122
+ content: `[Assistant]: ${content}`,
123
+ });
124
+ this.assistantTurns++;
125
+ this.trimHistory();
126
+ }
127
+ /**
128
+ * Add a system message to the history.
129
+ */
130
+ addSystemMessage(content) {
131
+ this.history.push({
132
+ type: 'message',
133
+ role: 'system',
134
+ content,
135
+ });
136
+ this.trimHistory();
137
+ }
138
+ /**
139
+ * Reset the conversation to start fresh.
140
+ * Generates a new session ID and clears history.
141
+ */
142
+ reset() {
143
+ this.history = [];
144
+ this.userTurns = 0;
145
+ this.assistantTurns = 0;
146
+ this.startedAt = new Date();
147
+ this.lastMessageAt = null;
148
+ this.metadata.session_id = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
149
+ }
150
+ /**
151
+ * Clear the history but keep the same session ID.
152
+ */
153
+ clearHistory() {
154
+ this.history = [];
155
+ this.userTurns = 0;
156
+ this.assistantTurns = 0;
157
+ }
158
+ /**
159
+ * Fork this conversation to create a branch.
160
+ * The new conversation has the same history but a new session ID.
161
+ */
162
+ fork() {
163
+ const forked = new Conversation(this.client, {
164
+ model: this.model,
165
+ maxHistoryLength: this.maxHistoryLength,
166
+ userId: this.metadata.user_id,
167
+ walletId: this.metadata.wallet_id,
168
+ });
169
+ // Copy history
170
+ for (const msg of this.history) {
171
+ if (msg.role === 'user') {
172
+ if (msg.content.startsWith('[Assistant]: ')) {
173
+ forked.history.push(msg);
174
+ forked.assistantTurns++;
175
+ }
176
+ else {
177
+ forked.history.push(msg);
178
+ forked.userTurns++;
179
+ }
180
+ }
181
+ else {
182
+ forked.history.push(msg);
183
+ }
184
+ }
185
+ return forked;
186
+ }
187
+ /**
188
+ * Serialize the conversation to JSON for persistence.
189
+ */
190
+ toJSON() {
191
+ return {
192
+ sessionId: this.metadata.session_id,
193
+ history: [...this.history],
194
+ metadata: { ...this.metadata },
195
+ stats: this.stats,
196
+ };
197
+ }
198
+ /**
199
+ * Restore conversation state from JSON.
200
+ */
201
+ static fromJSON(client, data) {
202
+ const conversation = new Conversation(client, {
203
+ userId: data.metadata.user_id,
204
+ walletId: data.metadata.wallet_id,
205
+ sessionId: data.sessionId,
206
+ });
207
+ // Restore history
208
+ for (const msg of data.history) {
209
+ conversation.history.push(msg);
210
+ if (msg.role === 'user') {
211
+ if (msg.content.startsWith('[Assistant]: ')) {
212
+ conversation.assistantTurns++;
213
+ }
214
+ else {
215
+ conversation.userTurns++;
216
+ }
217
+ }
218
+ }
219
+ return conversation;
220
+ }
221
+ /**
222
+ * Trim history to stay within maxHistoryLength.
223
+ */
224
+ trimHistory() {
225
+ if (this.history.length > this.maxHistoryLength) {
226
+ const excess = this.history.length - this.maxHistoryLength;
227
+ this.history = this.history.slice(excess);
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,55 @@
1
+ export interface HttpStreamOptions {
2
+ url: string;
3
+ method: string;
4
+ headers: Record<string, string>;
5
+ body: string;
6
+ timeout: number;
7
+ }
8
+ export interface StreamingHttpClientOptions {
9
+ baseUrl: string;
10
+ timeout: number;
11
+ }
12
+ export interface StreamRequestOptions {
13
+ method: string;
14
+ headers?: Record<string, string>;
15
+ body: string;
16
+ }
17
+ /**
18
+ * Makes an HTTP request using Node's native http/https modules and returns
19
+ * an async iterator that yields chunks as they arrive (true streaming).
20
+ *
21
+ * @param options - Request options
22
+ * @returns AsyncIterable that yields string chunks
23
+ */
24
+ export declare function httpStreamRequest(options: HttpStreamOptions): AsyncIterable<string>;
25
+ /**
26
+ * High-level HTTP streaming client that provides methods for streaming
27
+ * requests with proper chunk and line parsing.
28
+ */
29
+ export declare class StreamingHttpClient {
30
+ private baseUrl;
31
+ private timeout;
32
+ private aborted;
33
+ private currentIterator;
34
+ constructor(options: StreamingHttpClientOptions);
35
+ /**
36
+ * Stream raw chunks from a request.
37
+ *
38
+ * @param path - URL path (will be appended to baseUrl)
39
+ * @param options - Request options
40
+ * @returns AsyncIterable of raw string chunks
41
+ */
42
+ stream(path: string, options: StreamRequestOptions): AsyncIterable<string>;
43
+ /**
44
+ * Stream parsed NDJSON lines from a request.
45
+ *
46
+ * @param path - URL path (will be appended to baseUrl)
47
+ * @param options - Request options
48
+ * @returns AsyncIterable of complete JSON lines (without newline)
49
+ */
50
+ streamLines(path: string, options: StreamRequestOptions): AsyncIterable<string>;
51
+ /**
52
+ * Abort the current in-progress request.
53
+ */
54
+ abort(): void;
55
+ }
@@ -0,0 +1,359 @@
1
+ // HTTP Streaming Module
2
+ //
3
+ // This module provides low-level HTTP streaming using Node's native http/https modules.
4
+ // It yields data incrementally as it arrives, unlike fetch which buffers the response.
5
+ import * as http from 'node:http';
6
+ import * as https from 'node:https';
7
+ import { ChaosError, ChaosTimeoutError } from './types.js';
8
+ // ============================================================================
9
+ // httpStreamRequest - Low-level streaming function
10
+ // ============================================================================
11
+ /**
12
+ * Makes an HTTP request using Node's native http/https modules and returns
13
+ * an async iterator that yields chunks as they arrive (true streaming).
14
+ *
15
+ * @param options - Request options
16
+ * @returns AsyncIterable that yields string chunks
17
+ */
18
+ export function httpStreamRequest(options) {
19
+ const { url, method, headers, body, timeout } = options;
20
+ return {
21
+ [Symbol.asyncIterator]() {
22
+ const parsedUrl = new URL(url);
23
+ const isHttps = parsedUrl.protocol === 'https:';
24
+ const httpModule = isHttps ? https : http;
25
+ let request = null;
26
+ let response = null;
27
+ let destroyed = false;
28
+ let pendingChunks = [];
29
+ let resolveNext = null;
30
+ let rejectNext = null;
31
+ let ended = false;
32
+ let error = null;
33
+ let requestStarted = false;
34
+ let requestPromise = null;
35
+ // Store timeout reference so we can restart it
36
+ let timeoutId = null;
37
+ // Store the startRequest reject function so timeout can use it
38
+ let startRequestReject = null;
39
+ const cleanup = () => {
40
+ if (timeoutId) {
41
+ clearTimeout(timeoutId);
42
+ timeoutId = null;
43
+ }
44
+ if (request && !destroyed) {
45
+ destroyed = true;
46
+ request.destroy();
47
+ }
48
+ if (response) {
49
+ response.removeAllListeners();
50
+ }
51
+ };
52
+ const resetTimeout = () => {
53
+ if (timeoutId) {
54
+ clearTimeout(timeoutId);
55
+ }
56
+ if (!ended && !destroyed && !error) {
57
+ timeoutId = setTimeout(() => {
58
+ const err = new ChaosTimeoutError(timeout);
59
+ // Set error BEFORE cleanup so that handleEnd doesn't resolve
60
+ error = err;
61
+ // Reject the appropriate promise
62
+ if (rejectNext) {
63
+ // We're waiting for data during iteration
64
+ const reject = rejectNext;
65
+ rejectNext = null;
66
+ resolveNext = null;
67
+ reject(err);
68
+ }
69
+ else if (startRequestReject) {
70
+ // We're still waiting for the response headers
71
+ const reject = startRequestReject;
72
+ startRequestReject = null;
73
+ reject(err);
74
+ }
75
+ cleanup();
76
+ }, timeout);
77
+ }
78
+ };
79
+ const handleError = (err) => {
80
+ if (error)
81
+ return; // Already have an error
82
+ error = err;
83
+ cleanup();
84
+ if (rejectNext) {
85
+ rejectNext(err);
86
+ rejectNext = null;
87
+ resolveNext = null;
88
+ }
89
+ };
90
+ const handleData = (chunk) => {
91
+ // Reset timeout on each data chunk
92
+ resetTimeout();
93
+ const str = chunk.toString('utf8');
94
+ if (resolveNext) {
95
+ resolveNext({ value: str, done: false });
96
+ resolveNext = null;
97
+ rejectNext = null;
98
+ }
99
+ else {
100
+ pendingChunks.push(str);
101
+ }
102
+ };
103
+ const handleEnd = () => {
104
+ ended = true;
105
+ // Don't cleanup yet - let the caller handle it
106
+ if (timeoutId) {
107
+ clearTimeout(timeoutId);
108
+ timeoutId = null;
109
+ }
110
+ if (resolveNext && !error) {
111
+ resolveNext({ value: undefined, done: true });
112
+ resolveNext = null;
113
+ rejectNext = null;
114
+ }
115
+ };
116
+ // Start the request
117
+ const requestOptions = {
118
+ hostname: parsedUrl.hostname,
119
+ port: parsedUrl.port || (isHttps ? 443 : 80),
120
+ path: parsedUrl.pathname + parsedUrl.search,
121
+ method,
122
+ headers: {
123
+ ...headers,
124
+ 'Content-Length': Buffer.byteLength(body, 'utf8').toString(),
125
+ },
126
+ };
127
+ const startRequest = () => {
128
+ return new Promise((resolve, reject) => {
129
+ // Store reject so timeout can use it
130
+ startRequestReject = reject;
131
+ // Start initial timeout
132
+ resetTimeout();
133
+ request = httpModule.request(requestOptions, (res) => {
134
+ response = res;
135
+ // Reset timeout when we get response
136
+ resetTimeout();
137
+ // Check for HTTP errors
138
+ if (res.statusCode && res.statusCode >= 400) {
139
+ let errorBody = '';
140
+ res.on('data', (chunk) => {
141
+ errorBody += chunk.toString();
142
+ });
143
+ res.on('end', () => {
144
+ const err = new ChaosError(`HTTP error: ${res.statusCode} ${res.statusMessage || ''}`, res.statusCode);
145
+ handleError(err);
146
+ reject(err);
147
+ });
148
+ res.on('error', () => {
149
+ // Ignore errors during error body collection
150
+ });
151
+ return;
152
+ }
153
+ // Set up data handlers
154
+ res.on('data', handleData);
155
+ res.on('end', handleEnd);
156
+ res.on('error', (err) => {
157
+ handleError(new ChaosError(`Stream error: ${err.message}`));
158
+ });
159
+ res.on('close', () => {
160
+ // If closed without ending properly, it's an error
161
+ if (!ended && !error) {
162
+ handleError(new ChaosError('Connection closed unexpectedly'));
163
+ }
164
+ });
165
+ // Clear startRequestReject since we've resolved
166
+ startRequestReject = null;
167
+ resolve();
168
+ });
169
+ request.on('error', (err) => {
170
+ const chaosErr = new ChaosError(`Connection error: ${err.message}`);
171
+ handleError(chaosErr);
172
+ reject(chaosErr);
173
+ });
174
+ request.on('timeout', () => {
175
+ const err = new ChaosTimeoutError(timeout);
176
+ handleError(err);
177
+ reject(err);
178
+ });
179
+ // Send the request body
180
+ request.write(body);
181
+ request.end();
182
+ });
183
+ };
184
+ return {
185
+ async next() {
186
+ // Start request on first next() call
187
+ if (!requestStarted) {
188
+ requestStarted = true;
189
+ requestPromise = startRequest();
190
+ try {
191
+ await requestPromise;
192
+ }
193
+ catch (err) {
194
+ throw err;
195
+ }
196
+ }
197
+ // Check for error
198
+ if (error) {
199
+ throw error;
200
+ }
201
+ // Check for pending chunks
202
+ if (pendingChunks.length > 0) {
203
+ return { value: pendingChunks.shift(), done: false };
204
+ }
205
+ // Check if ended
206
+ if (ended) {
207
+ return { value: undefined, done: true };
208
+ }
209
+ // Wait for next chunk (timeout will fire if nothing comes)
210
+ return new Promise((resolve, reject) => {
211
+ resolveNext = resolve;
212
+ rejectNext = reject;
213
+ });
214
+ },
215
+ async return() {
216
+ cleanup();
217
+ return { value: undefined, done: true };
218
+ },
219
+ async throw(err) {
220
+ cleanup();
221
+ throw err;
222
+ },
223
+ };
224
+ },
225
+ };
226
+ }
227
+ // ============================================================================
228
+ // StreamingHttpClient - High-level streaming client
229
+ // ============================================================================
230
+ /**
231
+ * High-level HTTP streaming client that provides methods for streaming
232
+ * requests with proper chunk and line parsing.
233
+ */
234
+ export class StreamingHttpClient {
235
+ baseUrl;
236
+ timeout;
237
+ aborted = false;
238
+ currentIterator = null;
239
+ constructor(options) {
240
+ this.baseUrl = options.baseUrl;
241
+ this.timeout = options.timeout;
242
+ }
243
+ /**
244
+ * Stream raw chunks from a request.
245
+ *
246
+ * @param path - URL path (will be appended to baseUrl)
247
+ * @param options - Request options
248
+ * @returns AsyncIterable of raw string chunks
249
+ */
250
+ stream(path, options) {
251
+ const fullUrl = this.baseUrl + path;
252
+ this.aborted = false;
253
+ const self = this;
254
+ const innerIterable = httpStreamRequest({
255
+ url: fullUrl,
256
+ method: options.method,
257
+ headers: options.headers || {},
258
+ body: options.body,
259
+ timeout: this.timeout,
260
+ });
261
+ return {
262
+ [Symbol.asyncIterator]() {
263
+ const iterator = innerIterable[Symbol.asyncIterator]();
264
+ self.currentIterator = iterator;
265
+ return {
266
+ async next() {
267
+ if (self.aborted) {
268
+ // Call return to cleanup the underlying iterator
269
+ await iterator.return?.();
270
+ throw new ChaosError('Request aborted');
271
+ }
272
+ try {
273
+ const result = await iterator.next();
274
+ if (self.aborted) {
275
+ await iterator.return?.();
276
+ throw new ChaosError('Request aborted');
277
+ }
278
+ return result;
279
+ }
280
+ catch (err) {
281
+ if (self.aborted) {
282
+ throw new ChaosError('Request aborted');
283
+ }
284
+ throw err;
285
+ }
286
+ },
287
+ async return() {
288
+ self.currentIterator = null;
289
+ return iterator.return?.() || { value: undefined, done: true };
290
+ },
291
+ async throw(err) {
292
+ self.currentIterator = null;
293
+ return iterator.throw?.(err) || Promise.reject(err);
294
+ },
295
+ };
296
+ },
297
+ };
298
+ }
299
+ /**
300
+ * Stream parsed NDJSON lines from a request.
301
+ *
302
+ * @param path - URL path (will be appended to baseUrl)
303
+ * @param options - Request options
304
+ * @returns AsyncIterable of complete JSON lines (without newline)
305
+ */
306
+ streamLines(path, options) {
307
+ const rawStream = this.stream(path, options);
308
+ return {
309
+ [Symbol.asyncIterator]() {
310
+ const rawIterator = rawStream[Symbol.asyncIterator]();
311
+ let buffer = '';
312
+ return {
313
+ async next() {
314
+ while (true) {
315
+ // Check for complete lines in buffer
316
+ const newlineIndex = buffer.indexOf('\n');
317
+ if (newlineIndex !== -1) {
318
+ const line = buffer.substring(0, newlineIndex);
319
+ buffer = buffer.substring(newlineIndex + 1);
320
+ if (line.trim()) {
321
+ return { value: line, done: false };
322
+ }
323
+ continue;
324
+ }
325
+ // Get more data
326
+ const result = await rawIterator.next();
327
+ if (result.done) {
328
+ // Handle remaining buffer
329
+ if (buffer.trim()) {
330
+ const remaining = buffer.trim();
331
+ buffer = '';
332
+ return { value: remaining, done: false };
333
+ }
334
+ return { value: undefined, done: true };
335
+ }
336
+ buffer += result.value;
337
+ }
338
+ },
339
+ async return() {
340
+ return rawIterator.return?.() || { value: undefined, done: true };
341
+ },
342
+ async throw(err) {
343
+ return rawIterator.throw?.(err) || Promise.reject(err);
344
+ },
345
+ };
346
+ },
347
+ };
348
+ }
349
+ /**
350
+ * Abort the current in-progress request.
351
+ */
352
+ abort() {
353
+ this.aborted = true;
354
+ if (this.currentIterator) {
355
+ this.currentIterator.return?.();
356
+ this.currentIterator = null;
357
+ }
358
+ }
359
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { Chaos as default, Chaos, WALLET_MODEL, ASK_MODEL } from './client.js';
2
+ export { httpStreamRequest, StreamingHttpClient } from './http-streaming.js';
3
+ export type { HttpStreamOptions, StreamingHttpClientOptions, StreamRequestOptions } from './http-streaming.js';
2
4
  export type { V1WalletRequest, V1AskRequest } from './request.js';
3
5
  export type { V1StreamEvent, V1FinalState } from './response.js';
4
6
  export type { ChaosConfig, CreateResponseParams, RequestMetadata, InputItem, Response, OutputItem, ContentPart, Block, OutputText, ChaosBlock, MarkdownBlock, TableBlock, TableColumnType, TableSortDirection, TableColumnConfig, ChartBlock, ChartSeries, ChartSegment, ChartAxis, ChartDataPoint, TransactionActionBlock, Primitive, PrimitiveIcon, PrimitiveLineItem, PrimitiveDisplay, RawTransaction, TransactionGroup, Risks, RiskInfoItem, InteractiveBlock, InteractiveOption, ResponseError, } from './types.js';
@@ -6,3 +8,5 @@ export { ChaosError, ChaosTimeoutError } from './types.js';
6
8
  export { extractText, extractBlocks, hasRisks, hasBlockers, isTableBlock, isChartBlock, isTransactionActionBlock, isInteractiveBlock, isMarkdownBlock, isOutputText, isChaosBlock, } from './types.js';
7
9
  export { BlockSchema, parseRawBlock, detectBlockType } from './schemas.js';
8
10
  export type { BlockParsed } from './schemas.js';
11
+ export { isAgentStatusMessage, isAgentMessage, isReportMessage, isFollowUpSuggestions, isUserInputMessage, parseAgentStatus, isTerminalStatus, extractAgentMessageText, extractSuggestions, extractReportBlock, parseStreamLine, parseStreamLines, } from './stream.js';
12
+ export type { MessageType, AgentStatus, StreamMessage, StreamMessageContext, } from './stream.js';
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  // Chaos AI SDK V1 - Public API
2
2
  export { Chaos as default, Chaos, WALLET_MODEL, ASK_MODEL } from './client.js';
3
+ export { httpStreamRequest, StreamingHttpClient } from './http-streaming.js';
3
4
  // Export error classes
4
5
  export { ChaosError, ChaosTimeoutError } from './types.js';
5
6
  // Export helper functions
6
7
  export { extractText, extractBlocks, hasRisks, hasBlockers, isTableBlock, isChartBlock, isTransactionActionBlock, isInteractiveBlock, isMarkdownBlock, isOutputText, isChaosBlock, } from './types.js';
7
8
  // Export schemas for validation
8
9
  export { BlockSchema, parseRawBlock, detectBlockType } from './schemas.js';
10
+ // Export stream message utilities
11
+ export { isAgentStatusMessage, isAgentMessage, isReportMessage, isFollowUpSuggestions, isUserInputMessage, parseAgentStatus, isTerminalStatus, extractAgentMessageText, extractSuggestions, extractReportBlock, parseStreamLine, parseStreamLines, } from './stream.js';