drej 0.4.0 → 0.5.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/src/client.ts DELETED
@@ -1,684 +0,0 @@
1
- export class DrejError extends Error {
2
- constructor(
3
- message: string,
4
- public readonly status: number,
5
- ) {
6
- super(message);
7
- this.name = "DrejError";
8
- }
9
- }
10
-
11
- // ── Types ──────────────────────────────────────────────────────────────────
12
-
13
- export interface Resources {
14
- cpu?: string;
15
- memory?: string;
16
- gpu?: string;
17
- }
18
-
19
- export interface ImageAuth {
20
- username: string;
21
- password: string;
22
- }
23
-
24
- export interface ImageSpec {
25
- uri: string;
26
- auth?: ImageAuth;
27
- }
28
-
29
- export type SandboxState =
30
- | "Pending"
31
- | "Running"
32
- | "Pausing"
33
- | "Paused"
34
- | "Resuming"
35
- | "Stopping"
36
- | "Terminated"
37
- | "Failed"
38
- | "Unknown";
39
-
40
- export interface SandboxStatus {
41
- state: SandboxState;
42
- reason?: string;
43
- message?: string;
44
- lastTransitionAt?: string;
45
- }
46
-
47
- export interface Sandbox {
48
- id: string;
49
- status: SandboxStatus;
50
- createdAt: string;
51
- expiresAt?: string | null;
52
- image?: ImageSpec;
53
- snapshotId?: string;
54
- entrypoint?: string[];
55
- metadata?: Record<string, string>;
56
- }
57
-
58
- export interface CreateSandboxOptions {
59
- image?: ImageSpec;
60
- snapshotId?: string;
61
- timeout?: number;
62
- resourceLimits?: Resources;
63
- entrypoint?: string[];
64
- env?: Record<string, string>;
65
- metadata?: Record<string, string>;
66
- secureAccess?: boolean;
67
- }
68
-
69
- export interface ListSandboxesOptions {
70
- state?: SandboxState;
71
- limit?: number;
72
- offset?: number;
73
- }
74
-
75
- export type SnapshotState = "Pending" | "Committing" | "Pushing" | "Ready" | "Failed";
76
-
77
- export interface Snapshot {
78
- id: string;
79
- sandboxId: string;
80
- state: SnapshotState;
81
- createdAt: string;
82
- }
83
-
84
- export interface ListSnapshotsOptions {
85
- sandboxId?: string;
86
- limit?: number;
87
- offset?: number;
88
- }
89
-
90
- export type SSEEventType =
91
- | "init"
92
- | "status"
93
- | "stdout"
94
- | "stderr"
95
- | "result"
96
- | "execution_complete"
97
- | "execution_count"
98
- | "error"
99
- | "ping"
100
- | "message";
101
-
102
- export interface SSEEvent {
103
- type: SSEEventType;
104
- text?: string;
105
- results?: Record<string, string>;
106
- error?: { name?: string; message: string };
107
- execution_count?: number;
108
- execution_time?: number;
109
- timestamp: number;
110
- }
111
-
112
- export interface CodeContext {
113
- id: string;
114
- language: string;
115
- }
116
-
117
- export interface ExecuteCodeOptions {
118
- code: string;
119
- context?: {
120
- id: string;
121
- language: string;
122
- };
123
- }
124
-
125
- export interface ExecuteCommandOptions {
126
- command: string;
127
- cwd?: string;
128
- background?: boolean;
129
- timeout?: number;
130
- uid?: number;
131
- gid?: number;
132
- envs?: Record<string, string>;
133
- }
134
-
135
- export interface CommandStatus {
136
- session: string;
137
- status: "running" | "completed" | "failed";
138
- exitCode?: number;
139
- }
140
-
141
- export interface FileInfo {
142
- path: string;
143
- size: number;
144
- mode: string;
145
- modifiedAt: string;
146
- isDirectory: boolean;
147
- }
148
-
149
- export interface DirectoryEntry {
150
- name: string;
151
- path: string;
152
- isDirectory: boolean;
153
- size?: number;
154
- }
155
-
156
- export interface FileReplacement {
157
- path: string;
158
- old: string;
159
- new: string;
160
- }
161
-
162
- export interface Metrics {
163
- cpu: number;
164
- memory: number;
165
- timestamp: string;
166
- }
167
-
168
- export interface DiagnosticLog {
169
- name: string;
170
- size: number;
171
- url?: string;
172
- inline?: string;
173
- }
174
-
175
- export interface DiagnosticEvent {
176
- timestamp: string;
177
- type: string;
178
- message: string;
179
- }
180
-
181
- export interface DrejClientOptions {
182
- baseUrl?: string;
183
- }
184
-
185
- // ── Workflow types ─────────────────────────────────────────────────────────
186
-
187
- export type WorkflowEventKind =
188
- | "run_started"
189
- | "step_start"
190
- | "step_complete"
191
- | "step_failed"
192
- | "step_rolled_back"
193
- | "workflow_complete"
194
- | "workflow_failed"
195
- | "checkpoint"
196
- | "exec_event"
197
- | "snapshot";
198
-
199
- export interface SnapshotConfig {
200
- /** Take a snapshot after these step indices (0-based). */
201
- afterSteps?: number[];
202
- /** Take a snapshot after every N steps. */
203
- everyNSteps?: number;
204
- }
205
-
206
- export interface RunOptions {
207
- snapshotConfig?: SnapshotConfig;
208
- }
209
-
210
- export interface WorkflowEvent {
211
- ts: number;
212
- workflowName: string;
213
- runId: string;
214
- stepIndex: number;
215
- branch?: number;
216
- event: WorkflowEventKind;
217
- payload?: unknown;
218
- error?: string;
219
- result?: unknown;
220
- }
221
-
222
- export class WorkflowRun implements AsyncIterable<WorkflowEvent> {
223
- constructor(
224
- public readonly name: string,
225
- public readonly id: string,
226
- private readonly _events: AsyncGenerator<WorkflowEvent>,
227
- ) {}
228
-
229
- [Symbol.asyncIterator](): AsyncIterator<WorkflowEvent> {
230
- return this._events;
231
- }
232
- }
233
-
234
- export type Predicate =
235
- | { op: "eq" | "neq"; field: string; value: unknown }
236
- | { op: "gt" | "lt" | "gte" | "lte"; field: string; value: number }
237
- | { op: "exists" | "not_exists"; field: string }
238
- | { op: "and" | "or"; predicates: Predicate[] };
239
-
240
- export type StepDef =
241
- | {
242
- type: "create_sandbox";
243
- image?: ImageSpec;
244
- snapshotId?: string;
245
- timeout?: number;
246
- entrypoint?: string[];
247
- env?: Record<string, string>;
248
- metadata?: Record<string, string>;
249
- resourceLimits?: Resources;
250
- }
251
- | { type: "exec_code"; code: string; context?: { id: string; language: string } }
252
- | { type: "exec_command"; command: string; cwd?: string; envs?: Record<string, string> }
253
- | { type: "delete_sandbox" }
254
- | { type: "write_file"; path: string; content: string; encoding?: "utf8" | "base64" }
255
- | { type: "retry"; step: StepDef; maxAttempts: number; delayMs?: number; backoff?: "fixed" | "exponential" }
256
- | { type: "conditional"; condition: Predicate; then: StepDef[]; else?: StepDef[] }
257
- | { type: "loop"; over?: string; items?: unknown[]; as: string; steps: StepDef[]; concurrently?: boolean }
258
- | { type: "parallel"; steps: StepDef[] }
259
- | { type: "sequence"; steps: StepDef[] };
260
-
261
- // ── SSE parsers ────────────────────────────────────────────────────────────
262
-
263
- async function* parseWorkflowSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<WorkflowEvent> {
264
- const reader = stream.getReader();
265
- const decoder = new TextDecoder();
266
- let buffer = "";
267
-
268
- try {
269
- while (true) {
270
- const { done, value } = await reader.read();
271
- if (done) break;
272
- buffer += decoder.decode(value, { stream: true });
273
- const blocks = buffer.split("\n\n");
274
- buffer = blocks.pop() ?? "";
275
- for (const block of blocks) {
276
- if (!block.trim()) continue;
277
- for (const line of block.split("\n")) {
278
- if (line.startsWith("data:")) {
279
- yield JSON.parse(line.slice(5).trim()) as WorkflowEvent;
280
- }
281
- }
282
- }
283
- }
284
- } finally {
285
- reader.releaseLock();
286
- }
287
- }
288
-
289
- async function* parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEEvent> {
290
- const reader = stream.getReader();
291
- const decoder = new TextDecoder();
292
- let buffer = "";
293
-
294
- try {
295
- while (true) {
296
- const { done, value } = await reader.read();
297
- if (done) break;
298
- buffer += decoder.decode(value, { stream: true });
299
- const blocks = buffer.split("\n\n");
300
- buffer = blocks.pop() ?? "";
301
- for (const block of blocks) {
302
- if (!block.trim()) continue;
303
- let type: string | undefined;
304
- let data: string | undefined;
305
- for (const line of block.split("\n")) {
306
- if (line.startsWith("event:")) type = line.slice(6).trim();
307
- else if (line.startsWith("data:")) data = line.slice(5).trim();
308
- }
309
- if (data !== undefined) {
310
- yield { type: (type ?? "message") as SSEEventType, ...JSON.parse(data) };
311
- }
312
- }
313
- }
314
- } finally {
315
- reader.releaseLock();
316
- }
317
- }
318
-
319
- // ── Client ─────────────────────────────────────────────────────────────────
320
-
321
- export class DrejClient {
322
- private baseUrl: string;
323
-
324
- constructor(options: DrejClientOptions = {}) {
325
- this.baseUrl = (options.baseUrl ?? "http://localhost:6000").replace(/\/$/, "");
326
- }
327
-
328
- private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
329
- const res = await fetch(`${this.baseUrl}${path}`, {
330
- method,
331
- headers: body !== undefined ? { "Content-Type": "application/json" } : {},
332
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
333
- });
334
- if (!res.ok) throw new DrejError("drej API error", res.status);
335
- if (res.status === 204) return undefined as T;
336
- return res.json() as Promise<T>;
337
- }
338
-
339
- private async *streamRequest(
340
- method: string,
341
- path: string,
342
- body?: unknown,
343
- ): AsyncGenerator<SSEEvent> {
344
- const res = await fetch(`${this.baseUrl}${path}`, {
345
- method,
346
- headers: body !== undefined ? { "Content-Type": "application/json" } : {},
347
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
348
- });
349
- if (!res.ok) throw new DrejError("drej API error", res.status);
350
- if (!res.body) return;
351
- yield* parseSSE(res.body);
352
- }
353
-
354
- // ── Health ───────────────────────────────────────────────────────────────
355
-
356
- health(): Promise<{ healthy: boolean }> {
357
- return this.request("GET", "/health");
358
- }
359
-
360
- // ── Sandbox lifecycle ────────────────────────────────────────────────────
361
-
362
- createSandbox(options: CreateSandboxOptions): Promise<Sandbox> {
363
- return this.request("POST", "/v1/sandboxes", options);
364
- }
365
-
366
- listSandboxes(options: ListSandboxesOptions = {}): Promise<Sandbox[]> {
367
- const params = new URLSearchParams();
368
- if (options.state) params.set("state", options.state);
369
- if (options.limit !== undefined) params.set("limit", String(options.limit));
370
- if (options.offset !== undefined) params.set("offset", String(options.offset));
371
- const qs = params.toString();
372
- return this.request("GET", `/v1/sandboxes${qs ? `?${qs}` : ""}`);
373
- }
374
-
375
- getSandbox(id: string): Promise<Sandbox> {
376
- return this.request("GET", `/v1/sandboxes/${id}`);
377
- }
378
-
379
- deleteSandbox(id: string): Promise<void> {
380
- return this.request("DELETE", `/v1/sandboxes/${id}`);
381
- }
382
-
383
- pauseSandbox(id: string): Promise<void> {
384
- return this.request("POST", `/v1/sandboxes/${id}/pause`);
385
- }
386
-
387
- resumeSandbox(id: string): Promise<void> {
388
- return this.request("POST", `/v1/sandboxes/${id}/resume`);
389
- }
390
-
391
- renewSandbox(id: string): Promise<void> {
392
- return this.request("POST", `/v1/sandboxes/${id}/renew`);
393
- }
394
-
395
- async waitForRunning(
396
- id: string,
397
- options: { timeoutMs?: number; pollIntervalMs?: number } = {},
398
- ): Promise<Sandbox> {
399
- const { timeoutMs = 60_000, pollIntervalMs = 1_000 } = options;
400
- const deadline = Date.now() + timeoutMs;
401
- while (Date.now() < deadline) {
402
- const sandbox = await this.getSandbox(id);
403
- const { state } = sandbox.status;
404
- if (state === "Running") return sandbox;
405
- if (state === "Failed" || state === "Terminated") {
406
- throw new DrejError(`Sandbox ${id} entered state ${state}`, 500);
407
- }
408
- await new Promise<void>((r) => setTimeout(r, pollIntervalMs));
409
- }
410
- throw new DrejError(`Sandbox ${id} did not reach Running within ${timeoutMs}ms`, 408);
411
- }
412
-
413
- // ── Snapshots ────────────────────────────────────────────────────────────
414
-
415
- createSnapshot(sandboxId: string): Promise<Snapshot> {
416
- return this.request("POST", `/v1/sandboxes/${sandboxId}/snapshots`);
417
- }
418
-
419
- listSnapshots(options: ListSnapshotsOptions = {}): Promise<Snapshot[]> {
420
- const params = new URLSearchParams();
421
- if (options.sandboxId) params.set("sandboxId", options.sandboxId);
422
- if (options.limit !== undefined) params.set("limit", String(options.limit));
423
- if (options.offset !== undefined) params.set("offset", String(options.offset));
424
- const qs = params.toString();
425
- return this.request("GET", `/v1/snapshots${qs ? `?${qs}` : ""}`);
426
- }
427
-
428
- getSnapshot(id: string): Promise<Snapshot> {
429
- return this.request("GET", `/v1/snapshots/${id}`);
430
- }
431
-
432
- deleteSnapshot(id: string): Promise<void> {
433
- return this.request("DELETE", `/v1/snapshots/${id}`);
434
- }
435
-
436
- // ── Diagnostics ──────────────────────────────────────────────────────────
437
-
438
- getDiagnosticLogs(sandboxId: string): Promise<DiagnosticLog[]> {
439
- return this.request("GET", `/v1/sandboxes/${sandboxId}/diagnostics/logs`);
440
- }
441
-
442
- getDiagnosticEvents(sandboxId: string): Promise<DiagnosticEvent[]> {
443
- return this.request("GET", `/v1/sandboxes/${sandboxId}/diagnostics/events`);
444
- }
445
-
446
- // ── Code execution ───────────────────────────────────────────────────────
447
-
448
- async *executeCode(sandboxId: string, options: ExecuteCodeOptions): AsyncGenerator<SSEEvent> {
449
- yield* this.streamRequest("POST", `/v1/sandboxes/${sandboxId}/exec/code`, options);
450
- }
451
-
452
- interruptCode(sandboxId: string): Promise<void> {
453
- return this.request("DELETE", `/v1/sandboxes/${sandboxId}/exec/code`);
454
- }
455
-
456
- // ── Code contexts ────────────────────────────────────────────────────────
457
-
458
- listContexts(sandboxId: string, language?: string): Promise<CodeContext[]> {
459
- const qs = language ? `?language=${encodeURIComponent(language)}` : "";
460
- return this.request("GET", `/v1/sandboxes/${sandboxId}/exec/contexts${qs}`);
461
- }
462
-
463
- createContext(sandboxId: string, language: string): Promise<CodeContext> {
464
- return this.request("POST", `/v1/sandboxes/${sandboxId}/exec/contexts`, { language });
465
- }
466
-
467
- clearContexts(sandboxId: string, language?: string): Promise<void> {
468
- const qs = language ? `?language=${encodeURIComponent(language)}` : "";
469
- return this.request("DELETE", `/v1/sandboxes/${sandboxId}/exec/contexts${qs}`);
470
- }
471
-
472
- deleteContext(sandboxId: string, contextId: string): Promise<void> {
473
- return this.request("DELETE", `/v1/sandboxes/${sandboxId}/exec/contexts/${contextId}`);
474
- }
475
-
476
- // ── Command execution ────────────────────────────────────────────────────
477
-
478
- async *executeCommand(
479
- sandboxId: string,
480
- options: ExecuteCommandOptions,
481
- ): AsyncGenerator<SSEEvent> {
482
- yield* this.streamRequest("POST", `/v1/sandboxes/${sandboxId}/exec/command`, options);
483
- }
484
-
485
- interruptCommand(sandboxId: string): Promise<void> {
486
- return this.request("DELETE", `/v1/sandboxes/${sandboxId}/exec/command`);
487
- }
488
-
489
- getCommandStatus(sandboxId: string, session: string): Promise<CommandStatus> {
490
- return this.request("GET", `/v1/sandboxes/${sandboxId}/exec/command/status/${session}`);
491
- }
492
-
493
- getCommandOutput(
494
- sandboxId: string,
495
- session: string,
496
- ): Promise<{ stdout: string; stderr: string }> {
497
- return this.request("GET", `/v1/sandboxes/${sandboxId}/exec/command/output/${session}`);
498
- }
499
-
500
- // ── Files ────────────────────────────────────────────────────────────────
501
-
502
- getFileInfo(sandboxId: string, path: string): Promise<FileInfo> {
503
- return this.request(
504
- "GET",
505
- `/v1/sandboxes/${sandboxId}/files/info?path=${encodeURIComponent(path)}`,
506
- );
507
- }
508
-
509
- deleteFile(sandboxId: string, path: string): Promise<void> {
510
- return this.request(
511
- "DELETE",
512
- `/v1/sandboxes/${sandboxId}/files?path=${encodeURIComponent(path)}`,
513
- );
514
- }
515
-
516
- setFilePermissions(sandboxId: string, path: string, mode: string): Promise<void> {
517
- return this.request("POST", `/v1/sandboxes/${sandboxId}/files/permissions`, { path, mode });
518
- }
519
-
520
- moveFile(sandboxId: string, from: string, to: string): Promise<void> {
521
- return this.request("POST", `/v1/sandboxes/${sandboxId}/files/move`, { from, to });
522
- }
523
-
524
- searchFiles(sandboxId: string, pattern: string, dir?: string): Promise<string[]> {
525
- const params = new URLSearchParams({ pattern });
526
- if (dir) params.set("dir", dir);
527
- return this.request("GET", `/v1/sandboxes/${sandboxId}/files/search?${params}`);
528
- }
529
-
530
- replaceInFiles(sandboxId: string, replacements: FileReplacement[]): Promise<void> {
531
- return this.request("POST", `/v1/sandboxes/${sandboxId}/files/replace`, { replacements });
532
- }
533
-
534
- async uploadFile(
535
- sandboxId: string,
536
- path: string,
537
- content: Blob | BufferSource | string,
538
- ): Promise<void> {
539
- const blob = content instanceof Blob ? content : new Blob([content]);
540
- const formData = new FormData();
541
- formData.append("file", blob, path.split("/").pop());
542
- formData.append("path", path);
543
- const res = await fetch(`${this.baseUrl}/v1/sandboxes/${sandboxId}/files/upload`, {
544
- method: "POST",
545
- body: formData,
546
- });
547
- if (!res.ok) throw new DrejError("drej API error", res.status);
548
- }
549
-
550
- async downloadFile(sandboxId: string, path: string): Promise<ReadableStream<Uint8Array>> {
551
- const res = await fetch(
552
- `${this.baseUrl}/v1/sandboxes/${sandboxId}/files/download?path=${encodeURIComponent(path)}`,
553
- );
554
- if (!res.ok) throw new DrejError("drej API error", res.status);
555
- if (!res.body) throw new Error("empty response body");
556
- return res.body;
557
- }
558
-
559
- // ── Directories ──────────────────────────────────────────────────────────
560
-
561
- listDirectory(sandboxId: string, path: string, depth?: number): Promise<DirectoryEntry[]> {
562
- const params = new URLSearchParams({ path });
563
- if (depth !== undefined) params.set("depth", String(depth));
564
- return this.request("GET", `/v1/sandboxes/${sandboxId}/directories?${params}`);
565
- }
566
-
567
- createDirectory(sandboxId: string, path: string): Promise<void> {
568
- return this.request("POST", `/v1/sandboxes/${sandboxId}/directories`, { path });
569
- }
570
-
571
- deleteDirectory(sandboxId: string, path: string): Promise<void> {
572
- return this.request(
573
- "DELETE",
574
- `/v1/sandboxes/${sandboxId}/directories?path=${encodeURIComponent(path)}`,
575
- );
576
- }
577
-
578
- // ── Metrics ──────────────────────────────────────────────────────────────
579
-
580
- getMetrics(sandboxId: string): Promise<Metrics> {
581
- return this.request("GET", `/v1/sandboxes/${sandboxId}/metrics`);
582
- }
583
-
584
- async *watchMetrics(sandboxId: string): AsyncGenerator<SSEEvent> {
585
- yield* this.streamRequest("GET", `/v1/sandboxes/${sandboxId}/metrics/watch`);
586
- }
587
-
588
- // ── Workflows ────────────────────────────────────────────────────────────
589
-
590
- async run(
591
- w: { build(): { name: string; steps: StepDef[] } },
592
- options?: RunOptions,
593
- ): Promise<WorkflowRun> {
594
- const { name, steps } = w.build();
595
- return this._startRun(name, steps, options?.snapshotConfig);
596
- }
597
-
598
- /**
599
- * Start a new run using a snapshot captured from a previous run.
600
- *
601
- * Reads the ledger for the given run, finds the latest snapshot entry,
602
- * and injects the snapshotId into the first `create_sandbox` step of the
603
- * provided workflow — so the new sandbox boots from that snapshot with the
604
- * previous environment already in place.
605
- */
606
- async replayFromSnapshot(
607
- name: string,
608
- runId: string,
609
- w: { build(): { name: string; steps: StepDef[] } },
610
- ): Promise<WorkflowRun> {
611
- const ledger = await this.getWorkflowLedger(name, runId);
612
- const snapEntry = [...ledger].reverse().find((e) => e.event === "snapshot");
613
- if (!snapEntry) throw new DrejError(`No snapshot found in ledger for ${name}/${runId}`, 404);
614
- const { snapshotId } = snapEntry.payload as { snapshotId: string };
615
-
616
- const { name: wfName, steps } = w.build();
617
- const replaySteps: StepDef[] = steps.map((step) =>
618
- step.type === "create_sandbox" ? { ...step, snapshotId } : step,
619
- );
620
-
621
- return this._startRun(wfName, replaySteps);
622
- }
623
-
624
- async resumeRun(
625
- name: string,
626
- runId: string,
627
- w: { build(): { name: string; steps: StepDef[] } } | StepDef[],
628
- ): Promise<WorkflowRun> {
629
- const steps = Array.isArray(w) ? w : w.build().steps;
630
- const res = await fetch(
631
- `${this.baseUrl}/v1/workflows/${encodeURIComponent(name)}/runs/${encodeURIComponent(runId)}/resume`,
632
- {
633
- method: "POST",
634
- headers: { "Content-Type": "application/json" },
635
- body: JSON.stringify({ steps }),
636
- },
637
- );
638
- if (!res.ok) throw new DrejError("drej API error", res.status);
639
- if (!res.body) throw new DrejError("empty response body", 500);
640
- return this._consumeRunStarted(name, res.body);
641
- }
642
-
643
- listWorkflowRuns(name: string): Promise<{ runs: string[] }> {
644
- return this.request("GET", `/v1/workflows/${encodeURIComponent(name)}/runs`);
645
- }
646
-
647
- getWorkflowLedger(name: string, runId: string): Promise<WorkflowEvent[]> {
648
- return this.request(
649
- "GET",
650
- `/v1/workflows/${encodeURIComponent(name)}/runs/${encodeURIComponent(runId)}/ledger`,
651
- );
652
- }
653
-
654
- private async _startRun(
655
- name: string,
656
- steps: StepDef[],
657
- snapshotConfig?: SnapshotConfig,
658
- ): Promise<WorkflowRun> {
659
- const res = await fetch(
660
- `${this.baseUrl}/v1/workflows/${encodeURIComponent(name)}/runs`,
661
- {
662
- method: "POST",
663
- headers: { "Content-Type": "application/json" },
664
- body: JSON.stringify({ steps, ...(snapshotConfig ? { snapshotConfig } : {}) }),
665
- },
666
- );
667
- if (!res.ok) throw new DrejError("drej API error", res.status);
668
- if (!res.body) throw new DrejError("empty response body", 500);
669
- return this._consumeRunStarted(name, res.body);
670
- }
671
-
672
- private async _consumeRunStarted(
673
- name: string,
674
- body: ReadableStream<Uint8Array>,
675
- ): Promise<WorkflowRun> {
676
- const stream = parseWorkflowSSE(body);
677
- const first = await stream.next();
678
- if (first.done || first.value.event !== "run_started") {
679
- throw new DrejError("expected run_started as first SSE event", 500);
680
- }
681
- const { runId } = first.value.payload as { runId: string };
682
- return new WorkflowRun(name, runId, stream);
683
- }
684
- }