@robosystems/client 1.12.1 → 1.13.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.
@@ -1,3 +1,4 @@
1
+ import type { TokenProvider } from './graphql/client';
1
2
  export interface OperationProgress {
2
3
  message: string;
3
4
  progressPercent?: number;
@@ -25,6 +26,7 @@ export declare class OperationClient {
25
26
  credentials?: 'include' | 'same-origin' | 'omit';
26
27
  headers?: Record<string, string>;
27
28
  token?: string;
29
+ tokenProvider?: TokenProvider;
28
30
  maxRetries?: number;
29
31
  retryDelay?: number;
30
32
  });
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { cancelOperation as cancelOperationSDK, getOperationStatus } from '../sdk.gen'
9
+ import type { TokenProvider } from './graphql/client'
9
10
  import { EventType, SSEClient } from './SSEClient'
10
11
 
11
12
  export interface OperationProgress {
@@ -35,6 +36,7 @@ export class OperationClient {
35
36
  credentials?: 'include' | 'same-origin' | 'omit'
36
37
  headers?: Record<string, string>
37
38
  token?: string // JWT token for authentication
39
+ tokenProvider?: TokenProvider // consulted on every SSE connect; wins over `token`
38
40
  maxRetries?: number
39
41
  retryDelay?: number
40
42
  }
@@ -46,6 +48,7 @@ export class OperationClient {
46
48
  credentials?: 'include' | 'same-origin' | 'omit'
47
49
  headers?: Record<string, string>
48
50
  token?: string // JWT token for authentication
51
+ tokenProvider?: TokenProvider // consulted on every SSE connect; wins over `token`
49
52
  maxRetries?: number
50
53
  retryDelay?: number
51
54
  }) {
@@ -1,3 +1,4 @@
1
+ import type { TokenProvider } from './graphql/client';
1
2
  export interface OperatorQueryRequest {
2
3
  message: string;
3
4
  history?: Array<{
@@ -13,6 +14,12 @@ export interface OperatorOptions {
13
14
  mode?: 'auto' | 'sync' | 'async';
14
15
  maxWait?: number;
15
16
  onProgress?: (message: string, percentage?: number) => void;
17
+ /**
18
+ * Interval between `/v1/operations/{id}/status` polls while the client
19
+ * follows a queued run it could not open an SSE stream for. Defaults to
20
+ * 2000 ms; only the fallback path uses it.
21
+ */
22
+ pollIntervalMs?: number;
16
23
  }
17
24
  export interface OperatorResult {
18
25
  content: string;
@@ -27,6 +34,12 @@ export interface OperatorResult {
27
34
  confidence_score?: number;
28
35
  execution_time?: number;
29
36
  timestamp?: string;
37
+ /**
38
+ * Present when the API reports a failed run inside an otherwise successful
39
+ * response (credit pre-flight, operator timeouts, cancelled runs). `content`
40
+ * then carries the explanation rather than an answer.
41
+ */
42
+ error_details?: Record<string, any>;
30
43
  }
31
44
  export interface QueuedOperatorResponse {
32
45
  status: 'queued';
@@ -34,15 +47,25 @@ export interface QueuedOperatorResponse {
34
47
  message: string;
35
48
  sse_endpoint?: string;
36
49
  }
50
+ export interface OperatorClientConfig {
51
+ baseUrl: string;
52
+ credentials?: 'include' | 'same-origin' | 'omit';
53
+ headers?: Record<string, string>;
54
+ /**
55
+ * Static JWT captured at construction. Prefer `tokenProvider` in the
56
+ * browser — see `SSEConfig` for why a captured token goes stale.
57
+ */
58
+ token?: string;
59
+ /**
60
+ * Consulted on every SSE connect so a rotated JWT keeps the progress
61
+ * stream authenticated. Wins over `token` when both are set.
62
+ */
63
+ tokenProvider?: TokenProvider;
64
+ }
37
65
  export declare class OperatorClient {
38
66
  private sseClient?;
39
67
  private config;
40
- constructor(config: {
41
- baseUrl: string;
42
- credentials?: 'include' | 'same-origin' | 'omit';
43
- headers?: Record<string, string>;
44
- token?: string;
45
- });
68
+ constructor(config: OperatorClientConfig);
46
69
  /**
47
70
  * Execute operator query with automatic operator selection
48
71
  */
@@ -51,23 +74,22 @@ export declare class OperatorClient {
51
74
  * Execute specific operator type
52
75
  */
53
76
  executeOperator(graphId: string, operatorType: string, request: OperatorQueryRequest, options?: OperatorOptions): Promise<OperatorResult>;
54
- private waitForOperatorCompletion;
55
- /**
56
- * Convenience method for simple agent queries with auto-selection
57
- */
58
- query(graphId: string, message: string, context?: Record<string, any>): Promise<OperatorResult>;
59
77
  /**
60
- * Execute financial agent for financial analysis
78
+ * Resolve an operator endpoint response: a sync 200 body is the result, a
79
+ * 202 with an `operation_id` is followed to completion.
61
80
  */
62
- analyzeFinancials(graphId: string, message: string, options?: OperatorOptions): Promise<OperatorResult>;
81
+ private settle;
82
+ private waitForOperatorCompletion;
63
83
  /**
64
- * Execute research agent for deep research
84
+ * Follow a queued run over `/v1/operations/{id}/status` until it settles.
85
+ * Used when the SSE stream could not be opened; `streamError` is folded
86
+ * into the failure message if polling cannot reach a verdict either.
65
87
  */
66
- research(graphId: string, message: string, options?: OperatorOptions): Promise<OperatorResult>;
88
+ private pollForCompletion;
67
89
  /**
68
- * Execute RAG agent for fast retrieval
90
+ * Convenience method for simple agent queries with auto-selection
69
91
  */
70
- rag(graphId: string, message: string, options?: OperatorOptions): Promise<OperatorResult>;
92
+ query(graphId: string, message: string, context?: Record<string, any>): Promise<OperatorResult>;
71
93
  /**
72
94
  * Cancel any active SSE connections
73
95
  */
@@ -8,6 +8,50 @@ exports.QueuedOperatorError = exports.OperatorClient = void 0;
8
8
  */
9
9
  const sdk_gen_1 = require("../sdk.gen");
10
10
  const SSEClient_1 = require("./SSEClient");
11
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
12
+ // Consecutive `/status` failures tolerated before the polling fallback gives
13
+ // up: one transient network error must not lose a run that is still going.
14
+ const MAX_CONSECUTIVE_POLL_FAILURES = 3;
15
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ const describeError = (err) => (err instanceof Error ? err.message : String(err));
17
+ /**
18
+ * Text for the `error` slot of a generated-client envelope: a thrown `Error`
19
+ * (network failure — the client returns those rather than throwing), an API
20
+ * body with `detail`, or whatever else came back.
21
+ */
22
+ const describeEnvelopeError = (err) => {
23
+ if (err instanceof Error)
24
+ return err.message;
25
+ if (err && typeof err === 'object' && 'detail' in err)
26
+ return String(err.detail);
27
+ if (err === undefined || err === null)
28
+ return 'empty response';
29
+ return typeof err === 'string' ? err : JSON.stringify(err);
30
+ };
31
+ /**
32
+ * Shape an operator payload — the sync 200 body, the `operation_completed`
33
+ * event, or the `/status` `result` — into the public result type. All three
34
+ * carry the same fields, so one mapper keeps them consistent.
35
+ */
36
+ function toOperatorResult(data) {
37
+ const result = {
38
+ content: data.content ?? '',
39
+ operator_used: data.operator_used || 'unknown',
40
+ mode_used: data.mode_used || 'standard',
41
+ metadata: data.metadata,
42
+ tokens_used: data.tokens_used,
43
+ confidence_score: data.confidence_score,
44
+ execution_time: data.execution_time,
45
+ timestamp: data.timestamp || new Date().toISOString(),
46
+ };
47
+ if (data.error_details) {
48
+ result.error_details = data.error_details;
49
+ }
50
+ return result;
51
+ }
52
+ /** Internal: a `/status` verdict that must not be retried. */
53
+ class PollAbort extends Error {
54
+ }
11
55
  class OperatorClient {
12
56
  constructor(config) {
13
57
  this.config = config;
@@ -29,32 +73,7 @@ class OperatorClient {
29
73
  },
30
74
  };
31
75
  const response = await (0, sdk_gen_1.autoSelectOperator)(data);
32
- const responseData = response.data;
33
- // Check if this is an immediate response (sync execution)
34
- if (responseData?.content !== undefined && responseData?.operator_used) {
35
- return {
36
- content: responseData.content,
37
- operator_used: responseData.operator_used,
38
- mode_used: responseData.mode_used,
39
- metadata: responseData.metadata,
40
- tokens_used: responseData.tokens_used,
41
- confidence_score: responseData.confidence_score,
42
- execution_time: responseData.execution_time,
43
- timestamp: new Date().toISOString(),
44
- };
45
- }
46
- // Check if this is a queued response (async background task execution)
47
- if (responseData?.operation_id) {
48
- const queuedResponse = responseData;
49
- // If user doesn't want to wait, throw with queue info
50
- if (options.maxWait === 0) {
51
- throw new QueuedOperatorError(queuedResponse);
52
- }
53
- // Use SSE to monitor the operation
54
- return this.waitForOperatorCompletion(queuedResponse.operation_id, options);
55
- }
56
- // Unexpected response format
57
- throw new Error('Unexpected response format from operator endpoint');
76
+ return this.settle(response.data, options);
58
77
  }
59
78
  /**
60
79
  * Execute specific operator type
@@ -73,126 +92,155 @@ class OperatorClient {
73
92
  },
74
93
  };
75
94
  const response = await (0, sdk_gen_1.executeSpecificOperator)(data);
76
- const responseData = response.data;
77
- // Check if this is an immediate response (sync execution)
95
+ return this.settle(response.data, options);
96
+ }
97
+ /**
98
+ * Resolve an operator endpoint response: a sync 200 body is the result, a
99
+ * 202 with an `operation_id` is followed to completion.
100
+ */
101
+ async settle(responseData, options) {
102
+ // Immediate response (sync execution)
78
103
  if (responseData?.content !== undefined && responseData?.operator_used) {
79
- return {
80
- content: responseData.content,
81
- operator_used: responseData.operator_used,
82
- mode_used: responseData.mode_used,
83
- metadata: responseData.metadata,
84
- tokens_used: responseData.tokens_used,
85
- confidence_score: responseData.confidence_score,
86
- execution_time: responseData.execution_time,
87
- timestamp: new Date().toISOString(),
88
- };
104
+ return toOperatorResult(responseData);
89
105
  }
90
- // Check if this is a queued response (async background task execution)
106
+ // Queued response (async background task execution)
91
107
  if (responseData?.operation_id) {
92
108
  const queuedResponse = responseData;
93
109
  // If user doesn't want to wait, throw with queue info
94
110
  if (options.maxWait === 0) {
95
111
  throw new QueuedOperatorError(queuedResponse);
96
112
  }
97
- // Use SSE to monitor the operation
98
113
  return this.waitForOperatorCompletion(queuedResponse.operation_id, options);
99
114
  }
100
115
  // Unexpected response format
101
116
  throw new Error('Unexpected response format from operator endpoint');
102
117
  }
103
118
  async waitForOperatorCompletion(operationId, options) {
119
+ const sseClient = new SSEClient_1.SSEClient(this.config);
120
+ this.sseClient = sseClient;
121
+ try {
122
+ await sseClient.connect(operationId);
123
+ }
124
+ catch (streamError) {
125
+ // The run is already queued and finishes whether or not anyone is
126
+ // watching, so a stream that cannot open — a revoked token, a proxy
127
+ // that will not hold the connection, the per-user connection cap —
128
+ // must not lose the result. Follow the operation over `/status`,
129
+ // which rides the regular REST auth path, instead.
130
+ this.sseClient = undefined;
131
+ return this.pollForCompletion(operationId, options, streamError);
132
+ }
104
133
  return new Promise((resolve, reject) => {
105
- const sseClient = new SSEClient_1.SSEClient(this.config);
106
- sseClient
107
- .connect(operationId)
108
- .then(() => {
109
- let result = null;
110
- // Listen for progress events
111
- sseClient.on(SSEClient_1.EventType.OPERATION_PROGRESS, (data) => {
112
- options.onProgress?.(data.message, data.progress_percentage);
113
- });
114
- // Listen for agent-specific events
115
- sseClient.on('operator_started', (data) => {
116
- options.onProgress?.(`Operator ${data.operator_type} started`, 0);
117
- });
118
- sseClient.on('operator_initialized', (data) => {
119
- options.onProgress?.(`${data.agent_name} initialized`, 10);
120
- });
121
- sseClient.on('progress', (data) => {
122
- options.onProgress?.(data.message, data.percentage);
123
- });
124
- sseClient.on('operator_completed', (data) => {
125
- result = {
126
- content: data.content,
127
- operator_used: data.operator_used,
128
- mode_used: data.mode_used,
129
- metadata: data.metadata,
130
- tokens_used: data.tokens_used,
131
- confidence_score: data.confidence_score,
132
- execution_time: data.execution_time,
133
- timestamp: data.timestamp || new Date().toISOString(),
134
- };
135
- sseClient.close();
134
+ let result = null;
135
+ const finish = () => {
136
+ sseClient.close();
137
+ if (this.sseClient === sseClient) {
138
+ this.sseClient = undefined;
139
+ }
140
+ };
141
+ // Listen for progress events
142
+ sseClient.on(SSEClient_1.EventType.OPERATION_PROGRESS, (data) => {
143
+ options.onProgress?.(data.message, data.progress_percentage);
144
+ });
145
+ // Listen for agent-specific events
146
+ sseClient.on('operator_started', (data) => {
147
+ options.onProgress?.(`Operator ${data.operator_type} started`, 0);
148
+ });
149
+ sseClient.on('operator_initialized', (data) => {
150
+ options.onProgress?.(`${data.agent_name} initialized`, 10);
151
+ });
152
+ sseClient.on('progress', (data) => {
153
+ options.onProgress?.(data.message, data.percentage);
154
+ });
155
+ sseClient.on('operator_completed', (data) => {
156
+ result = toOperatorResult(data);
157
+ finish();
158
+ resolve(result);
159
+ });
160
+ // Fallback to generic completion event
161
+ sseClient.on(SSEClient_1.EventType.OPERATION_COMPLETED, (data) => {
162
+ if (!result) {
163
+ result = toOperatorResult(data.result || data);
164
+ finish();
136
165
  resolve(result);
137
- });
138
- // Fallback to generic completion event
139
- sseClient.on(SSEClient_1.EventType.OPERATION_COMPLETED, (data) => {
140
- if (!result) {
141
- const operatorResult = data.result || data;
142
- result = {
143
- content: operatorResult.content || '',
144
- operator_used: operatorResult.operator_used || 'unknown',
145
- mode_used: operatorResult.mode_used || 'standard',
146
- metadata: operatorResult.metadata,
147
- tokens_used: operatorResult.tokens_used,
148
- confidence_score: operatorResult.confidence_score,
149
- execution_time: operatorResult.execution_time,
150
- timestamp: operatorResult.timestamp || new Date().toISOString(),
151
- };
152
- sseClient.close();
153
- resolve(result);
154
- }
155
- });
156
- sseClient.on(SSEClient_1.EventType.OPERATION_ERROR, (error) => {
157
- sseClient.close();
158
- reject(new Error(error.message || error.error));
159
- });
160
- sseClient.on(SSEClient_1.EventType.OPERATION_CANCELLED, () => {
161
- sseClient.close();
162
- reject(new Error('Agent execution cancelled'));
163
- });
164
- // Handle generic error event
165
- sseClient.on('error', (error) => {
166
- sseClient.close();
167
- reject(new Error(error.error || error.message || 'Agent execution failed'));
168
- });
169
- })
170
- .catch(reject);
166
+ }
167
+ });
168
+ sseClient.on(SSEClient_1.EventType.OPERATION_ERROR, (error) => {
169
+ finish();
170
+ reject(new Error(error.message || error.error));
171
+ });
172
+ sseClient.on(SSEClient_1.EventType.OPERATION_CANCELLED, () => {
173
+ finish();
174
+ reject(new Error('Agent execution cancelled'));
175
+ });
176
+ // Handle generic error event
177
+ sseClient.on('error', (error) => {
178
+ finish();
179
+ reject(new Error(error.error || error.message || 'Agent execution failed'));
180
+ });
171
181
  });
172
182
  }
173
183
  /**
174
- * Convenience method for simple agent queries with auto-selection
175
- */
176
- async query(graphId, message, context) {
177
- return this.executeQuery(graphId, { message, context }, { mode: 'auto' });
178
- }
179
- /**
180
- * Execute financial agent for financial analysis
184
+ * Follow a queued run over `/v1/operations/{id}/status` until it settles.
185
+ * Used when the SSE stream could not be opened; `streamError` is folded
186
+ * into the failure message if polling cannot reach a verdict either.
181
187
  */
182
- async analyzeFinancials(graphId, message, options = {}) {
183
- return this.executeOperator(graphId, 'financial', { message }, options);
184
- }
185
- /**
186
- * Execute research agent for deep research
187
- */
188
- async research(graphId, message, options = {}) {
189
- return this.executeOperator(graphId, 'research', { message }, options);
188
+ async pollForCompletion(operationId, options, streamError) {
189
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
190
+ const streamDetail = describeError(streamError);
191
+ let consecutiveFailures = 0;
192
+ options.onProgress?.('Live progress unavailable — waiting for the result');
193
+ for (;;) {
194
+ let status;
195
+ try {
196
+ const response = await (0, sdk_gen_1.getOperationStatus)({ path: { operation_id: operationId } });
197
+ if (response.error || !response.data) {
198
+ const httpStatus = response.response?.status;
199
+ const detail = describeEnvelopeError(response.error);
200
+ // A definitive 4xx (expired, not ours, unauthenticated) ends the
201
+ // wait; anything else is treated as transient and retried below.
202
+ if (httpStatus !== undefined &&
203
+ httpStatus >= 400 &&
204
+ httpStatus < 500 &&
205
+ httpStatus !== 429) {
206
+ throw new PollAbort(`Operator stream failed (${streamDetail}); status check failed (${httpStatus}: ${detail})`);
207
+ }
208
+ throw new Error(detail);
209
+ }
210
+ status = response.data;
211
+ consecutiveFailures = 0;
212
+ }
213
+ catch (pollError) {
214
+ if (pollError instanceof PollAbort) {
215
+ throw new Error(pollError.message);
216
+ }
217
+ consecutiveFailures += 1;
218
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
219
+ throw new Error(`Operator stream failed (${streamDetail}); status polling failed (${describeError(pollError)})`);
220
+ }
221
+ await sleep(interval);
222
+ continue;
223
+ }
224
+ switch (status.status) {
225
+ case 'completed':
226
+ return toOperatorResult(status.result || {});
227
+ case 'failed':
228
+ throw new Error(status.error || status.message || 'Operator run failed');
229
+ case 'cancelled':
230
+ throw new Error('Agent execution cancelled');
231
+ default:
232
+ if (status.message) {
233
+ options.onProgress?.(status.message);
234
+ }
235
+ await sleep(interval);
236
+ }
237
+ }
190
238
  }
191
239
  /**
192
- * Execute RAG agent for fast retrieval
240
+ * Convenience method for simple agent queries with auto-selection
193
241
  */
194
- async rag(graphId, message, options = {}) {
195
- return this.executeOperator(graphId, 'rag', { message }, options);
242
+ async query(graphId, message, context) {
243
+ return this.executeQuery(graphId, { message, context }, { mode: 'auto' });
196
244
  }
197
245
  /**
198
246
  * Cancel any active SSE connections