@instruments/materialgraph 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/runs.js DELETED
@@ -1,198 +0,0 @@
1
- /**
2
- * The workflow Run + source-material calls — the agentic half of the SDK. These
3
- * functions take the shared {@link HttpTransport} and speak the hosted Run
4
- * lifecycle: upload source material, start and read a workflow's Runs, and
5
- * stream (or drain) their events. `mg.workflows` and `uploadSourceMaterial`
6
- * are thin wrappers over exactly these calls.
7
- */
8
- import { MaterialGraphError } from "./errors.js";
9
- import { findSdkOperation, operationPath } from "./operations.js";
10
- import { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
11
- import { readSseFrames } from "./sse.js";
12
- const hasSourceIdentity = (source) => typeof source === "object" &&
13
- source !== null &&
14
- "sourceId" in source &&
15
- typeof source.sourceId === "string" &&
16
- "role" in source &&
17
- typeof source.role === "string" &&
18
- "filename" in source &&
19
- typeof source.filename === "string" &&
20
- "mediaType" in source &&
21
- typeof source.mediaType === "string";
22
- const hasSourceMeasurements = (source) => typeof source === "object" &&
23
- source !== null &&
24
- "sha256" in source &&
25
- typeof source.sha256 === "string" &&
26
- "bytes" in source &&
27
- typeof source.bytes === "number" &&
28
- "pages" in source &&
29
- (source.pages === null || typeof source.pages === "number");
30
- const isSourceReceipt = (value) => typeof value === "object" &&
31
- value !== null &&
32
- "batchId" in value &&
33
- typeof value.batchId === "string" &&
34
- "sources" in value &&
35
- Array.isArray(value.sources) &&
36
- value.sources.every((source) => typeof source === "object" &&
37
- source !== null &&
38
- hasSourceIdentity(source) &&
39
- hasSourceMeasurements(source));
40
- const isRunEvent = (value) => typeof value === "object" &&
41
- value !== null &&
42
- "sequence" in value &&
43
- typeof value.sequence === "number" &&
44
- "type" in value &&
45
- typeof value.type === "string";
46
- const runOperation = (operationId) => {
47
- const match = findSdkOperation(operationId);
48
- if (!match) {
49
- throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
50
- }
51
- return { ...match, operationId };
52
- };
53
- /** Build a `Blob` from raw bytes, normalizing to an ArrayBuffer-backed view. */
54
- const bytesToBlob = (input) => {
55
- const type = input.contentType !== undefined && input.contentType !== ""
56
- ? { type: input.contentType }
57
- : {};
58
- if (input.data instanceof ArrayBuffer) {
59
- return new Blob([input.data], type);
60
- }
61
- // A Uint8Array may be SharedArrayBuffer-backed; copy into a fresh view so the
62
- // Blob part is unambiguously ArrayBuffer-backed (no cast needed).
63
- const copy = new Uint8Array(input.data.byteLength);
64
- copy.set(input.data);
65
- return new Blob([copy], type);
66
- };
67
- /** Append one file to the multipart body, from either input form. */
68
- const appendFile = (form, field, input) => {
69
- if (input instanceof Blob) {
70
- if (input instanceof File) {
71
- form.append(field, input, input.name);
72
- }
73
- else {
74
- form.append(field, input);
75
- }
76
- return;
77
- }
78
- form.append(field, bytesToBlob(input), input.filename);
79
- };
80
- /**
81
- * Upload one atomic source-material batch (`POST /api/v1/source-material`).
82
- * Each entry of `{ files }` becomes one part of the repeatable multipart
83
- * `files` field.
84
- */
85
- export const uploadSourceMaterial = async (transport, input, signal) => {
86
- const form = new FormData();
87
- for (const file of input.files) {
88
- appendFile(form, "files", file);
89
- }
90
- const url = transport.buildUrl(operationPath(runOperation("upload_source_material")));
91
- // No Content-Type header: fetch sets multipart/form-data with the boundary.
92
- const { parsed } = await transport.send({
93
- body: form,
94
- headers: transport.authHeaders(),
95
- method: "POST",
96
- signal,
97
- url,
98
- });
99
- if (!isSourceReceipt(parsed)) {
100
- throw MaterialGraphError.unexpected("MaterialGraph source-material upload returned an invalid receipt");
101
- }
102
- return parsed;
103
- };
104
- /** Start an asynchronous Run of one workflow (`POST /api/v1/workflows/{workflowId}/runs`). */
105
- export const startWorkflowRun = async (transport, workflowId, body, signal) => await transport.requestJson(runOperation("create_public_run"), {
106
- body,
107
- pathParams: { workflowId },
108
- signal,
109
- });
110
- /** Get a Run's current state (`GET /api/v1/workflows/{workflowId}/runs/{runId}`). */
111
- export const getWorkflowRun = async (transport, workflowId, runId, signal) => await transport.requestJson(runOperation("get_public_run"), {
112
- pathParams: { runId, workflowId },
113
- signal,
114
- });
115
- /** Decode one SSE `data:` frame into a {@link RunEvent}, or `null` when unusable. */
116
- const decodeRunEvent = (data) => {
117
- if (data.length === 0) {
118
- return null;
119
- }
120
- let parsed;
121
- try {
122
- parsed = JSON.parse(data);
123
- }
124
- catch (cause) {
125
- throw MaterialGraphError.unexpected(`MaterialGraph run-event stream sent an unparseable data frame: ${String(cause)}`);
126
- }
127
- return isRunEvent(parsed) ? parsed : null;
128
- };
129
- /**
130
- * Stream ordered Run events as an async iterator (`GET
131
- * /api/v1/workflows/{workflowId}/runs/{runId}/events`, SSE). Iterates until the stream ends or
132
- * `signal` aborts. One connection is bounded server-side (~25s or a terminal
133
- * event); pass `after` to resume from the last observed sequence.
134
- * @yields {RunEvent} Each usable Run event from the stream.
135
- */
136
- export const streamRunEvents = async function* streamRunEvents(transport, workflowId, runId, options = {}) {
137
- const body = await transport.openStream(runOperation("list_public_run_events"), {
138
- pathParams: { runId, workflowId },
139
- ...(options.after === undefined
140
- ? {}
141
- : { query: { after: options.after } }),
142
- ...(options.signal ? { signal: options.signal } : {}),
143
- accept: "text/event-stream",
144
- subject: "run-event stream",
145
- });
146
- for await (const frame of readSseFrames(body, options.signal)) {
147
- const event = decodeRunEvent(frame.data);
148
- if (event) {
149
- yield event;
150
- }
151
- }
152
- };
153
- /**
154
- * Collect Run events until a terminal event, the deadline, or the stream ends —
155
- * the bounded events-log pattern the console uses. A single pass over
156
- * {@link streamRunEvents}; the deadline is enforced by aborting the stream.
157
- *
158
- * The deadline abort is this drain's OWN, expected termination — not a failure.
159
- * Where it lands decides how {@link streamRunEvents} surfaces it: mid-read it
160
- * ends the iterator cleanly, but while the underlying `fetch` is still pending —
161
- * the common case for a poll whose upstream has flushed no bytes yet (no new
162
- * events after the cursor, so no immediate SSE frame) — it rejects with an
163
- * `aborted` {@link MaterialGraphError}. Both mean the same thing here, so the
164
- * self-inflicted abort is swallowed and the collected events returned. Any other
165
- * failure (a real 4xx/5xx or transport error) still propagates, keeping the
166
- * fail-fast surface for genuine upstream problems.
167
- */
168
- export const drainRunEvents = async (transport, workflowId, runId, options) => {
169
- const collected = [];
170
- const controller = new AbortController();
171
- const timer = setTimeout(() => {
172
- controller.abort();
173
- }, options.deadlineMs);
174
- try {
175
- for await (const event of streamRunEvents(transport, workflowId, runId, {
176
- after: options.after,
177
- signal: controller.signal,
178
- })) {
179
- collected.push(event);
180
- if (TERMINAL_RUN_EVENT_TYPES.has(event.type)) {
181
- break;
182
- }
183
- }
184
- }
185
- catch (error) {
186
- const causedByDeadline = controller.signal.aborted &&
187
- error instanceof MaterialGraphError &&
188
- error.kind === "aborted";
189
- if (!causedByDeadline) {
190
- throw error;
191
- }
192
- }
193
- finally {
194
- clearTimeout(timer);
195
- controller.abort();
196
- }
197
- return collected;
198
- };
package/sse.d.ts DELETED
@@ -1,36 +0,0 @@
1
- /**
2
- * A minimal, dependency-free Server-Sent Events reader — the primitive behind
3
- * {@link WorkflowsClient.streamRunEvents}. Hand-rolled (no `eventsource`,
4
- * no `undici`) so the SDK stays zero-runtime-dep and works unchanged inside
5
- * Node route handlers, where `fetch` yields a byte `ReadableStream`.
6
- *
7
- * The parser follows the WHATWG event-stream tokenizer closely enough for the
8
- * `/api/v1/workflows/{workflowId}/runs/{runId}/events` contract: `\n`, `\r\n`, and `\r` line endings,
9
- * multi-line `data:` accumulation, comment (`:`-prefixed) lines, and a single
10
- * optional leading space after the field colon. Frames split across network
11
- * chunks are held in an internal buffer until their terminating blank line.
12
- */
13
- /** One dispatched SSE frame. `data` is the joined data lines (no trailing newline). */
14
- export type SseFrame = {
15
- id?: string;
16
- event?: string;
17
- data: string;
18
- };
19
- /**
20
- * A push-based SSE line assembler. Feed it decoded text with {@link push}; it
21
- * returns any frames completed by that text. Call {@link flush} once the stream
22
- * ends to dispatch a final frame that had data but no terminating blank line.
23
- */
24
- export declare class SseDecoder {
25
- #private;
26
- push(text: string): SseFrame[];
27
- /** Dispatch a trailing frame that had fields but no closing blank line. */
28
- flush(): SseFrame[];
29
- }
30
- /**
31
- * Iterate the SSE frames of a byte stream. Stops cleanly when the stream ends
32
- * or `signal` aborts (mid-read abort rejections are swallowed once the signal
33
- * is set). Frames with no `data` field (comments, bare `id:`) are not yielded.
34
- * @yields {SseFrame} Each completed frame containing fields.
35
- */
36
- export declare const readSseFrames: (body: ReadableStream<Uint8Array>, signal?: AbortSignal) => AsyncGenerator<SseFrame, void, unknown>;
package/sse.js DELETED
@@ -1,167 +0,0 @@
1
- /**
2
- * A minimal, dependency-free Server-Sent Events reader — the primitive behind
3
- * {@link WorkflowsClient.streamRunEvents}. Hand-rolled (no `eventsource`,
4
- * no `undici`) so the SDK stays zero-runtime-dep and works unchanged inside
5
- * Node route handlers, where `fetch` yields a byte `ReadableStream`.
6
- *
7
- * The parser follows the WHATWG event-stream tokenizer closely enough for the
8
- * `/api/v1/workflows/{workflowId}/runs/{runId}/events` contract: `\n`, `\r\n`, and `\r` line endings,
9
- * multi-line `data:` accumulation, comment (`:`-prefixed) lines, and a single
10
- * optional leading space after the field colon. Frames split across network
11
- * chunks are held in an internal buffer until their terminating blank line.
12
- */
13
- /**
14
- * A push-based SSE line assembler. Feed it decoded text with {@link push}; it
15
- * returns any frames completed by that text. Call {@link flush} once the stream
16
- * ends to dispatch a final frame that had data but no terminating blank line.
17
- */
18
- export class SseDecoder {
19
- #buffer = "";
20
- #dataLines = [];
21
- #id;
22
- #event;
23
- #hasFields = false;
24
- push(text) {
25
- this.#buffer += text;
26
- const frames = [];
27
- for (;;) {
28
- const lineEnd = this.#nextLineEnd();
29
- if (lineEnd === null) {
30
- break;
31
- }
32
- const line = this.#buffer.slice(0, lineEnd.index);
33
- this.#buffer = this.#buffer.slice(lineEnd.nextIndex);
34
- const frame = this.#consumeLine(line);
35
- if (frame) {
36
- frames.push(frame);
37
- }
38
- }
39
- return frames;
40
- }
41
- /** Dispatch a trailing frame that had fields but no closing blank line. */
42
- flush() {
43
- if (this.#buffer.length > 0) {
44
- // At end of stream a deferred trailing CR is a real terminator, not data.
45
- const line = this.#buffer.endsWith("\r")
46
- ? this.#buffer.slice(0, -1)
47
- : this.#buffer;
48
- this.#buffer = "";
49
- const frame = this.#consumeLine(line);
50
- if (frame) {
51
- return [frame];
52
- }
53
- }
54
- const frame = this.#dispatch();
55
- return frame ? [frame] : [];
56
- }
57
- /** Locate the next line terminator, deferring a lone trailing `\r` (may be `\r\n`). */
58
- #nextLineEnd() {
59
- for (let i = 0; i < this.#buffer.length; i += 1) {
60
- const char = this.#buffer[i];
61
- if (char === "\n") {
62
- return { index: i, nextIndex: i + 1 };
63
- }
64
- if (char === "\r") {
65
- if (i === this.#buffer.length - 1) {
66
- return null;
67
- }
68
- return this.#buffer[i + 1] === "\n"
69
- ? { index: i, nextIndex: i + 2 }
70
- : { index: i, nextIndex: i + 1 };
71
- }
72
- }
73
- return null;
74
- }
75
- #consumeLine(line) {
76
- if (line.length === 0) {
77
- return this.#dispatch();
78
- }
79
- if (line.startsWith(":")) {
80
- return undefined;
81
- }
82
- const colon = line.indexOf(":");
83
- const field = colon === -1 ? line : line.slice(0, colon);
84
- let value = colon === -1 ? "" : line.slice(colon + 1);
85
- if (value.startsWith(" ")) {
86
- value = value.slice(1);
87
- }
88
- if (field === "data") {
89
- this.#dataLines.push(value);
90
- this.#hasFields = true;
91
- }
92
- else if (field === "event") {
93
- this.#event = value;
94
- this.#hasFields = true;
95
- }
96
- else if (field === "id") {
97
- this.#id = value;
98
- this.#hasFields = true;
99
- }
100
- return undefined;
101
- }
102
- #dispatch() {
103
- if (!this.#hasFields) {
104
- return undefined;
105
- }
106
- const frame = { data: this.#dataLines.join("\n") };
107
- if (this.#event !== undefined) {
108
- frame.event = this.#event;
109
- }
110
- if (this.#id !== undefined) {
111
- frame.id = this.#id;
112
- }
113
- this.#dataLines = [];
114
- this.#event = undefined;
115
- this.#id = undefined;
116
- this.#hasFields = false;
117
- return frame;
118
- }
119
- }
120
- /**
121
- * Iterate the SSE frames of a byte stream. Stops cleanly when the stream ends
122
- * or `signal` aborts (mid-read abort rejections are swallowed once the signal
123
- * is set). Frames with no `data` field (comments, bare `id:`) are not yielded.
124
- * @yields {SseFrame} Each completed frame containing fields.
125
- */
126
- export const readSseFrames = async function* readSseFrames(body, signal) {
127
- const isAborted = () => signal?.aborted === true;
128
- const reader = body.getReader();
129
- const decoder = new TextDecoder();
130
- const sse = new SseDecoder();
131
- try {
132
- for (;;) {
133
- if (isAborted()) {
134
- return;
135
- }
136
- let result;
137
- try {
138
- result = await reader.read();
139
- }
140
- catch (error) {
141
- if (isAborted()) {
142
- return;
143
- }
144
- throw error;
145
- }
146
- if (result.done) {
147
- for (const frame of sse.push(decoder.decode())) {
148
- yield frame;
149
- }
150
- for (const frame of sse.flush()) {
151
- yield frame;
152
- }
153
- return;
154
- }
155
- const text = decoder.decode(result.value, { stream: true });
156
- for (const frame of sse.push(text)) {
157
- if (isAborted()) {
158
- return;
159
- }
160
- yield frame;
161
- }
162
- }
163
- }
164
- finally {
165
- reader.releaseLock();
166
- }
167
- };
@@ -1,54 +0,0 @@
1
- import type { HttpTransport } from "./http-transport.js";
2
- /**
3
- * The workflow Run surface, namespaced as `mg.workflows`. A Run is the start of
4
- * one registered Mastra workflow and nothing else: you name the workflow, hand
5
- * it its input, and follow the Run it returns. Replaces the flat `createRun()`,
6
- * `run()`, `streamRunEvents()` and `drainRunEvents()` methods, retired with no
7
- * shim when `/api/v1/runs` moved under `/api/v1/workflows/{workflowId}/runs`.
8
- *
9
- * THE QUESTION: "do a whole job for me" — the ASYNC lens. Everything else on
10
- * the client answers in one round trip; a Run may suspend for human input and
11
- * finish minutes later. `start()` acknowledges without executing, `run()` polls
12
- * the ledger, and `streamRunEvents()` follows it live.
13
- *
14
- * `workflowId` is the registered workflow's id, such as `edit-collection`. A
15
- * Run is read under the workflow that started it: asking for it under another
16
- * workflow's id is a 404.
17
- *
18
- * Starting a Run needs the `mg:compose` or `mg:write` scope, plus whatever
19
- * scope the workflow itself declares.
20
- */
21
- import type { RunId } from "./identifiers.js";
22
- import type { CreatePublicRunRequest, CreatePublicRunResponse, DrainRunEventsOptions, PublicRunResponse, RunEvent, StreamRunEventsOptions } from "./run-types.js";
23
- export declare class WorkflowsClient {
24
- #private;
25
- constructor(options: {
26
- transport: HttpTransport;
27
- });
28
- /**
29
- * Start an asynchronous Run of `workflowId` (POST
30
- * `/api/v1/workflows/{workflowId}/runs`). Pass `idempotencyKey` so a retry
31
- * replays rather than duplicates. Follow with {@link streamRunEvents} (live)
32
- * or {@link run} (poll).
33
- */
34
- start(workflowId: string, body: CreatePublicRunRequest, signal?: AbortSignal): Promise<CreatePublicRunResponse>;
35
- /**
36
- * THE QUESTION: "where has this Run got to?" — the caller-owned, redacted
37
- * ledger record. Polling; prefer {@link streamRunEvents} when a human is
38
- * waiting.
39
- */
40
- run(workflowId: string, runId: RunId, signal?: AbortSignal): Promise<PublicRunResponse>;
41
- /**
42
- * Stream ordered Run events as an async iterator (GET
43
- * `/api/v1/workflows/{workflowId}/runs/{runId}/events`, SSE). Iterates until
44
- * the stream ends or the signal aborts; pass `after` to resume from the last
45
- * observed sequence.
46
- */
47
- streamRunEvents(workflowId: string, runId: RunId, options?: StreamRunEventsOptions): AsyncGenerator<RunEvent>;
48
- /**
49
- * Collect Run events until a terminal event, the deadline, or the stream
50
- * ends — the bounded events-log pattern (a single pass over
51
- * {@link streamRunEvents}).
52
- */
53
- drainRunEvents(workflowId: string, runId: RunId, options: DrainRunEventsOptions): Promise<RunEvent[]>;
54
- }
@@ -1,41 +0,0 @@
1
- import { drainRunEvents, getWorkflowRun, startWorkflowRun, streamRunEvents, } from "./runs.js";
2
- export class WorkflowsClient {
3
- #transport;
4
- constructor(options) {
5
- this.#transport = options.transport;
6
- }
7
- /**
8
- * Start an asynchronous Run of `workflowId` (POST
9
- * `/api/v1/workflows/{workflowId}/runs`). Pass `idempotencyKey` so a retry
10
- * replays rather than duplicates. Follow with {@link streamRunEvents} (live)
11
- * or {@link run} (poll).
12
- */
13
- async start(workflowId, body, signal) {
14
- return await startWorkflowRun(this.#transport, workflowId, body, signal);
15
- }
16
- /**
17
- * THE QUESTION: "where has this Run got to?" — the caller-owned, redacted
18
- * ledger record. Polling; prefer {@link streamRunEvents} when a human is
19
- * waiting.
20
- */
21
- async run(workflowId, runId, signal) {
22
- return await getWorkflowRun(this.#transport, workflowId, runId, signal);
23
- }
24
- /**
25
- * Stream ordered Run events as an async iterator (GET
26
- * `/api/v1/workflows/{workflowId}/runs/{runId}/events`, SSE). Iterates until
27
- * the stream ends or the signal aborts; pass `after` to resume from the last
28
- * observed sequence.
29
- */
30
- streamRunEvents(workflowId, runId, options) {
31
- return streamRunEvents(this.#transport, workflowId, runId, options);
32
- }
33
- /**
34
- * Collect Run events until a terminal event, the deadline, or the stream
35
- * ends — the bounded events-log pattern (a single pass over
36
- * {@link streamRunEvents}).
37
- */
38
- async drainRunEvents(workflowId, runId, options) {
39
- return await drainRunEvents(this.#transport, workflowId, runId, options);
40
- }
41
- }