@vertesia/client 1.4.0-dev.20260615.051508Z → 1.4.0-dev.20260629.130134Z

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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/client",
3
- "version": "1.4.0-dev.20260615.051508Z",
3
+ "version": "1.4.0-dev.20260629.130134Z",
4
4
  "type": "module",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -14,17 +14,17 @@
14
14
  "@rollup/plugin-commonjs": "^29.0.3",
15
15
  "@rollup/plugin-node-resolve": "^16.0.3",
16
16
  "@rollup/plugin-terser": "^1.0.0",
17
- "@types/node": "^24.12.4",
18
- "rollup": "^4.61.0",
17
+ "@types/node": "^24.13.2",
18
+ "rollup": "^4.61.1",
19
19
  "typescript": "^6.0.3",
20
- "vitest": "^4.1.8",
20
+ "vitest": "^4.1.9",
21
21
  "@vertesia/tsconfig": "0.1.0"
22
22
  },
23
23
  "dependencies": {
24
24
  "eventsource": "^4.1.0",
25
- "@llumiverse/common": "1.3.0",
26
- "@vertesia/api-fetch-client": "1.4.0-dev.20260615.051508Z",
27
- "@vertesia/common": "1.4.0-dev.20260615.051508Z"
25
+ "@llumiverse/common": "1.4.0-dev.20260629.090753Z",
26
+ "@vertesia/common": "1.4.0-dev.20260629.130134Z",
27
+ "@vertesia/api-fetch-client": "1.4.0-dev.20260629.130134Z"
28
28
  },
29
29
  "exports": {
30
30
  ".": {
@@ -53,8 +53,9 @@
53
53
  ],
54
54
  "scripts": {
55
55
  "lint": "biome lint src",
56
- "build": "rm -rf ./lib ./tsconfig.tsbuildinfo && tsc && pnpm exec rollup -c",
57
- "clean": "rm -rf ./node_modules ./lib ./tsconfig.tsbuildinfo",
56
+ "clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
57
+ "build": "pnpm run clean:lib && tsc -p tsconfig.json && pnpm exec rollup -c",
58
+ "clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo",
58
59
  "test": "vitest run",
59
60
  "typecheck:test": "tsc -p tsconfig.test.json --noEmit"
60
61
  }
package/src/AppsApi.ts CHANGED
@@ -1,9 +1,5 @@
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,
7
3
  AppInstallation,
8
4
  AppInstallationKind,
9
5
  AppInstallationListEntry,
@@ -11,18 +7,11 @@ import type {
11
7
  AppInstallationWithManifest,
12
8
  AppManifest,
13
9
  AppManifestData,
14
- AppPackage,
15
- AppPackageScope,
16
10
  AppToolCollection,
17
- AppVersionListQuery,
18
- AppVersionRecord,
19
11
  CountResult,
20
12
  ProjectRef,
21
13
  RequireAtLeastOne,
22
- StartAppBuildRequest,
23
- StartAppBuildResponse,
24
14
  UpdateAppInstallationToolAllowlistPayload,
25
- UpsertAppVersionRequest,
26
15
  ValidateUrlRequest,
27
16
  ValidateUrlResponse,
28
17
  } from '@vertesia/common';
@@ -45,55 +34,14 @@ export default class AppsApi extends ApiTopic {
45
34
  }
46
35
 
47
36
  /**
48
- * Preview what the cascade delete would remove. Calls DELETE without confirm,
49
- * which the server treats as a dry-run and returns counts + paths.
37
+ * Delete an app manifest by id. The owning account must permit the operation.
38
+ * Note: also uninstall any existing installations of this manifest before deleting,
39
+ * otherwise the server may refuse or leave orphaned installation records.
50
40
  */
51
- previewDelete(id: string): Promise<AppDeleteSummary> {
41
+ delete(id: string): Promise<CountResult> {
52
42
  return this.del(`/${id}`);
53
43
  }
54
44
 
55
- /**
56
- * Cascade-delete an app and everything attached to it (versions,
57
- * installations + ACEs, git repo on the app-git server). Pass through
58
- * the dry-run summary; confirm flag is required server-side.
59
- */
60
- deleteApp(id: string): Promise<AppDeleteSummary> {
61
- return this.del(`/${id}`, { query: { confirm: 'true' } });
62
- }
63
-
64
- listVersions(query?: AppVersionListQuery): Promise<AppVersionRecord[]> {
65
- return this.get('/versions', {
66
- query: {
67
- ...(query?.app_id && { app_id: query.app_id }),
68
- ...(query?.kind && { kind: query.kind }),
69
- ...(query?.include_expired !== undefined && { include_expired: query.include_expired }),
70
- ...(query?.limit !== undefined && { limit: query.limit }),
71
- },
72
- });
73
- }
74
-
75
- upsertVersion(payload: UpsertAppVersionRequest): Promise<AppVersionRecord> {
76
- return this.post('/versions', { payload });
77
- }
78
-
79
- getVersion(recordId: string): Promise<AppVersionRecord> {
80
- return this.get(`/versions/${recordId}`);
81
- }
82
-
83
- activateVersion(recordId: string): Promise<ActivateAppVersionResponse> {
84
- return this.post(`/versions/${recordId}/activate`);
85
- }
86
-
87
- startBuild(appIdOrRecordId: string, payload: StartAppBuildRequest): Promise<StartAppBuildResponse> {
88
- return this.post(`/${encodeURIComponent(appIdOrRecordId)}/builds`, { payload });
89
- }
90
-
91
- getBuildProgress(appIdOrRecordId: string, workflowId: string, runId: string): Promise<AppBuildProgress> {
92
- return this.get(
93
- `/${encodeURIComponent(appIdOrRecordId)}/builds/${encodeURIComponent(workflowId)}/${encodeURIComponent(runId)}/progress`,
94
- );
95
- }
96
-
97
45
  /**
98
46
  * Get the list if tools provided by the given app.
99
47
  * @param appId
@@ -103,39 +51,6 @@ export default class AppsApi extends ApiTopic {
103
51
  return this.get(`/installations/${appInstallId}/tools`);
104
52
  }
105
53
 
106
- /**
107
- * Inspect an app's registration: resolved manifest state, what the published
108
- * package actually exposes per capability, endpoint reachability, install
109
- * state, and diagnostics. Use this to verify what an app registers vs what it
110
- * declares, instead of inferring it from failed object/import calls.
111
- */
112
- inspect(appIdOrName: string): Promise<AppInspectionResult> {
113
- return this.get(`/${appIdOrName}/inspect`);
114
- }
115
-
116
- /**
117
- * Get package capabilities exposed by an app installation.
118
- */
119
- getAppInstallationPackage(
120
- appInstallId: string,
121
- scope: AppPackageScope | AppPackageScope[] = 'all',
122
- ): Promise<AppPackage> {
123
- return this.get(`/installations/${appInstallId}/package`, {
124
- query: {
125
- scope: Array.isArray(scope) ? scope.join(',') : scope,
126
- },
127
- });
128
- }
129
-
130
- /**
131
- * Fetch the always-on system tools package served by studio-server.
132
- * Tools and skills (`learn_*`) are returned on separate fields so UIs can
133
- * render them distinctly. URLs are already resolved per deployment.
134
- */
135
- getSystemToolsPackage(scope: string = 'tools'): Promise<AppPackage> {
136
- return this.get('/studio-tools/package', { query: { scope } });
137
- }
138
-
139
54
  /**
140
55
  * @param ids - ids to filter by
141
56
  * @returns the app manifests but without the agent.tool property which can be big.
package/src/IamApi.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  DeleteByIdResult,
8
8
  PrincipalIdentity,
9
9
  RoleDefinition,
10
+ SystemRoleDefinition,
10
11
  } from '@vertesia/common';
11
12
  import { GroupsApi } from './GroupsApi.js';
12
13
 
@@ -42,9 +43,19 @@ export class RolesApi extends ApiTopic {
42
43
  super(parent, '/roles');
43
44
  }
44
45
 
46
+ /** List every built-in role across all partitions. Permissions are loosely typed (`string[]`). */
45
47
  list(): Promise<RoleDefinition[]> {
46
48
  return this.get('/');
47
49
  }
50
+
51
+ /**
52
+ * List system-domain roles only. Permissions are tightly typed as `Permission[]` —
53
+ * suited for client-side permission gating (button visibility, route guards).
54
+ * ABAC roles are excluded and must be consumed via the JWT `content_security` claim.
55
+ */
56
+ listSystem(): Promise<SystemRoleDefinition[]> {
57
+ return this.get('/system');
58
+ }
48
59
  }
49
60
 
50
61
  export class AcesApi extends ApiTopic {
@@ -7,6 +7,7 @@ import type {
7
7
  OAuthAuthorizeResponse,
8
8
  OAuthAuthStatus,
9
9
  OAuthMetadataResponse,
10
+ OAuthProviderExchangePayload,
10
11
  } from '@vertesia/common';
11
12
 
12
13
  export default class RemoteMcpConnectionsApi extends ApiTopic {
@@ -30,6 +31,12 @@ export default class RemoteMcpConnectionsApi extends ApiTopic {
30
31
  return this.get(`/authorize/${appInstallId}/${collectionId}`);
31
32
  }
32
33
 
34
+ exchange(code: string, state: string): Promise<McpOAuthConnectResponse> {
35
+ return this.post('/exchange', {
36
+ payload: { code, state } satisfies OAuthProviderExchangePayload,
37
+ });
38
+ }
39
+
33
40
  connect(appInstallId: string, collectionId: string): Promise<McpOAuthConnectResponse> {
34
41
  return this.post(`/connect/${appInstallId}/${collectionId}`, {});
35
42
  }
package/src/RunsApi.ts CHANGED
@@ -90,6 +90,7 @@ export class RunsApi extends ApiTopic {
90
90
 
91
91
  async create<ResultT = unknown, ParamsT = unknown>(
92
92
  payload: RunCreatePayload,
93
+ options?: { timeoutMs?: number | false | null; signal?: AbortSignal },
93
94
  ): Promise<EnhancedExecutionRun<ResultT, ParamsT>> {
94
95
  const sessionTags = (this.client as VertesiaClient).sessionTags;
95
96
  if (sessionTags) {
@@ -103,6 +104,8 @@ export class RunsApi extends ApiTopic {
103
104
  }
104
105
  const r = await this.post<ExecutionRun<ParamsT>>('/', {
105
106
  payload,
107
+ timeoutMs: options?.timeoutMs,
108
+ signal: options?.signal,
106
109
  });
107
110
  return enhanceExecutionRun<ResultT, ParamsT>(r);
108
111
  }
package/src/ToolsApi.ts CHANGED
@@ -13,9 +13,7 @@ import type {
13
13
  * - `GET /api/v1/tools` — list every tool visible to the current principal across all sources
14
14
  * - `POST /api/v1/tools/validate` — resolve a list of names against the same registry
15
15
  *
16
- * Note: this is distinct from `client.store.tools` (the static builtin catalog exposed by zeno-server)
17
- * — that one is global and source-incomplete (no app/interaction tools). Use this client for anything
18
- * project-aware.
16
+ * This is the canonical client for tool discovery and validation.
19
17
  */
20
18
  export default class ToolsApi extends ApiTopic {
21
19
  constructor(parent: ClientBase) {
@@ -42,6 +40,13 @@ export default class ToolsApi extends ApiTopic {
42
40
  return this.get('/', { query: Object.keys(params).length > 0 ? params : undefined });
43
41
  }
44
42
 
43
+ /**
44
+ * List only Vertesia-provided builtin tools and system skills.
45
+ */
46
+ listBuiltins(): Promise<AggregatedTool[]> {
47
+ return this.list({ sources: ['builtin'] });
48
+ }
49
+
45
50
  /**
46
51
  * Validate a list of tool names. Each result reports validity, source, app provenance,
47
52
  * and (for invalid names) a closest-match suggestion.
@@ -59,7 +59,8 @@ describe('Test Vertesia Client', () => {
59
59
  expect(client).toBeDefined();
60
60
  expect(client.baseUrl).toBe('https://api-preview.vertesia.io');
61
61
  expect(client.storeUrl).toBe('https://api-preview.vertesia.io');
62
- expect(client.tokenServerUrl).toBe('https://sts.vertesia.io');
62
+ // preview keeps its env segment: api-preview → sts-preview (its own STS)
63
+ expect(client.tokenServerUrl).toBe('https://sts-preview.vertesia.io');
63
64
  });
64
65
 
65
66
  test('Initialization with site api.dev1.vertesia.io', () => {
@@ -81,7 +82,8 @@ describe('Test Vertesia Client', () => {
81
82
  expect(client).toBeDefined();
82
83
  expect(client.baseUrl).toBe('https://api-preview.dev1.vertesia.io');
83
84
  expect(client.storeUrl).toBe('https://api-preview.dev1.vertesia.io');
84
- expect(client.tokenServerUrl).toBe('https://sts.dev1.vertesia.io');
85
+ // preview keeps its env segment: api-preview → sts-preview (its own STS)
86
+ expect(client.tokenServerUrl).toBe('https://sts-preview.dev1.vertesia.io');
85
87
  });
86
88
 
87
89
  test('Initialization with regional serverUrl (api.us1)', () => {
@@ -116,8 +118,8 @@ describe('Test Vertesia Client', () => {
116
118
 
117
119
  expect(client).toBeDefined();
118
120
  expect(client.baseUrl).toBe('https://api-preview.us1.vertesia.io');
119
- // preview strips -preview., then api → sts: sts.us1.vertesia.io
120
- expect(client.tokenServerUrl).toBe('https://sts.us1.vertesia.io');
121
+ // preview keeps its env segment: api-preview → sts-preview (its own versioned STS)
122
+ expect(client.tokenServerUrl).toBe('https://sts-preview.us1.vertesia.io');
121
123
  });
122
124
 
123
125
  test('Initialization with site localhost', () => {
package/src/client.ts CHANGED
@@ -65,6 +65,12 @@ export type VertesiaClientProps = {
65
65
  onRequest?: (request: Request) => void;
66
66
  onResponse?: (response: Response) => void;
67
67
  retryPolicy?: IRequestRetryPolicy;
68
+ /**
69
+ * Default request timeout in ms applied to every request (studio + store) unless overridden
70
+ * per-request via `timeoutMs`. `false`/`null`/`0` disables it. Aborts the whole request
71
+ * (connection + headers + body) via AbortSignal — browser + Node safe.
72
+ */
73
+ timeout?: number | false | null;
68
74
  fetch?: FETCH_FN | Promise<FETCH_FN>;
69
75
  };
70
76
 
@@ -107,7 +113,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
107
113
  static async fromAuthToken(
108
114
  token: string,
109
115
  payload?: AuthTokenPayload,
110
- endpoints?: { studio: string; store: string; token?: string; git?: string },
116
+ endpoints?: { studio: string; store: string; token?: string },
111
117
  ) {
112
118
  if (!payload) {
113
119
  payload = decodeJWT(token);
@@ -152,14 +158,17 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
152
158
  if (opts.tokenServerUrl) {
153
159
  this.tokenServerUrl = opts.tokenServerUrl;
154
160
  } else if (opts.site) {
155
- // Strip -preview (preview uses the same STS as production for the same region),
156
- // then replace api prefix with sts.
161
+ // Replace the leading api prefix with sts, preserving the env segment so each
162
+ // environment hits its own STS. preview/preprod run a separately versioned STS
163
+ // (sts-preview / sts-preprod); collapsing them onto the production sts.<region>
164
+ // can route to an older STS that rejects scopes the env-matched STS supports
165
+ // (e.g. `offline_access`).
157
166
  // Examples:
158
- // api.vertesia.io -> sts.vertesia.io
159
- // api-preview.vertesia.io -> sts.vertesia.io
160
- // api.us1.vertesia.io -> sts.us1.vertesia.io
161
- // api-preview.eu1.vertesia.io -> sts.eu1.vertesia.io
162
- const stsHost = opts.site.replace('api-preview.', 'api.').replace(/^api/, 'sts');
167
+ // api.vertesia.io -> sts.vertesia.io
168
+ // api-preview.vertesia.io -> sts-preview.vertesia.io
169
+ // api.us1.vertesia.io -> sts.us1.vertesia.io
170
+ // api-preview.eu1.vertesia.io -> sts-preview.eu1.vertesia.io
171
+ const stsHost = opts.site.replace(/^api/, 'sts');
163
172
  this.tokenServerUrl = `https://${stsHost}`;
164
173
  } else if (opts.serverUrl || opts.storeUrl) {
165
174
  // Determine STS URL based on environment in serverUrl or storeUrl
@@ -167,11 +176,12 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
167
176
  try {
168
177
  const url = new URL(urlToCheck);
169
178
  if (url.hostname.startsWith('api')) {
170
- // Strip -preview and replace api with sts.
179
+ // Replace the leading api prefix with sts, preserving the env segment so
180
+ // preview/preprod hit their own STS rather than production's.
171
181
  // api.us1.vertesia.io -> sts.us1.vertesia.io
172
- // api-preview.us1.vertesia.io -> sts.us1.vertesia.io
182
+ // api-preview.us1.vertesia.io -> sts-preview.us1.vertesia.io
173
183
  // api.vertesia.io -> sts.vertesia.io
174
- const stsHost = url.hostname.replace('api-preview.', 'api.').replace(/^api/, 'sts');
184
+ const stsHost = url.hostname.replace(/^api/, 'sts');
175
185
  this.tokenServerUrl = `https://${stsHost}`;
176
186
  } else {
177
187
  this.tokenServerUrl = 'https://sts.dev1.vertesia.io';
@@ -190,6 +200,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
190
200
  onRequest: opts.onRequest,
191
201
  onResponse: opts.onResponse,
192
202
  retryPolicy: opts.retryPolicy,
203
+ timeout: opts.timeout,
193
204
  fetch: opts.fetch,
194
205
  });
195
206
 
@@ -197,6 +208,10 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
197
208
  this.withRetryPolicy(opts.retryPolicy);
198
209
  }
199
210
 
211
+ if (opts.timeout !== undefined) {
212
+ this.withTimeout(opts.timeout);
213
+ }
214
+
200
215
  if (opts.apikey) {
201
216
  void this.withApiKey(opts.apikey);
202
217
  }
@@ -236,6 +251,11 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
236
251
  return super.withRetryPolicy(policy);
237
252
  }
238
253
 
254
+ withTimeout(timeoutMs?: number | false | null) {
255
+ this.store.withTimeout(timeoutMs);
256
+ return super.withTimeout(timeoutMs);
257
+ }
258
+
239
259
  async withApiKey(apiKey: string | null) {
240
260
  return this.withAuthCallback(
241
261
  apiKey
@@ -471,7 +491,6 @@ function getEndpointsFromDomain(domain: string) {
471
491
  studio: `http://localhost:8091`,
472
492
  store: `http://localhost:8092`,
473
493
  token: getRuntimeStsUrl() ?? 'https://sts.dev1.vertesia.io',
474
- git: 'https://git.dev1.vertesia.io',
475
494
  };
476
495
  } else {
477
496
  const url = `https://${domain}`;
@@ -481,7 +500,6 @@ function getEndpointsFromDomain(domain: string) {
481
500
  studio: url,
482
501
  store: url,
483
502
  token: url.replace('api', 'sts'),
484
- git: url.replace('api', 'git'),
485
503
  };
486
504
  }
487
505
  }
package/src/execute.ts CHANGED
@@ -10,6 +10,18 @@ import {
10
10
  } from '@vertesia/common';
11
11
  import type { VertesiaClient } from './client.js';
12
12
 
13
+ /**
14
+ * Client-side timeout for a synchronous (non-streaming) interaction execution. The request blocks on
15
+ * studio-server until the model finishes — and the LLM (e.g. image generation) can legitimately take
16
+ * minutes — so the per-request timeout here is long, overriding any short client default. Override
17
+ * via the VERTESIA_INTERACTION_TIMEOUT_MS env var.
18
+ */
19
+ const INTERACTION_EXECUTION_TIMEOUT_MS = (() => {
20
+ const raw = typeof process !== 'undefined' ? process.env?.VERTESIA_INTERACTION_TIMEOUT_MS : undefined;
21
+ const parsed = raw ? Number.parseInt(raw, 10) : NaN;
22
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 30 * 60 * 1000; // 30 minutes
23
+ })();
24
+
13
25
  export async function EventSourceProvider(): Promise<typeof EventSource> {
14
26
  if (typeof globalThis.EventSource === 'function') {
15
27
  return globalThis.EventSource;
@@ -35,11 +47,16 @@ export async function executeInteraction<P = unknown>(
35
47
  onChunk?: (chunk: string) => void,
36
48
  ): Promise<InteractionExecutionResult<P>> {
37
49
  const stream = !!onChunk;
38
- const response = (await client.runs.create({
39
- ...payload,
40
- interaction: interactionId,
41
- stream,
42
- })) as unknown as InteractionExecutionResult<P>;
50
+ const response = (await client.runs.create(
51
+ {
52
+ ...payload,
53
+ interaction: interactionId,
54
+ stream,
55
+ },
56
+ // Synchronous interaction execution blocks on the model; keep a long client timeout.
57
+ // (Streaming returns the run immediately, then streams over a separate channel — see below.)
58
+ { timeoutMs: stream ? undefined : INTERACTION_EXECUTION_TIMEOUT_MS },
59
+ )) as unknown as InteractionExecutionResult<P>;
43
60
  if (stream) {
44
61
  if (response.status === ExecutionRunStatus.failed) {
45
62
  return response;
@@ -78,6 +95,8 @@ export async function executeInteractionByName<P = unknown>(
78
95
  interaction,
79
96
  stream,
80
97
  } as NamedInteractionExecutionPayload,
98
+ // Synchronous interaction execution blocks on the model; keep a long client timeout.
99
+ timeoutMs: stream ? undefined : INTERACTION_EXECUTION_TIMEOUT_MS,
81
100
  });
82
101
  if (stream) {
83
102
  if (response.status === ExecutionRunStatus.failed) {
@@ -10,7 +10,6 @@ import {
10
10
  type AgentRunResponse,
11
11
  type AgentRunUpdatesResponse,
12
12
  type BindRunWorkflowPayload,
13
- type CompactMessage,
14
13
  type CreateAgentRunPayload,
15
14
  type CreateProcessRunPayload,
16
15
  type ErrorAnalyticsResponse,
@@ -55,6 +54,11 @@ import { EventSourceProvider } from '../execute.js';
55
54
  import { fetchSignedUrl } from './signed-url.js';
56
55
  import { shouldCloseAgentRunStream, shouldCloseCompactRunStream } from './stream-termination.js';
57
56
 
57
+ export interface AgentRunStreamMessagesOptions {
58
+ onHistoryLoaded?: (messages: AgentMessage[]) => void;
59
+ onHistoryError?: (error: unknown) => void;
60
+ }
61
+
58
62
  export class AgentsApi extends ApiTopic {
59
63
  constructor(parent: ClientBase) {
60
64
  super(parent, '/api/v1/agents');
@@ -278,7 +282,7 @@ export class AgentsApi extends ApiTopic {
278
282
  async retrieveMessages(id: string, since?: number): Promise<AgentMessage[]> {
279
283
  const query = since ? { since } : undefined;
280
284
  const response = (await this.get(`/${id}/updates`, { query })) as AgentRunUpdatesResponse;
281
- return response.messages.map((m: CompactMessage) => toAgentMessage(m, id));
285
+ return response.messages.map((m) => toAgentMessage(parseMessage(m), id));
282
286
  }
283
287
 
284
288
  /**
@@ -303,6 +307,7 @@ export class AgentsApi extends ApiTopic {
303
307
  onMessage?: (message: AgentMessage, exitFn?: (payload: unknown) => void) => void,
304
308
  since?: number,
305
309
  signal?: AbortSignal,
310
+ options?: AgentRunStreamMessagesOptions,
306
311
  ): Promise<unknown> {
307
312
  let resolveFn: (value: unknown) => void = () => {};
308
313
  let rejectFn: (reason?: unknown) => void = () => {};
@@ -313,6 +318,7 @@ export class AgentsApi extends ApiTopic {
313
318
 
314
319
  let reconnectAttempts = 0;
315
320
  let lastMessageTimestamp = since || 0;
321
+ const historyFetchStartedAt = Date.now();
316
322
  let isClosed = false;
317
323
  let currentSse: EventSource | null = null;
318
324
  let interval: ReturnType<typeof setInterval> | null = null;
@@ -356,61 +362,6 @@ export class AgentsApi extends ApiTopic {
356
362
  }
357
363
  };
358
364
 
359
- // SSE reconnection exhausted. The run is durable and still executing
360
- // server-side — only the live view dropped. Failing fatally here aborts
361
- // headless consumers (CLI `agents stream`, agent test harnesses) before
362
- // they can read the result. Instead degrade gracefully: poll GET /updates
363
- // for any messages we missed and exit cleanly once the run is terminal,
364
- // even if the terminal message never streamed.
365
- const POLL_INTERVAL_MS = 5000;
366
- const isTerminalStatus = (status: unknown): boolean =>
367
- typeof status === 'string' && ['completed', 'failed', 'cancelled'].includes(status);
368
- const degradeToPolling = (reason: unknown) => {
369
- if (isClosed) return;
370
- cleanup();
371
- console.warn(
372
- `Agent stream ${id}: SSE unavailable after ${maxReconnectAttempts} attempts; falling back to /updates polling.`,
373
- reason instanceof Error ? reason.message : reason,
374
- );
375
- const tick = async () => {
376
- if (isClosed) return;
377
- try {
378
- const recent = await this.retrieveMessages(id, lastMessageTimestamp || undefined);
379
- for (const msg of recent) {
380
- if (isClosed) return;
381
- if ((msg.timestamp || 0) <= lastMessageTimestamp) continue;
382
- lastMessageTimestamp = msg.timestamp || lastMessageTimestamp;
383
- if (onMessage) onMessage(msg, exit);
384
- if (isClosed) return;
385
- if (shouldCloseAgentRunStream(msg, id)) {
386
- exit(null);
387
- return;
388
- }
389
- }
390
- } catch {
391
- // transient poll error — retry next tick
392
- }
393
- if (isClosed) return;
394
- // Exit even when no terminal message ever streams, by reading the run record.
395
- try {
396
- const run = (await this.get(`/${id}`)) as { status?: unknown };
397
- if (isTerminalStatus(run?.status)) {
398
- exit(null);
399
- return;
400
- }
401
- } catch {
402
- // ignore — retry next tick
403
- }
404
- if (!isClosed) {
405
- reconnectTimer = setTimeout(() => {
406
- reconnectTimer = null;
407
- void tick();
408
- }, POLL_INTERVAL_MS);
409
- }
410
- };
411
- void tick();
412
- };
413
-
414
365
  if (signal) {
415
366
  if (signal.aborted) {
416
367
  isClosed = true;
@@ -428,23 +379,35 @@ export class AgentsApi extends ApiTopic {
428
379
  try {
429
380
  if (!isClosed) {
430
381
  const historical = await this.retrieveMessages(id, since);
431
- for (const msg of historical) {
382
+ options?.onHistoryLoaded?.(historical);
383
+ let shouldCloseAfterHistory = false;
384
+ for (let index = 0; index < historical.length; index++) {
385
+ const msg = historical[index];
432
386
  if (isClosed) break;
433
387
  lastMessageTimestamp = Math.max(lastMessageTimestamp, msg.timestamp || 0);
434
388
  if (onMessage) onMessage(msg, exit);
435
389
  if (isClosed) break;
436
390
 
437
- if (shouldCloseAgentRunStream(msg, id)) {
438
- exit(null);
439
- return promise;
440
- }
391
+ shouldCloseAfterHistory = index === historical.length - 1 && shouldCloseAgentRunStream(msg, id);
392
+ }
393
+ if (shouldCloseAfterHistory) {
394
+ exit(null);
395
+ return promise;
441
396
  }
442
397
  }
443
398
  } catch (err) {
444
399
  if (!isClosed) {
400
+ options?.onHistoryError?.(err);
445
401
  console.warn('Failed to fetch historical messages, continuing with SSE:', err);
446
402
  }
447
403
  }
404
+ if (!isClosed && lastMessageTimestamp <= 0) {
405
+ // The server only replays the GET-to-SSE handoff gap when `since > 0`.
406
+ // New runs often have no history yet, so use the GET start time as the
407
+ // cursor to avoid dropping messages emitted before the SSE subscription
408
+ // is active.
409
+ lastMessageTimestamp = Math.max(1, historyFetchStartedAt - 1);
410
+ }
448
411
 
449
412
  // 2. Connect to SSE for real-time updates
450
413
  const setupStream = async (isReconnect: boolean = false) => {
@@ -536,7 +499,9 @@ export class AgentsApi extends ApiTopic {
536
499
  if (!isClosed) void setupStream(true);
537
500
  }, delay);
538
501
  } else {
539
- degradeToPolling(
502
+ isClosed = true;
503
+ cleanup();
504
+ rejectFn(
540
505
  new Error(`SSE connection failed after ${maxReconnectAttempts} reconnection attempts`),
541
506
  );
542
507
  }
@@ -551,7 +516,9 @@ export class AgentsApi extends ApiTopic {
551
516
  if (!isClosed) void setupStream(true);
552
517
  }, delay);
553
518
  } else {
554
- degradeToPolling(err);
519
+ isClosed = true;
520
+ cleanup();
521
+ rejectFn(err);
555
522
  }
556
523
  }
557
524
  };
@@ -12,6 +12,8 @@ import {
12
12
  type DataStoreArchiveResult,
13
13
  type DataStoreDownloadInfo,
14
14
  type DataStoreItem,
15
+ type DataStoreMutateRowsPayload,
16
+ type DataStoreMutateRowsResult,
15
17
  type DataStoreTableDetail,
16
18
  type DataStoreTableDropResult,
17
19
  type DataStoreVersion,
@@ -393,6 +395,20 @@ export class DataApi extends ApiTopic {
393
395
  return this.post(`/${id}/query/batch`, { payload: { queries }, headers: this.storeHeaders(id) });
394
396
  }
395
397
 
398
+ /**
399
+ * Execute a single row mutation statement against the data store.
400
+ *
401
+ * Only UPDATE and DELETE statements are accepted. The mutation is versioned
402
+ * and rolled back automatically if the statement or subsequent persistence fails.
403
+ *
404
+ * @param id - Data store ID
405
+ * @param payload - Mutation SQL and commit message
406
+ * @returns Resulting version ID and affected table row counts
407
+ */
408
+ mutateRows(id: string, payload: DataStoreMutateRowsPayload): Promise<DataStoreMutateRowsResult> {
409
+ return this.post(`/${id}/mutate`, { payload, headers: this.storeHeaders(id) });
410
+ }
411
+
396
412
  /**
397
413
  * Validate SQL queries without executing them.
398
414
  *