roe-typescript 0.1.2 → 0.1.3

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 CHANGED
@@ -37,6 +37,31 @@ export ROE_API_KEY="your-api-key"
37
37
  export ROE_ORGANIZATION_ID="your-org-uuid"
38
38
  ```
39
39
 
40
+ ## Job Result Inspection
41
+
42
+ After waiting for a job, you can inspect its outcome using status helpers:
43
+
44
+ ```typescript
45
+ import { isJobSuccess, isJobFailure, isJobCancelled } from "roe-typescript";
46
+
47
+ const result = await job.wait();
48
+
49
+ if (isJobSuccess(result)) {
50
+ for (const output of result.outputs) {
51
+ console.log(`${output.key}: ${output.value}`);
52
+ }
53
+ } else if (isJobCancelled(result)) {
54
+ console.log("Job was cancelled");
55
+ } else if (isJobFailure(result)) {
56
+ console.log("Error:", result.error_message);
57
+ }
58
+
59
+ // Available fields
60
+ result.status // JobStatus enum value or null
61
+ result.error_message // Error string or null
62
+ result.outputs // AgentDatum[]
63
+ ```
64
+
40
65
  ## Full Example
41
66
 
42
67
  Create an agent that extracts structured data from websites:
@@ -34,6 +34,8 @@ export declare class AgentJobsAPI {
34
34
  retrieveStatusMany(jobIds: string[]): Promise<AgentJobStatusBatch[]>;
35
35
  retrieveResultMany(jobIds: string[]): Promise<AgentJobResultBatch[]>;
36
36
  downloadReference(jobId: string, resourceId: string, asAttachment?: boolean): Promise<Buffer>;
37
+ cancel(jobId: string): Promise<void>;
38
+ cancelAll(agentId: string): Promise<void>;
37
39
  deleteData(jobId: string): Promise<JobDataDeleteResponse>;
38
40
  private iterChunks;
39
41
  }
@@ -69,19 +71,22 @@ export declare class AgentsAPI {
69
71
  agentId: string;
70
72
  inputs: Record<string, unknown>;
71
73
  timeoutSeconds?: number;
74
+ metadata?: Record<string, unknown>;
72
75
  }): Promise<Job>;
73
76
  runMany(params: {
74
77
  agentId: string;
75
78
  batchInputs: Record<string, unknown>[];
76
79
  timeoutSeconds?: number;
80
+ metadata?: Record<string, unknown>;
77
81
  }): Promise<JobBatch>;
78
- runSync(agentId: string, inputs: Record<string, unknown>): Promise<AgentDatum[]>;
82
+ runSync(agentId: string, inputs: Record<string, unknown>, metadata?: Record<string, unknown>): Promise<AgentDatum[]>;
79
83
  runVersion(params: {
80
84
  agentId: string;
81
85
  versionId: string;
82
86
  inputs: Record<string, unknown>;
83
87
  timeoutSeconds?: number;
88
+ metadata?: Record<string, unknown>;
84
89
  }): Promise<Job>;
85
- runVersionSync(agentId: string, versionId: string, inputs: Record<string, unknown>): Promise<AgentDatum[]>;
90
+ runVersionSync(agentId: string, versionId: string, inputs: Record<string, unknown>, metadata?: Record<string, unknown>): Promise<AgentDatum[]>;
86
91
  private iterChunks;
87
92
  }
@@ -116,10 +116,18 @@ export class AgentJobsAPI {
116
116
  }
117
117
  async downloadReference(jobId, resourceId, asAttachment = false) {
118
118
  validateUuid(jobId, "jobId");
119
- validateUuid(resourceId, "resourceId");
119
+ // resourceId is a filename/identifier, not a UUID — no validation needed
120
120
  const params = asAttachment ? { download: "true" } : undefined;
121
121
  return this.http.getBytes(`/v1/agents/jobs/${jobId}/references/${resourceId}/`, params);
122
122
  }
123
+ async cancel(jobId) {
124
+ validateUuid(jobId, "jobId");
125
+ await this.http.post({ url: `/v1/agents/jobs/${jobId}/cancel/` });
126
+ }
127
+ async cancelAll(agentId) {
128
+ validateUuid(agentId, "agentId");
129
+ await this.http.post({ url: `/v1/agents/${agentId}/jobs/cancel-all/` });
130
+ }
123
131
  async deleteData(jobId) {
124
132
  validateUuid(jobId, "jobId");
125
133
  return this.http.post({
@@ -196,7 +204,7 @@ export class AgentsAPI {
196
204
  }
197
205
  async run(params) {
198
206
  validateUuid(params.agentId, "agentId");
199
- const response = await this.httpClient.postWithDynamicInputs(`/v1/agents/run/${params.agentId}/async/`, params.inputs);
207
+ const response = await this.httpClient.postWithDynamicInputs(`/v1/agents/run/${params.agentId}/async/`, params.inputs, undefined, params.metadata);
200
208
  // Handle both string response and object response formats
201
209
  const jobId = typeof response === "string" ? response : response?.job_id;
202
210
  if (!jobId || typeof jobId !== "string") {
@@ -213,9 +221,12 @@ export class AgentsAPI {
213
221
  for (const chunk of this.iterChunks(params.batchInputs, AgentsAPI.MAX_BATCH_SIZE)) {
214
222
  if (!chunk.length)
215
223
  continue;
224
+ const json = { inputs: chunk };
225
+ if (params.metadata !== undefined)
226
+ json.metadata = params.metadata;
216
227
  const resp = await this.httpClient.post({
217
228
  url: `/v1/agents/run/${params.agentId}/async/many/`,
218
- json: { inputs: chunk },
229
+ json,
219
230
  });
220
231
  // Validate each returned job ID
221
232
  for (const jobId of resp) {
@@ -230,15 +241,15 @@ export class AgentsAPI {
230
241
  }
231
242
  return new JobBatch({ agentsApi: this, jobIds: allJobIds, timeoutSeconds: params.timeoutSeconds });
232
243
  }
233
- async runSync(agentId, inputs) {
244
+ async runSync(agentId, inputs, metadata) {
234
245
  validateUuid(agentId, "agentId");
235
- const resp = await this.httpClient.postWithDynamicInputs(`/v1/agents/run/${agentId}/`, inputs);
246
+ const resp = await this.httpClient.postWithDynamicInputs(`/v1/agents/run/${agentId}/`, inputs, undefined, metadata);
236
247
  return resp;
237
248
  }
238
249
  async runVersion(params) {
239
250
  validateUuid(params.agentId, "agentId");
240
251
  validateUuid(params.versionId, "versionId");
241
- const response = await this.httpClient.postWithDynamicInputs(`/v1/agents/run/${params.agentId}/versions/${params.versionId}/async/`, params.inputs);
252
+ const response = await this.httpClient.postWithDynamicInputs(`/v1/agents/run/${params.agentId}/versions/${params.versionId}/async/`, params.inputs, undefined, params.metadata);
242
253
  // Handle both string response and object response formats
243
254
  const jobId = typeof response === "string" ? response : response?.job_id;
244
255
  if (!jobId || typeof jobId !== "string") {
@@ -249,10 +260,10 @@ export class AgentsAPI {
249
260
  }
250
261
  return new Job({ agentsApi: this, jobId, timeoutSeconds: params.timeoutSeconds });
251
262
  }
252
- async runVersionSync(agentId, versionId, inputs) {
263
+ async runVersionSync(agentId, versionId, inputs, metadata) {
253
264
  validateUuid(agentId, "agentId");
254
265
  validateUuid(versionId, "versionId");
255
- return this.httpClient.postWithDynamicInputs(`/v1/agents/run/${agentId}/versions/${versionId}/`, inputs);
266
+ return this.httpClient.postWithDynamicInputs(`/v1/agents/run/${agentId}/versions/${versionId}/`, inputs, undefined, metadata);
256
267
  }
257
268
  *iterChunks(items, size) {
258
269
  for (let i = 0; i < items.length; i += size) {
@@ -22,7 +22,7 @@ export declare class JobBatch {
22
22
  private readonly jobIds;
23
23
  private readonly timeoutSeconds;
24
24
  private completed;
25
- private statusCache;
25
+ private statuses;
26
26
  constructor(opts: {
27
27
  agentsApi: AgentsAPI;
28
28
  jobIds: string[];
@@ -33,5 +33,5 @@ export declare class JobBatch {
33
33
  intervalSeconds?: number;
34
34
  timeoutSeconds?: number;
35
35
  }): Promise<AgentJobResult[]>;
36
- retrieveStatus(): Promise<Record<string, number>>;
36
+ retrieveStatus(): Promise<Record<string, AgentJobStatus>>;
37
37
  }
@@ -20,11 +20,30 @@ export class Job {
20
20
  while (true) {
21
21
  const status = await this.retrieveStatus();
22
22
  const code = status.status;
23
- if (code === JobStatus.SUCCESS || code === JobStatus.CACHED) {
24
- return this.retrieveResult();
25
- }
26
- if (code === JobStatus.FAILURE || code === JobStatus.CANCELLED) {
27
- throw new Error(`Job ${this.jobId} failed with status ${code}${status.error_message ? `: ${status.error_message}` : ""}`);
23
+ if (code === JobStatus.SUCCESS ||
24
+ code === JobStatus.CACHED ||
25
+ code === JobStatus.FAILURE ||
26
+ code === JobStatus.CANCELLED) {
27
+ const isFailed = code === JobStatus.FAILURE || code === JobStatus.CANCELLED;
28
+ let result;
29
+ try {
30
+ result = await this.retrieveResult();
31
+ }
32
+ catch (err) {
33
+ if (!isFailed)
34
+ throw err;
35
+ result = {
36
+ agent_id: "",
37
+ agent_version_id: "",
38
+ inputs: [],
39
+ outputs: [],
40
+ input_tokens: null,
41
+ output_tokens: null,
42
+ };
43
+ }
44
+ result.status = code;
45
+ result.error_message = status.error_message ?? null;
46
+ return result;
28
47
  }
29
48
  if (Date.now() - start > timeoutSeconds * 1000) {
30
49
  throw new Error(`Job ${this.jobId} did not complete within ${timeoutSeconds} seconds`);
@@ -42,7 +61,7 @@ export class Job {
42
61
  export class JobBatch {
43
62
  constructor(opts) {
44
63
  this.completed = {};
45
- this.statusCache = {};
64
+ this.statuses = {};
46
65
  this.agentsApi = opts.agentsApi;
47
66
  this.jobIds = opts.jobIds;
48
67
  this.timeoutSeconds = opts.timeoutSeconds ?? 7200;
@@ -64,45 +83,86 @@ export class JobBatch {
64
83
  if (!pending.length)
65
84
  break;
66
85
  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
86
  const completedIds = [];
73
87
  for (const status of statusBatch) {
74
88
  const code = status.status;
75
- if (code === JobStatus.SUCCESS || code === JobStatus.CACHED) {
89
+ if (code === JobStatus.SUCCESS ||
90
+ code === JobStatus.CACHED ||
91
+ code === JobStatus.FAILURE ||
92
+ code === JobStatus.CANCELLED) {
76
93
  completedIds.push(status.id);
77
94
  }
78
95
  if (code !== undefined && code !== null) {
79
- this.statusCache[status.id] = code;
96
+ this.statuses[status.id] = {
97
+ status: code,
98
+ timestamp: status.timestamp ?? null,
99
+ error_message: status.error_message ?? null,
100
+ };
80
101
  }
81
102
  }
82
103
  if (completedIds.length) {
83
- const resultBatch = await this.agentsApi.jobs.retrieveResultMany(completedIds);
104
+ let resultBatch;
105
+ try {
106
+ resultBatch = await this.agentsApi.jobs.retrieveResultMany(completedIds);
107
+ }
108
+ catch (err) {
109
+ // Only synthesize for failed/cancelled — can't fake results for success/cached
110
+ const failedIds = completedIds.filter((id) => {
111
+ const s = this.statuses[id]?.status;
112
+ return s === JobStatus.FAILURE || s === JobStatus.CANCELLED;
113
+ });
114
+ if (failedIds.length < completedIds.length) {
115
+ // Some successful jobs had their results lost — must propagate
116
+ throw err;
117
+ }
118
+ resultBatch = failedIds.map((id) => ({
119
+ id,
120
+ status: this.statuses[id]?.status ?? null,
121
+ agent_id: null,
122
+ agent_version_id: null,
123
+ inputs: null,
124
+ result: null,
125
+ corrected_outputs: null,
126
+ input_tokens: null,
127
+ output_tokens: null,
128
+ }));
129
+ }
84
130
  for (const res of resultBatch) {
131
+ const jobStatus = res.status ?? this.statuses[res.id]?.status ?? null;
132
+ const isFailed = jobStatus === JobStatus.FAILURE || jobStatus === JobStatus.CANCELLED;
85
133
  if (!res.agent_id || !res.agent_version_id) {
86
- const id = res.id ?? 'unknown';
87
- throw new Error(`Job ${id} missing agent identifiers`);
134
+ if (!isFailed) {
135
+ const id = res.id ?? 'unknown';
136
+ throw new Error(`Job ${id} missing agent identifiers`);
137
+ }
88
138
  }
89
139
  // Use corrected_outputs as fallback if result is null/undefined
90
- const outputs = res.result ?? res.corrected_outputs;
91
- if (outputs == null) {
92
- // Both result and corrected_outputs are null - this may indicate an error or incomplete job
93
- throw new Error(`Job ${res.id} returned null or undefined result`);
140
+ const rawOutputs = res.result ?? res.corrected_outputs;
141
+ let outputs;
142
+ if (rawOutputs == null) {
143
+ if (!isFailed) {
144
+ throw new Error(`Job ${res.id} returned null or undefined result`);
145
+ }
146
+ outputs = [];
94
147
  }
95
- if (!Array.isArray(outputs)) {
96
- // Result exists but is not an array - unexpected format
97
- throw new Error(`Job ${res.id} returned unexpected result format: ${typeof outputs}`);
148
+ else if (!Array.isArray(rawOutputs)) {
149
+ if (!isFailed) {
150
+ throw new Error(`Job ${res.id} returned unexpected result format: ${typeof rawOutputs}`);
151
+ }
152
+ outputs = [];
153
+ }
154
+ else {
155
+ outputs = rawOutputs;
98
156
  }
99
157
  this.completed[res.id] = {
100
- agent_id: res.agent_id,
101
- agent_version_id: res.agent_version_id,
158
+ agent_id: res.agent_id ?? "",
159
+ agent_version_id: res.agent_version_id ?? "",
102
160
  inputs: res.inputs ?? [],
103
161
  input_tokens: res.input_tokens,
104
162
  output_tokens: res.output_tokens,
105
163
  outputs,
164
+ status: jobStatus,
165
+ error_message: this.statuses[res.id]?.error_message ?? null,
106
166
  };
107
167
  }
108
168
  }
@@ -117,14 +177,29 @@ export class JobBatch {
117
177
  return this.jobIds.map((id) => this.completed[id]);
118
178
  }
119
179
  async retrieveStatus() {
120
- const statusMap = { ...this.statusCache };
121
- const toQuery = this.jobIds.filter((id) => statusMap[id] === undefined);
180
+ const statusMap = {};
181
+ const toQuery = [];
182
+ const TERMINAL = new Set([JobStatus.SUCCESS, JobStatus.CACHED, JobStatus.FAILURE, JobStatus.CANCELLED]);
183
+ for (const id of this.jobIds) {
184
+ const cached = this.statuses[id];
185
+ if (cached !== undefined && TERMINAL.has(cached.status)) {
186
+ statusMap[id] = cached;
187
+ }
188
+ else {
189
+ toQuery.push(id);
190
+ }
191
+ }
122
192
  if (toQuery.length) {
123
- const statuses = await this.agentsApi.jobs.retrieveStatusMany(toQuery);
124
- for (const s of statuses) {
193
+ const batchStatuses = await this.agentsApi.jobs.retrieveStatusMany(toQuery);
194
+ for (const s of batchStatuses) {
125
195
  if (s.status !== undefined && s.status !== null) {
126
- statusMap[s.id] = s.status;
127
- this.statusCache[s.id] = s.status;
196
+ const js = {
197
+ status: s.status,
198
+ timestamp: s.timestamp ?? null,
199
+ error_message: s.error_message ?? null,
200
+ };
201
+ this.statuses[s.id] = js;
202
+ statusMap[s.id] = js;
128
203
  }
129
204
  }
130
205
  }
@@ -26,7 +26,7 @@ export type PaginatedResponse<T> = {
26
26
  };
27
27
  export type AgentJobStatus = {
28
28
  status: number;
29
- timestamp: number;
29
+ timestamp: number | null;
30
30
  error_message?: string | null;
31
31
  };
32
32
  export type Reference = {
@@ -43,12 +43,22 @@ export type AgentJobResult = {
43
43
  input_tokens?: number | null;
44
44
  output_tokens?: number | null;
45
45
  outputs: AgentDatum[];
46
+ status?: number | null;
47
+ error_message?: string | null;
46
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;
47
55
  export type AgentJobStatusBatch = {
48
56
  id: string;
49
57
  status?: number | null;
50
58
  created_at?: unknown;
51
59
  last_updated_at?: unknown;
60
+ timestamp?: number | null;
61
+ error_message?: string | null;
52
62
  };
53
63
  export type AgentJobResultBatch = {
54
64
  id: string;
@@ -34,3 +34,15 @@ export function extractReferencesFromOutputs(outputs) {
34
34
  export function getJobReferences(result) {
35
35
  return extractReferencesFromOutputs(result.outputs ?? []);
36
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
+ }
@@ -26,5 +26,5 @@ export declare class RoeHTTPClient {
26
26
  private executeWithFormData;
27
27
  put<T>(url: string, json?: Record<string, unknown>, params?: Record<string, unknown>): Promise<T>;
28
28
  delete(url: string, params?: Record<string, unknown>): Promise<void>;
29
- postWithDynamicInputs<T>(url: string, inputs: Record<string, unknown>, params?: Record<string, unknown>): Promise<T>;
29
+ postWithDynamicInputs<T>(url: string, inputs: Record<string, unknown>, params?: Record<string, unknown>, metadata?: Record<string, unknown>): Promise<T>;
30
30
  }
@@ -196,7 +196,7 @@ export class RoeHTTPClient {
196
196
  delete(url, params) {
197
197
  return this.execute(() => this.client.delete(url, { params }));
198
198
  }
199
- async postWithDynamicInputs(url, inputs, params) {
199
+ async postWithDynamicInputs(url, inputs, params, metadata) {
200
200
  const formData = {};
201
201
  const files = {};
202
202
  // Process inputs and detect file paths asynchronously
@@ -224,6 +224,9 @@ export class RoeHTTPClient {
224
224
  formData[key] = value;
225
225
  }
226
226
  }
227
+ if (metadata !== undefined) {
228
+ formData.metadata = JSON.stringify(metadata);
229
+ }
227
230
  return this.post({ url, formData, files, params });
228
231
  }
229
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roe-typescript",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",