@vertesia/client 1.4.0-dev.20260615.051508Z → 1.4.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 (64) hide show
  1. package/README.md +29 -0
  2. package/lib/AppsApi.d.ts +5 -33
  3. package/lib/AppsApi.d.ts.map +1 -1
  4. package/lib/AppsApi.js +4 -63
  5. package/lib/AppsApi.js.map +1 -1
  6. package/lib/IamApi.d.ts +8 -1
  7. package/lib/IamApi.d.ts.map +1 -1
  8. package/lib/IamApi.js +9 -0
  9. package/lib/IamApi.js.map +1 -1
  10. package/lib/RemoteMcpConnectionsApi.d.ts +1 -0
  11. package/lib/RemoteMcpConnectionsApi.d.ts.map +1 -1
  12. package/lib/RemoteMcpConnectionsApi.js +5 -0
  13. package/lib/RemoteMcpConnectionsApi.js.map +1 -1
  14. package/lib/RunsApi.d.ts +4 -1
  15. package/lib/RunsApi.d.ts.map +1 -1
  16. package/lib/RunsApi.js +3 -1
  17. package/lib/RunsApi.js.map +1 -1
  18. package/lib/ToolsApi.d.ts +5 -3
  19. package/lib/ToolsApi.d.ts.map +1 -1
  20. package/lib/ToolsApi.js +7 -3
  21. package/lib/ToolsApi.js.map +1 -1
  22. package/lib/client.d.ts +7 -1
  23. package/lib/client.d.ts.map +1 -1
  24. package/lib/client.js +22 -12
  25. package/lib/client.js.map +1 -1
  26. package/lib/execute.d.ts.map +1 -1
  27. package/lib/execute.js +17 -1
  28. package/lib/execute.js.map +1 -1
  29. package/lib/store/AgentsApi.d.ts +5 -1
  30. package/lib/store/AgentsApi.d.ts.map +1 -1
  31. package/lib/store/AgentsApi.js +26 -68
  32. package/lib/store/AgentsApi.js.map +1 -1
  33. package/lib/store/DataApi.d.ts +12 -1
  34. package/lib/store/DataApi.d.ts.map +1 -1
  35. package/lib/store/DataApi.js +13 -0
  36. package/lib/store/DataApi.js.map +1 -1
  37. package/lib/store/WorkflowsApi.d.ts.map +1 -1
  38. package/lib/store/WorkflowsApi.js +19 -7
  39. package/lib/store/WorkflowsApi.js.map +1 -1
  40. package/lib/store/client.d.ts +1 -2
  41. package/lib/store/client.d.ts.map +1 -1
  42. package/lib/store/client.js +3 -2
  43. package/lib/store/client.js.map +1 -1
  44. package/lib/vertesia-client.js +1 -1
  45. package/lib/vertesia-client.js.map +1 -1
  46. package/package.json +10 -9
  47. package/src/AppsApi.ts +4 -89
  48. package/src/IamApi.ts +11 -0
  49. package/src/RemoteMcpConnectionsApi.ts +7 -0
  50. package/src/RunsApi.ts +3 -0
  51. package/src/ToolsApi.ts +8 -3
  52. package/src/client.test.ts +6 -4
  53. package/src/client.ts +31 -13
  54. package/src/execute.ts +24 -5
  55. package/src/store/AgentsApi.ts +31 -64
  56. package/src/store/DataApi.ts +16 -0
  57. package/src/store/WorkflowsApi.ts +19 -7
  58. package/src/store/client.ts +4 -2
  59. package/src/store/stream-gap.test.ts +230 -0
  60. package/lib/store/ToolsApi.d.ts +0 -14
  61. package/lib/store/ToolsApi.d.ts.map +0 -1
  62. package/lib/store/ToolsApi.js +0 -17
  63. package/lib/store/ToolsApi.js.map +0 -1
  64. package/src/store/ToolsApi.ts +0 -19
@@ -157,8 +157,8 @@ export class WorkflowsApi extends ApiTopic {
157
157
  const response = (await this.get(`/runs/${workflowId}/${runId}/updates`, {
158
158
  query,
159
159
  })) as WorkflowRunUpdatesResponse;
160
- // Convert compact messages to AgentMessage for backward compatibility
161
- return response.messages.map((m: CompactMessage) => toAgentMessage(m, runId));
160
+ // Normalize compact and legacy messages to AgentMessage for backward compatibility.
161
+ return response.messages.map((m) => toAgentMessage(parseMessage(m), runId));
162
162
  }
163
163
 
164
164
  /**
@@ -180,6 +180,7 @@ export class WorkflowsApi extends ApiTopic {
180
180
  return new Promise<unknown>((resolve, reject) => {
181
181
  let reconnectAttempts = 0;
182
182
  let lastMessageTimestamp = since || 0;
183
+ const historyFetchStartedAt = Date.now();
183
184
  let isClosed = false;
184
185
  let currentSse: EventSource | null = null;
185
186
  let interval: ReturnType<typeof setInterval> | null = null;
@@ -374,19 +375,30 @@ export class WorkflowsApi extends ApiTopic {
374
375
  // 1. Fetch historical messages via GET /updates (gzip-compressed if > 3KB)
375
376
  try {
376
377
  const historical = await this.retrieveMessages(workflowId, runId, since);
377
- for (const msg of historical) {
378
+ let shouldCloseAfterHistory = false;
379
+ for (let index = 0; index < historical.length; index++) {
380
+ const msg = historical[index];
378
381
  lastMessageTimestamp = Math.max(lastMessageTimestamp, msg.timestamp || 0);
379
382
  if (onMessage) {
380
383
  onMessage(msg, exit);
381
384
  }
382
- if (shouldCloseAgentRunStream(msg, runId)) {
383
- exit(null);
384
- return;
385
- }
385
+ shouldCloseAfterHistory =
386
+ index === historical.length - 1 && shouldCloseAgentRunStream(msg, runId);
387
+ }
388
+ if (shouldCloseAfterHistory) {
389
+ exit(null);
390
+ return;
386
391
  }
387
392
  } catch (err) {
388
393
  console.warn('Failed to fetch historical messages, continuing with SSE:', err);
389
394
  }
395
+ if (!isClosed && lastMessageTimestamp <= 0) {
396
+ // The server only replays the GET-to-SSE handoff gap when
397
+ // `since > 0`. New runs can have no history at the first
398
+ // fetch, so use the GET start time as the cursor to avoid
399
+ // dropping messages emitted before the SSE subscription is active.
400
+ lastMessageTimestamp = Math.max(1, historyFetchStartedAt - 1);
401
+ }
390
402
 
391
403
  // 2. Connect to SSE
392
404
  void setupStream(false);
@@ -22,7 +22,6 @@ import { QueryApi } from './QueryApi.js';
22
22
  import { RenderingApi } from './RenderingApi.js';
23
23
  import { SchedulesApi } from './SchedulesApi.js';
24
24
  import { TaskApi } from './TaskApi.js';
25
- import { ToolsApi } from './ToolsApi.js';
26
25
  import { TypesApi } from './TypesApi.js';
27
26
  import { VERSION, VERSION_HEADER } from './version.js';
28
27
  import { WorkflowsApi } from './WorkflowsApi.js';
@@ -34,6 +33,7 @@ export interface ZenoClientProps {
34
33
  onRequest?: (request: Request) => void;
35
34
  onResponse?: (response: Response) => void;
36
35
  retryPolicy?: IRequestRetryPolicy;
36
+ timeout?: number | false | null;
37
37
  fetch?: FETCH_FN | Promise<FETCH_FN>;
38
38
  }
39
39
 
@@ -53,6 +53,9 @@ export class ZenoClient extends AbstractFetchClient<ZenoClient> {
53
53
  if (opts.retryPolicy) {
54
54
  this.withRetryPolicy(opts.retryPolicy);
55
55
  }
56
+ if (opts.timeout !== undefined) {
57
+ this.withTimeout(opts.timeout);
58
+ }
56
59
  this.onRequest = opts.onRequest;
57
60
  this.onResponse = opts.onResponse;
58
61
  this.errorFactory = (err: RequestError) => {
@@ -104,7 +107,6 @@ export class ZenoClient extends AbstractFetchClient<ZenoClient> {
104
107
  email = new EmailApi(this);
105
108
  pendingAsks = new PendingAsksApi(this);
106
109
  data = new DataApi(this);
107
- tools = new ToolsApi(this);
108
110
  indexing = new IndexingApi(this);
109
111
  query = new QueryApi(this);
110
112
  hiveMemory = new HiveMemoryApi(this);
@@ -0,0 +1,230 @@
1
+ import { type AgentMessage, AgentMessageType } from '@vertesia/common';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { VertesiaClient } from '../client.js';
4
+
5
+ class FakeEventSource {
6
+ static readonly CONNECTING = 0;
7
+ static readonly OPEN = 1;
8
+ static readonly CLOSED = 2;
9
+
10
+ static urls: string[] = [];
11
+
12
+ readonly url: string;
13
+ readonly withCredentials = false;
14
+ readyState = FakeEventSource.CONNECTING;
15
+ onopen: ((event: Event) => void) | null = null;
16
+ onmessage: ((event: MessageEvent) => void) | null = null;
17
+ onerror: ((event: Event) => void) | null = null;
18
+
19
+ constructor(url: string) {
20
+ this.url = url;
21
+ FakeEventSource.urls.push(url);
22
+ }
23
+
24
+ close() {
25
+ this.readyState = FakeEventSource.CLOSED;
26
+ }
27
+
28
+ addEventListener() {}
29
+
30
+ removeEventListener() {}
31
+
32
+ dispatchEvent() {
33
+ return true;
34
+ }
35
+ }
36
+
37
+ function createClient(): VertesiaClient {
38
+ const client = new VertesiaClient({
39
+ serverUrl: 'https://studio.example.test',
40
+ storeUrl: 'https://store.example.test',
41
+ });
42
+ client.withAuthCallback(async () => 'Bearer test-token');
43
+ return client;
44
+ }
45
+
46
+ function message(timestamp: number): AgentMessage {
47
+ return {
48
+ type: AgentMessageType.THOUGHT,
49
+ timestamp,
50
+ workflow_run_id: 'run-1',
51
+ message: 'historical',
52
+ workstream_id: 'main',
53
+ };
54
+ }
55
+
56
+ function mockApiGet<T extends object>(api: T, response: unknown) {
57
+ return vi
58
+ .spyOn(api as unknown as { get: (path: string, options?: unknown) => Promise<unknown> }, 'get')
59
+ .mockResolvedValue(response);
60
+ }
61
+
62
+ describe('streamMessages GET-to-SSE handoff', () => {
63
+ beforeEach(() => {
64
+ FakeEventSource.urls = [];
65
+ vi.stubGlobal('EventSource', FakeEventSource);
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.unstubAllGlobals();
70
+ vi.restoreAllMocks();
71
+ });
72
+
73
+ it('uses a positive SSE since cursor for an agent run with empty initial history', async () => {
74
+ const client = createClient();
75
+ vi.spyOn(client.agents, 'retrieveMessages').mockResolvedValue([]);
76
+ const abort = new AbortController();
77
+
78
+ const stream = client.agents.streamMessages('agent-run-1', undefined, undefined, abort.signal);
79
+
80
+ await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
81
+
82
+ const streamUrl = new URL(FakeEventSource.urls[0]);
83
+ expect(streamUrl.searchParams.get('skipHistory')).toBe('true');
84
+ expect(Number(streamUrl.searchParams.get('since'))).toBeGreaterThan(0);
85
+
86
+ abort.abort();
87
+ await stream;
88
+ });
89
+
90
+ it('keeps the latest historical timestamp as the SSE since cursor for an agent run', async () => {
91
+ const client = createClient();
92
+ vi.spyOn(client.agents, 'retrieveMessages').mockResolvedValue([message(1_234)]);
93
+ const abort = new AbortController();
94
+
95
+ const stream = client.agents.streamMessages('agent-run-1', undefined, undefined, abort.signal);
96
+
97
+ await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
98
+
99
+ const streamUrl = new URL(FakeEventSource.urls[0]);
100
+ expect(streamUrl.searchParams.get('since')).toBe('1234');
101
+
102
+ abort.abort();
103
+ await stream;
104
+ });
105
+
106
+ it('delivers full agent history before closing on a terminal historical message', async () => {
107
+ const client = createClient();
108
+ const complete = {
109
+ ...message(1_000),
110
+ type: AgentMessageType.COMPLETE,
111
+ message: 'previous turn complete',
112
+ };
113
+ const latest = {
114
+ ...message(2_000),
115
+ message: 'newer historical message',
116
+ };
117
+ vi.spyOn(client.agents, 'retrieveMessages').mockResolvedValue([complete, latest]);
118
+ const delivered: AgentMessage[] = [];
119
+ const abort = new AbortController();
120
+
121
+ const stream = client.agents.streamMessages(
122
+ 'agent-run-1',
123
+ (msg) => {
124
+ delivered.push(msg);
125
+ },
126
+ undefined,
127
+ abort.signal,
128
+ );
129
+
130
+ await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
131
+
132
+ expect(delivered.map((msg) => msg.message)).toEqual(['previous turn complete', 'newer historical message']);
133
+ expect(new URL(FakeEventSource.urls[0]).searchParams.get('since')).toBe('2000');
134
+
135
+ abort.abort();
136
+ await stream;
137
+ });
138
+
139
+ it('normalizes legacy agent history messages before returning them to the UI', async () => {
140
+ const client = createClient();
141
+ mockApiGet(client.agents, {
142
+ messages: [
143
+ {
144
+ type: 8,
145
+ timestamp: 1_000,
146
+ workflow_run_id: 'legacy-run',
147
+ message: 'What are the news headlines in France today?',
148
+ workstream_id: 'main',
149
+ details: { event_class: 'user_content' },
150
+ },
151
+ {
152
+ type: 7,
153
+ timestamp: 2_000,
154
+ workflow_run_id: 'legacy-run',
155
+ message: 'Here are the headlines.',
156
+ workstream_id: 'main',
157
+ details: { streamed: true },
158
+ },
159
+ ],
160
+ });
161
+
162
+ const messages = await client.agents.retrieveMessages('agent-run-1');
163
+
164
+ expect(messages).toEqual([
165
+ expect.objectContaining({
166
+ type: AgentMessageType.QUESTION,
167
+ timestamp: 1_000,
168
+ workflow_run_id: 'agent-run-1',
169
+ message: 'What are the news headlines in France today?',
170
+ details: { event_class: 'user_content' },
171
+ }),
172
+ expect.objectContaining({
173
+ type: AgentMessageType.ANSWER,
174
+ timestamp: 2_000,
175
+ workflow_run_id: 'agent-run-1',
176
+ message: 'Here are the headlines.',
177
+ details: { streamed: true },
178
+ }),
179
+ ]);
180
+ });
181
+
182
+ it('normalizes legacy workflow history messages before returning them to the UI', async () => {
183
+ const client = createClient();
184
+ mockApiGet(client.workflows, {
185
+ messages: [
186
+ {
187
+ type: 'answer',
188
+ timestamp: 3_000,
189
+ workflow_run_id: 'legacy-run',
190
+ message: 'Workflow answer',
191
+ workstream_id: 'main',
192
+ },
193
+ ],
194
+ });
195
+
196
+ const messages = await client.workflows.retrieveMessages('workflow-1', 'workflow-run-1');
197
+
198
+ expect(messages).toEqual([
199
+ expect.objectContaining({
200
+ type: AgentMessageType.ANSWER,
201
+ timestamp: 3_000,
202
+ workflow_run_id: 'workflow-run-1',
203
+ message: 'Workflow answer',
204
+ }),
205
+ ]);
206
+ });
207
+
208
+ it('uses a positive SSE since cursor for a workflow run with empty initial history', async () => {
209
+ const client = createClient();
210
+ vi.spyOn(client.workflows, 'retrieveMessages').mockResolvedValue([]);
211
+ const abort = new AbortController();
212
+
213
+ const stream = client.workflows.streamMessages(
214
+ 'workflow-1',
215
+ 'workflow-run-1',
216
+ undefined,
217
+ undefined,
218
+ abort.signal,
219
+ );
220
+
221
+ await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
222
+
223
+ const streamUrl = new URL(FakeEventSource.urls[0]);
224
+ expect(streamUrl.searchParams.get('skipHistory')).toBe('true');
225
+ expect(Number(streamUrl.searchParams.get('since'))).toBeGreaterThan(0);
226
+
227
+ abort.abort();
228
+ await stream;
229
+ });
230
+ });
@@ -1,14 +0,0 @@
1
- import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
2
- import type { BuiltinToolsCatalogResponse } from '@vertesia/common';
3
- /**
4
- * API for accessing the builtin tools catalog
5
- */
6
- export declare class ToolsApi extends ApiTopic {
7
- constructor(parent: ClientBase);
8
- /**
9
- * Get the builtin tools catalog
10
- * @returns List of all available builtin tools with their descriptions and parameter schemas
11
- */
12
- getBuiltinCatalog(): Promise<BuiltinToolsCatalogResponse>;
13
- }
14
- //# sourceMappingURL=ToolsApi.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ToolsApi.d.ts","sourceRoot":"","sources":["../../src/store/ToolsApi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAEpE;;GAEG;AACH,qBAAa,QAAS,SAAQ,QAAQ;gBACtB,MAAM,EAAE,UAAU;IAI9B;;;OAGG;IACH,iBAAiB,IAAI,OAAO,CAAC,2BAA2B,CAAC;CAG5D"}
@@ -1,17 +0,0 @@
1
- import { ApiTopic } from '@vertesia/api-fetch-client';
2
- /**
3
- * API for accessing the builtin tools catalog
4
- */
5
- export class ToolsApi extends ApiTopic {
6
- constructor(parent) {
7
- super(parent, '/api/v1/tools');
8
- }
9
- /**
10
- * Get the builtin tools catalog
11
- * @returns List of all available builtin tools with their descriptions and parameter schemas
12
- */
13
- getBuiltinCatalog() {
14
- return this.get('/');
15
- }
16
- }
17
- //# sourceMappingURL=ToolsApi.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ToolsApi.js","sourceRoot":"","sources":["../../src/store/ToolsApi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAmB,MAAM,4BAA4B,CAAC;AAGvE;;GAEG;AACH,MAAM,OAAO,QAAS,SAAQ,QAAQ;IAClC,YAAY,MAAkB;QAC1B,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,iBAAiB;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;CACJ"}
@@ -1,19 +0,0 @@
1
- import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
2
- import type { BuiltinToolsCatalogResponse } from '@vertesia/common';
3
-
4
- /**
5
- * API for accessing the builtin tools catalog
6
- */
7
- export class ToolsApi extends ApiTopic {
8
- constructor(parent: ClientBase) {
9
- super(parent, '/api/v1/tools');
10
- }
11
-
12
- /**
13
- * Get the builtin tools catalog
14
- * @returns List of all available builtin tools with their descriptions and parameter schemas
15
- */
16
- getBuiltinCatalog(): Promise<BuiltinToolsCatalogResponse> {
17
- return this.get('/');
18
- }
19
- }