@vertesia/client 1.5.0-dev.20260910.052912Z → 1.6.0-dev.20260912.110338Z
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/DelegationGrantsApi.d.ts +11 -0
- package/lib/DelegationGrantsApi.d.ts.map +1 -0
- package/lib/DelegationGrantsApi.js +22 -0
- package/lib/DelegationGrantsApi.js.map +1 -0
- package/lib/InteractionBase.d.ts +52 -0
- package/lib/InteractionBase.d.ts.map +1 -1
- package/lib/InteractionsApi.d.ts +1 -1
- package/lib/InteractionsApi.d.ts.map +1 -1
- package/lib/InteractionsApi.js +2 -2
- package/lib/InteractionsApi.js.map +1 -1
- package/lib/RunsApi.d.ts +4 -9
- package/lib/RunsApi.d.ts.map +1 -1
- package/lib/RunsApi.js +2 -0
- package/lib/RunsApi.js.map +1 -1
- package/lib/client.d.ts +2 -0
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +2 -0
- package/lib/client.js.map +1 -1
- package/lib/execute.d.ts +2 -1
- package/lib/execute.d.ts.map +1 -1
- package/lib/execute.js +3 -2
- package/lib/execute.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/store/AgentsApi.d.ts +12 -1
- package/lib/store/AgentsApi.d.ts.map +1 -1
- package/lib/store/AgentsApi.js +23 -0
- package/lib/store/AgentsApi.js.map +1 -1
- package/lib/store/ObjectsApi.d.ts.map +1 -1
- package/lib/store/ObjectsApi.js +4 -3
- package/lib/store/ObjectsApi.js.map +1 -1
- package/lib/store/SchedulesApi.d.ts +17 -5
- package/lib/store/SchedulesApi.d.ts.map +1 -1
- package/lib/store/SchedulesApi.js +17 -5
- package/lib/store/SchedulesApi.js.map +1 -1
- package/lib/store/signed-url.d.ts +3 -2
- package/lib/store/signed-url.d.ts.map +1 -1
- package/lib/store/signed-url.js +8 -2
- package/lib/store/signed-url.js.map +1 -1
- package/lib/vertesia-client.js +9 -9
- package/lib/vertesia-client.js.map +1 -1
- package/package.json +9 -9
- package/src/DelegationGrantsApi.ts +22 -0
- package/src/InteractionsApi.ts +2 -0
- package/src/RunsApi.test.ts +24 -0
- package/src/RunsApi.ts +7 -9
- package/src/client.ts +2 -0
- package/src/execute.test.ts +22 -0
- package/src/execute.ts +4 -1
- package/src/index.ts +1 -0
- package/src/store/AgentsApi.history.test.ts +29 -0
- package/src/store/AgentsApi.ts +22 -1
- package/src/store/ObjectsApi.test.ts +50 -0
- package/src/store/ObjectsApi.ts +5 -3
- package/src/store/SchedulesApi.ts +17 -5
- package/src/store/signed-url.test.ts +24 -2
- package/src/store/signed-url.ts +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0-dev.20260912.110338Z",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/node": "^24.13.3",
|
|
15
|
-
"
|
|
15
|
+
"@vertesia/tsconfig": "0.1.0",
|
|
16
|
+
"rolldown": "1.2.6",
|
|
16
17
|
"typescript": "7.0.2",
|
|
17
|
-
"vitest": "^4.1.
|
|
18
|
-
"@vertesia/tsconfig": "0.1.0"
|
|
18
|
+
"vitest": "^4.1.11"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"
|
|
22
|
-
"@vertesia/api-fetch-client": "1.
|
|
23
|
-
"@vertesia/common": "1.
|
|
24
|
-
"
|
|
21
|
+
"@llumiverse/common": "1.6.0-dev.20260912.104205Z",
|
|
22
|
+
"@vertesia/api-fetch-client": "1.6.0-dev.20260912.110338Z",
|
|
23
|
+
"@vertesia/common": "1.6.0-dev.20260912.110338Z",
|
|
24
|
+
"eventsource": "^5.1.1"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ai",
|
|
49
49
|
"typescript"
|
|
50
50
|
],
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "504e8df031c3950ec324c865385caf460b1c1cd7",
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "biome lint src",
|
|
54
54
|
"clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
|
|
2
|
+
import type { CreateDelegationGrantPayload, DelegationGrant } from '@vertesia/common';
|
|
3
|
+
export default class DelegationGrantsApi extends ApiTopic {
|
|
4
|
+
constructor(parent: ClientBase) {
|
|
5
|
+
super(parent, '/api/v1/iam/delegation-grants');
|
|
6
|
+
}
|
|
7
|
+
create(payload: CreateDelegationGrantPayload): Promise<DelegationGrant> {
|
|
8
|
+
return this.post('/', { payload });
|
|
9
|
+
}
|
|
10
|
+
list(): Promise<DelegationGrant[]> {
|
|
11
|
+
return this.get('/');
|
|
12
|
+
}
|
|
13
|
+
mine(): Promise<DelegationGrant[]> {
|
|
14
|
+
return this.get('/me');
|
|
15
|
+
}
|
|
16
|
+
retrieve(id: string): Promise<DelegationGrant> {
|
|
17
|
+
return this.get(`/${id}`);
|
|
18
|
+
}
|
|
19
|
+
revoke(id: string): Promise<DelegationGrant> {
|
|
20
|
+
return this.post(`/${id}/revoke`);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/InteractionsApi.ts
CHANGED
|
@@ -227,12 +227,14 @@ export default class InteractionsApi extends ApiTopic {
|
|
|
227
227
|
nameWithTag: string,
|
|
228
228
|
payload: InteractionExecutionPayload = {},
|
|
229
229
|
onChunk?: (chunk: string) => void,
|
|
230
|
+
requestOptions?: IRequestParams,
|
|
230
231
|
): Promise<EnhancedInteractionExecutionResult<ResultT, ParamsT>> {
|
|
231
232
|
const r = await executeInteractionByName<ParamsT>(
|
|
232
233
|
this.client as VertesiaClient,
|
|
233
234
|
nameWithTag,
|
|
234
235
|
payload,
|
|
235
236
|
onChunk,
|
|
237
|
+
requestOptions,
|
|
236
238
|
).catch((err) => {
|
|
237
239
|
if (err instanceof ServerError && hasIdPayload(err.payload)) {
|
|
238
240
|
throw err.updateDetails({ run_id: err.payload.id });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ToolResultsPayload, UserMessagePayload } from '@vertesia/common';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { VertesiaClient } from './client.js';
|
|
4
|
+
|
|
5
|
+
describe('RunsApi resume request options', () => {
|
|
6
|
+
it('sends private headers on tool-result and user-message resumes', async () => {
|
|
7
|
+
const requests: Request[] = [];
|
|
8
|
+
const client = new VertesiaClient({
|
|
9
|
+
serverUrl: 'https://studio.example.com',
|
|
10
|
+
storeUrl: 'https://zeno.example.com',
|
|
11
|
+
fetch: vi.fn(async () => Response.json({ result: [], prompt: [] })),
|
|
12
|
+
onRequest: (request) => requests.push(request),
|
|
13
|
+
});
|
|
14
|
+
const options = { headers: { 'x-vertesia-required-tool-name': 'write_artifact' } };
|
|
15
|
+
|
|
16
|
+
await client.runs.sendToolResults({} as ToolResultsPayload, options);
|
|
17
|
+
await client.runs.sendUserMessage({} as UserMessagePayload, options);
|
|
18
|
+
|
|
19
|
+
expect(requests.map((request) => request.headers.get('x-vertesia-required-tool-name'))).toEqual([
|
|
20
|
+
'write_artifact',
|
|
21
|
+
'write_artifact',
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/RunsApi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExecutionResponse } from '@llumiverse/common';
|
|
2
|
-
import { ApiTopic, type ClientBase } from '@vertesia/api-fetch-client';
|
|
2
|
+
import { ApiTopic, type ClientBase, type IRequestParams } from '@vertesia/api-fetch-client';
|
|
3
3
|
import type {
|
|
4
4
|
ComputeRunFacetPayload,
|
|
5
5
|
ComputeRunFacetsResponse,
|
|
@@ -34,6 +34,8 @@ export interface FilterOption {
|
|
|
34
34
|
|
|
35
35
|
export type { ComputeRunFacetsResponse } from '@vertesia/common';
|
|
36
36
|
|
|
37
|
+
type ResumeRequestOptions = Pick<IRequestParams, 'headers' | 'signal' | 'timeoutMs'>;
|
|
38
|
+
|
|
37
39
|
export class RunsApi extends ApiTopic {
|
|
38
40
|
constructor(parent: ClientBase) {
|
|
39
41
|
super(parent, '/api/v1/runs');
|
|
@@ -116,12 +118,10 @@ export class RunsApi extends ApiTopic {
|
|
|
116
118
|
* @param payload
|
|
117
119
|
* @returns
|
|
118
120
|
*/
|
|
119
|
-
sendToolResults(
|
|
120
|
-
payload: ToolResultsPayload,
|
|
121
|
-
options?: { timeoutMs?: number | false | null; signal?: AbortSignal },
|
|
122
|
-
): Promise<ExecutionResponse> {
|
|
121
|
+
sendToolResults(payload: ToolResultsPayload, options?: ResumeRequestOptions): Promise<ExecutionResponse> {
|
|
123
122
|
return this.post(`/tool-results`, {
|
|
124
123
|
payload,
|
|
124
|
+
headers: options?.headers,
|
|
125
125
|
timeoutMs: options?.timeoutMs,
|
|
126
126
|
signal: options?.signal,
|
|
127
127
|
});
|
|
@@ -132,12 +132,10 @@ export class RunsApi extends ApiTopic {
|
|
|
132
132
|
* @param payload
|
|
133
133
|
* @returns
|
|
134
134
|
*/
|
|
135
|
-
sendUserMessage(
|
|
136
|
-
payload: UserMessagePayload,
|
|
137
|
-
options?: { timeoutMs?: number | false | null; signal?: AbortSignal },
|
|
138
|
-
): Promise<ExecutionResponse> {
|
|
135
|
+
sendUserMessage(payload: UserMessagePayload, options?: ResumeRequestOptions): Promise<ExecutionResponse> {
|
|
139
136
|
return this.post(`/user-message`, {
|
|
140
137
|
payload,
|
|
138
|
+
headers: options?.headers,
|
|
141
139
|
timeoutMs: options?.timeoutMs,
|
|
142
140
|
signal: options?.signal,
|
|
143
141
|
});
|
package/src/client.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ApiKeysApi } from './ApiKeysApi.js';
|
|
|
7
7
|
import AppsApi from './AppsApi.js';
|
|
8
8
|
import AuditTrailApi from './AuditTrailApi.js';
|
|
9
9
|
import CommandsApi from './CommandsApi.js';
|
|
10
|
+
import DelegationGrantsApi from './DelegationGrantsApi.js';
|
|
10
11
|
import EnvironmentsApi from './EnvironmentsApi.js';
|
|
11
12
|
import { IamApi } from './IamApi.js';
|
|
12
13
|
import InteractionsApi from './InteractionsApi.js';
|
|
@@ -450,6 +451,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
|
|
|
450
451
|
analytics = new AnalyticsApi(this);
|
|
451
452
|
auditTrail = new AuditTrailApi(this);
|
|
452
453
|
training = new TrainingApi(this);
|
|
454
|
+
delegationGrants = new DelegationGrantsApi(this);
|
|
453
455
|
users = new UsersApi(this);
|
|
454
456
|
iam = new IamApi(this);
|
|
455
457
|
refs = new RefsApi(this);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { VertesiaClient } from './client.js';
|
|
3
|
+
import { executeInteractionByName } from './execute.js';
|
|
4
|
+
|
|
5
|
+
describe('executeInteractionByName', () => {
|
|
6
|
+
it('forwards private per-request headers without placing them in the execution payload', async () => {
|
|
7
|
+
const post = vi.fn().mockResolvedValue({ id: 'run-1', status: 'completed' });
|
|
8
|
+
const client = { post } as unknown as VertesiaClient;
|
|
9
|
+
|
|
10
|
+
await executeInteractionByName(client, 'sys:test', { data: { message: 'hello' } }, undefined, {
|
|
11
|
+
headers: { 'x-vertesia-required-tool-name': 'app_workspace_init' },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(post).toHaveBeenCalledWith(
|
|
15
|
+
'/api/v1/execute',
|
|
16
|
+
expect.objectContaining({
|
|
17
|
+
headers: { 'x-vertesia-required-tool-name': 'app_workspace_init' },
|
|
18
|
+
payload: expect.not.objectContaining({ required_tool_name: expect.anything() }),
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/execute.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IRequestParams } from '@vertesia/api-fetch-client';
|
|
1
2
|
import {
|
|
2
3
|
type AsyncExecutionPayload,
|
|
3
4
|
type AsyncExecutionResult,
|
|
@@ -87,16 +88,18 @@ export async function executeInteractionByName<P = unknown>(
|
|
|
87
88
|
interaction: string,
|
|
88
89
|
payload: InteractionExecutionPayload = {},
|
|
89
90
|
onChunk?: (chunk: string) => void,
|
|
91
|
+
requestOptions?: IRequestParams,
|
|
90
92
|
): Promise<InteractionExecutionResult<P>> {
|
|
91
93
|
const stream = !!onChunk;
|
|
92
94
|
const response = await client.post<InteractionExecutionResult<P>>('/api/v1/execute', {
|
|
95
|
+
...requestOptions,
|
|
93
96
|
payload: {
|
|
94
97
|
...payload,
|
|
95
98
|
interaction,
|
|
96
99
|
stream,
|
|
97
100
|
} as NamedInteractionExecutionPayload,
|
|
98
101
|
// Synchronous interaction execution blocks on the model; keep a long client timeout.
|
|
99
|
-
timeoutMs: stream ? undefined : INTERACTION_EXECUTION_TIMEOUT_MS,
|
|
102
|
+
timeoutMs: requestOptions?.timeoutMs ?? (stream ? undefined : INTERACTION_EXECUTION_TIMEOUT_MS),
|
|
100
103
|
});
|
|
101
104
|
if (stream) {
|
|
102
105
|
if (response.status === ExecutionRunStatus.failed) {
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AsyncExecutionResult } from '@vertesia/common';
|
|
|
3
3
|
export type { EnableEnvironmentModelPayload, OrphanedAppInstallation } from '@vertesia/common';
|
|
4
4
|
export { getOAuthPermissionScopes, Permission } from '@vertesia/common';
|
|
5
5
|
export * from './client.js';
|
|
6
|
+
export { default as DelegationGrantsApi } from './DelegationGrantsApi.js';
|
|
6
7
|
export type { GroupsQueryOptions } from './GroupsApi.js';
|
|
7
8
|
export * from './InteractionBase.js';
|
|
8
9
|
export * from './InteractionOutput.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { VertesiaClient } from '../client.js';
|
|
3
|
+
|
|
4
|
+
describe('agent history cursor transport', () => {
|
|
5
|
+
it('passes the cursor and response metadata through for main and child details', async () => {
|
|
6
|
+
const urls: URL[] = [];
|
|
7
|
+
const response = { history: { type: 'agent', mode: 'delta', next_from: 'next', agentTasks: [] } };
|
|
8
|
+
const client = new VertesiaClient({
|
|
9
|
+
serverUrl: 'https://studio.test',
|
|
10
|
+
storeUrl: 'https://store.test',
|
|
11
|
+
fetch: (async (input: Request | string) => {
|
|
12
|
+
urls.push(new URL(typeof input === 'string' ? input : input.url));
|
|
13
|
+
return Response.json(response);
|
|
14
|
+
}) as typeof fetch,
|
|
15
|
+
});
|
|
16
|
+
const options = { includeHistory: true, hydratePayloads: true, from: 'opaque/+=' };
|
|
17
|
+
expect(await client.agents.getRunDetails('agent', options)).toEqual(response);
|
|
18
|
+
expect(await client.agents.getChildDetails('agent', 'child', options)).toEqual(response);
|
|
19
|
+
expect(urls.map((url) => url.pathname)).toEqual([
|
|
20
|
+
'/api/v1/agents/agent/details',
|
|
21
|
+
'/api/v1/agents/agent/children/child/details',
|
|
22
|
+
]);
|
|
23
|
+
for (const url of urls) {
|
|
24
|
+
expect(url.searchParams.get('from')).toBe(options.from);
|
|
25
|
+
expect(url.searchParams.get('include_history')).toBe('true');
|
|
26
|
+
expect(url.searchParams.get('hydrate_payloads')).toBe('true');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/store/AgentsApi.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
type AgentMessage,
|
|
8
8
|
type AgentRun,
|
|
9
9
|
type AgentRunDetailsStreamEvent,
|
|
10
|
+
type AgentRunFeedbackPayload,
|
|
11
|
+
type AgentRunFeedbackResponse,
|
|
10
12
|
type AgentRunInternals,
|
|
11
13
|
type AgentRunResponse,
|
|
12
14
|
type AgentRunUpdatesResponse,
|
|
@@ -165,9 +167,25 @@ export class AgentsApi extends ApiTopic {
|
|
|
165
167
|
if (query?.cursor) params.cursor = query.cursor;
|
|
166
168
|
if (query?.sort) params.sort = query.sort;
|
|
167
169
|
if (query?.order) params.order = query.order;
|
|
170
|
+
if (query?.evaluation_severity?.length) params.evaluation_severity = query.evaluation_severity.join(',');
|
|
171
|
+
if (query?.evaluation_flag?.length) params.evaluation_flag = query.evaluation_flag.join(',');
|
|
172
|
+
if (query?.feedback_rating) params.feedback_rating = query.feedback_rating;
|
|
173
|
+
if (query?.contradicted !== undefined) params.contradicted = String(query.contradicted);
|
|
168
174
|
return params;
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Rate an agent run: thumbs up or down, with an optional reason code and comment.
|
|
179
|
+
*
|
|
180
|
+
* `feedback_id` is the client's idempotency key: a retried request with the same id is a no-op.
|
|
181
|
+
* `status: 'replaced'` means an earlier rating by the same user on the same scope was
|
|
182
|
+
* superseded; `disabled` means this deployment does not record feedback. All answer 200 because
|
|
183
|
+
* a rating is never the user's error.
|
|
184
|
+
*/
|
|
185
|
+
recordFeedback(id: string, payload: AgentRunFeedbackPayload): Promise<AgentRunFeedbackResponse> {
|
|
186
|
+
return this.post(`/${id}/feedback`, { payload });
|
|
187
|
+
}
|
|
188
|
+
|
|
171
189
|
/**
|
|
172
190
|
* Search agent runs via Elasticsearch (full-text + filters).
|
|
173
191
|
*/
|
|
@@ -666,11 +684,13 @@ export class AgentsApi extends ApiTopic {
|
|
|
666
684
|
options?: {
|
|
667
685
|
includeHistory?: boolean;
|
|
668
686
|
hydratePayloads?: boolean;
|
|
687
|
+
from?: string;
|
|
669
688
|
},
|
|
670
689
|
): Promise<WorkflowRunWithDetails> {
|
|
671
690
|
const query: Record<string, string> = {};
|
|
672
691
|
if (options?.includeHistory) query.include_history = 'true';
|
|
673
692
|
if (options?.hydratePayloads) query.hydrate_payloads = 'true';
|
|
693
|
+
if (options?.from !== undefined) query.from = options.from;
|
|
674
694
|
return this.get(`/${id}/details`, { query });
|
|
675
695
|
}
|
|
676
696
|
|
|
@@ -777,11 +797,12 @@ export class AgentsApi extends ApiTopic {
|
|
|
777
797
|
getChildDetails(
|
|
778
798
|
id: string,
|
|
779
799
|
childWorkflowId: string,
|
|
780
|
-
options?: { includeHistory?: boolean; hydratePayloads?: boolean },
|
|
800
|
+
options?: { includeHistory?: boolean; hydratePayloads?: boolean; from?: string },
|
|
781
801
|
): Promise<WorkflowRunWithDetails> {
|
|
782
802
|
const query: Record<string, string> = {};
|
|
783
803
|
if (options?.includeHistory) query.include_history = 'true';
|
|
784
804
|
if (options?.hydratePayloads) query.hydrate_payloads = 'true';
|
|
805
|
+
if (options?.from !== undefined) query.from = options.from;
|
|
785
806
|
return this.get(`/${id}/children/${childWorkflowId}/details`, { query });
|
|
786
807
|
}
|
|
787
808
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ZenoClient } from './client.js';
|
|
3
|
+
|
|
4
|
+
describe('ObjectsApi revision labels', () => {
|
|
5
|
+
it.each(['v1.2', 'Review — 日本語', 'Révision – café', '100% complete & reviewed + approved'])(
|
|
6
|
+
'preserves the revision label %s through the query contract',
|
|
7
|
+
async (revisionLabel) => {
|
|
8
|
+
const requests: Request[] = [];
|
|
9
|
+
const client = new ZenoClient({
|
|
10
|
+
serverUrl: 'https://api.example.com',
|
|
11
|
+
fetch: vi.fn(async () => Response.json({ id: 'document-id' })),
|
|
12
|
+
onRequest: (request) => requests.push(request),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
await client.objects.update(
|
|
16
|
+
'document-id',
|
|
17
|
+
{ name: 'Updated document' },
|
|
18
|
+
{
|
|
19
|
+
createRevision: true,
|
|
20
|
+
revisionLabel,
|
|
21
|
+
ifMatch: 'previous-etag',
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(requests).toHaveLength(1);
|
|
26
|
+
const request = requests[0];
|
|
27
|
+
expect(request.method).toBe('PUT');
|
|
28
|
+
expect(new URL(request.url).searchParams.get('revision_label')).toBe(revisionLabel);
|
|
29
|
+
expect(request.headers.has('x-revision-label')).toBe(false);
|
|
30
|
+
expect(request.headers.get('x-create-revision')).toBe('true');
|
|
31
|
+
expect(request.headers.get('if-match')).toBe('previous-etag');
|
|
32
|
+
expect(await request.clone().json()).toEqual({ name: 'Updated document' });
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
it('does not send a label when updating in place', async () => {
|
|
37
|
+
const requests: Request[] = [];
|
|
38
|
+
const client = new ZenoClient({
|
|
39
|
+
serverUrl: 'https://api.example.com',
|
|
40
|
+
fetch: vi.fn(async () => Response.json({ id: 'document-id' })),
|
|
41
|
+
onRequest: (request) => requests.push(request),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await client.objects.update('document-id', {}, { revisionLabel: 'Unused — label' });
|
|
45
|
+
|
|
46
|
+
expect(new URL(requests[0].url).searchParams.has('revision_label')).toBe(false);
|
|
47
|
+
expect(requests[0].headers.has('x-create-revision')).toBe(false);
|
|
48
|
+
expect(requests[0].headers.has('x-revision-label')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/store/ObjectsApi.ts
CHANGED
|
@@ -437,9 +437,6 @@ export class ObjectsApi extends ApiTopic {
|
|
|
437
437
|
}
|
|
438
438
|
if (options?.createRevision) {
|
|
439
439
|
headers[ContentObjectApiHeaders.CREATE_REVISION] = 'true';
|
|
440
|
-
if (options.revisionLabel) {
|
|
441
|
-
headers[ContentObjectApiHeaders.REVISION_LABEL] = options.revisionLabel;
|
|
442
|
-
}
|
|
443
440
|
}
|
|
444
441
|
if (options?.suppressWorkflows) {
|
|
445
442
|
headers[ContentObjectApiHeaders.SUPPRESS_WORKFLOWS] = 'true';
|
|
@@ -447,6 +444,11 @@ export class ObjectsApi extends ApiTopic {
|
|
|
447
444
|
|
|
448
445
|
return this.put(`/${id}`, {
|
|
449
446
|
payload: updatePayload,
|
|
447
|
+
// The existing query contract preserves Unicode labels; HTTP headers only accept ByteString values.
|
|
448
|
+
query:
|
|
449
|
+
options?.createRevision && options.revisionLabel
|
|
450
|
+
? { revision_label: options.revisionLabel }
|
|
451
|
+
: undefined,
|
|
450
452
|
headers,
|
|
451
453
|
});
|
|
452
454
|
}
|
|
@@ -8,9 +8,9 @@ import type {
|
|
|
8
8
|
} from '@vertesia/common';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Client API for managing
|
|
11
|
+
* Client API for managing recurring schedules.
|
|
12
12
|
*
|
|
13
|
-
* Schedules allow agents to run on a recurring basis using cron expressions.
|
|
13
|
+
* Schedules allow agents or processes to run on a recurring basis using cron expressions.
|
|
14
14
|
*/
|
|
15
15
|
export class SchedulesApi extends ApiTopic {
|
|
16
16
|
constructor(parent: ClientBase) {
|
|
@@ -35,7 +35,7 @@ export class SchedulesApi extends ApiTopic {
|
|
|
35
35
|
/**
|
|
36
36
|
* Create a new schedule.
|
|
37
37
|
*
|
|
38
|
-
* @param payload -
|
|
38
|
+
* @param payload - Agent or process schedule configuration
|
|
39
39
|
* @returns The created schedule
|
|
40
40
|
*
|
|
41
41
|
* @example
|
|
@@ -48,6 +48,18 @@ export class SchedulesApi extends ApiTopic {
|
|
|
48
48
|
* vars: { reportType: 'summary' }
|
|
49
49
|
* });
|
|
50
50
|
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const schedule = await client.schedules.create({
|
|
55
|
+
* name: 'Nightly Refresh',
|
|
56
|
+
* target: 'process',
|
|
57
|
+
* process: '68c01a23456789abcdef0123',
|
|
58
|
+
* cron_expression: '0 2 * * *',
|
|
59
|
+
* timezone: 'UTC',
|
|
60
|
+
* context: { collection: 'reports' }
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
51
63
|
*/
|
|
52
64
|
create(payload: CreateSchedulePayload): Promise<AgentSchedule> {
|
|
53
65
|
return this.post('/', { payload });
|
|
@@ -77,9 +89,9 @@ export class SchedulesApi extends ApiTopic {
|
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
/**
|
|
80
|
-
* Trigger an immediate execution of the scheduled
|
|
92
|
+
* Trigger an immediate execution of the scheduled workflow.
|
|
81
93
|
*
|
|
82
|
-
* This runs the agent immediately without waiting for the next scheduled time.
|
|
94
|
+
* This runs the agent or process immediately without waiting for the next scheduled time.
|
|
83
95
|
*
|
|
84
96
|
* @param id - Schedule ID
|
|
85
97
|
* @returns Success message
|
|
@@ -173,10 +173,32 @@ describe('fetchSignedUrl', () => {
|
|
|
173
173
|
|
|
174
174
|
it('honors a numeric Retry-After header when scheduling the retry', async () => {
|
|
175
175
|
fetchMock
|
|
176
|
-
.mockResolvedValueOnce(response(503, 'slow down', { 'retry-after': '
|
|
176
|
+
.mockResolvedValueOnce(response(503, 'slow down', { 'retry-after': '30' }))
|
|
177
177
|
.mockResolvedValueOnce(response(200, 'ok'));
|
|
178
|
-
const
|
|
178
|
+
const pending = fetchSignedUrl('https://storage/x');
|
|
179
|
+
await vi.advanceTimersByTimeAsync(29999);
|
|
180
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
181
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
182
|
+
const res = await pending;
|
|
179
183
|
expect(res.status).toBe(200);
|
|
180
184
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
181
185
|
});
|
|
182
186
|
});
|
|
187
|
+
|
|
188
|
+
describe('Retry-After request budget', () => {
|
|
189
|
+
it.each(['3600', '2147484', '1e308', new Date(Date.now() + 3600000).toUTCString()])(
|
|
190
|
+
'returns the intact response without an early retry for %s',
|
|
191
|
+
async (retryAfter) => {
|
|
192
|
+
const res = new Response('retry later', { status: 503, headers: { 'Retry-After': retryAfter } });
|
|
193
|
+
const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(res);
|
|
194
|
+
try {
|
|
195
|
+
const result = await fetchSignedUrl('https://storage/x');
|
|
196
|
+
expect(result).toBe(res);
|
|
197
|
+
expect(await result.text()).toBe('retry later');
|
|
198
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
199
|
+
} finally {
|
|
200
|
+
fetch.mockRestore();
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
});
|
package/src/store/signed-url.ts
CHANGED
|
@@ -20,6 +20,7 @@ const DEFAULT_RETRYABLE_STATUSES: ReadonlySet<number> = new Set([429, 500, 502,
|
|
|
20
20
|
const DEFAULT_ATTEMPTS = 4;
|
|
21
21
|
const DEFAULT_BASE_DELAY_MS = 500;
|
|
22
22
|
const DEFAULT_MAX_DELAY_MS = 8_000;
|
|
23
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
23
24
|
|
|
24
25
|
export interface SignedUrlFetchOptions {
|
|
25
26
|
method?: string;
|
|
@@ -29,7 +30,7 @@ export interface SignedUrlFetchOptions {
|
|
|
29
30
|
attempts?: number;
|
|
30
31
|
/** Base delay for exponential backoff, in milliseconds. Defaults to 500. */
|
|
31
32
|
baseDelayMs?: number;
|
|
32
|
-
/** Upper bound for
|
|
33
|
+
/** Upper bound for exponential backoff, in milliseconds. Defaults to 8000. Retry-After takes precedence. */
|
|
33
34
|
maxDelayMs?: number;
|
|
34
35
|
/** HTTP statuses to retry on. Defaults to 429, 500, 502, 503, 504. */
|
|
35
36
|
retryableStatuses?: ReadonlySet<number>;
|
|
@@ -58,7 +59,7 @@ function retryAfterMs(res: Response): number | undefined {
|
|
|
58
59
|
function backoffMs(attempt: number, baseDelayMs: number, maxDelayMs: number, res?: Response): number {
|
|
59
60
|
const retryAfter = res ? retryAfterMs(res) : undefined;
|
|
60
61
|
if (retryAfter !== undefined) {
|
|
61
|
-
return
|
|
62
|
+
return retryAfter;
|
|
62
63
|
}
|
|
63
64
|
// Exponential backoff with full jitter.
|
|
64
65
|
const capped = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
|
|
@@ -141,7 +142,8 @@ async function toReplayableBody(body: BodyInit | null | undefined): Promise<Body
|
|
|
141
142
|
/**
|
|
142
143
|
* `fetch()` a cloud-storage signed URL, retrying transient failures (connection
|
|
143
144
|
* errors and retryable HTTP statuses) with exponential backoff. Honors a
|
|
144
|
-
* `Retry-After` response header
|
|
145
|
+
* `Retry-After` response header up to 60 seconds. Longer delays return the response
|
|
146
|
+
* without retrying so the caller can schedule recovery outside this request.
|
|
145
147
|
*
|
|
146
148
|
* The returned `Response` is only guaranteed to be retried while it carries a
|
|
147
149
|
* retryable status; a non-retryable error response (e.g. 403, 404) is returned
|
|
@@ -167,6 +169,9 @@ export async function fetchSignedUrl(url: string, options: SignedUrlFetchOptions
|
|
|
167
169
|
if (res.ok || !retryableStatuses.has(res.status) || isLastAttempt) {
|
|
168
170
|
return res;
|
|
169
171
|
}
|
|
172
|
+
// Do not retry earlier than requested or park the caller beyond the retry budget.
|
|
173
|
+
// Returning the response also avoids overflowing JavaScript's timer range.
|
|
174
|
+
if ((retryAfterMs(res) ?? 0) > MAX_RETRY_AFTER_MS) return res;
|
|
170
175
|
// Retryable status: drain the body so the connection can be reused, then back off.
|
|
171
176
|
await res.body?.cancel().catch(() => undefined);
|
|
172
177
|
await sleep(backoffMs(attempt, baseDelayMs, maxDelayMs, res));
|