roe-typescript 0.1.2 → 1.0.0
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/README.md +48 -7
- package/dist/api/agents.d.ts +40 -27
- package/dist/api/agents.js +222 -112
- package/dist/api/policies.d.ts +15 -9
- package/dist/api/policies.js +75 -31
- package/dist/api/users.d.ts +18 -0
- package/dist/api/users.js +17 -0
- package/dist/client.d.ts +6 -2
- package/dist/client.js +17 -5
- package/dist/exceptions.js +8 -0
- package/dist/generated/client.d.ts +12 -0
- package/dist/generated/client.js +55 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +9 -5
- package/dist/models/file.d.ts +17 -0
- package/dist/models/file.js +36 -0
- package/dist/models/job.d.ts +30 -6
- package/dist/models/job.js +130 -37
- package/dist/utils/dynamicInputs.d.ts +30 -0
- package/dist/utils/dynamicInputs.js +114 -0
- package/dist/utils/middleware.d.ts +6 -0
- package/dist/utils/middleware.js +80 -0
- package/package.json +6 -6
- package/dist/models/agent.d.ts +0 -78
- package/dist/models/agent.js +0 -40
- package/dist/models/policy.d.ts +0 -19
- package/dist/models/policy.js +0 -1
- package/dist/models/responses.d.ts +0 -71
- package/dist/models/responses.js +0 -36
- package/dist/models/user.d.ts +0 -6
- package/dist/models/user.js +0 -1
- package/dist/utils/httpClient.d.ts +0 -30
- package/dist/utils/httpClient.js +0 -229
package/dist/models/job.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
export var JobStatus;
|
|
2
|
+
(function (JobStatus) {
|
|
3
|
+
JobStatus[JobStatus["PENDING"] = 0] = "PENDING";
|
|
4
|
+
JobStatus[JobStatus["STARTED"] = 1] = "STARTED";
|
|
5
|
+
JobStatus[JobStatus["RETRY"] = 2] = "RETRY";
|
|
6
|
+
JobStatus[JobStatus["SUCCESS"] = 3] = "SUCCESS";
|
|
7
|
+
JobStatus[JobStatus["FAILURE"] = 4] = "FAILURE";
|
|
8
|
+
JobStatus[JobStatus["CANCELLED"] = 5] = "CANCELLED";
|
|
9
|
+
JobStatus[JobStatus["CACHED"] = 6] = "CACHED";
|
|
10
|
+
})(JobStatus || (JobStatus = {}));
|
|
2
11
|
export class Job {
|
|
3
12
|
constructor(opts) {
|
|
4
13
|
this.agentsApi = opts.agentsApi;
|
|
@@ -20,11 +29,32 @@ export class Job {
|
|
|
20
29
|
while (true) {
|
|
21
30
|
const status = await this.retrieveStatus();
|
|
22
31
|
const code = status.status;
|
|
23
|
-
if (code === JobStatus.SUCCESS ||
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
if (code === JobStatus.SUCCESS ||
|
|
33
|
+
code === JobStatus.CACHED ||
|
|
34
|
+
code === JobStatus.FAILURE ||
|
|
35
|
+
code === JobStatus.CANCELLED) {
|
|
36
|
+
const isFailed = code === JobStatus.FAILURE || code === JobStatus.CANCELLED;
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = await this.retrieveResult();
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (!isFailed)
|
|
43
|
+
throw err;
|
|
44
|
+
result = {
|
|
45
|
+
agent_id: "",
|
|
46
|
+
agent_version_id: "",
|
|
47
|
+
inputs: [],
|
|
48
|
+
outputs: [],
|
|
49
|
+
input_tokens: null,
|
|
50
|
+
output_tokens: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...result,
|
|
55
|
+
status: code,
|
|
56
|
+
error_message: status.error_message ?? null,
|
|
57
|
+
};
|
|
28
58
|
}
|
|
29
59
|
if (Date.now() - start > timeoutSeconds * 1000) {
|
|
30
60
|
throw new Error(`Job ${this.jobId} did not complete within ${timeoutSeconds} seconds`);
|
|
@@ -42,7 +72,7 @@ export class Job {
|
|
|
42
72
|
export class JobBatch {
|
|
43
73
|
constructor(opts) {
|
|
44
74
|
this.completed = {};
|
|
45
|
-
this.
|
|
75
|
+
this.statuses = {};
|
|
46
76
|
this.agentsApi = opts.agentsApi;
|
|
47
77
|
this.jobIds = opts.jobIds;
|
|
48
78
|
this.timeoutSeconds = opts.timeoutSeconds ?? 7200;
|
|
@@ -64,45 +94,88 @@ export class JobBatch {
|
|
|
64
94
|
if (!pending.length)
|
|
65
95
|
break;
|
|
66
96
|
const statusBatch = await this.agentsApi.jobs.retrieveStatusMany(pending);
|
|
67
|
-
const failures = statusBatch.filter((s) => s.status === JobStatus.FAILURE || s.status === JobStatus.CANCELLED);
|
|
68
|
-
if (failures.length) {
|
|
69
|
-
const detail = failures.map((f) => `${f.id}:${f.status ?? "unknown"}`).join(', ');
|
|
70
|
-
throw new Error(`Jobs failed or cancelled: ${detail}`);
|
|
71
|
-
}
|
|
72
97
|
const completedIds = [];
|
|
73
98
|
for (const status of statusBatch) {
|
|
99
|
+
// The bulk status endpoint returns AgentJobStatus[]; the schema lacks an `id`
|
|
100
|
+
// field on each, but the backend includes it in practice — extract via cast.
|
|
101
|
+
const id = status.id;
|
|
102
|
+
if (!id) {
|
|
103
|
+
throw new Error("AgentJobStatus response is missing an `id` field; backend schema may have changed");
|
|
104
|
+
}
|
|
74
105
|
const code = status.status;
|
|
75
|
-
if (code === JobStatus.SUCCESS ||
|
|
76
|
-
|
|
106
|
+
if (code === JobStatus.SUCCESS ||
|
|
107
|
+
code === JobStatus.CACHED ||
|
|
108
|
+
code === JobStatus.FAILURE ||
|
|
109
|
+
code === JobStatus.CANCELLED) {
|
|
110
|
+
completedIds.push(id);
|
|
77
111
|
}
|
|
78
112
|
if (code !== undefined && code !== null) {
|
|
79
|
-
this.
|
|
113
|
+
this.statuses[id] = {
|
|
114
|
+
id,
|
|
115
|
+
status: code,
|
|
116
|
+
timestamp: status.timestamp,
|
|
117
|
+
error_message: status.error_message,
|
|
118
|
+
};
|
|
80
119
|
}
|
|
81
120
|
}
|
|
82
121
|
if (completedIds.length) {
|
|
83
|
-
|
|
122
|
+
let resultBatch;
|
|
123
|
+
try {
|
|
124
|
+
resultBatch = await this.agentsApi.jobs.retrieveResultMany(completedIds);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
// Only synthesize for failed/cancelled — can't fake results for success/cached
|
|
128
|
+
const failedIds = completedIds.filter((id) => {
|
|
129
|
+
const s = this.statuses[id]?.status;
|
|
130
|
+
return s === JobStatus.FAILURE || s === JobStatus.CANCELLED;
|
|
131
|
+
});
|
|
132
|
+
if (failedIds.length < completedIds.length) {
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
resultBatch = failedIds.map((id) => ({
|
|
136
|
+
id,
|
|
137
|
+
status: this.statuses[id]?.status ?? null,
|
|
138
|
+
agent_id: null,
|
|
139
|
+
agent_version_id: null,
|
|
140
|
+
cost: null,
|
|
141
|
+
inputs: null,
|
|
142
|
+
result: null,
|
|
143
|
+
corrected_outputs: null,
|
|
144
|
+
input_tokens: null,
|
|
145
|
+
output_tokens: null,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
84
148
|
for (const res of resultBatch) {
|
|
149
|
+
const jobStatus = res.status ?? this.statuses[res.id]?.status ?? null;
|
|
150
|
+
const isFailed = jobStatus === JobStatus.FAILURE || jobStatus === JobStatus.CANCELLED;
|
|
85
151
|
if (!res.agent_id || !res.agent_version_id) {
|
|
86
|
-
|
|
87
|
-
|
|
152
|
+
if (!isFailed) {
|
|
153
|
+
const id = res.id ?? "unknown";
|
|
154
|
+
throw new Error(`Job ${id} missing agent identifiers`);
|
|
155
|
+
}
|
|
88
156
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
157
|
+
const rawOutputs = res.result ?? res.corrected_outputs;
|
|
158
|
+
let outputs;
|
|
159
|
+
if (rawOutputs == null) {
|
|
160
|
+
if (!isFailed) {
|
|
161
|
+
throw new Error(`Job ${res.id} returned null or undefined result`);
|
|
162
|
+
}
|
|
163
|
+
outputs = [];
|
|
94
164
|
}
|
|
95
|
-
if (!Array.isArray(
|
|
96
|
-
|
|
97
|
-
|
|
165
|
+
else if (!Array.isArray(rawOutputs)) {
|
|
166
|
+
if (!isFailed) {
|
|
167
|
+
throw new Error(`Job ${res.id} returned unexpected result format: ${typeof rawOutputs}`);
|
|
168
|
+
}
|
|
169
|
+
outputs = [];
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
outputs = rawOutputs;
|
|
98
173
|
}
|
|
99
174
|
this.completed[res.id] = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
inputs: res.inputs ?? [],
|
|
103
|
-
input_tokens: res.input_tokens,
|
|
104
|
-
output_tokens: res.output_tokens,
|
|
175
|
+
...res,
|
|
176
|
+
status: jobStatus,
|
|
105
177
|
outputs,
|
|
178
|
+
error_message: this.statuses[res.id]?.error_message ?? null,
|
|
106
179
|
};
|
|
107
180
|
}
|
|
108
181
|
}
|
|
@@ -117,14 +190,34 @@ export class JobBatch {
|
|
|
117
190
|
return this.jobIds.map((id) => this.completed[id]);
|
|
118
191
|
}
|
|
119
192
|
async retrieveStatus() {
|
|
120
|
-
const statusMap = {
|
|
121
|
-
const toQuery =
|
|
193
|
+
const statusMap = {};
|
|
194
|
+
const toQuery = [];
|
|
195
|
+
const TERMINAL = new Set([JobStatus.SUCCESS, JobStatus.CACHED, JobStatus.FAILURE, JobStatus.CANCELLED]);
|
|
196
|
+
for (const id of this.jobIds) {
|
|
197
|
+
const cached = this.statuses[id];
|
|
198
|
+
if (cached !== undefined && TERMINAL.has(cached.status)) {
|
|
199
|
+
statusMap[id] = cached;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
toQuery.push(id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
122
205
|
if (toQuery.length) {
|
|
123
|
-
const
|
|
124
|
-
for (const s of
|
|
206
|
+
const batchStatuses = await this.agentsApi.jobs.retrieveStatusMany(toQuery);
|
|
207
|
+
for (const s of batchStatuses) {
|
|
208
|
+
const id = s.id;
|
|
209
|
+
if (!id) {
|
|
210
|
+
throw new Error("AgentJobStatus response is missing an `id` field; backend schema may have changed");
|
|
211
|
+
}
|
|
125
212
|
if (s.status !== undefined && s.status !== null) {
|
|
126
|
-
|
|
127
|
-
|
|
213
|
+
const js = {
|
|
214
|
+
id,
|
|
215
|
+
status: s.status,
|
|
216
|
+
timestamp: s.timestamp,
|
|
217
|
+
error_message: s.error_message,
|
|
218
|
+
};
|
|
219
|
+
this.statuses[id] = js;
|
|
220
|
+
statusMap[id] = js;
|
|
128
221
|
}
|
|
129
222
|
}
|
|
130
223
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RoeRawClient } from "../generated/client.js";
|
|
2
|
+
import { FileUpload } from "../models/file.js";
|
|
3
|
+
export type ClassifiedInputs = {
|
|
4
|
+
formData: Record<string, string>;
|
|
5
|
+
files: Record<string, FileUpload>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Splits a free-form `inputs` dict into form fields + file uploads, mirroring
|
|
9
|
+
* the detection rules from the legacy `RoeHTTPClient.postWithDynamicInputs`:
|
|
10
|
+
*
|
|
11
|
+
* - `FileUpload` instance -> file part
|
|
12
|
+
* - object with a `pipe` function (Node Readable) -> wrapped in FileUpload
|
|
13
|
+
* - string that's a valid UUID -> form field (treated as a Roe file ref)
|
|
14
|
+
* - string that resolves to a real file path -> opened as a FileUpload
|
|
15
|
+
* - other strings / numbers / booleans -> form field (stringified)
|
|
16
|
+
*/
|
|
17
|
+
export declare function classifyInputs(inputs: Record<string, unknown>): Promise<ClassifiedInputs>;
|
|
18
|
+
/**
|
|
19
|
+
* Posts a multipart body built from a free-form `inputs` dict (with optional
|
|
20
|
+
* sibling `metadata`) to a generated raw-client path. Bypasses openapi-fetch's
|
|
21
|
+
* default JSON serialization via the `bodySerializer` escape hatch and runs
|
|
22
|
+
* its own retry loop because Node `Readable` bodies cannot be re-read.
|
|
23
|
+
*
|
|
24
|
+
* The path / pathParams are intentionally typed loosely (`string` /
|
|
25
|
+
* `Record<string, string>`) — multipart agent-run endpoints have dynamic
|
|
26
|
+
* input keys that aren't statically modeled in the OpenAPI spec, so the
|
|
27
|
+
* generated typed-body variant is unusable here. Callers pass the path
|
|
28
|
+
* literal that matches `paths` in schema.d.ts.
|
|
29
|
+
*/
|
|
30
|
+
export declare function postDynamicInputs<T = unknown>(raw: RoeRawClient, path: string, pathParams: Record<string, string>, queryParams: Record<string, string | undefined>, inputs: Record<string, unknown>, metadata: Record<string, unknown> | undefined, maxRetries: number): Promise<T>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FileUpload } from "../models/file.js";
|
|
2
|
+
import { isFilePath, isUuidString } from "./fileDetection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sentinel header read by `retryMiddleware`. Multipart bodies aren't safely
|
|
5
|
+
* cloneable for retry by the middleware — the wrapper-side loop in
|
|
6
|
+
* `postDynamicInputs` rebuilds FormData per attempt instead.
|
|
7
|
+
*/
|
|
8
|
+
const RETRY_BYPASS_HEADER = "x-roe-retry-bypass";
|
|
9
|
+
const isRetriable = (status) => status >= 500 || status === 429 || status === 408;
|
|
10
|
+
/**
|
|
11
|
+
* Splits a free-form `inputs` dict into form fields + file uploads, mirroring
|
|
12
|
+
* the detection rules from the legacy `RoeHTTPClient.postWithDynamicInputs`:
|
|
13
|
+
*
|
|
14
|
+
* - `FileUpload` instance -> file part
|
|
15
|
+
* - object with a `pipe` function (Node Readable) -> wrapped in FileUpload
|
|
16
|
+
* - string that's a valid UUID -> form field (treated as a Roe file ref)
|
|
17
|
+
* - string that resolves to a real file path -> opened as a FileUpload
|
|
18
|
+
* - other strings / numbers / booleans -> form field (stringified)
|
|
19
|
+
*/
|
|
20
|
+
export async function classifyInputs(inputs) {
|
|
21
|
+
const formData = {};
|
|
22
|
+
const files = {};
|
|
23
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
24
|
+
if (value === undefined || value === null)
|
|
25
|
+
continue;
|
|
26
|
+
if (value instanceof FileUpload) {
|
|
27
|
+
files[key] = value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "object" && typeof value.pipe === "function") {
|
|
31
|
+
files[key] = new FileUpload({ file: value });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
if (isUuidString(value)) {
|
|
36
|
+
formData[key] = value;
|
|
37
|
+
}
|
|
38
|
+
else if (await isFilePath(value)) {
|
|
39
|
+
files[key] = new FileUpload({ path: value });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
formData[key] = value;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
47
|
+
formData[key] = String(value);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Unknown shape — JSON-encode so e.g. nested objects survive the round-trip.
|
|
51
|
+
formData[key] = JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
return { formData, files };
|
|
54
|
+
}
|
|
55
|
+
async function buildFormData(formData, files, metadata) {
|
|
56
|
+
const fd = new FormData();
|
|
57
|
+
for (const [k, v] of Object.entries(formData))
|
|
58
|
+
fd.append(k, v);
|
|
59
|
+
if (metadata !== undefined)
|
|
60
|
+
fd.append("metadata", JSON.stringify(metadata));
|
|
61
|
+
for (const [k, fu] of Object.entries(files)) {
|
|
62
|
+
fd.append(k, await fu.toBlob(), fu.effectiveFilename);
|
|
63
|
+
}
|
|
64
|
+
return fd;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Posts a multipart body built from a free-form `inputs` dict (with optional
|
|
68
|
+
* sibling `metadata`) to a generated raw-client path. Bypasses openapi-fetch's
|
|
69
|
+
* default JSON serialization via the `bodySerializer` escape hatch and runs
|
|
70
|
+
* its own retry loop because Node `Readable` bodies cannot be re-read.
|
|
71
|
+
*
|
|
72
|
+
* The path / pathParams are intentionally typed loosely (`string` /
|
|
73
|
+
* `Record<string, string>`) — multipart agent-run endpoints have dynamic
|
|
74
|
+
* input keys that aren't statically modeled in the OpenAPI spec, so the
|
|
75
|
+
* generated typed-body variant is unusable here. Callers pass the path
|
|
76
|
+
* literal that matches `paths` in schema.d.ts.
|
|
77
|
+
*/
|
|
78
|
+
export async function postDynamicInputs(raw, path, pathParams, queryParams, inputs, metadata, maxRetries) {
|
|
79
|
+
const { formData, files } = await classifyInputs(inputs);
|
|
80
|
+
let attempt = 0;
|
|
81
|
+
let lastErr;
|
|
82
|
+
while (attempt <= maxRetries) {
|
|
83
|
+
const fd = await buildFormData(formData, files, metadata);
|
|
84
|
+
try {
|
|
85
|
+
// openapi-fetch's typed POST is keyed on `paths`; we deliberately escape
|
|
86
|
+
// the type system here for multipart agent-run endpoints.
|
|
87
|
+
// NOTE: openapi-fetch skips `bodySerializer` entirely when `body` is
|
|
88
|
+
// `undefined`, so we pass the FormData as the body itself and use
|
|
89
|
+
// bodySerializer as an identity to preserve the FormData type signal
|
|
90
|
+
// (openapi-fetch then refrains from forcing Content-Type: application/json).
|
|
91
|
+
const result = await raw.POST(path, {
|
|
92
|
+
params: { path: pathParams, query: queryParams },
|
|
93
|
+
body: fd,
|
|
94
|
+
bodySerializer: (b) => b,
|
|
95
|
+
headers: { [RETRY_BYPASS_HEADER]: "1" },
|
|
96
|
+
});
|
|
97
|
+
// errorMiddleware throws on non-2xx, so reaching here means success.
|
|
98
|
+
// `error` is therefore always undefined; surface `data` directly.
|
|
99
|
+
return result.data;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
lastErr = err;
|
|
103
|
+
const status = err?.statusCode;
|
|
104
|
+
if (status !== undefined && isRetriable(status) && attempt < maxRetries) {
|
|
105
|
+
const backoff = Math.min(1000 * 2 ** attempt, 10000);
|
|
106
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
107
|
+
attempt += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw lastErr ?? new Error("postDynamicInputs exhausted retries without producing an error");
|
|
114
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Middleware } from "openapi-fetch";
|
|
2
|
+
import { RoeAuth } from "../auth.js";
|
|
3
|
+
export declare function shouldBypassRetry(request: Request): boolean;
|
|
4
|
+
export declare function authMiddleware(auth: RoeAuth): Middleware;
|
|
5
|
+
export declare function retryMiddleware(maxRetries: number): Middleware;
|
|
6
|
+
export declare const errorMiddleware: Middleware;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { extractErrorMessage, getExceptionForStatusCode } from "../exceptions.js";
|
|
2
|
+
const RETRY_BYPASS_HEADER = "x-roe-retry-bypass";
|
|
3
|
+
export function shouldBypassRetry(request) {
|
|
4
|
+
if (request.headers.get(RETRY_BYPASS_HEADER))
|
|
5
|
+
return true;
|
|
6
|
+
const ct = request.headers.get("content-type");
|
|
7
|
+
return !!ct && ct.toLowerCase().startsWith("multipart/");
|
|
8
|
+
}
|
|
9
|
+
export function authMiddleware(auth) {
|
|
10
|
+
return {
|
|
11
|
+
onRequest({ request }) {
|
|
12
|
+
for (const [k, v] of Object.entries(auth.getHeaders())) {
|
|
13
|
+
request.headers.set(k, v);
|
|
14
|
+
}
|
|
15
|
+
return request;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Per-request body cache populated in onRequest (where the body is still
|
|
20
|
+
// fresh) and consumed in onResponse to rebuild Requests on retry. Cloning
|
|
21
|
+
// the original request post-fetch fails in Node 18+ undici with
|
|
22
|
+
// `TypeError: Cannot perform 'clone' on a Request with a disturbed body`,
|
|
23
|
+
// because the body stream has already been consumed by the initial fetch.
|
|
24
|
+
const bodyCache = new WeakMap();
|
|
25
|
+
export function retryMiddleware(maxRetries) {
|
|
26
|
+
return {
|
|
27
|
+
async onRequest({ request }) {
|
|
28
|
+
if (!shouldBypassRetry(request) && request.body) {
|
|
29
|
+
// Clone-then-buffer here is safe — the original `request` is still
|
|
30
|
+
// un-disturbed and goes on to fetch unchanged. Only the clone is
|
|
31
|
+
// consumed.
|
|
32
|
+
bodyCache.set(request, await request.clone().arrayBuffer());
|
|
33
|
+
}
|
|
34
|
+
return request;
|
|
35
|
+
},
|
|
36
|
+
async onResponse({ request, response, options }) {
|
|
37
|
+
if (shouldBypassRetry(request))
|
|
38
|
+
return response;
|
|
39
|
+
const retriable = (status) => status >= 500 || status === 429 || status === 408;
|
|
40
|
+
let attempt = 0;
|
|
41
|
+
let res = response;
|
|
42
|
+
const doFetch = options.fetch;
|
|
43
|
+
const cachedBody = bodyCache.get(request);
|
|
44
|
+
while (attempt < maxRetries && retriable(res.status)) {
|
|
45
|
+
const backoffMs = Math.min(1000 * 2 ** attempt, 10000);
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
47
|
+
// Build a fresh Request per attempt — the original's body stream
|
|
48
|
+
// was disturbed by the previous fetch.
|
|
49
|
+
const retryReq = new Request(request.url, {
|
|
50
|
+
method: request.method,
|
|
51
|
+
headers: request.headers,
|
|
52
|
+
body: cachedBody ?? null,
|
|
53
|
+
});
|
|
54
|
+
res = await doFetch(retryReq);
|
|
55
|
+
attempt += 1;
|
|
56
|
+
}
|
|
57
|
+
bodyCache.delete(request);
|
|
58
|
+
return res;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export const errorMiddleware = {
|
|
63
|
+
async onResponse({ response }) {
|
|
64
|
+
if (response.ok)
|
|
65
|
+
return response;
|
|
66
|
+
let data = null;
|
|
67
|
+
try {
|
|
68
|
+
data = await response.clone().json();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
data = null;
|
|
72
|
+
}
|
|
73
|
+
const ExceptionClass = getExceptionForStatusCode(response.status);
|
|
74
|
+
const message = extractErrorMessage(data, response.status);
|
|
75
|
+
const responseObj = data && typeof data === "object" && !Array.isArray(data)
|
|
76
|
+
? data
|
|
77
|
+
: null;
|
|
78
|
+
throw new ExceptionClass(message, response.status, responseObj);
|
|
79
|
+
},
|
|
80
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roe-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "TypeScript SDK for the Roe AI API (feature parity with roe-python).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"generate-sdk": "bash scripts/generate-sdk",
|
|
16
17
|
"lint": "tsc --noEmit",
|
|
17
18
|
"test": "vitest run tests/unit --passWithNoTests",
|
|
18
19
|
"test:watch": "vitest",
|
|
19
20
|
"prepublishOnly": "npm run lint && npm run test && npm run build"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"axios": "^1.7.7",
|
|
23
|
-
"form-data": "^4.0.0",
|
|
24
23
|
"mime-types": "^2.1.35",
|
|
24
|
+
"openapi-fetch": "^0.13.8",
|
|
25
|
+
"undici": "^6.25.0",
|
|
25
26
|
"uuid": "^11.0.3"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
|
-
"@types/form-data": "^2.5.0",
|
|
29
29
|
"@types/mime-types": "^2.1.4",
|
|
30
|
-
"@types/uuid": "^10.0.0",
|
|
31
30
|
"@types/node": "^22.9.0",
|
|
31
|
+
"@types/uuid": "^10.0.0",
|
|
32
|
+
"openapi-typescript": "^7.6.1",
|
|
32
33
|
"typescript": "^5.6.3",
|
|
33
34
|
"vitest": "^1.6.0"
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
|
package/dist/models/agent.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { UserInfo } from "./user.js";
|
|
2
|
-
import { AgentsAPI } from "../api/agents.js";
|
|
3
|
-
import { Job } from "./job.js";
|
|
4
|
-
export type AgentInputDefinition = {
|
|
5
|
-
key: string;
|
|
6
|
-
data_type: string;
|
|
7
|
-
description: string;
|
|
8
|
-
example?: string;
|
|
9
|
-
accepts_multiple_files?: boolean | null;
|
|
10
|
-
};
|
|
11
|
-
export type BaseAgent = {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
creator?: UserInfo | null;
|
|
15
|
-
created_at: string;
|
|
16
|
-
disable_cache: boolean;
|
|
17
|
-
cache_failed_jobs: boolean;
|
|
18
|
-
organization_id: string;
|
|
19
|
-
engine_class_id: string;
|
|
20
|
-
current_version_id?: string | null;
|
|
21
|
-
job_count?: number;
|
|
22
|
-
most_recent_job?: string | null;
|
|
23
|
-
engine_name?: string;
|
|
24
|
-
};
|
|
25
|
-
export declare class BaseAgentWithApi implements BaseAgent {
|
|
26
|
-
id: string;
|
|
27
|
-
name: string;
|
|
28
|
-
creator?: UserInfo | null;
|
|
29
|
-
created_at: string;
|
|
30
|
-
disable_cache: boolean;
|
|
31
|
-
cache_failed_jobs: boolean;
|
|
32
|
-
organization_id: string;
|
|
33
|
-
engine_class_id: string;
|
|
34
|
-
current_version_id?: string | null;
|
|
35
|
-
job_count?: number;
|
|
36
|
-
most_recent_job?: string | null;
|
|
37
|
-
engine_name?: string;
|
|
38
|
-
private _agentsApi?;
|
|
39
|
-
constructor(data: BaseAgent);
|
|
40
|
-
setAgentsApi(api: AgentsAPI): void;
|
|
41
|
-
run(inputs: Record<string, unknown>): Promise<Job>;
|
|
42
|
-
listVersions(): Promise<AgentVersionWithApi[]>;
|
|
43
|
-
getCurrentVersion(): Promise<AgentVersionWithApi> | null;
|
|
44
|
-
}
|
|
45
|
-
export type AgentVersion = {
|
|
46
|
-
id: string;
|
|
47
|
-
name: string;
|
|
48
|
-
version_name: string;
|
|
49
|
-
creator?: UserInfo | null;
|
|
50
|
-
created_at: string;
|
|
51
|
-
description?: string | null;
|
|
52
|
-
engine_class_id: string;
|
|
53
|
-
engine_name: string;
|
|
54
|
-
input_definitions: AgentInputDefinition[];
|
|
55
|
-
engine_config: Record<string, unknown>;
|
|
56
|
-
organization_id: string;
|
|
57
|
-
readonly: boolean;
|
|
58
|
-
base_agent: BaseAgent;
|
|
59
|
-
};
|
|
60
|
-
export declare class AgentVersionWithApi implements AgentVersion {
|
|
61
|
-
id: string;
|
|
62
|
-
name: string;
|
|
63
|
-
version_name: string;
|
|
64
|
-
creator?: UserInfo | null;
|
|
65
|
-
created_at: string;
|
|
66
|
-
description?: string | null;
|
|
67
|
-
engine_class_id: string;
|
|
68
|
-
engine_name: string;
|
|
69
|
-
input_definitions: AgentInputDefinition[];
|
|
70
|
-
engine_config: Record<string, unknown>;
|
|
71
|
-
organization_id: string;
|
|
72
|
-
readonly: boolean;
|
|
73
|
-
base_agent: BaseAgent;
|
|
74
|
-
private _agentsApi?;
|
|
75
|
-
constructor(data: AgentVersion);
|
|
76
|
-
setAgentsApi(api: AgentsAPI): void;
|
|
77
|
-
run(inputs: Record<string, unknown>): Promise<Job>;
|
|
78
|
-
}
|
package/dist/models/agent.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export class BaseAgentWithApi {
|
|
2
|
-
constructor(data) {
|
|
3
|
-
Object.assign(this, data);
|
|
4
|
-
}
|
|
5
|
-
setAgentsApi(api) {
|
|
6
|
-
this._agentsApi = api;
|
|
7
|
-
}
|
|
8
|
-
run(inputs) {
|
|
9
|
-
if (!this._agentsApi)
|
|
10
|
-
throw new Error("Agents API not set");
|
|
11
|
-
return this._agentsApi.run({ agentId: this.id, inputs });
|
|
12
|
-
}
|
|
13
|
-
listVersions() {
|
|
14
|
-
if (!this._agentsApi)
|
|
15
|
-
throw new Error("Agents API not set");
|
|
16
|
-
return this._agentsApi.versions.list(this.id);
|
|
17
|
-
}
|
|
18
|
-
getCurrentVersion() {
|
|
19
|
-
if (!this._agentsApi)
|
|
20
|
-
throw new Error("Agents API not set");
|
|
21
|
-
if (!this.current_version_id)
|
|
22
|
-
return null;
|
|
23
|
-
return this._agentsApi.versions.retrieve(this.id, this.current_version_id);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export class AgentVersionWithApi {
|
|
27
|
-
constructor(data) {
|
|
28
|
-
Object.assign(this, data);
|
|
29
|
-
}
|
|
30
|
-
setAgentsApi(api) {
|
|
31
|
-
this._agentsApi = api;
|
|
32
|
-
}
|
|
33
|
-
run(inputs) {
|
|
34
|
-
if (!this._agentsApi)
|
|
35
|
-
throw new Error("Agents API not set");
|
|
36
|
-
if (!this.base_agent?.id)
|
|
37
|
-
throw new Error("AgentVersion missing base_agent id");
|
|
38
|
-
return this._agentsApi.runVersion({ agentId: this.base_agent.id, versionId: this.id, inputs });
|
|
39
|
-
}
|
|
40
|
-
}
|
package/dist/models/policy.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export type Policy = {
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
description: string;
|
|
5
|
-
organization_id: string;
|
|
6
|
-
current_version_id: string | null;
|
|
7
|
-
created_at: string;
|
|
8
|
-
updated_at: string;
|
|
9
|
-
};
|
|
10
|
-
export type PolicyVersion = {
|
|
11
|
-
id: string;
|
|
12
|
-
version_name: string;
|
|
13
|
-
content: Record<string, unknown>;
|
|
14
|
-
created_at: string;
|
|
15
|
-
updated_at: string;
|
|
16
|
-
policy: Policy | null;
|
|
17
|
-
created_by: Record<string, unknown> | null;
|
|
18
|
-
base_version_id: string | null;
|
|
19
|
-
};
|
package/dist/models/policy.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|