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.
@@ -1,4 +1,3 @@
1
- import { PaginationHelper } from "../utils/pagination.js";
2
1
  import { isUuidString } from "../utils/fileDetection.js";
3
2
  import { BadRequestError, RoeAPIException } from "../exceptions.js";
4
3
  /**
@@ -14,77 +13,122 @@ export class PolicyVersionsAPI {
14
13
  constructor(policiesApi) {
15
14
  this.policiesApi = policiesApi;
16
15
  }
17
- get http() {
18
- return this.policiesApi.httpClient;
16
+ get raw() {
17
+ return this.policiesApi.raw;
18
+ }
19
+ get organizationId() {
20
+ return this.policiesApi.config.organizationId;
19
21
  }
20
22
  async list(policyId) {
21
23
  validateUuid(policyId, "policyId");
22
- const data = await this.http.get(`/v1/policies/${policyId}/versions/`);
23
- if (Array.isArray(data))
24
- return data;
25
- return data.results ?? [];
24
+ const { data } = await this.raw.GET("/v1/policies/{policy_id}/versions/", {
25
+ params: {
26
+ path: { policy_id: policyId },
27
+ query: { organization_id: this.organizationId },
28
+ },
29
+ });
30
+ return data?.results ?? [];
26
31
  }
27
32
  async retrieve(policyId, versionId) {
28
33
  validateUuid(policyId, "policyId");
29
34
  validateUuid(versionId, "versionId");
30
- return this.http.get(`/v1/policies/${policyId}/versions/${versionId}/`);
35
+ const { data } = await this.raw.GET("/v1/policies/{policy_id}/versions/{version_id}/", {
36
+ params: {
37
+ path: { policy_id: policyId, version_id: versionId },
38
+ query: { organization_id: this.organizationId },
39
+ },
40
+ });
41
+ return data;
31
42
  }
32
43
  async create(params) {
33
44
  validateUuid(params.policyId, "policyId");
34
45
  if (params.baseVersionId !== undefined)
35
46
  validateUuid(params.baseVersionId, "baseVersionId");
36
- const payload = { content: params.content };
47
+ const body = {
48
+ content: params.content,
49
+ };
37
50
  if (params.versionName !== undefined)
38
- payload.version_name = params.versionName;
51
+ body.version_name = params.versionName;
39
52
  if (params.baseVersionId !== undefined)
40
- payload.base_version_id = params.baseVersionId;
41
- const data = await this.http.post({
42
- url: `/v1/policies/${params.policyId}/versions/`,
43
- json: payload,
53
+ body.base_version_id = params.baseVersionId;
54
+ const { data } = await this.raw.POST("/v1/policies/{policy_id}/versions/", {
55
+ params: {
56
+ path: { policy_id: params.policyId },
57
+ query: { organization_id: this.organizationId },
58
+ },
59
+ body,
44
60
  });
45
- if (!data.id) {
61
+ if (!data?.id) {
46
62
  throw new RoeAPIException(`Unexpected response from server: ${JSON.stringify(data)}`);
47
63
  }
48
- // POST returns partial data; re-fetch to get the full version
64
+ // POST returns a partial CreatePolicyVersion; re-fetch to get the full version.
49
65
  return this.retrieve(params.policyId, data.id);
50
66
  }
51
67
  }
52
68
  export class PoliciesAPI {
53
- constructor(config, httpClient) {
69
+ constructor(config, raw) {
54
70
  this.config = config;
55
- this.httpClient = httpClient;
71
+ this.raw = raw;
56
72
  this.versions = new PolicyVersionsAPI(this);
57
73
  }
58
74
  async list(params) {
59
- const query = PaginationHelper.buildQueryParams(this.config.organizationId, params?.page, params?.pageSize);
60
- return this.httpClient.get("/v1/policies/", query);
75
+ const { data } = await this.raw.GET("/v1/policies/", {
76
+ params: {
77
+ query: {
78
+ organization_id: this.config.organizationId,
79
+ page: params?.page,
80
+ page_size: params?.pageSize,
81
+ },
82
+ },
83
+ });
84
+ return data;
61
85
  }
62
86
  async retrieve(policyId) {
63
87
  validateUuid(policyId, "policyId");
64
- return this.httpClient.get(`/v1/policies/${policyId}/`);
88
+ const { data } = await this.raw.GET("/v1/policies/{id}/", {
89
+ params: {
90
+ path: { id: policyId },
91
+ query: { organization_id: this.config.organizationId },
92
+ },
93
+ });
94
+ return data;
65
95
  }
66
96
  async create(params) {
67
- const payload = {
97
+ const body = {
68
98
  name: params.name,
69
99
  content: params.content,
70
100
  description: params.description ?? "",
71
- organization_id: this.config.organizationId,
101
+ version_name: params.versionName ?? "version 1",
72
102
  };
73
- if (params.versionName !== undefined)
74
- payload.version_name = params.versionName;
75
- return this.httpClient.post({ url: "/v1/policies/", json: payload });
103
+ const { data } = await this.raw.POST("/v1/policies/", {
104
+ params: { query: { organization_id: this.config.organizationId } },
105
+ body,
106
+ });
107
+ return data;
76
108
  }
77
109
  async update(policyId, updates) {
78
110
  validateUuid(policyId, "policyId");
79
- const payload = {};
111
+ const body = {};
80
112
  if (updates.name !== undefined)
81
- payload.name = updates.name;
113
+ body.name = updates.name;
82
114
  if (updates.description !== undefined)
83
- payload.description = updates.description;
84
- return this.httpClient.put(`/v1/policies/${policyId}/`, payload);
115
+ body.description = updates.description;
116
+ const { data } = await this.raw.PATCH("/v1/policies/{id}/", {
117
+ params: {
118
+ path: { id: policyId },
119
+ query: { organization_id: this.config.organizationId },
120
+ },
121
+ body,
122
+ });
123
+ return data;
85
124
  }
86
125
  async delete(policyId) {
87
126
  validateUuid(policyId, "policyId");
88
- await this.httpClient.delete(`/v1/policies/${policyId}/`);
127
+ await this.raw.DELETE("/v1/policies/{id}/", {
128
+ params: {
129
+ path: { id: policyId },
130
+ query: { organization_id: this.config.organizationId },
131
+ },
132
+ });
89
133
  }
90
134
  }
@@ -0,0 +1,18 @@
1
+ import type { RoeConfig } from "../config.js";
2
+ import type { RoeRawClient } from "../generated/client.js";
3
+ import type { components } from "../generated/schema.js";
4
+ type User = components["schemas"]["UserInfo"];
5
+ export declare class UsersAPI {
6
+ readonly config: RoeConfig;
7
+ readonly raw: RoeRawClient;
8
+ constructor(config: RoeConfig, raw: RoeRawClient);
9
+ /**
10
+ * Retrieve the currently authenticated user.
11
+ *
12
+ * Wraps `GET /v1/users/current_user/`. The OpenAPI spec marks the response
13
+ * body as undocumented, so we read JSON via openapi-fetch's `parseAs` escape
14
+ * hatch and cast the result to `UserInfo` (the schema the backend returns).
15
+ */
16
+ me(): Promise<User>;
17
+ }
18
+ export {};
@@ -0,0 +1,17 @@
1
+ export class UsersAPI {
2
+ constructor(config, raw) {
3
+ this.config = config;
4
+ this.raw = raw;
5
+ }
6
+ /**
7
+ * Retrieve the currently authenticated user.
8
+ *
9
+ * Wraps `GET /v1/users/current_user/`. The OpenAPI spec marks the response
10
+ * body as undocumented, so we read JSON via openapi-fetch's `parseAs` escape
11
+ * hatch and cast the result to `UserInfo` (the schema the backend returns).
12
+ */
13
+ async me() {
14
+ const result = await this.raw.GET("/v1/users/current_user/", { parseAs: "json" });
15
+ return result.data;
16
+ }
17
+ }
package/dist/client.d.ts CHANGED
@@ -1,16 +1,20 @@
1
1
  import { AgentsAPI } from "./api/agents.js";
2
2
  import { PoliciesAPI } from "./api/policies.js";
3
+ import { UsersAPI } from "./api/users.js";
3
4
  import { RoeAuth } from "./auth.js";
4
5
  import { RoeConfig, RoeConfigInput } from "./config.js";
5
- import { RoeHTTPClient } from "./utils/httpClient.js";
6
+ import { type RoeRawClient } from "./generated/client.js";
6
7
  export declare class RoeClient {
7
8
  readonly config: RoeConfig;
8
9
  readonly auth: RoeAuth;
9
- readonly httpClient: RoeHTTPClient;
10
+ readonly raw: RoeRawClient;
10
11
  private readonly _agents;
11
12
  private readonly _policies;
13
+ private readonly _users;
12
14
  constructor(input?: RoeConfigInput);
13
15
  get agents(): AgentsAPI;
14
16
  get policies(): PoliciesAPI;
17
+ get users(): UsersAPI;
18
+ /** No-op kept for source compatibility; the raw client manages its own connections. */
15
19
  close(): void;
16
20
  }
package/dist/client.js CHANGED
@@ -1,15 +1,23 @@
1
1
  import { AgentsAPI } from "./api/agents.js";
2
2
  import { PoliciesAPI } from "./api/policies.js";
3
+ import { UsersAPI } from "./api/users.js";
3
4
  import { RoeAuth } from "./auth.js";
4
5
  import { RoeConfig } from "./config.js";
5
- import { RoeHTTPClient } from "./utils/httpClient.js";
6
+ import { createRoeRawClient } from "./generated/client.js";
6
7
  export class RoeClient {
7
8
  constructor(input = {}) {
8
9
  this.config = RoeConfig.fromEnv(input);
9
10
  this.auth = new RoeAuth(this.config);
10
- this.httpClient = new RoeHTTPClient(this.config, this.auth);
11
- this._agents = new AgentsAPI(this.config, this.httpClient);
12
- this._policies = new PoliciesAPI(this.config, this.httpClient);
11
+ this.raw = createRoeRawClient({
12
+ baseUrl: this.config.baseUrl,
13
+ // Auth headers are injected per-request by authMiddleware; no static
14
+ // headers option here so a stale snapshot can't shadow the live token.
15
+ auth: this.auth,
16
+ maxRetries: this.config.maxRetries,
17
+ });
18
+ this._agents = new AgentsAPI(this.config, this.raw);
19
+ this._policies = new PoliciesAPI(this.config, this.raw);
20
+ this._users = new UsersAPI(this.config, this.raw);
13
21
  }
14
22
  get agents() {
15
23
  return this._agents;
@@ -17,7 +25,11 @@ export class RoeClient {
17
25
  get policies() {
18
26
  return this._policies;
19
27
  }
28
+ get users() {
29
+ return this._users;
30
+ }
31
+ /** No-op kept for source compatibility; the raw client manages its own connections. */
20
32
  close() {
21
- this.httpClient.close();
33
+ /* no-op */
22
34
  }
23
35
  }
@@ -74,6 +74,14 @@ export function extractErrorMessage(data, statusCode) {
74
74
  return firstError.message;
75
75
  }
76
76
  }
77
+ for (const value of Object.values(obj)) {
78
+ if (Array.isArray(value) && value.length > 0) {
79
+ return value.map(String).join("; ");
80
+ }
81
+ if (typeof value === "string" && value.length > 0) {
82
+ return value;
83
+ }
84
+ }
77
85
  }
78
86
  // Fallback to generic message
79
87
  return `HTTP ${statusCode} error`;
@@ -0,0 +1,12 @@
1
+ import type { components, paths } from "./schema.js";
2
+ import type { RoeAuth } from "../auth.js";
3
+ export type RoeApiComponents = components;
4
+ export type RoeApiPaths = paths;
5
+ export type RoeRawClient = ReturnType<typeof createRoeRawClient>;
6
+ export declare function createRoeRawClient(options: {
7
+ baseUrl: string;
8
+ headers?: HeadersInit;
9
+ fetch?: typeof fetch;
10
+ auth?: RoeAuth;
11
+ maxRetries?: number;
12
+ }): import("openapi-fetch").Client<paths, `${string}/${string}`>;
@@ -0,0 +1,55 @@
1
+ // Hand-maintained boilerplate. Lives under src/generated/ alongside schema.d.ts
2
+ // because it imports from it. Drift CI regenerates schema.d.ts and verifies
3
+ // this file is unchanged.
4
+ import createClient from "openapi-fetch";
5
+ import { authMiddleware, errorMiddleware, retryMiddleware, } from "../utils/middleware.js";
6
+ // Default fetch: resolves globalThis.fetch at call time so vitest's
7
+ // vi.stubGlobal('fetch', ...) is honored. In Node 18+ globalThis.fetch is
8
+ // undici-backed, so streaming bodies work end-to-end. For the multipart-with-
9
+ // Node-stream case in AgentsAPI we'll import undici.fetch directly.
10
+ const defaultFetch = (...args) => globalThis.fetch(...args);
11
+ function isNetworkFetchError(error) {
12
+ return error instanceof TypeError && /fetch failed/i.test(error.message);
13
+ }
14
+ function networkRetryFetch(fetchImpl, maxRetries) {
15
+ return async (input, init) => {
16
+ let attempt = 0;
17
+ while (true) {
18
+ try {
19
+ // `Request` bodies are single-use. Clone before each attempt so a
20
+ // transient socket failure can be retried without disturbing the
21
+ // original request object openapi-fetch handed us.
22
+ const retryableInput = maxRetries > 0 && input instanceof Request ? input.clone() : input;
23
+ return await fetchImpl(retryableInput, init);
24
+ }
25
+ catch (error) {
26
+ if (attempt >= maxRetries || !isNetworkFetchError(error)) {
27
+ throw error;
28
+ }
29
+ const backoffMs = Math.min(1000 * 2 ** attempt, 10000);
30
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
31
+ attempt += 1;
32
+ }
33
+ }
34
+ };
35
+ }
36
+ export function createRoeRawClient(options) {
37
+ const maxRetries = options.maxRetries ?? 0;
38
+ const client = createClient({
39
+ baseUrl: options.baseUrl,
40
+ headers: options.headers,
41
+ fetch: networkRetryFetch(options.fetch ?? defaultFetch, maxRetries),
42
+ });
43
+ // openapi-fetch runs onRequest in registration order and onResponse in
44
+ // REVERSE registration order. We want responses to flow auth -> retry ->
45
+ // error so the error middleware sees the final post-retry response.
46
+ // Registration order is therefore the inverse: error -> retry -> auth.
47
+ client.use(errorMiddleware);
48
+ if (maxRetries > 0) {
49
+ client.use(retryMiddleware(maxRetries));
50
+ }
51
+ if (options.auth) {
52
+ client.use(authMiddleware(options.auth));
53
+ }
54
+ return client;
55
+ }
package/dist/index.d.ts CHANGED
@@ -2,11 +2,13 @@ export { RoeClient } from "./client.js";
2
2
  export { RoeConfig } from "./config.js";
3
3
  export type { RoeConfigInput } from "./config.js";
4
4
  export { RoeAuth } from "./auth.js";
5
+ export { createRoeRawClient } from "./generated/client.js";
6
+ export type { RoeApiComponents, RoeApiPaths, RoeRawClient } from "./generated/client.js";
7
+ export type { components, paths } from "./generated/schema.js";
5
8
  export * from "./exceptions.js";
6
9
  export * from "./api/agents.js";
7
10
  export * from "./api/policies.js";
8
- export * from "./models/agent.js";
9
- export * from "./models/policy.js";
10
- export * from "./models/job.js";
11
- export * from "./models/responses.js";
12
- export * from "./models/file.js";
11
+ export * from "./api/users.js";
12
+ export { Job, JobBatch, JobStatus } from "./models/job.js";
13
+ export type { JobResult, JobBatchItem } from "./models/job.js";
14
+ export { FileUpload } from "./models/file.js";
package/dist/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  export { RoeClient } from "./client.js";
2
2
  export { RoeConfig } from "./config.js";
3
3
  export { RoeAuth } from "./auth.js";
4
+ export { createRoeRawClient } from "./generated/client.js";
4
5
  export * from "./exceptions.js";
5
6
  export * from "./api/agents.js";
6
7
  export * from "./api/policies.js";
7
- export * from "./models/agent.js";
8
- export * from "./models/policy.js";
9
- export * from "./models/job.js";
10
- export * from "./models/responses.js";
11
- export * from "./models/file.js";
8
+ export * from "./api/users.js";
9
+ export { Job, JobBatch, JobStatus } from "./models/job.js";
10
+ export { FileUpload } from "./models/file.js";
11
+ // Hand-written response models removed in v1.0 (Agent, BaseAgent, Policy, PolicyVersion,
12
+ // AgentVersion, AgentDatum, AgentJobResult, AgentJobStatus, Reference, JobDataDeleteResponse,
13
+ // PaginatedResponse, UserInfo). Import the generated equivalents instead, e.g.:
14
+ // import type { components } from "@roe-ai/typescript";
15
+ // type Policy = components["schemas"]["Policy"];
@@ -10,6 +10,13 @@ export declare class FileUpload {
10
10
  readonly filename?: string;
11
11
  readonly mimeType?: string;
12
12
  private _openedStream;
13
+ /**
14
+ * Cached Blob built from the stream-backed source. Streams can only be read
15
+ * once, so we buffer their bytes on the first `toBlob()` call and reuse the
16
+ * result on retries — otherwise repeated calls would silently upload an
17
+ * empty file.
18
+ */
19
+ private _streamBlob;
13
20
  constructor(init: FileUploadInit);
14
21
  get effectiveFilename(): string;
15
22
  get effectiveMimeType(): string;
@@ -18,6 +25,16 @@ export declare class FileUpload {
18
25
  * Track opened streams so they can be closed later.
19
26
  */
20
27
  open(): NodeJS.ReadableStream;
28
+ /**
29
+ * Loads the file contents as a Blob with the resolved MIME type.
30
+ *
31
+ * Path-backed uploads are read fully into memory before the Blob is built.
32
+ * That keeps the wrapper cross-runtime (browser + Node) at the cost of
33
+ * peak RAM = file size. Real-world files are well under MAX_FILE_SIZE,
34
+ * but if you hit memory pressure switch to a streaming `Readable` and
35
+ * use `undici`-flavored FormData.
36
+ */
37
+ toBlob(): Promise<Blob>;
21
38
  /**
22
39
  * Closes any opened file stream to prevent resource leaks.
23
40
  * Should be called after the file upload is complete.
@@ -7,6 +7,13 @@ const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
7
7
  export class FileUpload {
8
8
  constructor(init) {
9
9
  this._openedStream = null;
10
+ /**
11
+ * Cached Blob built from the stream-backed source. Streams can only be read
12
+ * once, so we buffer their bytes on the first `toBlob()` call and reuse the
13
+ * result on retries — otherwise repeated calls would silently upload an
14
+ * empty file.
15
+ */
16
+ this._streamBlob = null;
10
17
  this.path = init.path;
11
18
  this.file = init.file;
12
19
  this.filename = init.filename;
@@ -58,6 +65,35 @@ export class FileUpload {
58
65
  }
59
66
  throw new Error("No file source available");
60
67
  }
68
+ /**
69
+ * Loads the file contents as a Blob with the resolved MIME type.
70
+ *
71
+ * Path-backed uploads are read fully into memory before the Blob is built.
72
+ * That keeps the wrapper cross-runtime (browser + Node) at the cost of
73
+ * peak RAM = file size. Real-world files are well under MAX_FILE_SIZE,
74
+ * but if you hit memory pressure switch to a streaming `Readable` and
75
+ * use `undici`-flavored FormData.
76
+ */
77
+ async toBlob() {
78
+ if (this.path) {
79
+ const buf = await fs.promises.readFile(this.path);
80
+ return new Blob([buf], { type: this.effectiveMimeType });
81
+ }
82
+ if (this.file) {
83
+ // Streams are single-shot. Buffer the bytes once and cache the resulting
84
+ // Blob so retry attempts (which call `toBlob()` again) don't end up
85
+ // uploading a zero-byte body.
86
+ if (this._streamBlob)
87
+ return this._streamBlob;
88
+ const chunks = [];
89
+ for await (const chunk of this.file) {
90
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
91
+ }
92
+ this._streamBlob = new Blob([Buffer.concat(chunks)], { type: this.effectiveMimeType });
93
+ return this._streamBlob;
94
+ }
95
+ throw new Error("No file source available");
96
+ }
61
97
  /**
62
98
  * Closes any opened file stream to prevent resource leaks.
63
99
  * Should be called after the file upload is complete.
@@ -1,5 +1,28 @@
1
1
  import { AgentsAPI } from "../api/agents.js";
2
- import { AgentJobResult, AgentJobStatus } from "./responses.js";
2
+ import type { components } from "../generated/schema.js";
3
+ export declare enum JobStatus {
4
+ PENDING = 0,
5
+ STARTED = 1,
6
+ RETRY = 2,
7
+ SUCCESS = 3,
8
+ FAILURE = 4,
9
+ CANCELLED = 5,
10
+ CACHED = 6
11
+ }
12
+ type AgentJobStatus = components["schemas"]["AgentJobStatus"];
13
+ type AgentJobResultResponse = components["schemas"]["AgentJobResultResponse"];
14
+ type AgentJobResultItem = components["schemas"]["AgentJobResultItem"];
15
+ type AgentDatum = components["schemas"]["AgentDatum"];
16
+ /** Result returned by `Job.wait()` — generated body merged with terminal status + error. */
17
+ export type JobResult = AgentJobResultResponse & {
18
+ status: number | null;
19
+ error_message: string | null;
20
+ };
21
+ /** Result returned by `JobBatch.wait()` — per-job item merged with normalized error message. */
22
+ export type JobBatchItem = AgentJobResultItem & {
23
+ outputs: AgentDatum[];
24
+ error_message: string | null;
25
+ };
3
26
  export declare class Job {
4
27
  private readonly agentsApi;
5
28
  private readonly jobId;
@@ -13,9 +36,9 @@ export declare class Job {
13
36
  wait(params?: {
14
37
  intervalSeconds?: number;
15
38
  timeoutSeconds?: number;
16
- }): Promise<AgentJobResult>;
39
+ }): Promise<JobResult>;
17
40
  retrieveStatus(): Promise<AgentJobStatus>;
18
- retrieveResult(): Promise<AgentJobResult>;
41
+ retrieveResult(): Promise<AgentJobResultResponse>;
19
42
  }
20
43
  export declare class JobBatch {
21
44
  private readonly agentsApi;
@@ -32,6 +55,7 @@ export declare class JobBatch {
32
55
  wait(params?: {
33
56
  intervalSeconds?: number;
34
57
  timeoutSeconds?: number;
35
- }): Promise<AgentJobResult[]>;
58
+ }): Promise<JobBatchItem[]>;
36
59
  retrieveStatus(): Promise<Record<string, AgentJobStatus>>;
37
60
  }
61
+ export {};
@@ -1,4 +1,13 @@
1
- import { JobStatus } from "./responses.js";
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;
@@ -41,9 +50,11 @@ export class Job {
41
50
  output_tokens: null,
42
51
  };
43
52
  }
44
- result.status = code;
45
- result.error_message = status.error_message ?? null;
46
- return result;
53
+ return {
54
+ ...result,
55
+ status: code,
56
+ error_message: status.error_message ?? null,
57
+ };
47
58
  }
48
59
  if (Date.now() - start > timeoutSeconds * 1000) {
49
60
  throw new Error(`Job ${this.jobId} did not complete within ${timeoutSeconds} seconds`);
@@ -85,18 +96,25 @@ export class JobBatch {
85
96
  const statusBatch = await this.agentsApi.jobs.retrieveStatusMany(pending);
86
97
  const completedIds = [];
87
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
+ }
88
105
  const code = status.status;
89
106
  if (code === JobStatus.SUCCESS ||
90
107
  code === JobStatus.CACHED ||
91
108
  code === JobStatus.FAILURE ||
92
109
  code === JobStatus.CANCELLED) {
93
- completedIds.push(status.id);
110
+ completedIds.push(id);
94
111
  }
95
112
  if (code !== undefined && code !== null) {
96
- this.statuses[status.id] = {
113
+ this.statuses[id] = {
114
+ id,
97
115
  status: code,
98
- timestamp: status.timestamp ?? null,
99
- error_message: status.error_message ?? null,
116
+ timestamp: status.timestamp,
117
+ error_message: status.error_message,
100
118
  };
101
119
  }
102
120
  }
@@ -112,7 +130,6 @@ export class JobBatch {
112
130
  return s === JobStatus.FAILURE || s === JobStatus.CANCELLED;
113
131
  });
114
132
  if (failedIds.length < completedIds.length) {
115
- // Some successful jobs had their results lost — must propagate
116
133
  throw err;
117
134
  }
118
135
  resultBatch = failedIds.map((id) => ({
@@ -120,6 +137,7 @@ export class JobBatch {
120
137
  status: this.statuses[id]?.status ?? null,
121
138
  agent_id: null,
122
139
  agent_version_id: null,
140
+ cost: null,
123
141
  inputs: null,
124
142
  result: null,
125
143
  corrected_outputs: null,
@@ -132,11 +150,10 @@ export class JobBatch {
132
150
  const isFailed = jobStatus === JobStatus.FAILURE || jobStatus === JobStatus.CANCELLED;
133
151
  if (!res.agent_id || !res.agent_version_id) {
134
152
  if (!isFailed) {
135
- const id = res.id ?? 'unknown';
153
+ const id = res.id ?? "unknown";
136
154
  throw new Error(`Job ${id} missing agent identifiers`);
137
155
  }
138
156
  }
139
- // Use corrected_outputs as fallback if result is null/undefined
140
157
  const rawOutputs = res.result ?? res.corrected_outputs;
141
158
  let outputs;
142
159
  if (rawOutputs == null) {
@@ -155,13 +172,9 @@ export class JobBatch {
155
172
  outputs = rawOutputs;
156
173
  }
157
174
  this.completed[res.id] = {
158
- agent_id: res.agent_id ?? "",
159
- agent_version_id: res.agent_version_id ?? "",
160
- inputs: res.inputs ?? [],
161
- input_tokens: res.input_tokens,
162
- output_tokens: res.output_tokens,
163
- outputs,
175
+ ...res,
164
176
  status: jobStatus,
177
+ outputs,
165
178
  error_message: this.statuses[res.id]?.error_message ?? null,
166
179
  };
167
180
  }
@@ -192,14 +205,19 @@ export class JobBatch {
192
205
  if (toQuery.length) {
193
206
  const batchStatuses = await this.agentsApi.jobs.retrieveStatusMany(toQuery);
194
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
+ }
195
212
  if (s.status !== undefined && s.status !== null) {
196
213
  const js = {
214
+ id,
197
215
  status: s.status,
198
- timestamp: s.timestamp ?? null,
199
- error_message: s.error_message ?? null,
216
+ timestamp: s.timestamp,
217
+ error_message: s.error_message,
200
218
  };
201
- this.statuses[s.id] = js;
202
- statusMap[s.id] = js;
219
+ this.statuses[id] = js;
220
+ statusMap[id] = js;
203
221
  }
204
222
  }
205
223
  }