@octavus/client-sdk 0.2.0 → 2.0.0

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 ADDED
@@ -0,0 +1,196 @@
1
+ # @octavus/client-sdk
2
+
3
+ Framework-agnostic client SDK for Octavus agents.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @octavus/client-sdk
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ This package provides a framework-agnostic client for streaming Octavus agent responses. It handles message state management, streaming events, and transport abstraction.
14
+
15
+ For React applications, use [`@octavus/react`](https://www.npmjs.com/package/@octavus/react) instead—it provides React hooks that wrap this SDK.
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { OctavusChat, createHttpTransport } from '@octavus/client-sdk';
21
+
22
+ // Create transport
23
+ const transport = createHttpTransport({
24
+ request: (payload, options) =>
25
+ fetch('/api/trigger', {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ sessionId, ...payload }),
29
+ signal: options?.signal,
30
+ }),
31
+ });
32
+
33
+ // Create chat client
34
+ const chat = new OctavusChat({ transport });
35
+
36
+ // Subscribe to state changes
37
+ const unsubscribe = chat.subscribe(() => {
38
+ console.log('Messages:', chat.messages);
39
+ console.log('Status:', chat.status);
40
+ });
41
+
42
+ // Send a message
43
+ await chat.send('user-message', { USER_MESSAGE: 'Hello!' }, { userMessage: { content: 'Hello!' } });
44
+
45
+ // Cleanup
46
+ unsubscribe();
47
+ ```
48
+
49
+ ## Transports
50
+
51
+ ### HTTP Transport (SSE)
52
+
53
+ Best for Next.js, Express, and HTTP-based applications:
54
+
55
+ ```typescript
56
+ import { createHttpTransport } from '@octavus/client-sdk';
57
+
58
+ const transport = createHttpTransport({
59
+ request: (payload, options) =>
60
+ fetch('/api/trigger', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ sessionId, ...payload }),
64
+ signal: options?.signal,
65
+ }),
66
+ });
67
+ ```
68
+
69
+ ### Socket Transport (WebSocket/SockJS)
70
+
71
+ Best for real-time applications with persistent connections:
72
+
73
+ ```typescript
74
+ import { createSocketTransport } from '@octavus/client-sdk';
75
+
76
+ const transport = createSocketTransport({
77
+ connect: () =>
78
+ new Promise((resolve, reject) => {
79
+ const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);
80
+ ws.onopen = () => resolve(ws);
81
+ ws.onerror = () => reject(new Error('Connection failed'));
82
+ }),
83
+ });
84
+
85
+ // Optional: eagerly connect and monitor state
86
+ transport.onConnectionStateChange((state, error) => {
87
+ console.log('Connection state:', state);
88
+ });
89
+ await transport.connect();
90
+ ```
91
+
92
+ ## Chat Client
93
+
94
+ ### Creating a Chat Instance
95
+
96
+ ```typescript
97
+ const chat = new OctavusChat({
98
+ transport,
99
+ initialMessages: [], // Optional: restore from server
100
+ onError: (error) => console.error(error),
101
+ onFinish: () => console.log('Done'),
102
+ onResourceUpdate: (name, value) => console.log(`Resource ${name}:`, value),
103
+ });
104
+ ```
105
+
106
+ ### Sending Messages
107
+
108
+ ```typescript
109
+ // Text message
110
+ await chat.send('user-message', { USER_MESSAGE: message }, { userMessage: { content: message } });
111
+
112
+ // With file attachments
113
+ await chat.send(
114
+ 'user-message',
115
+ { USER_MESSAGE: message, FILES: fileRefs },
116
+ { userMessage: { content: message, files: fileRefs } },
117
+ );
118
+ ```
119
+
120
+ ### State Properties
121
+
122
+ ```typescript
123
+ chat.messages; // UIMessage[] - all messages
124
+ chat.status; // 'idle' | 'streaming' | 'error'
125
+ chat.error; // OctavusError | null
126
+ ```
127
+
128
+ ### Stopping Generation
129
+
130
+ ```typescript
131
+ chat.stop(); // Stops streaming and finalizes partial content
132
+ ```
133
+
134
+ ## File Uploads
135
+
136
+ ```typescript
137
+ // Upload files separately (for progress tracking)
138
+ const fileRefs = await chat.uploadFiles(fileInput.files, (index, progress) => {
139
+ console.log(`File ${index}: ${progress}%`);
140
+ });
141
+
142
+ // Use the references in a message
143
+ await chat.send('user-message', { FILES: fileRefs }, { userMessage: { files: fileRefs } });
144
+ ```
145
+
146
+ Note: File uploads require configuring `requestUploadUrls` in the chat options.
147
+
148
+ ## Message Types
149
+
150
+ Messages contain ordered `parts` with typed content:
151
+
152
+ ```typescript
153
+ type UIMessagePart =
154
+ | UITextPart // Text content with streaming status
155
+ | UIReasoningPart // Model reasoning/thinking content
156
+ | UIToolCallPart // Tool call with args, result, and status
157
+ | UIOperationPart // Internal operations (e.g., set-resource)
158
+ | UISourcePart // URL or document sources
159
+ | UIFilePart // File attachments (uploaded or generated)
160
+ | UIObjectPart; // Structured output objects
161
+ ```
162
+
163
+ Each part includes a `type` discriminator and relevant fields. See TypeScript types for full field definitions.
164
+
165
+ ## Error Handling
166
+
167
+ Errors are structured with type classification:
168
+
169
+ ```typescript
170
+ import { isRateLimitError, isAuthenticationError } from '@octavus/client-sdk';
171
+
172
+ const chat = new OctavusChat({
173
+ transport,
174
+ onError: (error) => {
175
+ if (isRateLimitError(error)) {
176
+ showRetryButton(error.retryAfter);
177
+ } else if (isAuthenticationError(error)) {
178
+ redirectToLogin();
179
+ }
180
+ },
181
+ });
182
+ ```
183
+
184
+ ## Re-exports
185
+
186
+ This package re-exports everything from `@octavus/core`, so you don't need to install it separately.
187
+
188
+ ## Related Packages
189
+
190
+ - [`@octavus/react`](https://www.npmjs.com/package/@octavus/react) - React hooks and bindings
191
+ - [`@octavus/server-sdk`](https://www.npmjs.com/package/@octavus/server-sdk) - Server-side SDK
192
+ - [`@octavus/core`](https://www.npmjs.com/package/@octavus/core) - Shared types
193
+
194
+ ## License
195
+
196
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { StreamEvent, FileReference, UIMessage } from '@octavus/core';
1
+ import { StreamEvent, ToolResult, FileReference, UIMessage, OctavusError } from '@octavus/core';
2
2
  export * from '@octavus/core';
3
- export { isFileReference, isFileReferenceArray, isOtherThread } from '@octavus/core';
3
+ export { AppError, ConflictError, ForbiddenError, MAIN_THREAD, NotFoundError, OCTAVUS_SKILL_TOOLS, OctavusError, ValidationError, createApiErrorEvent, createErrorEvent, createInternalErrorEvent, errorToStreamEvent, generateId, getSkillSlugFromToolCall, isAbortError, isAuthenticationError, isFileReference, isFileReferenceArray, isMainThread, isOctavusSkillTool, isOtherThread, isProviderError, isRateLimitError, isRetryableError, isToolError, isValidationError, resolveThread, safeParseStreamEvent, safeParseUIMessage, safeParseUIMessages, threadForPart } from '@octavus/core';
4
4
 
5
5
  /**
6
6
  * Transport interface for delivering events from server to client.
@@ -16,6 +16,13 @@ interface Transport {
16
16
  * @param input - Input parameters for variable substitution
17
17
  */
18
18
  trigger(triggerName: string, input?: Record<string, unknown>): AsyncIterable<StreamEvent>;
19
+ /**
20
+ * Continue execution with tool results after client-side tool handling.
21
+ *
22
+ * @param executionId - The execution ID from the client-tool-request event
23
+ * @param results - All tool results (server + client) to send
24
+ */
25
+ continueWithToolResults(executionId: string, results: ToolResult[]): AsyncIterable<StreamEvent>;
19
26
  /** Stop the current stream. Safe to call when no stream is active. */
20
27
  stop(): void;
21
28
  }
@@ -191,7 +198,51 @@ interface UploadFilesOptions {
191
198
  */
192
199
  declare function uploadFiles(files: FileList | File[], options: UploadFilesOptions): Promise<FileReference[]>;
193
200
 
194
- type ChatStatus = 'idle' | 'streaming' | 'error';
201
+ type ChatStatus = 'idle' | 'streaming' | 'error' | 'awaiting-input';
202
+ /**
203
+ * Context provided to client tool handlers.
204
+ */
205
+ interface ClientToolContext {
206
+ /** Unique identifier for this tool call */
207
+ toolCallId: string;
208
+ /** Name of the tool being called */
209
+ toolName: string;
210
+ /** Signal for cancellation if user stops generation */
211
+ signal: AbortSignal;
212
+ }
213
+ /**
214
+ * Handler function for client-side tool execution.
215
+ * Can be:
216
+ * - An async function that executes automatically and returns a result
217
+ * - The string 'interactive' to indicate the tool requires user interaction
218
+ */
219
+ type ClientToolHandler = ((args: Record<string, unknown>, ctx: ClientToolContext) => Promise<unknown>) | 'interactive';
220
+ /**
221
+ * Interactive tool call awaiting user interaction.
222
+ * The `submit` and `cancel` methods are pre-bound to this tool call's ID.
223
+ */
224
+ interface InteractiveTool {
225
+ /** Unique identifier for this tool call */
226
+ toolCallId: string;
227
+ /** Name of the tool being called */
228
+ toolName: string;
229
+ /** Arguments passed to the tool */
230
+ args: Record<string, unknown>;
231
+ /**
232
+ * Submit a result for this tool call.
233
+ * Call this when the user has provided input.
234
+ *
235
+ * @param result - The result from user interaction
236
+ */
237
+ submit: (result: unknown) => void;
238
+ /**
239
+ * Cancel this tool call with an optional reason.
240
+ * Call this when the user dismisses the UI without providing input.
241
+ *
242
+ * @param reason - Optional reason for cancellation (default: 'User cancelled')
243
+ */
244
+ cancel: (reason?: string) => void;
245
+ }
195
246
  /**
196
247
  * Input for creating a user message.
197
248
  * Supports text content, structured object content, and file attachments.
@@ -234,10 +285,59 @@ interface OctavusChatOptions {
234
285
  * ```
235
286
  */
236
287
  requestUploadUrls?: UploadFilesOptions['requestUploadUrls'];
288
+ /**
289
+ * Client-side tool handlers.
290
+ * Register handlers for tools that should execute in the browser.
291
+ *
292
+ * - If a tool has a handler function: executes automatically
293
+ * - If a tool is marked as 'interactive': appears in `pendingClientTools` with bound `submit()`/`cancel()`
294
+ *
295
+ * @example Automatic client tool
296
+ * ```typescript
297
+ * clientTools: {
298
+ * 'get-browser-location': async () => {
299
+ * const pos = await new Promise((resolve, reject) => {
300
+ * navigator.geolocation.getCurrentPosition(resolve, reject);
301
+ * });
302
+ * return { lat: pos.coords.latitude, lng: pos.coords.longitude };
303
+ * },
304
+ * }
305
+ * ```
306
+ *
307
+ * @example Interactive client tool (user input required)
308
+ * ```typescript
309
+ * clientTools: {
310
+ * 'request-feedback': 'interactive',
311
+ * }
312
+ * // Then render UI based on pendingClientTools['request-feedback']
313
+ * // and call tool.submit(result) or tool.cancel()
314
+ * ```
315
+ */
316
+ clientTools?: Record<string, ClientToolHandler>;
237
317
  /** Initial messages (for session refresh) */
238
318
  initialMessages?: UIMessage[];
239
- /** Callback when an error occurs */
240
- onError?: (error: Error) => void;
319
+ /**
320
+ * Callback when an error occurs.
321
+ * Receives an OctavusError with structured error information.
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * onError: (error) => {
326
+ * console.error('Chat error:', {
327
+ * type: error.errorType,
328
+ * message: error.message,
329
+ * retryable: error.retryable,
330
+ * provider: error.provider,
331
+ * });
332
+ *
333
+ * // Handle specific error types
334
+ * if (isRateLimitError(error)) {
335
+ * showRetryButton(error.retryAfter);
336
+ * }
337
+ * }
338
+ * ```
339
+ */
340
+ onError?: (error: OctavusError) => void;
241
341
  /** Callback when streaming finishes successfully */
242
342
  onFinish?: () => void;
243
343
  /** Callback when streaming is stopped by user */
@@ -256,10 +356,12 @@ type Listener = () => void;
256
356
  *
257
357
  * const chat = new OctavusChat({
258
358
  * transport: createHttpTransport({
259
- * triggerRequest: (triggerName, input) =>
359
+ * request: (payload, options) =>
260
360
  * fetch('/api/trigger', {
261
361
  * method: 'POST',
262
- * body: JSON.stringify({ sessionId, triggerName, input }),
362
+ * headers: { 'Content-Type': 'application/json' },
363
+ * body: JSON.stringify({ sessionId, ...payload }),
364
+ * signal: options?.signal,
263
365
  * }),
264
366
  * }),
265
367
  * });
@@ -287,11 +389,41 @@ declare class OctavusChat {
287
389
  private options;
288
390
  private transport;
289
391
  private streamingState;
392
+ private _pendingToolsByName;
393
+ private _pendingToolsByCallId;
394
+ private _pendingClientToolsCache;
395
+ private _completedToolResults;
396
+ private _clientToolAbortController;
397
+ private _serverToolResults;
398
+ private _pendingExecutionId;
290
399
  private listeners;
291
400
  constructor(options: OctavusChatOptions);
292
401
  get messages(): UIMessage[];
293
402
  get status(): ChatStatus;
294
- get error(): Error | null;
403
+ /**
404
+ * The current error, if any.
405
+ * Contains structured error information including type, source, and retryability.
406
+ */
407
+ get error(): OctavusError | null;
408
+ /**
409
+ * Pending interactive tool calls keyed by tool name.
410
+ * Each tool has bound `submit()` and `cancel()` methods.
411
+ *
412
+ * @example
413
+ * ```tsx
414
+ * const feedbackTools = pendingClientTools['request-feedback'] ?? [];
415
+ *
416
+ * {feedbackTools.map(tool => (
417
+ * <FeedbackModal
418
+ * key={tool.toolCallId}
419
+ * {...tool.args}
420
+ * onSubmit={(result) => tool.submit(result)}
421
+ * onCancel={() => tool.cancel()}
422
+ * />
423
+ * ))}
424
+ * ```
425
+ */
426
+ get pendingClientTools(): Record<string, InteractiveTool[]>;
295
427
  /**
296
428
  * Subscribe to state changes. The callback is called whenever messages, status, or error changes.
297
429
  * @returns Unsubscribe function
@@ -301,6 +433,7 @@ declare class OctavusChat {
301
433
  private setMessages;
302
434
  private setStatus;
303
435
  private setError;
436
+ private updatePendingClientToolsCache;
304
437
  /**
305
438
  * Trigger the agent and optionally add a user message to the chat.
306
439
  *
@@ -345,10 +478,34 @@ declare class OctavusChat {
345
478
  * ```
346
479
  */
347
480
  uploadFiles(files: FileList | File[], onProgress?: (fileIndex: number, progress: number) => void): Promise<FileReference[]>;
481
+ /**
482
+ * Internal: Submit a result for a pending tool.
483
+ * Called by bound submit/cancel methods on InteractiveTool.
484
+ */
485
+ private submitToolResult;
348
486
  /** Stop the current streaming and finalize any partial message */
349
487
  stop(): void;
350
488
  private handleStreamEvent;
351
489
  private updateStreamingMessage;
490
+ /**
491
+ * Emit a tool-output-available event for a client tool result.
492
+ */
493
+ private emitToolOutputAvailable;
494
+ /**
495
+ * Emit a tool-output-error event for a client tool result.
496
+ */
497
+ private emitToolOutputError;
498
+ /**
499
+ * Continue execution with collected client tool results.
500
+ */
501
+ private continueWithClientToolResults;
502
+ /**
503
+ * Handle client tool request event.
504
+ *
505
+ * IMPORTANT: Interactive tools must be registered synchronously (before any await)
506
+ * to avoid a race condition where the finish event is processed before tools are added.
507
+ */
508
+ private handleClientToolRequest;
352
509
  }
353
510
 
354
511
  /**
@@ -359,10 +516,22 @@ declare class OctavusChat {
359
516
  */
360
517
  declare function parseSSEStream(response: Response, signal?: AbortSignal): AsyncGenerator<StreamEvent, void, unknown>;
361
518
 
362
- /**
363
- * Request options passed to the triggerRequest function.
364
- */
365
- interface TriggerRequestOptions {
519
+ /** Start a new trigger execution */
520
+ interface TriggerRequest {
521
+ type: 'trigger';
522
+ triggerName: string;
523
+ input?: Record<string, unknown>;
524
+ }
525
+ /** Continue execution after client-side tool handling */
526
+ interface ContinueRequest {
527
+ type: 'continue';
528
+ executionId: string;
529
+ toolResults: ToolResult[];
530
+ }
531
+ /** All request types supported by the HTTP transport */
532
+ type HttpRequest = TriggerRequest | ContinueRequest;
533
+ /** Request options passed to the request callback */
534
+ interface HttpRequestOptions {
366
535
  /** Abort signal to cancel the request */
367
536
  signal?: AbortSignal;
368
537
  }
@@ -371,26 +540,25 @@ interface TriggerRequestOptions {
371
540
  */
372
541
  interface HttpTransportOptions {
373
542
  /**
374
- * Function to make the trigger request.
375
- * Called each time `send()` is invoked on the chat.
543
+ * Function to make requests to your backend.
544
+ * Receives a discriminated union with `type` to identify the request kind.
376
545
  *
377
- * @param triggerName - The trigger name (e.g., 'user-message')
378
- * @param input - Input parameters for the trigger
379
- * @param options - Optional request options including abort signal
546
+ * @param request - The request payload (check `request.type` for the kind)
547
+ * @param options - Request options including abort signal
380
548
  * @returns Response with SSE stream body
381
549
  *
382
550
  * @example
383
551
  * ```typescript
384
- * triggerRequest: (triggerName, input, options) =>
385
- * fetch('/api/octavus', {
552
+ * request: (payload, options) =>
553
+ * fetch('/api/trigger', {
386
554
  * method: 'POST',
387
555
  * headers: { 'Content-Type': 'application/json' },
388
- * body: JSON.stringify({ sessionId, triggerName, input }),
556
+ * body: JSON.stringify({ sessionId, ...payload }),
389
557
  * signal: options?.signal,
390
- * }),
558
+ * })
391
559
  * ```
392
560
  */
393
- triggerRequest: (triggerName: string, input?: Record<string, unknown>, options?: TriggerRequestOptions) => Promise<Response>;
561
+ request: (request: HttpRequest, options?: HttpRequestOptions) => Promise<Response>;
394
562
  }
395
563
  /**
396
564
  * Create an HTTP transport using native fetch() and SSE parsing.
@@ -399,11 +567,11 @@ interface HttpTransportOptions {
399
567
  * @example
400
568
  * ```typescript
401
569
  * const transport = createHttpTransport({
402
- * triggerRequest: (triggerName, input, options) =>
403
- * fetch('/api/octavus', {
570
+ * request: (payload, options) =>
571
+ * fetch('/api/trigger', {
404
572
  * method: 'POST',
405
573
  * headers: { 'Content-Type': 'application/json' },
406
- * body: JSON.stringify({ sessionId, triggerName, input }),
574
+ * body: JSON.stringify({ sessionId, ...payload }),
407
575
  * signal: options?.signal,
408
576
  * }),
409
577
  * });
@@ -529,4 +697,4 @@ interface SocketTransportOptions {
529
697
  */
530
698
  declare function createSocketTransport(options: SocketTransportOptions): SocketTransport;
531
699
 
532
- export { type ChatStatus, type ConnectionState, type ConnectionStateListener, type HttpTransportOptions, OctavusChat, type OctavusChatOptions, type SocketLike, type SocketTransport, type SocketTransportOptions, type Transport, type TriggerRequestOptions, type UploadFilesOptions, type UploadUrlsResponse, type UserMessageInput, createHttpTransport, createSocketTransport, isSocketTransport, parseSSEStream, uploadFiles };
700
+ export { type ChatStatus, type ClientToolContext, type ClientToolHandler, type ConnectionState, type ConnectionStateListener, type ContinueRequest, type HttpRequest, type HttpRequestOptions, type HttpTransportOptions, type InteractiveTool, OctavusChat, type OctavusChatOptions, type SocketLike, type SocketTransport, type SocketTransportOptions, type Transport, type TriggerRequest, type UploadFilesOptions, type UploadUrlsResponse, type UserMessageInput, createHttpTransport, createSocketTransport, isSocketTransport, parseSSEStream, uploadFiles };