@vertesia/client 1.5.0-dev.20260725.083715Z → 1.5.0-dev.20260804.124748Z
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/AccountApi.d.ts +10 -7
- package/lib/AccountApi.d.ts.map +1 -1
- package/lib/AccountApi.js +7 -4
- package/lib/AccountApi.js.map +1 -1
- package/lib/ApiKeysApi.d.ts +3 -3
- package/lib/ApiKeysApi.d.ts.map +1 -1
- package/lib/ApiKeysApi.js.map +1 -1
- package/lib/EnvironmentsApi.d.ts +3 -6
- package/lib/EnvironmentsApi.d.ts.map +1 -1
- package/lib/EnvironmentsApi.js.map +1 -1
- package/lib/GroupsApi.d.ts +2 -0
- package/lib/GroupsApi.d.ts.map +1 -1
- package/lib/GroupsApi.js.map +1 -1
- package/lib/InteractionBase.d.ts +686 -2
- package/lib/InteractionBase.d.ts.map +1 -1
- package/lib/OAuthGrantsApi.d.ts +2 -4
- package/lib/OAuthGrantsApi.d.ts.map +1 -1
- package/lib/OAuthGrantsApi.js.map +1 -1
- package/lib/OAuthProvidersApi.d.ts +6 -1
- package/lib/OAuthProvidersApi.d.ts.map +1 -1
- package/lib/OAuthProvidersApi.js +5 -0
- package/lib/OAuthProvidersApi.js.map +1 -1
- package/lib/PromptsApi.d.ts +19 -30
- package/lib/PromptsApi.d.ts.map +1 -1
- package/lib/PromptsApi.js +1 -1
- package/lib/PromptsApi.js.map +1 -1
- package/lib/client.d.ts +10 -2
- package/lib/client.d.ts.map +1 -1
- package/lib/store/AgentsApi.d.ts.map +1 -1
- package/lib/store/AgentsApi.js +121 -77
- package/lib/store/AgentsApi.js.map +1 -1
- package/lib/store/ObjectsApi.d.ts +3 -8
- package/lib/store/ObjectsApi.d.ts.map +1 -1
- package/lib/store/ObjectsApi.js +2 -2
- package/lib/store/ObjectsApi.js.map +1 -1
- package/lib/store/TaskApi.d.ts +9 -0
- package/lib/store/TaskApi.d.ts.map +1 -1
- package/lib/store/TaskApi.js +11 -16
- package/lib/store/TaskApi.js.map +1 -1
- package/lib/store/TypeCatalogApi.d.ts +21 -7
- package/lib/store/TypeCatalogApi.d.ts.map +1 -1
- package/lib/store/TypeCatalogApi.js +16 -2
- package/lib/store/TypeCatalogApi.js.map +1 -1
- package/lib/store/TypesApi.d.ts +2 -2
- package/lib/store/TypesApi.d.ts.map +1 -1
- package/lib/store/TypesApi.js.map +1 -1
- package/lib/store/version.d.ts +1 -2
- package/lib/store/version.d.ts.map +1 -1
- package/lib/store/version.js +3 -2
- 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 -5
- package/src/AccountApi.test.ts +30 -0
- package/src/AccountApi.ts +10 -8
- package/src/ApiKeysApi.ts +4 -3
- package/src/EnvironmentsApi.ts +3 -11
- package/src/GroupsApi.ts +2 -0
- package/src/OAuthGrantsApi.ts +2 -1
- package/src/OAuthProvidersApi.ts +6 -1
- package/src/PromptsApi.ts +22 -25
- package/src/store/AgentsApi.stream.test.ts +298 -0
- package/src/store/AgentsApi.ts +120 -68
- package/src/store/ObjectsApi.ts +8 -8
- package/src/store/TaskApi.ts +11 -11
- package/src/store/TypeCatalogApi.ts +21 -7
- package/src/store/TypesApi.ts +2 -1
- package/src/store/version.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/client",
|
|
3
|
-
"version": "1.5.0-dev.
|
|
3
|
+
"version": "1.5.0-dev.20260804.124748Z",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"eventsource": "^4.1.0",
|
|
22
|
-
"@llumiverse/common": "1.5.0-dev.
|
|
23
|
-
"@vertesia/
|
|
24
|
-
"@vertesia/
|
|
22
|
+
"@llumiverse/common": "1.5.0-dev.20260804.122431Z",
|
|
23
|
+
"@vertesia/common": "1.5.0-dev.20260804.124748Z",
|
|
24
|
+
"@vertesia/api-fetch-client": "1.5.0-dev.20260804.124748Z"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ai",
|
|
49
49
|
"typescript"
|
|
50
50
|
],
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "87ac9166db2b38bd1be6e37adf0414ea83e402ee",
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "biome lint src",
|
|
54
54
|
"clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { VertesiaClient } from './client.js';
|
|
3
|
+
|
|
4
|
+
describe('AccountApi', () => {
|
|
5
|
+
it('returns onboarding.completed_at as the ISO string received over JSON', async () => {
|
|
6
|
+
const completedAt = '2026-07-29T10:00:00.000Z';
|
|
7
|
+
const fetchMock = vi.fn(async () => {
|
|
8
|
+
return new Response(
|
|
9
|
+
JSON.stringify({
|
|
10
|
+
id: 'account-id',
|
|
11
|
+
onboarding: { completed: true, completed_at: completedAt },
|
|
12
|
+
}),
|
|
13
|
+
{
|
|
14
|
+
status: 200,
|
|
15
|
+
headers: { 'content-type': 'application/json' },
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
const client = new VertesiaClient({
|
|
20
|
+
serverUrl: 'https://api.example.com',
|
|
21
|
+
storeUrl: 'https://api.example.com',
|
|
22
|
+
fetch: fetchMock,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const account = await client.account.info();
|
|
26
|
+
|
|
27
|
+
expect(account.onboarding.completed_at).toBe(completedAt);
|
|
28
|
+
expect(account.onboarding.completed_at).not.toBeInstanceOf(Date);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/AccountApi.ts
CHANGED
|
@@ -9,10 +9,9 @@ import type {
|
|
|
9
9
|
OnboardingProgress,
|
|
10
10
|
ProjectRef,
|
|
11
11
|
StripeBillingStatusResponse,
|
|
12
|
-
TransientToken,
|
|
13
12
|
UpdateAccountPayload,
|
|
14
13
|
User,
|
|
15
|
-
|
|
14
|
+
UserInviteToken,
|
|
16
15
|
} from '@vertesia/common';
|
|
17
16
|
|
|
18
17
|
export default class AccountApi extends ApiTopic {
|
|
@@ -56,17 +55,20 @@ export default class AccountApi extends ApiTopic {
|
|
|
56
55
|
|
|
57
56
|
/**
|
|
58
57
|
* Fetch Invites for Principal
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
58
|
+
*
|
|
59
|
+
* Returns `UserInviteToken[]`, not `TransientToken<UserInviteTokenData>[]`. The latter declared
|
|
60
|
+
* `Date` for `expires`, `created_at` and `updated_at`, which no HTTP response can carry — the
|
|
61
|
+
* client hands back whatever `JSON.parse` produced, so those three were always strings. It now
|
|
62
|
+
* says so.
|
|
63
|
+
*/
|
|
64
|
+
listInvites(): Promise<UserInviteToken[]> {
|
|
62
65
|
return this.get('/invites');
|
|
63
66
|
}
|
|
64
67
|
/**
|
|
65
68
|
* Fetch Invites for specific account or project
|
|
66
69
|
* @param type Filter for the type of invitation, either "project" or "account"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
listInvitation(type: 'project' | 'account' = 'project'): Promise<TransientToken<UserInviteTokenData>[]> {
|
|
70
|
+
*/
|
|
71
|
+
listInvitation(type: 'project' | 'account' = 'project'): Promise<UserInviteToken[]> {
|
|
70
72
|
return this.get(`/invites/${type}`);
|
|
71
73
|
}
|
|
72
74
|
|
package/src/ApiKeysApi.ts
CHANGED
|
@@ -4,9 +4,10 @@ import type {
|
|
|
4
4
|
ApiKeyReadResponse,
|
|
5
5
|
ApiKeyWithValue,
|
|
6
6
|
AuthTokenResponse,
|
|
7
|
-
|
|
7
|
+
CreateApiKeyPayload,
|
|
8
8
|
CreatePublicKeyPayload,
|
|
9
9
|
DeleteOperationResult,
|
|
10
|
+
UpdateApiKeyPayload,
|
|
10
11
|
} from '@vertesia/common';
|
|
11
12
|
|
|
12
13
|
export class ApiKeysApi extends ApiTopic {
|
|
@@ -28,7 +29,7 @@ export class ApiKeysApi extends ApiTopic {
|
|
|
28
29
|
* ALL REQUESTS ARE LOGGED IN SECURITY AUDIT LOG
|
|
29
30
|
* @returns ApiKeyWithValue
|
|
30
31
|
*/
|
|
31
|
-
create(payload:
|
|
32
|
+
create(payload: CreateApiKeyPayload): Promise<ApiKeyWithValue> {
|
|
32
33
|
return this.post('/', { payload });
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -36,7 +37,7 @@ export class ApiKeysApi extends ApiTopic {
|
|
|
36
37
|
* Update an existing ApiKey for account
|
|
37
38
|
* @returns ApiKey
|
|
38
39
|
*/
|
|
39
|
-
update(id: string, payload:
|
|
40
|
+
update(id: string, payload: UpdateApiKeyPayload): Promise<ApiKey> {
|
|
40
41
|
return this.put(`/${id}`, { payload });
|
|
41
42
|
}
|
|
42
43
|
|
package/src/EnvironmentsApi.ts
CHANGED
|
@@ -5,11 +5,9 @@ import type {
|
|
|
5
5
|
EmbeddingsApiResult,
|
|
6
6
|
EnableEnvironmentModelPayload,
|
|
7
7
|
ExecutionEnvironment,
|
|
8
|
+
ExecutionEnvironmentConfigUpdatePayload,
|
|
8
9
|
ExecutionEnvironmentCreatePayload,
|
|
9
|
-
ExecutionEnvironmentRef,
|
|
10
10
|
ExecutionEnvironmentUpdatePayload,
|
|
11
|
-
LoadBalancingEnvConfig,
|
|
12
|
-
MediatorEnvConfig,
|
|
13
11
|
MigrateInteractionsPayload,
|
|
14
12
|
MigrateInteractionsResult,
|
|
15
13
|
} from '@vertesia/common';
|
|
@@ -23,7 +21,7 @@ export default class EnvironmentsApi extends ApiTopic {
|
|
|
23
21
|
* List all environments for the current project
|
|
24
22
|
* @param all if true, list all environments, otherwise only the ones for the current project
|
|
25
23
|
*/
|
|
26
|
-
list(all: boolean = false): Promise<
|
|
24
|
+
list(all: boolean = false): Promise<ExecutionEnvironment[]> {
|
|
27
25
|
const query = all ? { all: true } : undefined;
|
|
28
26
|
|
|
29
27
|
return this.get('/', { query });
|
|
@@ -54,13 +52,7 @@ export default class EnvironmentsApi extends ApiTopic {
|
|
|
54
52
|
* @param payload
|
|
55
53
|
* @returns
|
|
56
54
|
*/
|
|
57
|
-
updateConfig(
|
|
58
|
-
id: string,
|
|
59
|
-
payload: {
|
|
60
|
-
enabled_models?: AIModel[];
|
|
61
|
-
config?: MediatorEnvConfig | LoadBalancingEnvConfig;
|
|
62
|
-
},
|
|
63
|
-
): Promise<ExecutionEnvironment> {
|
|
55
|
+
updateConfig(id: string, payload: ExecutionEnvironmentConfigUpdatePayload): Promise<ExecutionEnvironment> {
|
|
64
56
|
return this.put(`/${id}/config`, {
|
|
65
57
|
payload,
|
|
66
58
|
});
|
package/src/GroupsApi.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface GroupsQueryOptions {
|
|
|
12
12
|
tags?: string[];
|
|
13
13
|
limit?: number;
|
|
14
14
|
offset?: number;
|
|
15
|
+
/** Only return groups usable in this project (org-wide + restricted-to-this-project). */
|
|
16
|
+
project?: string;
|
|
15
17
|
[key: string]: string | string[] | number | undefined;
|
|
16
18
|
}
|
|
17
19
|
|
package/src/OAuthGrantsApi.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
OAuthGrant,
|
|
6
6
|
OAuthGrantListResponse,
|
|
7
7
|
OAuthGrantRevokeResponse,
|
|
8
|
+
RevokeOAuthGrantQuery,
|
|
8
9
|
} from '@vertesia/common';
|
|
9
10
|
|
|
10
11
|
function toQueryRecord(query?: ListOAuthGrantsQuery): Record<string, string> | undefined {
|
|
@@ -32,7 +33,7 @@ export default class OAuthGrantsApi extends ApiTopic {
|
|
|
32
33
|
return this.get(`/${grantId}`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
revoke(grantId: string, query?:
|
|
36
|
+
revoke(grantId: string, query?: RevokeOAuthGrantQuery): Promise<OAuthGrantRevokeResponse> {
|
|
36
37
|
return this.del(`/${grantId}`, { query });
|
|
37
38
|
}
|
|
38
39
|
|
package/src/OAuthProvidersApi.ts
CHANGED
|
@@ -30,7 +30,12 @@ export default class OAuthProvidersApi extends ApiTopic {
|
|
|
30
30
|
return this.put(`/${id}`, { payload });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `SuccessResponse`, not `void`: the endpoint has always returned `{ success: true }` and the
|
|
35
|
+
* document has always published it. Typing it as `void` meant a caller could not read the one
|
|
36
|
+
* field it sends without casting.
|
|
37
|
+
*/
|
|
38
|
+
remove(id: string): Promise<SuccessResponse> {
|
|
34
39
|
return this.del(`/${id}`);
|
|
35
40
|
}
|
|
36
41
|
|
package/src/PromptsApi.ts
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
import type { PromptRole } from '@llumiverse/common';
|
|
2
1
|
import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
|
|
3
2
|
import type {
|
|
3
|
+
ComputedFacetResponse,
|
|
4
4
|
ComputePromptFacetPayload,
|
|
5
5
|
PromptSearchPayload,
|
|
6
6
|
PromptSearchQuery,
|
|
7
7
|
PromptTemplate,
|
|
8
8
|
PromptTemplateCreatePayload,
|
|
9
9
|
PromptTemplateForkPayload,
|
|
10
|
+
PromptTemplateInteractionsResponse,
|
|
10
11
|
PromptTemplateRef,
|
|
11
12
|
PromptTemplateUpdatePayload,
|
|
12
|
-
|
|
13
|
+
RenderPromptPayload,
|
|
14
|
+
RenderPromptResponse,
|
|
13
15
|
} from '@vertesia/common';
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Use `ComputedFacetResponse` from `@vertesia/common`, which is what the endpoint
|
|
19
|
+
* publishes. This restated the facet buckets in their pre-collapse shape: `total` is a number by
|
|
20
|
+
* the time the response is written, not a one-element array.
|
|
21
|
+
*/
|
|
22
|
+
export type ComputePromptFacetsResponse = ComputedFacetResponse;
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
name: string;
|
|
23
|
-
role: PromptRole;
|
|
24
|
-
content_type: TemplateType;
|
|
25
|
-
rendered: string;
|
|
26
|
-
}
|
|
24
|
+
/** @deprecated Use `RenderPromptResponse` from `@vertesia/common`. */
|
|
25
|
+
export type PromptRenderResponse = RenderPromptResponse;
|
|
27
26
|
|
|
28
27
|
export default class PromptsApi extends ApiTopic {
|
|
29
28
|
constructor(parent: ClientBase) {
|
|
@@ -48,9 +47,9 @@ export default class PromptsApi extends ApiTopic {
|
|
|
48
47
|
/**
|
|
49
48
|
* Get the list of all prompt facets
|
|
50
49
|
* @param payload query payload to filter facet search
|
|
51
|
-
* @returns
|
|
50
|
+
* @returns ComputedFacetResponse
|
|
52
51
|
**/
|
|
53
|
-
computeFacets(query: ComputePromptFacetPayload): Promise<
|
|
52
|
+
computeFacets(query: ComputePromptFacetPayload): Promise<ComputedFacetResponse> {
|
|
54
53
|
return this.post('/facets', {
|
|
55
54
|
payload: query,
|
|
56
55
|
});
|
|
@@ -125,7 +124,7 @@ export default class PromptsApi extends ApiTopic {
|
|
|
125
124
|
* @throws 404 if not found
|
|
126
125
|
* @throws 403 if the prompt is not in the current project
|
|
127
126
|
*/
|
|
128
|
-
render(id: string, payload:
|
|
127
|
+
render(id: string, payload: RenderPromptPayload): Promise<RenderPromptResponse> {
|
|
129
128
|
return this.post(`/${id}/render`, {
|
|
130
129
|
payload,
|
|
131
130
|
});
|
|
@@ -153,7 +152,7 @@ export default class PromptsApi extends ApiTopic {
|
|
|
153
152
|
/**
|
|
154
153
|
* Retrieve list of interactions that use the prompt template
|
|
155
154
|
*/
|
|
156
|
-
listInteractions(id: string): Promise<
|
|
155
|
+
listInteractions(id: string): Promise<PromptTemplateInteractionsResponse> {
|
|
157
156
|
return this.get(`/${id}/interactions`);
|
|
158
157
|
}
|
|
159
158
|
|
|
@@ -167,11 +166,9 @@ export default class PromptsApi extends ApiTopic {
|
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}[];
|
|
177
|
-
}
|
|
169
|
+
/**
|
|
170
|
+
* @deprecated Use `PromptTemplateInteractionsResponse` from `@vertesia/common`, which is what the
|
|
171
|
+
* endpoint publishes. This declared `versions: string[]`; the endpoint has always sent
|
|
172
|
+
* `[{ version: number }]`.
|
|
173
|
+
*/
|
|
174
|
+
export type ListInteractionsResponse = PromptTemplateInteractionsResponse;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { type AgentMessage, AgentMessageType, type CompactMessage } from '@vertesia/common';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { VertesiaClient } from '../client.js';
|
|
4
|
+
|
|
5
|
+
const AGENT_RUN_ID = 'run-1';
|
|
6
|
+
const STORE_URL = 'https://store.example.test';
|
|
7
|
+
|
|
8
|
+
class FakeEventSource {
|
|
9
|
+
static readonly CONNECTING = 0;
|
|
10
|
+
static readonly OPEN = 1;
|
|
11
|
+
static readonly CLOSED = 2;
|
|
12
|
+
|
|
13
|
+
static instances: FakeEventSource[] = [];
|
|
14
|
+
static urls: string[] = [];
|
|
15
|
+
|
|
16
|
+
readonly url: string;
|
|
17
|
+
readonly withCredentials = false;
|
|
18
|
+
readyState = FakeEventSource.CONNECTING;
|
|
19
|
+
onopen: ((event: Event) => void) | null = null;
|
|
20
|
+
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
21
|
+
onerror: ((event: Event) => void) | null = null;
|
|
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() {}
|
|
34
|
+
removeEventListener() {}
|
|
35
|
+
dispatchEvent() {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fail() {
|
|
40
|
+
this.onerror?.(new Event('error'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
emit(message: CompactMessage) {
|
|
44
|
+
this.onmessage?.({ data: JSON.stringify(message) } as MessageEvent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Harness {
|
|
49
|
+
client: VertesiaClient;
|
|
50
|
+
/** `since` query value of every GET /updates call after the initial history fetch. */
|
|
51
|
+
pollSince: number[];
|
|
52
|
+
setHistory: (messages: CompactMessage[]) => void;
|
|
53
|
+
setPollResponse: (handler: (since: number) => CompactMessage[]) => void;
|
|
54
|
+
setRunStatus: (status: string) => void;
|
|
55
|
+
issuedTokens: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createHarness(): Harness {
|
|
59
|
+
let history: CompactMessage[] = [];
|
|
60
|
+
let pollResponse: (since: number) => CompactMessage[] = () => [];
|
|
61
|
+
let runStatus = 'running';
|
|
62
|
+
let historyServed = false;
|
|
63
|
+
const pollSince: number[] = [];
|
|
64
|
+
const issuedTokens: string[] = [];
|
|
65
|
+
|
|
66
|
+
const fetchMock = vi.fn(async (input: Request | string) => {
|
|
67
|
+
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
68
|
+
if (url.pathname.endsWith('/updates')) {
|
|
69
|
+
if (!historyServed) {
|
|
70
|
+
historyServed = true;
|
|
71
|
+
return Response.json({ messages: history });
|
|
72
|
+
}
|
|
73
|
+
const since = Number(url.searchParams.get('since') ?? 0);
|
|
74
|
+
pollSince.push(since);
|
|
75
|
+
return Response.json({ messages: pollResponse(since) });
|
|
76
|
+
}
|
|
77
|
+
if (url.pathname.endsWith(`/${AGENT_RUN_ID}`)) {
|
|
78
|
+
return Response.json({ id: AGENT_RUN_ID, status: runStatus });
|
|
79
|
+
}
|
|
80
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const client = new VertesiaClient({
|
|
84
|
+
serverUrl: 'https://studio.example.test',
|
|
85
|
+
storeUrl: STORE_URL,
|
|
86
|
+
fetch: fetchMock as unknown as typeof fetch,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Mimics a refreshing credential provider: every call hands out a newer token.
|
|
90
|
+
client.withAuthCallback(async () => {
|
|
91
|
+
const token = `token-${issuedTokens.length + 1}`;
|
|
92
|
+
issuedTokens.push(token);
|
|
93
|
+
return `Bearer ${token}`;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
client,
|
|
98
|
+
pollSince,
|
|
99
|
+
issuedTokens,
|
|
100
|
+
setHistory: (messages) => {
|
|
101
|
+
history = messages;
|
|
102
|
+
},
|
|
103
|
+
setPollResponse: (handler) => {
|
|
104
|
+
pollResponse = handler;
|
|
105
|
+
},
|
|
106
|
+
setRunStatus: (status) => {
|
|
107
|
+
runStatus = status;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Advance fake timers and drain the microtask queue so awaited fetches settle. */
|
|
113
|
+
async function settle(ms = 0) {
|
|
114
|
+
await vi.advanceTimersByTimeAsync(ms);
|
|
115
|
+
for (let i = 0; i < 20; i++) {
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Longer than the capped backoff delay (30s + jitter). */
|
|
121
|
+
const BACKOFF_CEILING_MS = 40000;
|
|
122
|
+
|
|
123
|
+
async function failCurrentConnection() {
|
|
124
|
+
FakeEventSource.instances[FakeEventSource.instances.length - 1].fail();
|
|
125
|
+
await settle(BACKOFF_CEILING_MS);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe('AgentsApi.streamMessages reconnection', () => {
|
|
129
|
+
let logs: string[];
|
|
130
|
+
let warnings: string[];
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
vi.useFakeTimers();
|
|
134
|
+
FakeEventSource.instances = [];
|
|
135
|
+
FakeEventSource.urls = [];
|
|
136
|
+
logs = [];
|
|
137
|
+
warnings = [];
|
|
138
|
+
vi.stubGlobal('EventSource', FakeEventSource);
|
|
139
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
140
|
+
logs.push(args.map(String).join(' '));
|
|
141
|
+
});
|
|
142
|
+
vi.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
|
|
143
|
+
warnings.push(args.map(String).join(' '));
|
|
144
|
+
});
|
|
145
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
vi.unstubAllGlobals();
|
|
150
|
+
vi.restoreAllMocks();
|
|
151
|
+
vi.useRealTimers();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('resolves a fresh auth token for every reconnection attempt', async () => {
|
|
155
|
+
const harness = createHarness();
|
|
156
|
+
const abort = new AbortController();
|
|
157
|
+
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
158
|
+
await settle();
|
|
159
|
+
|
|
160
|
+
expect(FakeEventSource.instances).toHaveLength(1);
|
|
161
|
+
await failCurrentConnection();
|
|
162
|
+
await failCurrentConnection();
|
|
163
|
+
expect(FakeEventSource.instances).toHaveLength(3);
|
|
164
|
+
|
|
165
|
+
const tokens = FakeEventSource.urls.map((url) => new URL(url).searchParams.get('access_token'));
|
|
166
|
+
expect(tokens).toHaveLength(3);
|
|
167
|
+
expect(new Set(tokens).size).toBe(3);
|
|
168
|
+
expect(tokens.every((token) => token?.startsWith('token-'))).toBe(true);
|
|
169
|
+
|
|
170
|
+
abort.abort();
|
|
171
|
+
await settle();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('never announces an attempt number above the stated maximum', async () => {
|
|
175
|
+
const harness = createHarness();
|
|
176
|
+
const abort = new AbortController();
|
|
177
|
+
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
178
|
+
await settle();
|
|
179
|
+
|
|
180
|
+
// 10 reconnects are allowed; the 11th failure must degrade instead of announcing 11/10.
|
|
181
|
+
for (let i = 0; i < 11; i++) {
|
|
182
|
+
await failCurrentConnection();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const announced = logs
|
|
186
|
+
.map((line) => /\(attempt (\d+)\/(\d+)\)/.exec(line))
|
|
187
|
+
.filter((match): match is RegExpExecArray => match !== null)
|
|
188
|
+
.map((match) => ({ attempt: Number(match[1]), max: Number(match[2]) }));
|
|
189
|
+
|
|
190
|
+
expect(announced).toHaveLength(10);
|
|
191
|
+
expect(announced.map((entry) => entry.attempt)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
192
|
+
expect(announced.every((entry) => entry.attempt <= entry.max)).toBe(true);
|
|
193
|
+
expect(FakeEventSource.instances).toHaveLength(11);
|
|
194
|
+
expect(warnings.some((line) => line.includes('SSE unavailable after 10 attempts'))).toBe(true);
|
|
195
|
+
|
|
196
|
+
abort.abort();
|
|
197
|
+
await settle();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('polls GET /updates from the last received timestamp and emits the messages', async () => {
|
|
201
|
+
const harness = createHarness();
|
|
202
|
+
harness.setHistory([{ t: AgentMessageType.UPDATE, m: 'history', ts: 1000 }]);
|
|
203
|
+
harness.setPollResponse((since) =>
|
|
204
|
+
since < 2000 ? [{ t: AgentMessageType.UPDATE, m: 'polled', ts: 2000 }] : [],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const received: AgentMessage[] = [];
|
|
208
|
+
const abort = new AbortController();
|
|
209
|
+
void harness.client.agents.streamMessages(
|
|
210
|
+
AGENT_RUN_ID,
|
|
211
|
+
(message) => received.push(message),
|
|
212
|
+
undefined,
|
|
213
|
+
abort.signal,
|
|
214
|
+
);
|
|
215
|
+
await settle();
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < 11; i++) {
|
|
218
|
+
await failCurrentConnection();
|
|
219
|
+
}
|
|
220
|
+
await settle(6000);
|
|
221
|
+
|
|
222
|
+
// The first poll resumes at the history cursor, later polls at the last polled message.
|
|
223
|
+
expect(harness.pollSince[0]).toBe(1000);
|
|
224
|
+
expect(harness.pollSince[1]).toBe(2000);
|
|
225
|
+
expect(received.map((message) => message.message)).toEqual(['history', 'polled']);
|
|
226
|
+
// Polled messages reach the caller as fully-formed AgentMessages, like the SSE path.
|
|
227
|
+
expect(received[1]).toMatchObject({
|
|
228
|
+
type: AgentMessageType.UPDATE,
|
|
229
|
+
workflow_run_id: AGENT_RUN_ID,
|
|
230
|
+
workstream_id: 'main',
|
|
231
|
+
timestamp: 2000,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
abort.abort();
|
|
235
|
+
await settle();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('warns on every polling failure instead of failing silently', async () => {
|
|
239
|
+
const harness = createHarness();
|
|
240
|
+
harness.setHistory([{ t: AgentMessageType.UPDATE, m: 'history', ts: 1000 }]);
|
|
241
|
+
harness.setPollResponse(() => {
|
|
242
|
+
throw new Error('boom');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const abort = new AbortController();
|
|
246
|
+
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
247
|
+
await settle();
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < 11; i++) {
|
|
250
|
+
await failCurrentConnection();
|
|
251
|
+
}
|
|
252
|
+
await settle(6000);
|
|
253
|
+
|
|
254
|
+
const pollWarnings = warnings.filter((line) => line.includes('GET /updates failed while polling'));
|
|
255
|
+
expect(pollWarnings.length).toBeGreaterThanOrEqual(2);
|
|
256
|
+
expect(pollWarnings[0]).toContain('consecutive failures: 1');
|
|
257
|
+
expect(pollWarnings[1]).toContain('consecutive failures: 2');
|
|
258
|
+
|
|
259
|
+
abort.abort();
|
|
260
|
+
await settle();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('retries SSE after the polling fallback so an outage is not permanent', async () => {
|
|
264
|
+
const harness = createHarness();
|
|
265
|
+
const abort = new AbortController();
|
|
266
|
+
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
267
|
+
await settle();
|
|
268
|
+
|
|
269
|
+
for (let i = 0; i < 11; i++) {
|
|
270
|
+
await failCurrentConnection();
|
|
271
|
+
}
|
|
272
|
+
const connectionsBeforeRetry = FakeEventSource.instances.length;
|
|
273
|
+
expect(warnings.some((line) => line.includes('SSE retried every 60s'))).toBe(true);
|
|
274
|
+
|
|
275
|
+
await settle(61000);
|
|
276
|
+
|
|
277
|
+
expect(FakeEventSource.instances.length).toBe(connectionsBeforeRetry + 1);
|
|
278
|
+
expect(logs.some((line) => line.includes('retrying the SSE connection'))).toBe(true);
|
|
279
|
+
|
|
280
|
+
abort.abort();
|
|
281
|
+
await settle();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('closes the stream when polling observes a terminal run status', async () => {
|
|
285
|
+
const harness = createHarness();
|
|
286
|
+
const abort = new AbortController();
|
|
287
|
+
const done = harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
288
|
+
await settle();
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < 11; i++) {
|
|
291
|
+
await failCurrentConnection();
|
|
292
|
+
}
|
|
293
|
+
harness.setRunStatus('completed');
|
|
294
|
+
await settle(6000);
|
|
295
|
+
|
|
296
|
+
await expect(done).resolves.toBeNull();
|
|
297
|
+
});
|
|
298
|
+
});
|