@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
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.20260717.131047Z",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/node": "^24.13.2",
|
|
15
|
-
"rolldown": "1.1.
|
|
15
|
+
"rolldown": "1.1.4",
|
|
16
16
|
"typescript": "^6.0.3",
|
|
17
17
|
"vitest": "^4.1.9",
|
|
18
18
|
"@vertesia/tsconfig": "0.1.0"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"eventsource": "^4.1.0",
|
|
22
22
|
"@llumiverse/common": "1.5.0-dev.20260713.114357Z",
|
|
23
|
-
"@vertesia/
|
|
24
|
-
"@vertesia/
|
|
23
|
+
"@vertesia/common": "1.5.0-dev.20260717.131047Z",
|
|
24
|
+
"@vertesia/api-fetch-client": "1.5.0-dev.20260717.131047Z"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"ai",
|
|
49
49
|
"typescript"
|
|
50
50
|
],
|
|
51
|
+
"gitHead": "8aaad228827b2927db8e6aa75b27bfac90fec36f",
|
|
51
52
|
"scripts": {
|
|
52
53
|
"lint": "biome lint src",
|
|
53
54
|
"clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
|
package/src/AppsApi.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ApiTopic, type ClientBase, type ServerError } from '@vertesia/api-fetch-client';
|
|
2
2
|
import type {
|
|
3
|
+
ActivateAppVersionResponse,
|
|
4
|
+
AppBuildProgress,
|
|
5
|
+
AppDeleteSummary,
|
|
6
|
+
AppInspectionResult,
|
|
3
7
|
AppInstallation,
|
|
4
8
|
AppInstallationKind,
|
|
5
9
|
AppInstallationListEntry,
|
|
@@ -7,11 +11,21 @@ import type {
|
|
|
7
11
|
AppInstallationWithManifest,
|
|
8
12
|
AppManifest,
|
|
9
13
|
AppManifestData,
|
|
14
|
+
AppPackage,
|
|
15
|
+
AppPackageScope,
|
|
16
|
+
AppRepoFile,
|
|
17
|
+
AppRepoRefs,
|
|
18
|
+
AppRepoTree,
|
|
10
19
|
AppToolCollection,
|
|
20
|
+
AppVersionListQuery,
|
|
21
|
+
AppVersionRecord,
|
|
11
22
|
CountResult,
|
|
12
23
|
ProjectRef,
|
|
13
24
|
RequireAtLeastOne,
|
|
25
|
+
StartAppBuildRequest,
|
|
26
|
+
StartAppBuildResponse,
|
|
14
27
|
UpdateAppInstallationToolAllowlistPayload,
|
|
28
|
+
UpsertAppVersionRequest,
|
|
15
29
|
ValidateUrlRequest,
|
|
16
30
|
ValidateUrlResponse,
|
|
17
31
|
} from '@vertesia/common';
|
|
@@ -34,14 +48,55 @@ export default class AppsApi extends ApiTopic {
|
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* otherwise the server may refuse or leave orphaned installation records.
|
|
51
|
+
* Preview what the cascade delete would remove. Calls DELETE without confirm,
|
|
52
|
+
* which the server treats as a dry-run and returns counts + paths.
|
|
40
53
|
*/
|
|
41
|
-
|
|
54
|
+
previewDelete(id: string): Promise<AppDeleteSummary> {
|
|
42
55
|
return this.del(`/${id}`);
|
|
43
56
|
}
|
|
44
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Cascade-delete an app and everything attached to it (versions,
|
|
60
|
+
* installations + ACEs, git repo on the app-git server). Pass through
|
|
61
|
+
* the dry-run summary; confirm flag is required server-side.
|
|
62
|
+
*/
|
|
63
|
+
deleteApp(id: string): Promise<AppDeleteSummary> {
|
|
64
|
+
return this.del(`/${id}`, { query: { confirm: 'true' } });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
listVersions(query?: AppVersionListQuery): Promise<AppVersionRecord[]> {
|
|
68
|
+
return this.get('/versions', {
|
|
69
|
+
query: {
|
|
70
|
+
...(query?.app_id && { app_id: query.app_id }),
|
|
71
|
+
...(query?.kind && { kind: query.kind }),
|
|
72
|
+
...(query?.include_expired !== undefined && { include_expired: query.include_expired }),
|
|
73
|
+
...(query?.limit !== undefined && { limit: query.limit }),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
upsertVersion(payload: UpsertAppVersionRequest): Promise<AppVersionRecord> {
|
|
79
|
+
return this.post('/versions', { payload });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getVersion(recordId: string): Promise<AppVersionRecord> {
|
|
83
|
+
return this.get(`/versions/${recordId}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
activateVersion(recordId: string): Promise<ActivateAppVersionResponse> {
|
|
87
|
+
return this.post(`/versions/${recordId}/activate`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
startBuild(appIdOrRecordId: string, payload: StartAppBuildRequest): Promise<StartAppBuildResponse> {
|
|
91
|
+
return this.post(`/${encodeURIComponent(appIdOrRecordId)}/builds`, { payload });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getBuildProgress(appIdOrRecordId: string, workflowId: string, runId: string): Promise<AppBuildProgress> {
|
|
95
|
+
return this.get(
|
|
96
|
+
`/${encodeURIComponent(appIdOrRecordId)}/builds/${encodeURIComponent(workflowId)}/${encodeURIComponent(runId)}/progress`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
45
100
|
/**
|
|
46
101
|
* Get the list if tools provided by the given app.
|
|
47
102
|
* @param appId
|
|
@@ -51,6 +106,80 @@ export default class AppsApi extends ApiTopic {
|
|
|
51
106
|
return this.get(`/installations/${appInstallId}/tools`);
|
|
52
107
|
}
|
|
53
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Inspect an app's registration: resolved manifest state, what the published
|
|
111
|
+
* package actually exposes per capability, endpoint reachability, install
|
|
112
|
+
* state, and diagnostics. Use this to verify what an app registers vs what it
|
|
113
|
+
* declares, instead of inferring it from failed object/import calls.
|
|
114
|
+
*/
|
|
115
|
+
inspect(appIdOrName: string): Promise<AppInspectionResult> {
|
|
116
|
+
return this.get(`/${appIdOrName}/inspect`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List files/directories under a prefix in the app's git repository (default
|
|
121
|
+
* branch unless `ref` is given). Read-only; reads live from the git server
|
|
122
|
+
* without a clone. Used by the Build > Design view to surface generated `docs/`.
|
|
123
|
+
*/
|
|
124
|
+
getRepoTree(appIdOrName: string, options?: { prefix?: string; ref?: string }): Promise<AppRepoTree> {
|
|
125
|
+
return this.get(`/${encodeURIComponent(appIdOrName)}/repo/tree`, {
|
|
126
|
+
query: { prefix: options?.prefix, ref: options?.ref },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read the UTF-8 content of a single file in the app's git repository (default
|
|
132
|
+
* branch unless `ref` is given). Read-only; reads live from the git server.
|
|
133
|
+
*/
|
|
134
|
+
getRepoFile(appIdOrName: string, path: string, options?: { ref?: string }): Promise<AppRepoFile> {
|
|
135
|
+
return this.get(`/${encodeURIComponent(appIdOrName)}/repo/file`, {
|
|
136
|
+
query: { path, ref: options?.ref },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* List the branches and tags of the app's git repository, each resolved to its latest commit.
|
|
142
|
+
* Read-only; reads live from the git server without a clone. Used by the Source view to let a
|
|
143
|
+
* user pick a ref to build from.
|
|
144
|
+
*/
|
|
145
|
+
getRepoRefs(appIdOrName: string): Promise<AppRepoRefs> {
|
|
146
|
+
return this.get(`/${encodeURIComponent(appIdOrName)}/repo/refs`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get the published package an app exposes, filtered by scope. App-owned (in-code) artifacts are
|
|
151
|
+
* package-resolved (no per-id route), so this is how a client reads the full definition — type
|
|
152
|
+
* schema, interaction prompt, process definition, dashboard spec — to visualize an exposed id.
|
|
153
|
+
*/
|
|
154
|
+
getAppPackage(appIdOrName: string, scope: AppPackageScope | AppPackageScope[] = 'all'): Promise<AppPackage> {
|
|
155
|
+
return this.get(`/${encodeURIComponent(appIdOrName)}/package`, {
|
|
156
|
+
query: { scope: Array.isArray(scope) ? scope.join(',') : scope },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get package capabilities exposed by an app installation.
|
|
162
|
+
*/
|
|
163
|
+
getAppInstallationPackage(
|
|
164
|
+
appInstallId: string,
|
|
165
|
+
scope: AppPackageScope | AppPackageScope[] = 'all',
|
|
166
|
+
): Promise<AppPackage> {
|
|
167
|
+
return this.get(`/installations/${appInstallId}/package`, {
|
|
168
|
+
query: {
|
|
169
|
+
scope: Array.isArray(scope) ? scope.join(',') : scope,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fetch the always-on system tools package served by studio-server.
|
|
176
|
+
* Tools and skills (`learn_*`) are returned on separate fields so UIs can
|
|
177
|
+
* render them distinctly. URLs are already resolved per deployment.
|
|
178
|
+
*/
|
|
179
|
+
getSystemToolsPackage(scope: string = 'tools'): Promise<AppPackage> {
|
|
180
|
+
return this.get('/studio-tools/package', { query: { scope } });
|
|
181
|
+
}
|
|
182
|
+
|
|
54
183
|
/**
|
|
55
184
|
* @param ids - ids to filter by
|
|
56
185
|
* @returns the app manifests but without the agent.tool property which can be big.
|
package/src/RunsApi.ts
CHANGED
|
@@ -115,9 +115,14 @@ export class RunsApi extends ApiTopic {
|
|
|
115
115
|
* @param payload
|
|
116
116
|
* @returns
|
|
117
117
|
*/
|
|
118
|
-
sendToolResults(
|
|
118
|
+
sendToolResults(
|
|
119
|
+
payload: ToolResultsPayload,
|
|
120
|
+
options?: { timeoutMs?: number | false | null; signal?: AbortSignal },
|
|
121
|
+
): Promise<ExecutionResponse> {
|
|
119
122
|
return this.post(`/tool-results`, {
|
|
120
123
|
payload,
|
|
124
|
+
timeoutMs: options?.timeoutMs,
|
|
125
|
+
signal: options?.signal,
|
|
121
126
|
});
|
|
122
127
|
}
|
|
123
128
|
|
|
@@ -126,9 +131,14 @@ export class RunsApi extends ApiTopic {
|
|
|
126
131
|
* @param payload
|
|
127
132
|
* @returns
|
|
128
133
|
*/
|
|
129
|
-
sendUserMessage(
|
|
134
|
+
sendUserMessage(
|
|
135
|
+
payload: UserMessagePayload,
|
|
136
|
+
options?: { timeoutMs?: number | false | null; signal?: AbortSignal },
|
|
137
|
+
): Promise<ExecutionResponse> {
|
|
130
138
|
return this.post(`/user-message`, {
|
|
131
139
|
payload,
|
|
140
|
+
timeoutMs: options?.timeoutMs,
|
|
141
|
+
signal: options?.signal,
|
|
132
142
|
});
|
|
133
143
|
}
|
|
134
144
|
|
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
|
|
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';
|
|
@@ -116,7 +116,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
|
|
|
116
116
|
static async fromAuthToken(
|
|
117
117
|
token: string,
|
|
118
118
|
payload?: AuthTokenPayload,
|
|
119
|
-
endpoints?: { studio: string; store: string; token?: string },
|
|
119
|
+
endpoints?: { studio: string; store: string; token?: string; git?: string },
|
|
120
120
|
) {
|
|
121
121
|
if (!payload) {
|
|
122
122
|
payload = decodeJWT(token);
|
|
@@ -240,6 +240,20 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
|
|
|
240
240
|
return this;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Pin the app version this client's requests resolve against. A generated app's UI sets this from
|
|
245
|
+
* its build-time `VITE_APP_VERSION` so studio/zeno resolve its `app:<app>:…` refs against that
|
|
246
|
+
* published version (candidate testing) instead of current. Pass null/empty to clear (→ current).
|
|
247
|
+
*/
|
|
248
|
+
withAppVersion(version: string | null | undefined) {
|
|
249
|
+
if (!version) {
|
|
250
|
+
delete this.headers[APP_VERSION_HEADER];
|
|
251
|
+
} else {
|
|
252
|
+
this.headers[APP_VERSION_HEADER] = String(version);
|
|
253
|
+
}
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
243
257
|
/**
|
|
244
258
|
* Overwrite to keep store and composable clients synchronized on the auth callback
|
|
245
259
|
* @param authCb
|
|
@@ -500,6 +514,7 @@ function getEndpointsFromDomain(domain: string) {
|
|
|
500
514
|
studio: `http://localhost:8091`,
|
|
501
515
|
store: `http://localhost:8092`,
|
|
502
516
|
token: getRuntimeStsUrl() ?? 'https://sts.dev1.vertesia.io',
|
|
517
|
+
git: 'https://git.dev1.vertesia.io',
|
|
503
518
|
};
|
|
504
519
|
} else {
|
|
505
520
|
const url = `https://${domain}`;
|
|
@@ -509,6 +524,7 @@ function getEndpointsFromDomain(domain: string) {
|
|
|
509
524
|
studio: url,
|
|
510
525
|
store: url,
|
|
511
526
|
token: url.replace('api', 'sts'),
|
|
527
|
+
git: url.replace('api', 'git'),
|
|
512
528
|
};
|
|
513
529
|
}
|
|
514
530
|
}
|
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';
|
|
@@ -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
|
+
}
|
package/src/store/AgentsApi.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
520
|
-
cleanup();
|
|
521
|
-
rejectFn(err);
|
|
572
|
+
degradeToPolling(err);
|
|
522
573
|
}
|
|
523
574
|
}
|
|
524
575
|
};
|