@vertesia/client 0.55.0 → 0.57.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.
Files changed (37) hide show
  1. package/lib/cjs/PluginsApi.js +7 -5
  2. package/lib/cjs/PluginsApi.js.map +1 -1
  3. package/lib/cjs/store/ObjectsApi.js +4 -4
  4. package/lib/cjs/store/ObjectsApi.js.map +1 -1
  5. package/lib/cjs/store/WorkflowsApi.js +128 -57
  6. package/lib/cjs/store/WorkflowsApi.js.map +1 -1
  7. package/lib/esm/PluginsApi.js +7 -5
  8. package/lib/esm/PluginsApi.js.map +1 -1
  9. package/lib/esm/store/ObjectsApi.js +4 -4
  10. package/lib/esm/store/ObjectsApi.js.map +1 -1
  11. package/lib/esm/store/WorkflowsApi.js +128 -57
  12. package/lib/esm/store/WorkflowsApi.js.map +1 -1
  13. package/lib/tsconfig.tsbuildinfo +1 -1
  14. package/lib/types/EnvironmentsApi.d.ts +1 -1
  15. package/lib/types/EnvironmentsApi.d.ts.map +1 -1
  16. package/lib/types/PluginsApi.d.ts +2 -4
  17. package/lib/types/PluginsApi.d.ts.map +1 -1
  18. package/lib/types/ProjectsApi.d.ts +2 -2
  19. package/lib/types/ProjectsApi.d.ts.map +1 -1
  20. package/lib/types/RunsApi.d.ts +1 -1
  21. package/lib/types/RunsApi.d.ts.map +1 -1
  22. package/lib/types/TrainingApi.d.ts +1 -1
  23. package/lib/types/TrainingApi.d.ts.map +1 -1
  24. package/lib/types/store/ObjectsApi.d.ts +3 -1
  25. package/lib/types/store/ObjectsApi.d.ts.map +1 -1
  26. package/lib/types/store/WorkflowsApi.d.ts +2 -2
  27. package/lib/types/store/WorkflowsApi.d.ts.map +1 -1
  28. package/lib/vertesia-client.js +2 -0
  29. package/lib/vertesia-client.js.map +1 -0
  30. package/package.json +10 -5
  31. package/src/EnvironmentsApi.ts +1 -1
  32. package/src/PluginsApi.ts +7 -5
  33. package/src/ProjectsApi.ts +2 -2
  34. package/src/RunsApi.ts +1 -1
  35. package/src/TrainingApi.ts +1 -1
  36. package/src/store/ObjectsApi.ts +23 -19
  37. package/src/store/WorkflowsApi.ts +143 -64
@@ -57,16 +57,11 @@ export class WorkflowsApi extends ApiTopic {
57
57
  return this.post(`/execute/${name}`, { payload });
58
58
  }
59
59
 
60
- postMessage(runId: string, message: string, type?: AgentMessageType, details?: any): Promise<void> {
60
+ postMessage(runId: string, msg: AgentMessage): Promise<void> {
61
61
  if (!runId) {
62
62
  throw new Error("runId is required");
63
63
  }
64
- const payload = {
65
- message,
66
- type,
67
- details,
68
- };
69
- return this.post(`/runs/${runId}/updates`, { payload });
64
+ return this.post(`/runs/${runId}/updates`, { payload: msg });
70
65
  }
71
66
 
72
67
  retrieveMessages(runId: string, since?: number): Promise<AgentMessage[]> {
@@ -76,71 +71,155 @@ export class WorkflowsApi extends ApiTopic {
76
71
  return this.get(`/runs/${runId}/updates`, { query });
77
72
  }
78
73
 
79
- streamMessages(runId: string, onMessage?: (message: AgentMessage) => void, since?: number): Promise<void> {
80
- return new Promise(async (resolve, reject) => {
81
- try {
82
- const EventSourceImpl = await EventSourceProvider();
83
- const client = this.client as VertesiaClient;
84
- const streamUrl = new URL(client.workflows.baseUrl + "/runs/" + runId + "/stream");
85
-
86
- if (since) {
87
- streamUrl.searchParams.set("since", since.toString());
74
+ async streamMessages(runId: string, onMessage?: (message: AgentMessage) => void, since?: number): Promise<void> {
75
+ return new Promise((resolve, reject) => {
76
+ let reconnectAttempts = 0;
77
+ let lastMessageTimestamp = since || 0;
78
+ let isClosed = false;
79
+ let currentSse: EventSource | null = null;
80
+ let interval: NodeJS.Timeout | null = null;
81
+
82
+ const maxReconnectAttempts = 10;
83
+ const baseDelay = 1000; // 1 second base delay
84
+ const maxDelay = 30000; // 30 seconds max delay
85
+
86
+ const calculateBackoffDelay = (attempts: number): number => {
87
+ const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempts), maxDelay);
88
+ // Add jitter to prevent thundering herd
89
+ const jitter = Math.random() * 0.1 * exponentialDelay;
90
+ return exponentialDelay + jitter;
91
+ };
92
+
93
+ const cleanup = () => {
94
+ if (interval) {
95
+ clearInterval(interval);
96
+ interval = null;
88
97
  }
89
-
90
- const bearerToken = client._auth ? await client._auth() : undefined;
91
- if (!bearerToken) return reject(new Error("No auth token available"));
92
-
93
- const token = bearerToken.split(" ")[1];
94
- streamUrl.searchParams.set("access_token", token);
95
-
96
- const sse = new EventSourceImpl(streamUrl.href);
97
- let isClosed = false;
98
-
99
- sse.onmessage = (ev: MessageEvent) => {
100
- if (!ev.data || ev.data.startsWith(":")) {
101
- console.log("Received comment or heartbeat; ignoring it.: ", ev.data);
98
+ if (currentSse) {
99
+ currentSse.close();
100
+ currentSse = null;
101
+ }
102
+ };
103
+
104
+ const setupStream = async (isReconnect: boolean = false) => {
105
+ if (isClosed) return;
106
+
107
+ try {
108
+ const EventSourceImpl = await EventSourceProvider();
109
+ const client = this.client as VertesiaClient;
110
+ const streamUrl = new URL(client.workflows.baseUrl + "/runs/" + runId + "/stream");
111
+
112
+ // Use the timestamp of the last received message for reconnection
113
+ if (lastMessageTimestamp > 0) {
114
+ streamUrl.searchParams.set("since", lastMessageTimestamp.toString());
115
+ }
116
+
117
+ const bearerToken = client._auth ? await client._auth() : undefined;
118
+ if (!bearerToken) {
119
+ reject(new Error("No auth token available"));
102
120
  return;
103
121
  }
122
+
123
+ const token = bearerToken.split(" ")[1];
124
+ streamUrl.searchParams.set("access_token", token);
104
125
 
105
- try {
106
- const message = JSON.parse(ev.data) as AgentMessage;
107
- if (onMessage) onMessage(message);
108
-
109
- if (message.type === AgentMessageType.COMPLETE) {
110
- sse.close();
126
+ if (isReconnect) {
127
+ console.log(`Reconnecting to SSE stream for run ${runId} (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`);
128
+ }
129
+
130
+ const sse = new EventSourceImpl(streamUrl.href);
131
+ currentSse = sse;
132
+
133
+ // Prevent Node from exiting prematurely
134
+ interval = setInterval(() => {}, 1000);
135
+
136
+ sse.onopen = () => {
137
+ if (isReconnect) {
138
+ console.log(`Successfully reconnected to SSE stream for run ${runId}`);
139
+ }
140
+ // Reset reconnect attempts on successful connection
141
+ reconnectAttempts = 0;
142
+ };
143
+
144
+ sse.onmessage = (ev: MessageEvent) => {
145
+ if (!ev.data || ev.data.startsWith(":")) {
146
+ console.log("Received comment or heartbeat; ignoring it.: ", ev.data);
147
+ return;
148
+ }
149
+
150
+ try {
151
+ const message = JSON.parse(ev.data) as AgentMessage;
152
+
153
+ // Update last message timestamp for reconnection
154
+ if (message.timestamp) {
155
+ lastMessageTimestamp = Math.max(lastMessageTimestamp, message.timestamp);
156
+ }
157
+
158
+ if (onMessage) onMessage(message);
159
+
160
+ // Only close the stream when the main workstream completes
161
+ if (message.type === AgentMessageType.COMPLETE && (!message.workstream_id || message.workstream_id === 'main')) {
162
+ console.log("Closing stream due to COMPLETE message from main workstream");
163
+ if (!isClosed) {
164
+ isClosed = true;
165
+ cleanup();
166
+ resolve();
167
+ }
168
+ } else if (message.type === AgentMessageType.COMPLETE) {
169
+ console.log(`Received COMPLETE message from non-main workstream: ${message.workstream_id || 'unknown'}, keeping stream open`);
170
+ }
171
+ } catch (err) {
172
+ console.error("Failed to parse SSE message:", err, ev.data);
173
+ }
174
+ };
175
+
176
+ sse.onerror = (err: any) => {
177
+ if (isClosed) return;
178
+
179
+ console.warn(`SSE stream error for run ${runId}:`, err);
180
+ cleanup();
181
+
182
+ // Check if we should attempt reconnection
183
+ if (reconnectAttempts < maxReconnectAttempts) {
184
+ const delay = calculateBackoffDelay(reconnectAttempts);
185
+ console.log(`Attempting to reconnect in ${delay}ms (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`);
186
+
187
+ reconnectAttempts++;
188
+ setTimeout(() => {
189
+ if (!isClosed) {
190
+ setupStream(true);
191
+ }
192
+ }, delay);
193
+ } else {
194
+ console.error(`Failed to reconnect to SSE stream for run ${runId} after ${maxReconnectAttempts} attempts`);
111
195
  isClosed = true;
112
- resolve();
196
+ reject(new Error(`SSE connection failed after ${maxReconnectAttempts} reconnection attempts`));
113
197
  }
114
- } catch (err) {
115
- console.error("Failed to parse SSE message:", err, ev.data);
116
- }
117
- };
118
-
119
- sse.onerror = (err: any) => {
120
- if (!isClosed) {
121
- console.error("SSE stream error:", err);
122
- sse.close();
198
+ };
199
+ } catch (err) {
200
+ console.error("Error setting up SSE stream:", err);
201
+ if (reconnectAttempts < maxReconnectAttempts) {
202
+ const delay = calculateBackoffDelay(reconnectAttempts);
203
+ reconnectAttempts++;
204
+ setTimeout(() => {
205
+ if (!isClosed) {
206
+ setupStream(true);
207
+ }
208
+ }, delay);
209
+ } else {
123
210
  reject(err);
124
211
  }
125
- };
126
-
127
- // Prevent Node from exiting prematurely
128
- const interval = setInterval(() => { }, 1000);
129
-
130
- // Cleanup when stream resolves
131
- const cleanup = () => {
132
- clearInterval(interval);
133
- };
134
-
135
- // Attach cleanup
136
- sse.addEventListener("close", () => {
137
- isClosed = true;
138
- cleanup();
139
- resolve();
140
- });
141
- } catch (err) {
142
- reject(err);
143
- }
212
+ }
213
+ };
214
+
215
+ // Start the async setup process
216
+ setupStream(false);
217
+
218
+ // Return cleanup function for external cancellation
219
+ return () => {
220
+ isClosed = true;
221
+ cleanup();
222
+ };
144
223
  });
145
224
  }
146
225