@instruments/materialgraph 0.1.0 → 0.3.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.
Files changed (56) hide show
  1. package/by-spec-types.d.ts +73 -0
  2. package/by-spec-types.js +7 -0
  3. package/client.d.ts +186 -7
  4. package/client.js +242 -125
  5. package/codes-client.d.ts +82 -0
  6. package/codes-client.js +73 -0
  7. package/codes-types.d.ts +238 -0
  8. package/codes-types.js +1 -0
  9. package/colour-aggregate-types.d.ts +93 -0
  10. package/colour-aggregate-types.js +1 -0
  11. package/colours-client.d.ts +52 -0
  12. package/colours-client.js +39 -0
  13. package/composition-types.d.ts +343 -0
  14. package/composition-types.js +10 -0
  15. package/http-transport.d.ts +74 -0
  16. package/http-transport.js +201 -0
  17. package/index.d.ts +19 -0
  18. package/index.js +9 -0
  19. package/match-stream-types.d.ts +150 -0
  20. package/match-stream-types.js +28 -0
  21. package/match-stream.d.ts +22 -0
  22. package/match-stream.js +280 -0
  23. package/material-block-types.d.ts +23 -2
  24. package/material-types.d.ts +92 -0
  25. package/material-types.js +1 -0
  26. package/ndjson.d.ts +46 -0
  27. package/ndjson.js +103 -0
  28. package/operations.d.ts +6 -2
  29. package/operations.js +30 -2
  30. package/package.json +1 -1
  31. package/project-types.d.ts +67 -0
  32. package/project-types.js +16 -0
  33. package/projects.d.ts +13 -0
  34. package/projects.js +46 -0
  35. package/run-events.d.ts +82 -0
  36. package/run-events.js +29 -0
  37. package/run-types.d.ts +266 -0
  38. package/run-types.js +6 -0
  39. package/runs.d.ts +84 -0
  40. package/runs.js +253 -0
  41. package/schedule-types.d.ts +88 -0
  42. package/schedule-types.js +12 -0
  43. package/search-refinement-types.d.ts +26 -1
  44. package/search-result-validation.d.ts +27 -0
  45. package/search-result-validation.js +174 -0
  46. package/source-material-validation.d.ts +3 -0
  47. package/source-material-validation.js +109 -0
  48. package/spec-types.d.ts +82 -0
  49. package/spec-types.js +44 -0
  50. package/specifications.d.ts +72 -0
  51. package/specifications.js +82 -0
  52. package/sse.d.ts +35 -0
  53. package/sse.js +147 -0
  54. package/types.d.ts +204 -77
  55. package/validation.d.ts +119 -0
  56. package/validation.js +241 -0
package/run-types.d.ts ADDED
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Hand-mirrored wire types for the MaterialGraph public Run + source-material
3
+ * surface (spec 0.7.0). Like {@link ./types.ts}, these interfaces carry NO
4
+ * runtime dependency on any `@materialgraph/*` package — they mirror the shapes
5
+ * in `apps/web/public/openapi.yaml` (`list_public_capabilities`,
6
+ * `create_public_run`, `get_public_run`, `list_public_run_events`,
7
+ * `resume_public_run`, `cancel_public_run`, `upload_source_material`).
8
+ *
9
+ * TOLERANT-PARSING CONTRACT: response enums are widened with {@link OpenEnum}
10
+ * so a new server value still type-checks, and objects are read structurally —
11
+ * unknown keys pass through untouched.
12
+ */
13
+ import type { JsonValue, OpenEnum } from "./types.js";
14
+ /**
15
+ * A file to upload. Two forms so the SDK is ergonomic on BOTH Node route
16
+ * handlers and modern runtimes: pass a platform `Blob`/`File` directly, or the
17
+ * bytes plus a filename. The SDK builds the multipart body internally — callers
18
+ * never touch `FormData` or wrestle a `string`-typed binary field.
19
+ */
20
+ export type FileInput = Blob | {
21
+ /** Raw file bytes. */
22
+ data: Uint8Array | ArrayBuffer;
23
+ /** Filename recorded on the multipart part (drives the stored receipt filename). */
24
+ filename: string;
25
+ /** Optional MIME type; the server re-validates by magic bytes regardless. */
26
+ contentType?: string;
27
+ };
28
+ /**
29
+ * A heterogeneous pile of source files (multipart field `files`, repeatable):
30
+ * any mix of PDF, plain-text/markdown, and images (≥1, ≤16). The server
31
+ * classifies each file and assigns its receipt {@link SourceMaterialReceiptFile.role};
32
+ * the caller no longer pre-sorts a brief from its references.
33
+ */
34
+ export interface UploadSourceMaterialFilesInput {
35
+ files: FileInput[];
36
+ }
37
+ /**
38
+ * The legacy pre-classified shape — a single PDF `brief` plus optional image
39
+ * `references` (multipart fields `brief` + `references`). Still accepted by the
40
+ * server, folded into the pile. Prefer {@link UploadSourceMaterialFilesInput}.
41
+ */
42
+ export interface UploadSourceMaterialLegacyInput {
43
+ /** The single PDF brief for the batch (multipart field `brief`). */
44
+ brief: FileInput;
45
+ /** Up to 12 reference images (multipart field `references`, repeatable). */
46
+ references?: FileInput[];
47
+ }
48
+ /**
49
+ * Input for {@link MaterialGraphClient.uploadSourceMaterial} — either the
50
+ * pile-of-files shape or the legacy brief+references shape.
51
+ */
52
+ export type UploadSourceMaterialInput = UploadSourceMaterialFilesInput | UploadSourceMaterialLegacyInput;
53
+ /** One stored file in an upload receipt. */
54
+ export interface SourceMaterialReceiptFile {
55
+ /** The stored file id (`smf_…`). */
56
+ sourceId: string;
57
+ /**
58
+ * The pipeline-assigned role. The vocabulary grows over time; {@link OpenEnum}
59
+ * keeps a newer server value type-checking. `unclassified` is a legitimate
60
+ * terminal value when the pipeline could not place the file.
61
+ */
62
+ role: OpenEnum<"brief" | "reference_image" | "document" | "document_image" | "inspiration_image" | "unclassified">;
63
+ filename: string;
64
+ mediaType: string;
65
+ /** Lowercase hex SHA-256 of the stored bytes. */
66
+ sha256: string;
67
+ bytes: number;
68
+ /** PDF page count; null for images. */
69
+ pages: number | null;
70
+ }
71
+ /** The `201` receipt from `POST /api/v1/source-material`. */
72
+ export interface SourceMaterialBatchReceipt {
73
+ /** The stored batch id (`smb_…`). */
74
+ batchId: string;
75
+ sources: SourceMaterialReceiptFile[];
76
+ }
77
+ /**
78
+ * One analysis computed over an uploaded image's bytes.
79
+ *
80
+ * Analyses are CONTENT-ADDRESSED: keyed by the file's SHA-256, not by the
81
+ * upload or the uploading key. Re-uploading an image someone has already
82
+ * analysed therefore returns that analysis immediately, with no work. That
83
+ * sharing is by design and discloses nothing — you only ever see analyses of
84
+ * images you hold yourself.
85
+ */
86
+ export interface SourceImageAnalysis {
87
+ /** The kind of read. {@link OpenEnum} keeps a newer server value type-checking. */
88
+ kind: OpenEnum<"scene_match">;
89
+ /** The model version that produced {@link SourceImageAnalysis.result}. */
90
+ model: string;
91
+ /**
92
+ * `failed` analyses are RETURNED, not hidden — so "we looked and it did not
93
+ * work" is distinguishable from "nobody has looked yet", which is an empty
94
+ * {@link SourceMaterialBatchFile.analyses}.
95
+ */
96
+ status: OpenEnum<"ok" | "failed">;
97
+ /**
98
+ * The payload verbatim, or the failure record when `status` is `failed`. For
99
+ * `scene_match`: the detected regions with their candidate variant ids and
100
+ * scores. The interior shape belongs to the producing model and may gain
101
+ * fields, so it is typed as JSON rather than pinned here.
102
+ */
103
+ result: Record<string, JsonValue>;
104
+ /** ISO-8601 timestamp of the most recent analysis. */
105
+ analysedAt: string;
106
+ }
107
+ /** One file as `GET /api/v1/source-material/{batchId}` returns it. */
108
+ export interface SourceMaterialBatchFile extends SourceMaterialReceiptFile {
109
+ /** Fetchable URL for the stored bytes. */
110
+ url: string;
111
+ /**
112
+ * ALWAYS an array. `[]` means nothing has been computed yet — never a missing
113
+ * field. Preprocessing is triggered manually today; when it becomes automatic
114
+ * only the trigger changes and this contract does not.
115
+ */
116
+ analyses: SourceImageAnalysis[];
117
+ }
118
+ /**
119
+ * The `200` body of `GET /api/v1/source-material/{batchId}` — the batch, every
120
+ * file it holds, and the analyses computed over each file's bytes.
121
+ */
122
+ export interface SourceMaterialBatchDetail {
123
+ /** The stored batch id (`smb_…`). */
124
+ batchId: string;
125
+ /** ISO-8601 timestamp of the upload. */
126
+ createdAt: string;
127
+ sources: SourceMaterialBatchFile[];
128
+ }
129
+ export type CapabilityKind = OpenEnum<"agent" | "tool" | "workflow">;
130
+ export type CapabilityExposure = OpenEnum<"public" | "partner" | "internal">;
131
+ export type CapabilityScope = OpenEnum<"mg:read" | "mg:write" | "mg:ingest" | "mg:admin">;
132
+ export type CapabilitySideEffectClass = OpenEnum<"read_only" | "browser_local" | "external_read" | "writes_proposed" | "writes_persisted" | "planned_only">;
133
+ export interface CapabilityRetryPolicy {
134
+ maxAttempts: number;
135
+ backoff: OpenEnum<"none" | "fixed" | "exponential">;
136
+ }
137
+ export interface CapabilityInvokeLink {
138
+ href: string;
139
+ method: OpenEnum<"GET" | "POST">;
140
+ }
141
+ /** One invocable capability from the governed public manifest. */
142
+ export interface PublicCapabilityMetadata {
143
+ publicId: string;
144
+ runtimeId: string;
145
+ kind: CapabilityKind;
146
+ exposure: CapabilityExposure;
147
+ name: string;
148
+ description: string;
149
+ schemas: {
150
+ input: Record<string, unknown>;
151
+ output: Record<string, unknown>;
152
+ };
153
+ sideEffectClass: CapabilitySideEffectClass;
154
+ scopes: CapabilityScope[];
155
+ timeoutMs: number;
156
+ retryPolicy: CapabilityRetryPolicy;
157
+ tags: string[];
158
+ links: {
159
+ invoke: CapabilityInvokeLink;
160
+ };
161
+ }
162
+ /** The `GET /api/v1/capabilities` response envelope. */
163
+ export interface CapabilityManifestResponse {
164
+ capabilities: PublicCapabilityMetadata[];
165
+ }
166
+ export type PublicRunStatus = OpenEnum<"queued" | "claimed" | "running" | "suspended" | "succeeded" | "failed" | "canceled">;
167
+ export interface PublicRunFailure {
168
+ code: string;
169
+ message: string;
170
+ retryable: boolean;
171
+ detail?: Record<string, unknown>;
172
+ }
173
+ export interface PublicRunArtifactRef {
174
+ kind: string;
175
+ ref: string;
176
+ }
177
+ /**
178
+ * The caller-owned, redacted Run ledger record. Prompts, raw model messages,
179
+ * reasoning, credentials, provider payloads, and source-document contents are
180
+ * never returned — inputs and outputs are the `redacted*Json` projections.
181
+ */
182
+ export interface PublicRun {
183
+ id: string;
184
+ capabilityKind: CapabilityKind;
185
+ capabilityId: string;
186
+ clientReference: string | null;
187
+ status: PublicRunStatus;
188
+ redactedInputJson: Record<string, unknown>;
189
+ redactedOutputJson: Record<string, unknown> | null;
190
+ usageJson: Record<string, unknown> | null;
191
+ artifactRefs: PublicRunArtifactRef[] | null;
192
+ failureJson: PublicRunFailure | null;
193
+ suspensionStepId: string | null;
194
+ eventCursor: number;
195
+ attempts: number;
196
+ startedAt: string | null;
197
+ cancellationRequestedAt: string | null;
198
+ completedAt: string | null;
199
+ createdAt: string;
200
+ updatedAt: string;
201
+ }
202
+ /** Body for `POST /api/v1/runs` ({@link MaterialGraphClient.createRun}). */
203
+ export interface CreatePublicRunRequest {
204
+ capabilityKind: CapabilityKind;
205
+ capabilityId: string;
206
+ /** The capability input, validated server-side against its live input schema. */
207
+ inputJson: unknown;
208
+ /** Stable caller key making Run creation idempotent across retries. */
209
+ idempotencyKey?: string;
210
+ /** Free-form caller correlation reference, echoed back on the Run. */
211
+ clientReference?: string;
212
+ }
213
+ /** The `POST /api/v1/runs` response (`200` replay or `202` accepted). */
214
+ export interface CreatePublicRunResponse {
215
+ accepted: boolean;
216
+ replayed: boolean;
217
+ workflowRunId: string;
218
+ run: PublicRun;
219
+ }
220
+ /** The `GET /api/v1/runs/{runId}` and resume/cancel response envelope. */
221
+ export interface PublicRunResponse {
222
+ run: PublicRun;
223
+ }
224
+ /** Body for `POST /api/v1/runs/{runId}/resume`. */
225
+ export interface ResumePublicRunRequest {
226
+ /** The resume payload, validated against the suspended step's resume schema. */
227
+ resumeData: unknown;
228
+ }
229
+ /**
230
+ * One redacted Run event, decoded from an SSE `data:` frame. The stream yields
231
+ * these in ascending {@link RunEvent.sequence} order. `payloadJson` is read
232
+ * structurally; use {@link parseRunEvent} to narrow the composition family.
233
+ */
234
+ export interface RunEvent {
235
+ sequence: number;
236
+ type: string;
237
+ payloadJson: unknown;
238
+ }
239
+ /** Run event types that close the stream — a terminal event ends a drain. */
240
+ export declare const TERMINAL_RUN_EVENT_TYPES: ReadonlySet<string>;
241
+ /** Options for {@link MaterialGraphClient.streamRunEvents}. */
242
+ export interface StreamRunEventsOptions {
243
+ /** Resume the stream after this sequence (exclusive). Omitted starts at 0. */
244
+ after?: number;
245
+ /** Abort the stream; iteration stops cleanly. */
246
+ signal?: AbortSignal;
247
+ }
248
+ /** Options for {@link MaterialGraphClient.drainRunEvents}. */
249
+ export interface DrainRunEventsOptions {
250
+ /** Resume after this sequence (exclusive). */
251
+ after?: number;
252
+ /** Stop collecting after this many milliseconds even without a terminal event. */
253
+ deadlineMs: number;
254
+ }
255
+ /** Options for {@link MaterialGraphClient.resumeRun}. */
256
+ export interface ResumeRunOptions {
257
+ /**
258
+ * The step the caller believes is suspended. When it mismatches the run
259
+ * doc's live `suspensionStepId` the API answers RFC-7807 409 (MG-608) —
260
+ * which is what stops a stale client resuming a step the run has already
261
+ * moved past. Omitted, the resume is unguarded, exactly as before.
262
+ */
263
+ stepId?: string;
264
+ /** Abort the request. */
265
+ signal?: AbortSignal;
266
+ }
package/run-types.js ADDED
@@ -0,0 +1,6 @@
1
+ /** Run event types that close the stream — a terminal event ends a drain. */
2
+ export const TERMINAL_RUN_EVENT_TYPES = new Set([
3
+ "succeeded",
4
+ "failed",
5
+ "canceled",
6
+ ]);
package/runs.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { HttpTransport } from "./http-transport.js";
2
+ import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
3
+ import { type CapabilityManifestResponse, type CreatePublicRunRequest, type CreatePublicRunResponse, type DrainRunEventsOptions, type PublicRunResponse, type ResumeRunOptions, type RunEvent, type SourceMaterialBatchDetail, type SourceMaterialBatchReceipt, type StreamRunEventsOptions, type UploadSourceMaterialInput } from "./run-types.js";
4
+ /**
5
+ * Upload one atomic source-material batch (`POST /api/v1/source-material`).
6
+ * Accepts the pile-of-files shape (`{ files }` → repeatable `files` field) or
7
+ * the legacy `{ brief, references }` shape (→ `brief` + repeatable `references`
8
+ * fields). Both wire to multipart the server accepts.
9
+ */
10
+ export declare function uploadSourceMaterial(transport: HttpTransport, input: UploadSourceMaterialInput, signal?: AbortSignal): Promise<SourceMaterialBatchReceipt>;
11
+ /**
12
+ * Read back one uploaded batch (`GET /api/v1/source-material/{batchId}`): every
13
+ * file with a fetchable `url`, and every analysis computed over its bytes
14
+ * inlined as `analyses` — always an array, `[]` when nothing has been computed.
15
+ *
16
+ * A batch you do not own is indistinguishable from one that does not exist:
17
+ * both raise a 404, never a 403, so batch ids cannot be probed for existence.
18
+ *
19
+ * The body is VALIDATED, not trusted: `checkSourceMaterialBatchDetail` mirrors
20
+ * the server's Zod contract so a malformed response fails here with a path into
21
+ * the payload rather than as an undefined three frames away in caller code.
22
+ */
23
+ export declare function getSourceMaterialBatch(transport: HttpTransport, batchId: string, signal?: AbortSignal): Promise<SourceMaterialBatchDetail>;
24
+ /** List invocable agents, workflows, and tools (`GET /api/v1/capabilities`). */
25
+ export declare function capabilities(transport: HttpTransport, signal?: AbortSignal): Promise<CapabilityManifestResponse>;
26
+ /** Start an asynchronous capability Run (`POST /api/v1/runs`). */
27
+ export declare function createRun(transport: HttpTransport, body: CreatePublicRunRequest, signal?: AbortSignal): Promise<CreatePublicRunResponse>;
28
+ /** Get a Run's current state (`GET /api/v1/runs/{runId}`). */
29
+ export declare function getRun(transport: HttpTransport, runId: string, signal?: AbortSignal): Promise<PublicRunResponse>;
30
+ /**
31
+ * Export a completed compose Run's scheme as a finish schedule — one row per
32
+ * slot, every attribute value carrying its source and verification state
33
+ * (`GET /api/v1/runs/{runId}/schedule`).
34
+ *
35
+ * The `.xlsx` form of the same export is deliberately NOT an SDK method: this
36
+ * transport parses every response as text/JSON and has no binary read path, so
37
+ * a bytes-returning method would mean a second transport surface for one file.
38
+ * Fetch `GET /api/v1/runs/{runId}/schedule.xlsx` directly with the same bearer
39
+ * key when you want the workbook.
40
+ */
41
+ export declare function getRunSchedule(transport: HttpTransport, runId: string, options?: GetRunScheduleOptions): Promise<FinishScheduleResponse>;
42
+ /**
43
+ * Resume a suspended Run (`POST /api/v1/runs/{runId}/resume`).
44
+ *
45
+ * The third argument takes either an `AbortSignal` (legacy) or
46
+ * {@link ResumeRunOptions}.
47
+ *
48
+ * `stepId` is forwarded whenever it is PRESENT — `!== undefined`, not truthy.
49
+ * An empty string is a present-but-malformed value and must reach the server
50
+ * so its schema rejects it; treating it as absent would silently downgrade a
51
+ * guarded resume into an unguarded one, which is the exact failure this guard
52
+ * exists to prevent.
53
+ *
54
+ * Omitting it sends a body byte-identical to what this sent before. Note the
55
+ * wire cannot distinguish `{ stepId: undefined }` from no key at all —
56
+ * `JSON.stringify` drops both — so the distinction here is construction
57
+ * intent, kept so the serialised body is provably unchanged for old callers.
58
+ */
59
+ export declare function resumeRun(transport: HttpTransport, runId: string, resumeData: unknown, signalOrOptions?: AbortSignal | ResumeRunOptions): Promise<PublicRunResponse>;
60
+ /** Request durable Run cancellation (`POST /api/v1/runs/{runId}/cancel`). */
61
+ export declare function cancelRun(transport: HttpTransport, runId: string, signal?: AbortSignal): Promise<PublicRunResponse>;
62
+ /**
63
+ * Stream ordered Run events as an async iterator (`GET
64
+ * /api/v1/runs/{runId}/events`, SSE). Iterates until the stream ends or
65
+ * `signal` aborts. One connection is bounded server-side (~25s or a terminal
66
+ * event); pass `after` to resume from the last observed sequence.
67
+ */
68
+ export declare function streamRunEvents(transport: HttpTransport, runId: string, options?: StreamRunEventsOptions): AsyncGenerator<RunEvent, void, unknown>;
69
+ /**
70
+ * Collect Run events until a terminal event, the deadline, or the stream ends —
71
+ * the bounded events-log pattern the console uses. A single pass over
72
+ * {@link streamRunEvents}; the deadline is enforced by aborting the stream.
73
+ *
74
+ * The deadline abort is this drain's OWN, expected termination — not a failure.
75
+ * Where it lands decides how {@link streamRunEvents} surfaces it: mid-read it
76
+ * ends the iterator cleanly, but while the underlying `fetch` is still pending —
77
+ * the common case for a poll whose upstream has flushed no bytes yet (no new
78
+ * events after the cursor, so no immediate SSE frame) — it rejects with an
79
+ * `aborted` {@link MaterialGraphError}. Both mean the same thing here, so the
80
+ * self-inflicted abort is swallowed and the collected events returned. Any other
81
+ * failure (a real 4xx/5xx or transport error) still propagates, keeping the
82
+ * fail-fast surface for genuine upstream problems.
83
+ */
84
+ export declare function drainRunEvents(transport: HttpTransport, runId: string, options: DrainRunEventsOptions): Promise<RunEvent[]>;
package/runs.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The Run + source-material namespace — the agentic half of the SDK. These
3
+ * functions take the shared {@link HttpTransport} and speak the hosted Run
4
+ * lifecycle: upload source material, list capabilities, create/read/resume/
5
+ * cancel Runs, and stream (or drain) their events. Box's proxy routes are thin
6
+ * wrappers over exactly these calls.
7
+ */
8
+ import { MaterialGraphError } from "./errors.js";
9
+ import { HttpTransport } from "./http-transport.js";
10
+ import { findSdkOperation, operationPath } from "./operations.js";
11
+ import { checkSourceMaterialBatchDetail } from "./source-material-validation.js";
12
+ import { readSseFrames } from "./sse.js";
13
+ import { TERMINAL_RUN_EVENT_TYPES, } from "./run-types.js";
14
+ function runOperation(operationId) {
15
+ const match = findSdkOperation(operationId);
16
+ if (!match)
17
+ throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
18
+ return match;
19
+ }
20
+ /** Build a `Blob` from raw bytes, normalising to an ArrayBuffer-backed view. */
21
+ function bytesToBlob(input) {
22
+ const type = input.contentType ? { type: input.contentType } : {};
23
+ if (input.data instanceof ArrayBuffer)
24
+ return new Blob([input.data], type);
25
+ // A Uint8Array may be SharedArrayBuffer-backed; copy into a fresh view so the
26
+ // Blob part is unambiguously ArrayBuffer-backed (no cast needed).
27
+ const copy = new Uint8Array(input.data.byteLength);
28
+ copy.set(input.data);
29
+ return new Blob([copy], type);
30
+ }
31
+ /** Append one file to the multipart body, from either input form. */
32
+ function appendFile(form, field, input) {
33
+ if (input instanceof Blob) {
34
+ if (input instanceof File)
35
+ form.append(field, input, input.name);
36
+ else
37
+ form.append(field, input);
38
+ return;
39
+ }
40
+ form.append(field, bytesToBlob(input), input.filename);
41
+ }
42
+ /**
43
+ * Upload one atomic source-material batch (`POST /api/v1/source-material`).
44
+ * Accepts the pile-of-files shape (`{ files }` → repeatable `files` field) or
45
+ * the legacy `{ brief, references }` shape (→ `brief` + repeatable `references`
46
+ * fields). Both wire to multipart the server accepts.
47
+ */
48
+ export async function uploadSourceMaterial(transport, input, signal) {
49
+ const form = new FormData();
50
+ if ("files" in input) {
51
+ for (const file of input.files)
52
+ appendFile(form, "files", file);
53
+ }
54
+ else {
55
+ appendFile(form, "brief", input.brief);
56
+ for (const reference of input.references ?? [])
57
+ appendFile(form, "references", reference);
58
+ }
59
+ const url = transport.buildUrl(operationPath(runOperation("upload_source_material")));
60
+ // No Content-Type header: fetch sets multipart/form-data with the boundary.
61
+ const { parsed } = await transport.send({
62
+ method: "POST",
63
+ url,
64
+ headers: transport.authHeaders(),
65
+ body: form,
66
+ signal,
67
+ });
68
+ return parsed;
69
+ }
70
+ /**
71
+ * Read back one uploaded batch (`GET /api/v1/source-material/{batchId}`): every
72
+ * file with a fetchable `url`, and every analysis computed over its bytes
73
+ * inlined as `analyses` — always an array, `[]` when nothing has been computed.
74
+ *
75
+ * A batch you do not own is indistinguishable from one that does not exist:
76
+ * both raise a 404, never a 403, so batch ids cannot be probed for existence.
77
+ *
78
+ * The body is VALIDATED, not trusted: `checkSourceMaterialBatchDetail` mirrors
79
+ * the server's Zod contract so a malformed response fails here with a path into
80
+ * the payload rather than as an undefined three frames away in caller code.
81
+ */
82
+ export async function getSourceMaterialBatch(transport, batchId, signal) {
83
+ const body = await transport.requestJson(runOperation("get_source_material_batch"), {
84
+ pathParams: { batchId },
85
+ signal,
86
+ });
87
+ return checkSourceMaterialBatchDetail(body, batchId);
88
+ }
89
+ /** List invocable agents, workflows, and tools (`GET /api/v1/capabilities`). */
90
+ export function capabilities(transport, signal) {
91
+ return transport.requestJson(runOperation("list_public_capabilities"), {
92
+ signal,
93
+ });
94
+ }
95
+ /** Start an asynchronous capability Run (`POST /api/v1/runs`). */
96
+ export function createRun(transport, body, signal) {
97
+ return transport.requestJson(runOperation("create_public_run"), {
98
+ body,
99
+ signal,
100
+ });
101
+ }
102
+ /** Get a Run's current state (`GET /api/v1/runs/{runId}`). */
103
+ export function getRun(transport, runId, signal) {
104
+ return transport.requestJson(runOperation("get_public_run"), {
105
+ pathParams: { runId },
106
+ signal,
107
+ });
108
+ }
109
+ /**
110
+ * Export a completed compose Run's scheme as a finish schedule — one row per
111
+ * slot, every attribute value carrying its source and verification state
112
+ * (`GET /api/v1/runs/{runId}/schedule`).
113
+ *
114
+ * The `.xlsx` form of the same export is deliberately NOT an SDK method: this
115
+ * transport parses every response as text/JSON and has no binary read path, so
116
+ * a bytes-returning method would mean a second transport surface for one file.
117
+ * Fetch `GET /api/v1/runs/{runId}/schedule.xlsx` directly with the same bearer
118
+ * key when you want the workbook.
119
+ */
120
+ export function getRunSchedule(transport, runId, options = {}) {
121
+ return transport.requestJson(runOperation("get_run_finish_schedule"), {
122
+ pathParams: { runId },
123
+ ...(options.schemeId ? { query: { schemeId: options.schemeId } } : {}),
124
+ ...(options.signal ? { signal: options.signal } : {}),
125
+ });
126
+ }
127
+ /**
128
+ * Normalise `resumeRun`'s overloaded third argument.
129
+ *
130
+ * A bare `AbortSignal` stays supported because it is the published shape:
131
+ * reordering that parameter would break any consumer passing it positionally,
132
+ * and this ships with no version bump.
133
+ *
134
+ * Discriminated on `instanceof AbortSignal`, NOT on the presence of `stepId`.
135
+ * `{ signal }` with no `stepId` is a perfectly valid options object, and a
136
+ * key-presence test would misread it as a signal and silently drop the abort.
137
+ */
138
+ function resumeOptions(value) {
139
+ if (value === undefined)
140
+ return {};
141
+ return value instanceof AbortSignal ? { signal: value } : value;
142
+ }
143
+ /**
144
+ * Resume a suspended Run (`POST /api/v1/runs/{runId}/resume`).
145
+ *
146
+ * The third argument takes either an `AbortSignal` (legacy) or
147
+ * {@link ResumeRunOptions}.
148
+ *
149
+ * `stepId` is forwarded whenever it is PRESENT — `!== undefined`, not truthy.
150
+ * An empty string is a present-but-malformed value and must reach the server
151
+ * so its schema rejects it; treating it as absent would silently downgrade a
152
+ * guarded resume into an unguarded one, which is the exact failure this guard
153
+ * exists to prevent.
154
+ *
155
+ * Omitting it sends a body byte-identical to what this sent before. Note the
156
+ * wire cannot distinguish `{ stepId: undefined }` from no key at all —
157
+ * `JSON.stringify` drops both — so the distinction here is construction
158
+ * intent, kept so the serialised body is provably unchanged for old callers.
159
+ */
160
+ export function resumeRun(transport, runId, resumeData, signalOrOptions) {
161
+ const { stepId, signal } = resumeOptions(signalOrOptions);
162
+ return transport.requestJson(runOperation("resume_public_run"), {
163
+ body: { resumeData, ...(stepId !== undefined ? { stepId } : {}) },
164
+ pathParams: { runId },
165
+ signal,
166
+ });
167
+ }
168
+ /** Request durable Run cancellation (`POST /api/v1/runs/{runId}/cancel`). */
169
+ export function cancelRun(transport, runId, signal) {
170
+ return transport.requestJson(runOperation("cancel_public_run"), {
171
+ body: {},
172
+ pathParams: { runId },
173
+ signal,
174
+ });
175
+ }
176
+ /** Decode one SSE `data:` frame into a {@link RunEvent}, or `null` when unusable. */
177
+ function decodeRunEvent(data) {
178
+ if (data.length === 0)
179
+ return null;
180
+ let parsed;
181
+ try {
182
+ parsed = JSON.parse(data);
183
+ }
184
+ catch (cause) {
185
+ throw MaterialGraphError.unexpected(`MaterialGraph run-event stream sent an unparseable data frame: ${String(cause)}`);
186
+ }
187
+ if (typeof parsed !== "object" || parsed === null)
188
+ return null;
189
+ const record = parsed;
190
+ if (typeof record.sequence !== "number" || typeof record.type !== "string")
191
+ return null;
192
+ return parsed;
193
+ }
194
+ /**
195
+ * Stream ordered Run events as an async iterator (`GET
196
+ * /api/v1/runs/{runId}/events`, SSE). Iterates until the stream ends or
197
+ * `signal` aborts. One connection is bounded server-side (~25s or a terminal
198
+ * event); pass `after` to resume from the last observed sequence.
199
+ */
200
+ export async function* streamRunEvents(transport, runId, options = {}) {
201
+ const body = await transport.openStream(runOperation("list_public_run_events"), {
202
+ pathParams: { runId },
203
+ ...(options.after === undefined ? {} : { query: { after: options.after } }),
204
+ ...(options.signal ? { signal: options.signal } : {}),
205
+ accept: "text/event-stream",
206
+ subject: "run-event stream",
207
+ });
208
+ for await (const frame of readSseFrames(body, options.signal)) {
209
+ const event = decodeRunEvent(frame.data);
210
+ if (event)
211
+ yield event;
212
+ }
213
+ }
214
+ /**
215
+ * Collect Run events until a terminal event, the deadline, or the stream ends —
216
+ * the bounded events-log pattern the console uses. A single pass over
217
+ * {@link streamRunEvents}; the deadline is enforced by aborting the stream.
218
+ *
219
+ * The deadline abort is this drain's OWN, expected termination — not a failure.
220
+ * Where it lands decides how {@link streamRunEvents} surfaces it: mid-read it
221
+ * ends the iterator cleanly, but while the underlying `fetch` is still pending —
222
+ * the common case for a poll whose upstream has flushed no bytes yet (no new
223
+ * events after the cursor, so no immediate SSE frame) — it rejects with an
224
+ * `aborted` {@link MaterialGraphError}. Both mean the same thing here, so the
225
+ * self-inflicted abort is swallowed and the collected events returned. Any other
226
+ * failure (a real 4xx/5xx or transport error) still propagates, keeping the
227
+ * fail-fast surface for genuine upstream problems.
228
+ */
229
+ export async function drainRunEvents(transport, runId, options) {
230
+ const collected = [];
231
+ const controller = new AbortController();
232
+ const timer = setTimeout(() => controller.abort(), options.deadlineMs);
233
+ try {
234
+ for await (const event of streamRunEvents(transport, runId, {
235
+ after: options.after,
236
+ signal: controller.signal,
237
+ })) {
238
+ collected.push(event);
239
+ if (TERMINAL_RUN_EVENT_TYPES.has(event.type))
240
+ break;
241
+ }
242
+ }
243
+ catch (error) {
244
+ const causedByDeadline = controller.signal.aborted && error instanceof MaterialGraphError && error.kind === "aborted";
245
+ if (!causedByDeadline)
246
+ throw error;
247
+ }
248
+ finally {
249
+ clearTimeout(timer);
250
+ controller.abort();
251
+ }
252
+ return collected;
253
+ }