@vertesia/client 1.5.0-dev.20260714.072725Z → 1.5.0-dev.20260722.120446Z

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 (87) hide show
  1. package/lib/AppsApi.d.ts +89 -5
  2. package/lib/AppsApi.d.ts.map +1 -1
  3. package/lib/AppsApi.js +153 -4
  4. package/lib/AppsApi.js.map +1 -1
  5. package/lib/AuditTrailApi.d.ts +2 -1
  6. package/lib/AuditTrailApi.d.ts.map +1 -1
  7. package/lib/AuditTrailApi.js +3 -0
  8. package/lib/AuditTrailApi.js.map +1 -1
  9. package/lib/ProjectsApi.d.ts +3 -1
  10. package/lib/ProjectsApi.d.ts.map +1 -1
  11. package/lib/ProjectsApi.js +16 -8
  12. package/lib/ProjectsApi.js.map +1 -1
  13. package/lib/RunsApi.d.ts +8 -2
  14. package/lib/RunsApi.d.ts.map +1 -1
  15. package/lib/RunsApi.js +6 -2
  16. package/lib/RunsApi.js.map +1 -1
  17. package/lib/ViewsApi.d.ts +21 -0
  18. package/lib/ViewsApi.d.ts.map +1 -0
  19. package/lib/ViewsApi.js +41 -0
  20. package/lib/ViewsApi.js.map +1 -0
  21. package/lib/client.d.ts +10 -1
  22. package/lib/client.d.ts.map +1 -1
  23. package/lib/client.js +20 -0
  24. package/lib/client.js.map +1 -1
  25. package/lib/index.d.ts +2 -0
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +2 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/managed-sse.d.ts +27 -0
  30. package/lib/managed-sse.d.ts.map +1 -0
  31. package/lib/managed-sse.js +159 -0
  32. package/lib/managed-sse.js.map +1 -0
  33. package/lib/store/AgentsApi.d.ts.map +1 -1
  34. package/lib/store/AgentsApi.js +61 -6
  35. package/lib/store/AgentsApi.js.map +1 -1
  36. package/lib/store/AnalyzeDocApi.d.ts +9 -11
  37. package/lib/store/AnalyzeDocApi.d.ts.map +1 -1
  38. package/lib/store/AnalyzeDocApi.js +12 -36
  39. package/lib/store/AnalyzeDocApi.js.map +1 -1
  40. package/lib/store/EventsApi.d.ts +15 -1
  41. package/lib/store/EventsApi.d.ts.map +1 -1
  42. package/lib/store/EventsApi.js +57 -0
  43. package/lib/store/EventsApi.js.map +1 -1
  44. package/lib/store/FilesApi.d.ts.map +1 -1
  45. package/lib/store/FilesApi.js +6 -0
  46. package/lib/store/FilesApi.js.map +1 -1
  47. package/lib/store/ViewsApi.d.ts +12 -0
  48. package/lib/store/ViewsApi.d.ts.map +1 -0
  49. package/lib/store/ViewsApi.js +17 -0
  50. package/lib/store/ViewsApi.js.map +1 -0
  51. package/lib/store/client.d.ts +5 -1
  52. package/lib/store/client.d.ts.map +1 -1
  53. package/lib/store/client.js +13 -0
  54. package/lib/store/client.js.map +1 -1
  55. package/lib/store/index.d.ts +2 -0
  56. package/lib/store/index.d.ts.map +1 -1
  57. package/lib/store/index.js +2 -0
  58. package/lib/store/index.js.map +1 -1
  59. package/lib/store/version.d.ts +1 -0
  60. package/lib/store/version.d.ts.map +1 -1
  61. package/lib/store/version.js +1 -0
  62. package/lib/store/version.js.map +1 -1
  63. package/lib/vertesia-client.js +10 -10
  64. package/lib/vertesia-client.js.map +1 -1
  65. package/package.json +8 -7
  66. package/src/AppsApi.test.ts +124 -0
  67. package/src/AppsApi.ts +205 -4
  68. package/src/AuditTrailApi.ts +10 -1
  69. package/src/ProjectsApi.test.ts +33 -0
  70. package/src/ProjectsApi.ts +19 -8
  71. package/src/RunsApi.ts +12 -2
  72. package/src/ViewsApi.test.ts +36 -0
  73. package/src/ViewsApi.ts +60 -0
  74. package/src/client.ts +21 -2
  75. package/src/index.ts +2 -0
  76. package/src/managed-sse.ts +205 -0
  77. package/src/store/AgentsApi.ts +57 -6
  78. package/src/store/AnalyzeDocApi.ts +16 -54
  79. package/src/store/EventsApi.test.ts +131 -0
  80. package/src/store/EventsApi.ts +80 -0
  81. package/src/store/FilesApi.test.ts +69 -0
  82. package/src/store/FilesApi.ts +8 -1
  83. package/src/store/ViewsApi.test.ts +33 -0
  84. package/src/store/ViewsApi.ts +20 -0
  85. package/src/store/client.ts +13 -1
  86. package/src/store/index.ts +2 -0
  87. package/src/store/version.ts +1 -0
package/src/client.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AbstractFetchClient, type FETCH_FN, type IRequestRetryPolicy } from '@vertesia/api-fetch-client';
2
- import type { AuthTokenPayload, AuthTokenResponse } from '@vertesia/common';
2
+ import { APP_VERSION_HEADER, type AuthTokenPayload, type AuthTokenResponse } from '@vertesia/common';
3
3
  import AccountApi from './AccountApi.js';
4
4
  import AccountsApi from './AccountsApi.js';
5
5
  import AnalyticsApi from './AnalyticsApi.js';
@@ -28,6 +28,7 @@ import { VERSION, VERSION_HEADER } from './store/version.js';
28
28
  import ToolsApi from './ToolsApi.js';
29
29
  import TrainingApi from './TrainingApi.js';
30
30
  import UsersApi from './UsersApi.js';
31
+ import ViewsApi from './ViewsApi.js';
31
32
 
32
33
  /**
33
34
  * 1 min threshold constant in ms
@@ -116,7 +117,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
116
117
  static async fromAuthToken(
117
118
  token: string,
118
119
  payload?: AuthTokenPayload,
119
- endpoints?: { studio: string; store: string; token?: string },
120
+ endpoints?: { studio: string; store: string; token?: string; git?: string },
120
121
  ) {
121
122
  if (!payload) {
122
123
  payload = decodeJWT(token);
@@ -206,6 +207,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
206
207
  timeout: opts.timeout,
207
208
  fetch: opts.fetch,
208
209
  });
210
+ this.views = new ViewsApi(this, this.store.views);
209
211
 
210
212
  if (opts.retryPolicy) {
211
213
  this.withRetryPolicy(opts.retryPolicy);
@@ -240,6 +242,20 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
240
242
  return this;
241
243
  }
242
244
 
245
+ /**
246
+ * Pin the app version this client's requests resolve against. A generated app's UI sets this from
247
+ * its build-time `VITE_APP_VERSION` so studio/zeno resolve its `app:<app>:…` refs against that
248
+ * exact built version instead of the promoted version. Pass null/empty to clear (→ promoted).
249
+ */
250
+ withAppVersion(version: string | null | undefined) {
251
+ if (!version) {
252
+ delete this.headers[APP_VERSION_HEADER];
253
+ } else {
254
+ this.headers[APP_VERSION_HEADER] = String(version);
255
+ }
256
+ return this;
257
+ }
258
+
243
259
  /**
244
260
  * Overwrite to keep store and composable clients synchronized on the auth callback
245
261
  * @param authCb
@@ -411,6 +427,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
411
427
  apps = new AppsApi(this);
412
428
  tools = new ToolsApi(this);
413
429
  quota = new QuotaApi(this);
430
+ views: ViewsApi;
414
431
  }
415
432
 
416
433
  function isApiKey(apiKey: string) {
@@ -500,6 +517,7 @@ function getEndpointsFromDomain(domain: string) {
500
517
  studio: `http://localhost:8091`,
501
518
  store: `http://localhost:8092`,
502
519
  token: getRuntimeStsUrl() ?? 'https://sts.dev1.vertesia.io',
520
+ git: 'https://git.dev1.vertesia.io',
503
521
  };
504
522
  } else {
505
523
  const url = `https://${domain}`;
@@ -509,6 +527,7 @@ function getEndpointsFromDomain(domain: string) {
509
527
  studio: url,
510
528
  store: url,
511
529
  token: url.replace('api', 'sts'),
530
+ git: url.replace('api', 'git'),
512
531
  };
513
532
  }
514
533
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export type { GroupsQueryOptions } from './GroupsApi.js';
7
7
  export * from './InteractionBase.js';
8
8
  export * from './InteractionOutput.js';
9
9
  export { default as InternalSecretsApi } from './InternalSecretsApi.js';
10
+ export * from './managed-sse.js';
10
11
  export { default as OAuthClientsApi } from './OAuthClientsApi.js';
11
12
  export { default as OAuthGrantsApi } from './OAuthGrantsApi.js';
12
13
  export { default as OAuthProvidersApi } from './OAuthProvidersApi.js';
@@ -17,4 +18,5 @@ export type { ComputeRunFacetsResponse, FilterOption } from './RunsApi.js';
17
18
  export { default as SecretsApi } from './SecretsApi.js';
18
19
  export * from './StreamSource.js';
19
20
  export * from './store/index.js';
21
+ export { default as ViewsApi } from './ViewsApi.js';
20
22
  export type { AsyncExecutionResult };
@@ -0,0 +1,205 @@
1
+ import { EventSourceProvider } from './execute.js';
2
+
3
+ export type ManagedEventSourceStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';
4
+
5
+ export interface ManagedEventSourceEvent<T> {
6
+ data: T;
7
+ event_type: string;
8
+ event_id?: string;
9
+ }
10
+
11
+ export interface ManagedEventSourceConnection {
12
+ readonly closed: boolean;
13
+ close(): void;
14
+ }
15
+
16
+ export interface ManagedEventSourceOptions<T> {
17
+ url: URL | string | (() => URL | string | Promise<URL | string>);
18
+ event_types?: string[];
19
+ signal?: AbortSignal;
20
+ max_reconnect_attempts?: number;
21
+ base_reconnect_delay_ms?: number;
22
+ max_reconnect_delay_ms?: number;
23
+ last_event_id_query_param?: string;
24
+ get_access_token?: () => Promise<string | undefined>;
25
+ parse?: (data: string, eventType: string) => T;
26
+ get_cursor?: (data: T) => string | undefined;
27
+ on_event: (event: ManagedEventSourceEvent<T>) => void;
28
+ on_error?: (error: unknown) => void;
29
+ on_status?: (status: ManagedEventSourceStatus) => void;
30
+ }
31
+
32
+ function defaultParse<T>(data: string): T {
33
+ return JSON.parse(data) as T;
34
+ }
35
+
36
+ function calculateBackoffDelay(attempts: number, baseDelayMs: number, maxDelayMs: number): number {
37
+ const exponentialDelay = Math.min(baseDelayMs * 2 ** attempts, maxDelayMs);
38
+ const jitter = Math.random() * 0.1 * exponentialDelay;
39
+ return exponentialDelay + jitter;
40
+ }
41
+
42
+ function withAccessToken(url: URL, bearerToken: string | undefined): void {
43
+ if (!bearerToken) {
44
+ return;
45
+ }
46
+ const token = bearerToken.startsWith('Bearer ') ? bearerToken.slice('Bearer '.length) : bearerToken;
47
+ url.searchParams.set('access_token', token);
48
+ }
49
+
50
+ async function resolveUrl(value: ManagedEventSourceOptions<unknown>['url']): Promise<URL> {
51
+ const resolved = typeof value === 'function' ? await value() : value;
52
+ return resolved instanceof URL ? new URL(resolved.href) : new URL(resolved);
53
+ }
54
+
55
+ export function openManagedEventSource<T>(options: ManagedEventSourceOptions<T>): ManagedEventSourceConnection {
56
+ const eventTypes = options.event_types?.length ? options.event_types : ['message'];
57
+ const maxReconnectAttempts = options.max_reconnect_attempts ?? 10;
58
+ const baseDelayMs = options.base_reconnect_delay_ms ?? 1000;
59
+ const maxDelayMs = options.max_reconnect_delay_ms ?? 30_000;
60
+ const parse = options.parse ?? defaultParse<T>;
61
+
62
+ let closed = false;
63
+ let reconnectAttempts = 0;
64
+ let currentSse: EventSource | null = null;
65
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
66
+ let abortHandler: (() => void) | null = null;
67
+ let connectionOpenedAt = 0;
68
+ let lastEventId: string | undefined;
69
+
70
+ const emitStatus = (status: ManagedEventSourceStatus) => {
71
+ options.on_status?.(status);
72
+ };
73
+
74
+ const cleanupCurrentSource = () => {
75
+ if (currentSse) {
76
+ currentSse.close();
77
+ currentSse = null;
78
+ }
79
+ if (reconnectTimer) {
80
+ clearTimeout(reconnectTimer);
81
+ reconnectTimer = null;
82
+ }
83
+ };
84
+
85
+ const close = () => {
86
+ if (closed) {
87
+ return;
88
+ }
89
+ closed = true;
90
+ cleanupCurrentSource();
91
+ if (options.signal && abortHandler) {
92
+ options.signal.removeEventListener('abort', abortHandler);
93
+ abortHandler = null;
94
+ }
95
+ emitStatus('closed');
96
+ };
97
+
98
+ const scheduleReconnect = (error: unknown) => {
99
+ if (closed) {
100
+ return;
101
+ }
102
+ cleanupCurrentSource();
103
+ const connectionDuration = connectionOpenedAt ? Date.now() - connectionOpenedAt : 0;
104
+ if (connectionDuration > 5000) {
105
+ reconnectAttempts = 0;
106
+ }
107
+ if (reconnectAttempts >= maxReconnectAttempts) {
108
+ close();
109
+ options.on_error?.(error);
110
+ return;
111
+ }
112
+ const delay = calculateBackoffDelay(reconnectAttempts, baseDelayMs, maxDelayMs);
113
+ reconnectAttempts += 1;
114
+ emitStatus('reconnecting');
115
+ reconnectTimer = setTimeout(() => {
116
+ reconnectTimer = null;
117
+ void connect(true);
118
+ }, delay);
119
+ };
120
+
121
+ const handleMessage = (eventType: string, ev: MessageEvent) => {
122
+ if (closed || !ev.data || String(ev.data).startsWith(':')) {
123
+ return;
124
+ }
125
+ try {
126
+ const data = parse(String(ev.data), eventType);
127
+ const cursor = options.get_cursor?.(data);
128
+ lastEventId = ev.lastEventId || cursor || lastEventId;
129
+ options.on_event({
130
+ data,
131
+ event_type: eventType,
132
+ event_id: ev.lastEventId || cursor,
133
+ });
134
+ } catch (err) {
135
+ options.on_error?.(err);
136
+ }
137
+ };
138
+
139
+ const connect = async (isReconnect: boolean) => {
140
+ if (closed) {
141
+ return;
142
+ }
143
+ try {
144
+ emitStatus(isReconnect ? 'reconnecting' : 'connecting');
145
+ const EventSourceImpl = await EventSourceProvider();
146
+ if (closed) {
147
+ return;
148
+ }
149
+ const url = await resolveUrl(options.url);
150
+ if (
151
+ lastEventId &&
152
+ options.last_event_id_query_param &&
153
+ !url.searchParams.has(options.last_event_id_query_param)
154
+ ) {
155
+ url.searchParams.set(options.last_event_id_query_param, lastEventId);
156
+ }
157
+ withAccessToken(url, await options.get_access_token?.());
158
+ if (closed) {
159
+ return;
160
+ }
161
+ const sse = new EventSourceImpl(url.href);
162
+ currentSse = sse;
163
+ connectionOpenedAt = 0;
164
+
165
+ sse.onopen = () => {
166
+ connectionOpenedAt = Date.now();
167
+ emitStatus('open');
168
+ };
169
+
170
+ for (const eventType of eventTypes) {
171
+ sse.addEventListener(eventType, (ev) => handleMessage(eventType, ev as MessageEvent));
172
+ }
173
+
174
+ sse.onerror = (err: unknown) => {
175
+ scheduleReconnect(err);
176
+ };
177
+ } catch (err) {
178
+ scheduleReconnect(err);
179
+ }
180
+ };
181
+
182
+ if (options.signal) {
183
+ if (options.signal.aborted) {
184
+ closed = true;
185
+ emitStatus('closed');
186
+ return {
187
+ get closed() {
188
+ return closed;
189
+ },
190
+ close,
191
+ };
192
+ }
193
+ abortHandler = close;
194
+ options.signal.addEventListener('abort', abortHandler, { once: true });
195
+ }
196
+
197
+ void connect(false);
198
+
199
+ return {
200
+ get closed() {
201
+ return closed;
202
+ },
203
+ close,
204
+ };
205
+ }
@@ -362,6 +362,61 @@ export class AgentsApi extends ApiTopic {
362
362
  }
363
363
  };
364
364
 
365
+ // SSE reconnection exhausted. The run is durable and still executing
366
+ // server-side — only the live view dropped. Failing fatally here aborts
367
+ // headless consumers (CLI `agents stream`, agent test harnesses) before
368
+ // they can read the result. Instead degrade gracefully: poll GET /updates
369
+ // for any messages we missed and exit cleanly once the run is terminal,
370
+ // even if the terminal message never streamed.
371
+ const POLL_INTERVAL_MS = 5000;
372
+ const isTerminalStatus = (status: unknown): boolean =>
373
+ typeof status === 'string' && ['completed', 'failed', 'cancelled'].includes(status);
374
+ const degradeToPolling = (reason: unknown) => {
375
+ if (isClosed) return;
376
+ cleanup();
377
+ console.warn(
378
+ `Agent stream ${id}: SSE unavailable after ${maxReconnectAttempts} attempts; falling back to /updates polling.`,
379
+ reason instanceof Error ? reason.message : reason,
380
+ );
381
+ const tick = async () => {
382
+ if (isClosed) return;
383
+ try {
384
+ const recent = await this.retrieveMessages(id, lastMessageTimestamp || undefined);
385
+ for (const msg of recent) {
386
+ if (isClosed) return;
387
+ if ((msg.timestamp || 0) <= lastMessageTimestamp) continue;
388
+ lastMessageTimestamp = msg.timestamp || lastMessageTimestamp;
389
+ if (onMessage) onMessage(msg, exit);
390
+ if (isClosed) return;
391
+ if (shouldCloseAgentRunStream(msg, id)) {
392
+ exit(null);
393
+ return;
394
+ }
395
+ }
396
+ } catch {
397
+ // transient poll error — retry next tick
398
+ }
399
+ if (isClosed) return;
400
+ // Exit even when no terminal message ever streams, by reading the run record.
401
+ try {
402
+ const run = (await this.get(`/${id}`)) as { status?: unknown };
403
+ if (isTerminalStatus(run?.status)) {
404
+ exit(null);
405
+ return;
406
+ }
407
+ } catch {
408
+ // ignore — retry next tick
409
+ }
410
+ if (!isClosed) {
411
+ reconnectTimer = setTimeout(() => {
412
+ reconnectTimer = null;
413
+ void tick();
414
+ }, POLL_INTERVAL_MS);
415
+ }
416
+ };
417
+ void tick();
418
+ };
419
+
365
420
  if (signal) {
366
421
  if (signal.aborted) {
367
422
  isClosed = true;
@@ -499,9 +554,7 @@ export class AgentsApi extends ApiTopic {
499
554
  if (!isClosed) void setupStream(true);
500
555
  }, delay);
501
556
  } else {
502
- isClosed = true;
503
- cleanup();
504
- rejectFn(
557
+ degradeToPolling(
505
558
  new Error(`SSE connection failed after ${maxReconnectAttempts} reconnection attempts`),
506
559
  );
507
560
  }
@@ -516,9 +569,7 @@ export class AgentsApi extends ApiTopic {
516
569
  if (!isClosed) void setupStream(true);
517
570
  }, delay);
518
571
  } else {
519
- isClosed = true;
520
- cleanup();
521
- rejectFn(err);
572
+ degradeToPolling(err);
522
573
  }
523
574
  }
524
575
  };
@@ -1,16 +1,10 @@
1
- import { ApiTopic, type ClientBase, type IRequestParams } from '@vertesia/api-fetch-client';
1
+ import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
2
2
  import type {
3
- AdaptedTable,
4
- AdaptTablesRequest,
5
3
  DocAnalyzeRunStatusResponse,
6
- DocAnalyzerResultResponse,
7
- DocImage,
8
- DocTableCsv,
9
- DocTableJson,
10
- ExportTableFormats,
11
- GetAdaptedTablesRequestQuery,
12
- PdfToRichtextOptions,
13
- WorkflowRunStatus,
4
+ DocumentPrepOptions,
5
+ GroundedAssistantResponse,
6
+ GroundedExtractionRequest,
7
+ GroundedExtractionResultResponse,
14
8
  } from '@vertesia/common';
15
9
 
16
10
  export class AnalyzeDocApi extends ApiTopic {
@@ -21,7 +15,7 @@ export class AnalyzeDocApi extends ApiTopic {
21
15
  super(parent, `/${objectId}/analyze`);
22
16
  }
23
17
 
24
- async start(payload: PdfToRichtextOptions): Promise<DocAnalyzeRunStatusResponse> {
18
+ async start(payload: DocumentPrepOptions): Promise<DocAnalyzeRunStatusResponse> {
25
19
  return this.post('/', { payload });
26
20
  }
27
21
 
@@ -29,51 +23,19 @@ export class AnalyzeDocApi extends ApiTopic {
29
23
  return this.get('/status');
30
24
  }
31
25
 
32
- async getResults(): Promise<DocAnalyzerResultResponse> {
33
- return this.get('/results');
26
+ async startGroundedExtraction(payload?: GroundedExtractionRequest): Promise<DocAnalyzeRunStatusResponse> {
27
+ return this.post('/grounded', { payload: payload ?? {} });
34
28
  }
35
29
 
36
- async adaptTables(payload: AdaptTablesRequest): Promise<WorkflowRunStatus> {
37
- return this.post('/adapt_tables', { payload });
30
+ async getGroundedExtractionResult(): Promise<GroundedExtractionResultResponse> {
31
+ return this.get('/grounded/result');
38
32
  }
39
33
 
40
- async getAdaptedTables(
41
- runId?: string,
42
- query?: GetAdaptedTablesRequestQuery,
43
- ): Promise<Record<number, AdaptedTable>> {
44
- const path = runId ? `/adapt_tables/${runId}` : '/adapt_tables';
45
-
46
- // Build query parameters
47
- const queryParams: NonNullable<IRequestParams['query']> = {};
48
- if (query?.format) queryParams.format = query.format;
49
- if (query?.raw !== undefined) queryParams.raw = query.raw;
50
-
51
- // If format is CSV, set response type to text to avoid automatic JSON parsing
52
- const options: IRequestParams & { responseType?: 'text' } = { query: queryParams };
53
- if (query?.format === 'csv') {
54
- options.responseType = 'text';
55
- }
56
-
57
- return this.get(path, options);
58
- }
59
-
60
- async getXml(): Promise<string> {
61
- return this.get('/xml');
62
- }
63
-
64
- async getTables(format?: ExportTableFormats): Promise<DocTableCsv[] | DocTableJson[]> {
65
- const options: IRequestParams = {};
66
- if (format) {
67
- options.query = { format: format };
68
- }
69
- return this.get('/tables', options);
70
- }
71
-
72
- async getImages(): Promise<DocImage[]> {
73
- return this.get('/images');
74
- }
75
-
76
- async getAnnotated(): Promise<{ url: string }> {
77
- return this.get('/annotated');
34
+ /**
35
+ * Launch the interactive grounded extraction assistant for this document.
36
+ * Returns the agent_run_id used to stream/render the conversation.
37
+ */
38
+ async startGroundedAssistant(payload?: GroundedExtractionRequest): Promise<GroundedAssistantResponse> {
39
+ return this.post('/grounded/assistant', { payload: payload ?? {} });
78
40
  }
79
41
  }
@@ -0,0 +1,131 @@
1
+ import type { EventDeliveryStreamSnapshot } from '@vertesia/common';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { VertesiaClient } from '../client.js';
4
+
5
+ type Listener = (event: MessageEvent) => void;
6
+
7
+ class FakeEventSource {
8
+ static readonly CONNECTING = 0;
9
+ static readonly OPEN = 1;
10
+ static readonly CLOSED = 2;
11
+
12
+ static instances: FakeEventSource[] = [];
13
+ static urls: string[] = [];
14
+
15
+ readonly url: string;
16
+ readonly withCredentials = false;
17
+ readyState = FakeEventSource.CONNECTING;
18
+ onopen: ((event: Event) => void) | null = null;
19
+ onmessage: ((event: MessageEvent) => void) | null = null;
20
+ onerror: ((event: Event) => void) | null = null;
21
+ private readonly listeners = new Map<string, Listener[]>();
22
+
23
+ constructor(url: string) {
24
+ this.url = url;
25
+ FakeEventSource.urls.push(url);
26
+ FakeEventSource.instances.push(this);
27
+ }
28
+
29
+ close() {
30
+ this.readyState = FakeEventSource.CLOSED;
31
+ }
32
+
33
+ addEventListener(type: string, listener: Listener) {
34
+ const listeners = this.listeners.get(type) ?? [];
35
+ listeners.push(listener);
36
+ this.listeners.set(type, listeners);
37
+ }
38
+
39
+ removeEventListener() {}
40
+
41
+ dispatchEvent() {
42
+ return true;
43
+ }
44
+
45
+ emit(type: string, data: unknown, lastEventId = '') {
46
+ const event = { data: JSON.stringify(data), lastEventId } as MessageEvent;
47
+ for (const listener of this.listeners.get(type) ?? []) {
48
+ listener(event);
49
+ }
50
+ if (type === 'message') {
51
+ this.onmessage?.(event);
52
+ }
53
+ }
54
+ }
55
+
56
+ function createClient(): VertesiaClient {
57
+ const client = new VertesiaClient({
58
+ serverUrl: 'https://studio.example.test',
59
+ storeUrl: 'https://store.example.test',
60
+ });
61
+ client.withAuthCallback(async () => 'Bearer test-token');
62
+ return client;
63
+ }
64
+
65
+ describe('EventsApi.subscribeDeliveries', () => {
66
+ beforeEach(() => {
67
+ FakeEventSource.instances = [];
68
+ FakeEventSource.urls = [];
69
+ vi.stubGlobal('EventSource', FakeEventSource);
70
+ });
71
+
72
+ afterEach(() => {
73
+ vi.unstubAllGlobals();
74
+ vi.restoreAllMocks();
75
+ });
76
+
77
+ it('serializes stream filters and auth token into the EventSource URL', async () => {
78
+ const client = createClient();
79
+ const subscription = client.events.subscribeDeliveries({
80
+ resource_id: 'agentic-coworker-platform',
81
+ resource_type: ['app', 'workflow_run'],
82
+ event_category: ['workflow', 'system'],
83
+ action: ['create', 'workflow_completed'],
84
+ outbox_status: ['failed'],
85
+ include_event: true,
86
+ poll_interval_ms: 1500,
87
+ on_envelope: () => {},
88
+ });
89
+
90
+ await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
91
+ const url = new URL(FakeEventSource.urls[0]);
92
+
93
+ expect(url.href).toContain('https://store.example.test/api/v1/events/stream');
94
+ expect(url.searchParams.get('access_token')).toBe('test-token');
95
+ expect(url.searchParams.get('resource_id')).toBe('agentic-coworker-platform');
96
+ expect(url.searchParams.get('resource_type')).toBe('app,workflow_run');
97
+ expect(url.searchParams.get('event_category')).toBe('workflow,system');
98
+ expect(url.searchParams.get('action')).toBe('create,workflow_completed');
99
+ expect(url.searchParams.get('outbox_status')).toBe('failed');
100
+ expect(url.searchParams.get('include_event')).toBe('true');
101
+ expect(url.searchParams.get('poll_interval_ms')).toBe('1500');
102
+
103
+ subscription.close();
104
+ });
105
+
106
+ it('dispatches typed snapshot envelopes to the snapshot callback', async () => {
107
+ const client = createClient();
108
+ const snapshots: EventDeliveryStreamSnapshot[] = [];
109
+ const subscription = client.events.subscribeDeliveries({
110
+ resource_id: 'demo-app',
111
+ on_snapshot: (_items, envelope) => snapshots.push(envelope),
112
+ });
113
+
114
+ await vi.waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
115
+ FakeEventSource.instances[0].emit(
116
+ 'snapshot',
117
+ {
118
+ type: 'snapshot',
119
+ emitted_at: '2026-06-27T00:00:00.000Z',
120
+ cursor: 'evt_1',
121
+ deliveries: [],
122
+ } satisfies EventDeliveryStreamSnapshot,
123
+ 'evt_1',
124
+ );
125
+
126
+ expect(snapshots).toHaveLength(1);
127
+ expect(snapshots[0].cursor).toBe('evt_1');
128
+
129
+ subscription.close();
130
+ });
131
+ });