@vertesia/client 1.5.0-dev.20260714.072725Z → 1.5.0-dev.20260717.131047Z
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/lib/AppsApi.d.ts +61 -5
- package/lib/AppsApi.d.ts.map +1 -1
- package/lib/AppsApi.js +100 -4
- package/lib/AppsApi.js.map +1 -1
- package/lib/RunsApi.d.ts +8 -2
- package/lib/RunsApi.d.ts.map +1 -1
- package/lib/RunsApi.js +6 -2
- package/lib/RunsApi.js.map +1 -1
- package/lib/client.d.ts +8 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +17 -0
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/managed-sse.d.ts +27 -0
- package/lib/managed-sse.d.ts.map +1 -0
- package/lib/managed-sse.js +159 -0
- package/lib/managed-sse.js.map +1 -0
- package/lib/store/AgentsApi.d.ts.map +1 -1
- package/lib/store/AgentsApi.js +61 -6
- package/lib/store/AgentsApi.js.map +1 -1
- package/lib/store/EventsApi.d.ts +15 -1
- package/lib/store/EventsApi.d.ts.map +1 -1
- package/lib/store/EventsApi.js +57 -0
- package/lib/store/EventsApi.js.map +1 -1
- package/lib/store/FilesApi.d.ts.map +1 -1
- package/lib/store/FilesApi.js +6 -0
- package/lib/store/FilesApi.js.map +1 -1
- package/lib/store/client.d.ts +3 -1
- package/lib/store/client.d.ts.map +1 -1
- package/lib/store/client.js +11 -0
- package/lib/store/client.js.map +1 -1
- package/lib/store/index.d.ts +1 -0
- package/lib/store/index.d.ts.map +1 -1
- package/lib/store/index.js +1 -0
- package/lib/store/index.js.map +1 -1
- package/lib/store/version.d.ts +1 -0
- package/lib/store/version.d.ts.map +1 -1
- package/lib/store/version.js +1 -0
- package/lib/store/version.js.map +1 -1
- package/lib/vertesia-client.js +10 -10
- package/lib/vertesia-client.js.map +1 -1
- package/package.json +5 -4
- package/src/AppsApi.ts +133 -4
- package/src/RunsApi.ts +12 -2
- package/src/client.ts +18 -2
- package/src/index.ts +1 -0
- package/src/managed-sse.ts +205 -0
- package/src/store/AgentsApi.ts +57 -6
- package/src/store/EventsApi.test.ts +131 -0
- package/src/store/EventsApi.ts +80 -0
- package/src/store/FilesApi.test.ts +69 -0
- package/src/store/FilesApi.ts +8 -1
- package/src/store/client.ts +11 -1
- package/src/store/index.ts +1 -0
- package/src/store/version.ts +1 -0
|
@@ -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
|
+
});
|
package/src/store/EventsApi.ts
CHANGED
|
@@ -2,12 +2,32 @@ import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
|
|
|
2
2
|
import type {
|
|
3
3
|
EventDeliveryQueueSummaryPayload,
|
|
4
4
|
EventDeliveryQueueSummaryResponse,
|
|
5
|
+
EventDeliveryStreamEnvelope,
|
|
6
|
+
EventDeliveryStreamItem,
|
|
7
|
+
EventDeliveryStreamSnapshot,
|
|
8
|
+
EventDeliveryStreamUpdate,
|
|
5
9
|
ListEventDeliveriesPayload,
|
|
6
10
|
ListEventDeliveriesResponse,
|
|
11
|
+
StreamEventDeliveriesQuery,
|
|
7
12
|
} from '@vertesia/common';
|
|
13
|
+
import {
|
|
14
|
+
type ManagedEventSourceConnection,
|
|
15
|
+
type ManagedEventSourceStatus,
|
|
16
|
+
openManagedEventSource,
|
|
17
|
+
} from '../managed-sse.js';
|
|
8
18
|
import { EventIngestChannelsApi } from './EventIngestChannelsApi.js';
|
|
9
19
|
import { EventSubscriptionsApi } from './EventSubscriptionsApi.js';
|
|
10
20
|
|
|
21
|
+
export interface SubscribeEventDeliveriesOptions extends StreamEventDeliveriesQuery {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
max_reconnect_attempts?: number;
|
|
24
|
+
on_snapshot?: (deliveries: EventDeliveryStreamItem[], envelope: EventDeliveryStreamSnapshot) => void;
|
|
25
|
+
on_delivery?: (delivery: EventDeliveryStreamItem, envelope: EventDeliveryStreamUpdate) => void;
|
|
26
|
+
on_envelope?: (envelope: EventDeliveryStreamEnvelope) => void;
|
|
27
|
+
on_error?: (error: unknown) => void;
|
|
28
|
+
on_status?: (status: ManagedEventSourceStatus) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export class EventsApi extends ApiTopic {
|
|
12
32
|
readonly subscriptions: EventSubscriptionsApi;
|
|
13
33
|
readonly channels: EventIngestChannelsApi;
|
|
@@ -25,4 +45,64 @@ export class EventsApi extends ApiTopic {
|
|
|
25
45
|
queueSummary(payload: EventDeliveryQueueSummaryPayload = {}): Promise<EventDeliveryQueueSummaryResponse> {
|
|
26
46
|
return this.post('/deliveries/queue-summary', { payload });
|
|
27
47
|
}
|
|
48
|
+
|
|
49
|
+
subscribeDeliveries(options: SubscribeEventDeliveriesOptions): ManagedEventSourceConnection {
|
|
50
|
+
const { signal, max_reconnect_attempts, on_snapshot, on_delivery, on_envelope, on_error, on_status, ...query } =
|
|
51
|
+
options;
|
|
52
|
+
|
|
53
|
+
return openManagedEventSource<EventDeliveryStreamEnvelope>({
|
|
54
|
+
url: () => this.buildStreamUrl(query),
|
|
55
|
+
event_types: ['snapshot', 'event', 'heartbeat', 'error'],
|
|
56
|
+
signal,
|
|
57
|
+
max_reconnect_attempts,
|
|
58
|
+
last_event_id_query_param: 'since_event_id',
|
|
59
|
+
get_access_token: async () => {
|
|
60
|
+
const client = this.client as ClientBase & { _auth?: () => Promise<string> };
|
|
61
|
+
return client._auth ? await client._auth() : undefined;
|
|
62
|
+
},
|
|
63
|
+
get_cursor: (envelope) => envelope.cursor,
|
|
64
|
+
on_status,
|
|
65
|
+
on_error,
|
|
66
|
+
on_event: ({ data }) => {
|
|
67
|
+
on_envelope?.(data);
|
|
68
|
+
if (data.type === 'snapshot') {
|
|
69
|
+
on_snapshot?.(data.deliveries, data);
|
|
70
|
+
} else if (data.type === 'event') {
|
|
71
|
+
on_delivery?.(data.item, data);
|
|
72
|
+
} else if (data.type === 'error') {
|
|
73
|
+
on_error?.(new Error(data.error));
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private buildStreamUrl(query: StreamEventDeliveriesQuery): URL {
|
|
80
|
+
const url = new URL(`${this.baseUrl}/stream`);
|
|
81
|
+
this.setStreamParam(url, 'limit', query.limit);
|
|
82
|
+
this.setStreamParam(url, 'event_id', query.event_id);
|
|
83
|
+
this.setStreamParam(url, 'resource_id', query.resource_id);
|
|
84
|
+
this.setStreamListParam(url, 'resource_type', query.resource_type);
|
|
85
|
+
this.setStreamListParam(url, 'event_category', query.event_category);
|
|
86
|
+
this.setStreamListParam(url, 'action', query.action);
|
|
87
|
+
this.setStreamListParam(url, 'outbox_status', query.outbox_status);
|
|
88
|
+
this.setStreamParam(url, 'since_event_id', query.since_event_id);
|
|
89
|
+
this.setStreamParam(url, 'since_created_at', query.since_created_at);
|
|
90
|
+
this.setStreamParam(url, 'include_event', query.include_event);
|
|
91
|
+
this.setStreamParam(url, 'poll_interval_ms', query.poll_interval_ms);
|
|
92
|
+
return url;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private setStreamParam(url: URL, key: string, value: string | number | boolean | undefined): void {
|
|
96
|
+
if (value === undefined || value === null || value === '') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
url.searchParams.set(key, String(value));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private setStreamListParam(url: URL, key: string, value: string[] | undefined): void {
|
|
103
|
+
if (!value?.length) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
url.searchParams.set(key, value.join(','));
|
|
107
|
+
}
|
|
28
108
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ZenoClient } from './client.js';
|
|
3
|
+
|
|
4
|
+
describe('FilesApi', () => {
|
|
5
|
+
it('retries signed upload URL creation after a transient connection failure', async () => {
|
|
6
|
+
let attempts = 0;
|
|
7
|
+
const fetchSignedUploadUrl = async (): Promise<Response> => {
|
|
8
|
+
attempts++;
|
|
9
|
+
if (attempts === 1) {
|
|
10
|
+
throw new TypeError('fetch failed');
|
|
11
|
+
}
|
|
12
|
+
return new Response(
|
|
13
|
+
JSON.stringify({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' }),
|
|
14
|
+
{
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { 'content-type': 'application/json' },
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const client = new ZenoClient({
|
|
22
|
+
serverUrl: 'https://store.test',
|
|
23
|
+
apikey: 'token',
|
|
24
|
+
fetch: fetchSignedUploadUrl,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = await client.files.getUploadUrl({
|
|
28
|
+
name: 'conversation.json',
|
|
29
|
+
id: 'agents/run/conversation.json',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' });
|
|
33
|
+
expect(attempts).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('retries signed upload URL creation after a transient 503', async () => {
|
|
37
|
+
let attempts = 0;
|
|
38
|
+
const fetchSignedUploadUrl = async (): Promise<Response> => {
|
|
39
|
+
attempts++;
|
|
40
|
+
if (attempts === 1) {
|
|
41
|
+
return new Response(JSON.stringify({ message: 'try again' }), {
|
|
42
|
+
status: 503,
|
|
43
|
+
headers: { 'content-type': 'application/json' },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return new Response(
|
|
47
|
+
JSON.stringify({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' }),
|
|
48
|
+
{
|
|
49
|
+
status: 200,
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const client = new ZenoClient({
|
|
56
|
+
serverUrl: 'https://store.test',
|
|
57
|
+
apikey: 'token',
|
|
58
|
+
fetch: fetchSignedUploadUrl,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await client.files.getUploadUrl({
|
|
62
|
+
name: 'conversation.json',
|
|
63
|
+
id: 'agents/run/conversation.json',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(result).toEqual({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' });
|
|
67
|
+
expect(attempts).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/store/FilesApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
|
|
1
|
+
import { ApiTopic, type ClientBase, type IRequestRetryPolicy } from '@vertesia/api-fetch-client';
|
|
2
2
|
import type {
|
|
3
3
|
BucketReadAccessStatusResponse,
|
|
4
4
|
BulkUploadUrlsPayload,
|
|
@@ -21,6 +21,12 @@ import { fetchSignedUrl } from './signed-url.js';
|
|
|
21
21
|
export const MEMORIES_PREFIX = 'memories';
|
|
22
22
|
export const ARTIFACTS_PREFIX = 'agents';
|
|
23
23
|
|
|
24
|
+
const FILE_SIGNING_RETRY_POLICY: IRequestRetryPolicy = {
|
|
25
|
+
attempts: 3,
|
|
26
|
+
methods: ['POST'],
|
|
27
|
+
statuses: [502, 503, 504],
|
|
28
|
+
};
|
|
29
|
+
|
|
24
30
|
export function getMemoryFilePath(name: string) {
|
|
25
31
|
const nameWithExt = name.endsWith('.tar.gz') ? name : `${name}.tar.gz`;
|
|
26
32
|
return `${MEMORIES_PREFIX}/${nameWithExt}`;
|
|
@@ -95,6 +101,7 @@ export class FilesApi extends ApiTopic {
|
|
|
95
101
|
getUploadUrl(payload: GetUploadUrlPayload): Promise<GetFileUrlResponse> {
|
|
96
102
|
return this.post('/upload-url', {
|
|
97
103
|
payload,
|
|
104
|
+
retryPolicy: FILE_SIGNING_RETRY_POLICY,
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
107
|
|
package/src/store/client.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type IRequestRetryPolicy,
|
|
5
5
|
type RequestError,
|
|
6
6
|
} from '@vertesia/api-fetch-client';
|
|
7
|
-
import type
|
|
7
|
+
import { APP_VERSION_HEADER, type BulkOperationPayload, type BulkOperationResponse } from '@vertesia/common';
|
|
8
8
|
import { AgentsApi } from './AgentsApi.js';
|
|
9
9
|
import { CollectionsApi } from './CollectionsApi.js';
|
|
10
10
|
import { CostApi } from './CostApi.js';
|
|
@@ -77,6 +77,16 @@ export class ZenoClient extends AbstractFetchClient<ZenoClient> {
|
|
|
77
77
|
return this;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Pin the app version this client's requests resolve against (see VertesiaClient.withAppVersion). */
|
|
81
|
+
withAppVersion(version: string | null | undefined) {
|
|
82
|
+
if (!version) {
|
|
83
|
+
delete this.headers[APP_VERSION_HEADER];
|
|
84
|
+
} else {
|
|
85
|
+
this.headers[APP_VERSION_HEADER] = String(version);
|
|
86
|
+
}
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
withApiKey(apiKey: string | null) {
|
|
81
91
|
return this.withAuthCallback(apiKey ? () => Promise.resolve(`Bearer ${apiKey}`) : undefined);
|
|
82
92
|
}
|
package/src/store/index.ts
CHANGED
package/src/store/version.ts
CHANGED