roe-typescript 0.1.3 → 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.
@@ -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.1.3",
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
-
@@ -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
- }
@@ -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
- }
@@ -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
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,81 +0,0 @@
1
- export declare enum JobStatus {
2
- PENDING = 0,
3
- STARTED = 1,
4
- RETRY = 2,
5
- SUCCESS = 3,
6
- FAILURE = 4,
7
- CANCELLED = 5,
8
- CACHED = 6
9
- }
10
- export type ErrorResponse = {
11
- detail: string;
12
- status_code: number;
13
- };
14
- export type AgentDatum = {
15
- key: string;
16
- description: string;
17
- data_type: string;
18
- value: string;
19
- cost?: number | null;
20
- };
21
- export type PaginatedResponse<T> = {
22
- count: number;
23
- next: string | null;
24
- previous: string | null;
25
- results: T[];
26
- };
27
- export type AgentJobStatus = {
28
- status: number;
29
- timestamp: number | null;
30
- error_message?: string | null;
31
- };
32
- export type Reference = {
33
- url: string;
34
- resource_id: string;
35
- };
36
- export declare function referenceFromUrl(url: string): Reference;
37
- export declare function extractReferencesFromOutputs(outputs: AgentDatum[]): Reference[];
38
- export declare function getJobReferences(result: Pick<AgentJobResult, "outputs">): Reference[];
39
- export type AgentJobResult = {
40
- agent_id: string;
41
- agent_version_id: string;
42
- inputs: unknown[];
43
- input_tokens?: number | null;
44
- output_tokens?: number | null;
45
- outputs: AgentDatum[];
46
- status?: number | null;
47
- error_message?: string | null;
48
- };
49
- /** Returns true if the job status is SUCCESS or CACHED. */
50
- export declare function isJobSuccess(result: AgentJobResult): boolean;
51
- /** Returns true if the job status is FAILURE or CANCELLED (any non-success terminal state). */
52
- export declare function isJobFailure(result: AgentJobResult): boolean;
53
- /** Returns true if the job status is specifically CANCELLED. */
54
- export declare function isJobCancelled(result: AgentJobResult): boolean;
55
- export type AgentJobStatusBatch = {
56
- id: string;
57
- status?: number | null;
58
- created_at?: unknown;
59
- last_updated_at?: unknown;
60
- timestamp?: number | null;
61
- error_message?: string | null;
62
- };
63
- export type AgentJobResultBatch = {
64
- id: string;
65
- status?: number | null;
66
- result?: AgentDatum[] | unknown | null;
67
- corrected_outputs?: AgentDatum[] | null;
68
- agent_id?: string | null;
69
- agent_version_id?: string | null;
70
- cost?: number | null;
71
- inputs?: unknown[] | null;
72
- input_tokens?: number | null;
73
- output_tokens?: number | null;
74
- };
75
- export type JobDataDeleteResponse = {
76
- status: string;
77
- deleted_count: number;
78
- failed_count: number;
79
- outputs_sanitized: boolean;
80
- errors?: string[] | null;
81
- };
@@ -1,48 +0,0 @@
1
- import { v4 as uuidv4 } from "uuid";
2
- export var JobStatus;
3
- (function (JobStatus) {
4
- JobStatus[JobStatus["PENDING"] = 0] = "PENDING";
5
- JobStatus[JobStatus["STARTED"] = 1] = "STARTED";
6
- JobStatus[JobStatus["RETRY"] = 2] = "RETRY";
7
- JobStatus[JobStatus["SUCCESS"] = 3] = "SUCCESS";
8
- JobStatus[JobStatus["FAILURE"] = 4] = "FAILURE";
9
- JobStatus[JobStatus["CANCELLED"] = 5] = "CANCELLED";
10
- JobStatus[JobStatus["CACHED"] = 6] = "CACHED";
11
- })(JobStatus || (JobStatus = {}));
12
- export function referenceFromUrl(url) {
13
- const resource_id = url.split("/references/").at(-1)?.replace(/\/+$/, "") ?? uuidv4();
14
- return { url, resource_id };
15
- }
16
- export function extractReferencesFromOutputs(outputs) {
17
- const references = [];
18
- for (const output of outputs) {
19
- try {
20
- const parsed = JSON.parse(output.value);
21
- const refs = Array.isArray(parsed?.references) ? parsed.references : [];
22
- for (const ref of refs) {
23
- if (typeof ref === "string" && ref.includes("/references/")) {
24
- references.push(referenceFromUrl(ref));
25
- }
26
- }
27
- }
28
- catch {
29
- // ignore unparsable outputs
30
- }
31
- }
32
- return references;
33
- }
34
- export function getJobReferences(result) {
35
- return extractReferencesFromOutputs(result.outputs ?? []);
36
- }
37
- /** Returns true if the job status is SUCCESS or CACHED. */
38
- export function isJobSuccess(result) {
39
- return result.status === JobStatus.SUCCESS || result.status === JobStatus.CACHED;
40
- }
41
- /** Returns true if the job status is FAILURE or CANCELLED (any non-success terminal state). */
42
- export function isJobFailure(result) {
43
- return result.status === JobStatus.FAILURE || result.status === JobStatus.CANCELLED;
44
- }
45
- /** Returns true if the job status is specifically CANCELLED. */
46
- export function isJobCancelled(result) {
47
- return result.status === JobStatus.CANCELLED;
48
- }
@@ -1,6 +0,0 @@
1
- export type UserInfo = {
2
- id: number;
3
- email: string;
4
- first_name?: string;
5
- last_name?: string;
6
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,30 +0,0 @@
1
- import { FileUpload } from "../models/file.js";
2
- import { RoeAuth } from "../auth.js";
3
- import { RoeConfig } from "../config.js";
4
- export declare class RoeHTTPClient {
5
- private readonly config;
6
- private readonly auth;
7
- private readonly client;
8
- private readonly maxRetries;
9
- constructor(config: RoeConfig, auth: RoeAuth);
10
- close(): void;
11
- private execute;
12
- private handleResponse;
13
- get<T>(url: string, params?: Record<string, unknown>): Promise<T>;
14
- getBytes(url: string, params?: Record<string, unknown>): Promise<Buffer>;
15
- post<T>(options: {
16
- url: string;
17
- json?: Record<string, unknown> | unknown[];
18
- formData?: Record<string, unknown>;
19
- files?: Record<string, FileUpload | NodeJS.ReadableStream | string>;
20
- params?: Record<string, unknown>;
21
- }): Promise<T>;
22
- /**
23
- * Execute a POST request with FormData that contains files.
24
- * Rebuilds FormData on each retry attempt to handle stream consumption.
25
- */
26
- private executeWithFormData;
27
- put<T>(url: string, json?: Record<string, unknown>, params?: Record<string, unknown>): Promise<T>;
28
- delete(url: string, params?: Record<string, unknown>): Promise<void>;
29
- postWithDynamicInputs<T>(url: string, inputs: Record<string, unknown>, params?: Record<string, unknown>, metadata?: Record<string, unknown>): Promise<T>;
30
- }