@werft-ai/sdk 0.0.0-stage → 0.1.2

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 (6) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/README.md +41 -2
  4. package/index.d.ts +37825 -0
  5. package/index.js +1241 -0
  6. package/package.json +48 -4
package/index.js ADDED
@@ -0,0 +1,1241 @@
1
+ import createClient from "openapi-fetch";
2
+ // ../client/src/errors.ts
3
+ /**
4
+ * What every failed call becomes. Callers branch on `code` (the server's
5
+ * `error.code`), never on the HTTP status alone, and show `message` as the
6
+ * server wrote it — a 403 says which scope was missing.
7
+ */
8
+ var ApiError = class extends Error {
9
+ status;
10
+ code;
11
+ requestId;
12
+ retryable;
13
+ details;
14
+ retryAfterSeconds;
15
+ mismatch;
16
+ constructor(status, code, message, detail = {}) {
17
+ super(message);
18
+ this.status = status;
19
+ this.code = code;
20
+ this.name = "ApiError";
21
+ this.requestId = detail.requestId ?? null;
22
+ this.retryable = detail.retryable ?? null;
23
+ this.details = detail.details ?? null;
24
+ this.retryAfterSeconds = detail.retryAfterSeconds ?? null;
25
+ this.mismatch = detail.mismatch ?? null;
26
+ }
27
+ };
28
+ const text$1 = (value) => typeof value === "string" ? value : void 0;
29
+ /** Seconds from a `Retry-After` header; an HTTP date or junk reads as none. */
30
+ function retryAfterSeconds(response) {
31
+ const seconds = Number(response.headers.get("retry-after") ?? NaN);
32
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
33
+ }
34
+ /** A non-2xx response and its parsed body, as one error shape. */
35
+ function errorFromResponse$1(response, body) {
36
+ const error = body?.error;
37
+ return new ApiError(response.status, text$1(error?.code) ?? "UNKNOWN", text$1(error?.message) ?? response.statusText, {
38
+ requestId: text$1(error?.request_id) ?? response.headers.get("x-request-id"),
39
+ retryable: typeof error?.retryable === "boolean" ? error.retryable : null,
40
+ details: error?.details ?? null,
41
+ retryAfterSeconds: retryAfterSeconds(response)
42
+ });
43
+ }
44
+ // ../client/src/contract.ts
45
+ function contractPolicy(options) {
46
+ const mode = options.validate ?? "off";
47
+ const warn = options.onContractMismatch ?? ((mismatch) => console.warn("API contract mismatch", mismatch));
48
+ const policy = {
49
+ mode,
50
+ validators: options.validators ?? {},
51
+ report(mismatch, status, requestId) {
52
+ if (mode === "off" || mismatch.issues.length === 0) return;
53
+ if (mode === "warn") return warn(mismatch);
54
+ throw new ApiError(status, "CLIENT_CONTRACT_MISMATCH", `${mismatch.operation} answered outside the API contract`, {
55
+ requestId,
56
+ mismatch
57
+ });
58
+ },
59
+ check(operation, validator, value, status, requestId) {
60
+ if (mode === "off" || !validator) return;
61
+ const issues = validator(value).map(({ path, message }) => ({
62
+ path: String(path),
63
+ message: String(message)
64
+ }));
65
+ policy.report({
66
+ operation,
67
+ issues
68
+ }, status, requestId);
69
+ }
70
+ };
71
+ return policy;
72
+ }
73
+ // ../client/src/client.ts
74
+ const CSRF_HEADER_NAME = "x-requested-with";
75
+ const CSRF_HEADER_VALUE = "agent-platform-web";
76
+ const csrfHeader = { onRequest({ request }) {
77
+ request.headers.set(CSRF_HEADER_NAME, CSRF_HEADER_VALUE);
78
+ return request;
79
+ } };
80
+ const noCsrfHeader = { onRequest({ request }) {
81
+ request.headers.delete(CSRF_HEADER_NAME);
82
+ return request;
83
+ } };
84
+ const METHODS = [
85
+ "GET",
86
+ "PUT",
87
+ "POST",
88
+ "DELETE",
89
+ "OPTIONS",
90
+ "HEAD",
91
+ "PATCH",
92
+ "TRACE"
93
+ ];
94
+ /**
95
+ * A call may override the client's credentials, and middleware never sees
96
+ * the call's options (Request.credentials is not reliable either: Bun reports
97
+ * "include" for all), so the header is decided per call here.
98
+ */
99
+ function csrfPerCall(client, fallback) {
100
+ const withCsrf = (init) => ({
101
+ ...init,
102
+ middleware: [...init?.middleware ?? [], (init?.credentials ?? fallback) === "omit" ? noCsrfHeader : csrfHeader]
103
+ });
104
+ const wrapped = { ...client };
105
+ for (const method of METHODS) {
106
+ const call = client[method];
107
+ wrapped[method] = (url, init) => call(url, withCsrf(init));
108
+ }
109
+ const request = client.request;
110
+ wrapped.request = (method, url, init) => request(method, url, withCsrf(init));
111
+ return wrapped;
112
+ }
113
+ const policies = /* @__PURE__ */ new WeakMap();
114
+ /** The contract policy a client was made with; streams apply it too. */
115
+ function policyOf(client) {
116
+ let policy;
117
+ for (let c = client; c && !policy; c = Object.getPrototypeOf(c)) policy = policies.get(c);
118
+ if (!policy) throw new TypeError("client was not made by createApiClient");
119
+ return policy;
120
+ }
121
+ function createApiClient(options = {}) {
122
+ const credentials = options.credentials ?? "same-origin";
123
+ const client = csrfPerCall(createClient({
124
+ baseUrl: options.baseUrl ?? "",
125
+ credentials,
126
+ ...options.headers ? { headers: { ...options.headers } } : {},
127
+ ...options.fetch ? { fetch: options.fetch } : {}
128
+ }), credentials);
129
+ const policy = contractPolicy(options);
130
+ policies.set(client, policy);
131
+ if (policy.mode !== "off") client.use(contractCheck(policy));
132
+ return client;
133
+ }
134
+ function contractCheck(policy) {
135
+ return { async onResponse({ request, response, schemaPath }) {
136
+ const operation = `${request.method} ${schemaPath}`;
137
+ const validator = policy.validators.responses?.[operation];
138
+ if (!validator || !response.ok || response.status === 204) return;
139
+ const requestId = response.headers.get("x-request-id");
140
+ let body;
141
+ try {
142
+ body = JSON.parse(await response.clone().text());
143
+ } catch {
144
+ policy.report({
145
+ operation,
146
+ issues: [{
147
+ path: "",
148
+ message: "body is not JSON"
149
+ }]
150
+ }, response.status, requestId);
151
+ return;
152
+ }
153
+ policy.check(operation, validator, body, response.status, requestId);
154
+ } };
155
+ }
156
+ // ../client/src/sse.ts
157
+ /**
158
+ * Turns decoded text, split anywhere, into frames: the subset of the
159
+ * WHATWG event-stream format the server writes (id, event, data, comments).
160
+ */
161
+ function createFrameParser() {
162
+ let buffer = "";
163
+ let id;
164
+ let event = "";
165
+ let data = [];
166
+ return { push(text) {
167
+ buffer += text;
168
+ const frames = [];
169
+ for (;;) {
170
+ const end = buffer.search(/\r\n|\r|\n/);
171
+ if (end < 0 || end === buffer.length - 1 && buffer[end] === "\r") break;
172
+ const line = buffer.slice(0, end);
173
+ buffer = buffer.slice(end + (buffer.startsWith("\r\n", end) ? 2 : 1));
174
+ if (line === "") {
175
+ if (data.length > 0) frames.push({
176
+ id,
177
+ event: event || "message",
178
+ data: data.join("\n")
179
+ });
180
+ id = void 0;
181
+ event = "";
182
+ data = [];
183
+ continue;
184
+ }
185
+ if (line.startsWith(":")) continue;
186
+ const colon = line.indexOf(":");
187
+ const field = colon < 0 ? line : line.slice(0, colon);
188
+ let value = colon < 0 ? "" : line.slice(colon + 1);
189
+ if (value.startsWith(" ")) value = value.slice(1);
190
+ if (field === "id" && !value.includes("\0")) id = value;
191
+ else if (field === "event") event = value;
192
+ else if (field === "data") data.push(value);
193
+ }
194
+ return frames;
195
+ } };
196
+ }
197
+ const EVENT_NAMES = {
198
+ system: true,
199
+ assistant: true,
200
+ tool_use: true,
201
+ tool_result: true,
202
+ question: true,
203
+ result: true,
204
+ status: true,
205
+ error: true
206
+ };
207
+ Object.keys(EVENT_NAMES);
208
+ /**
209
+ * The envelope check every frame gets, in every mode and without a schema
210
+ * library: what a frame must hold to be shown at all.
211
+ */
212
+ function readFrame(raw, sessionId) {
213
+ const issue = (path, message) => ({
214
+ kind: "mismatch",
215
+ issues: [{
216
+ path,
217
+ message
218
+ }]
219
+ });
220
+ if (!raw.id) return issue("id", "missing");
221
+ if (!Object.hasOwn(EVENT_NAMES, raw.event)) return {
222
+ kind: "unknown",
223
+ issues: [{
224
+ path: "event",
225
+ message: "unknown kind"
226
+ }]
227
+ };
228
+ let envelope;
229
+ try {
230
+ envelope = JSON.parse(raw.data);
231
+ } catch {
232
+ return issue("data", "not JSON");
233
+ }
234
+ if (typeof envelope !== "object" || envelope === null) return issue("data", "not an object");
235
+ const fields = envelope;
236
+ if (fields.schema_version !== 1) return issue("data.schema_version", `not 1`);
237
+ if (typeof fields.session_id !== "string" || fields.session_id.toLowerCase() !== sessionId.toLowerCase()) return issue("data.session_id", "not the subscribed session");
238
+ if (typeof fields.occurred_at !== "string") return issue("data.occurred_at", "not a string");
239
+ for (const key of ["turn_id", "attempt_id"]) {
240
+ const value = fields[key];
241
+ if (value !== null && typeof value !== "string") return issue(`data.${key}`, "not a string or null");
242
+ }
243
+ if (!("data" in fields)) return issue("data.data", "missing");
244
+ return {
245
+ kind: "event",
246
+ frame: {
247
+ id: raw.id,
248
+ occurredAt: fields.occurred_at,
249
+ turnId: fields.turn_id,
250
+ attemptId: fields.attempt_id,
251
+ payload: {
252
+ event: raw.event,
253
+ data: fields.data
254
+ }
255
+ },
256
+ wire: {
257
+ id: raw.id,
258
+ event: raw.event,
259
+ data: envelope
260
+ }
261
+ };
262
+ }
263
+ // ../client/src/events.ts
264
+ const OPERATION = "SSE /v1/sessions/{id}/events";
265
+ const DEDUPE_WINDOW = 1e3;
266
+ /**
267
+ * Session events over fetch: unlike EventSource it carries the client's
268
+ * headers, sees HTTP statuses, and resumes with Last-Event-ID after any drop.
269
+ * Frames of a kind or envelope version this client does not know are dropped,
270
+ * but the cursor still moves past them, or a reconnect would stall on them.
271
+ */
272
+ async function subscribeSessionEvents(client, sessionId, options) {
273
+ const { signal } = options;
274
+ const policy = policyOf(client);
275
+ const initialMs = options.retry?.initialMs ?? 1e3;
276
+ const maxMs = options.retry?.maxMs ?? 3e4;
277
+ const idleMs = options.retry?.idleMs ?? 45e3;
278
+ const delivered = /* @__PURE__ */ new Set();
279
+ let cursor = options.lastEventId || void 0;
280
+ let backoff = initialMs;
281
+ /** Returns false once the subscription must end. */
282
+ const receive = (raw) => {
283
+ const reading = readFrame(raw, sessionId);
284
+ if (reading.kind === "event" && delivered.has(reading.frame.id)) return true;
285
+ if (raw.id) cursor = raw.id;
286
+ try {
287
+ if (reading.kind !== "event") {
288
+ if (reading.kind === "mismatch" || policy.mode === "strict") policy.report({
289
+ operation: OPERATION,
290
+ issues: reading.issues
291
+ }, 200, null);
292
+ return true;
293
+ }
294
+ policy.check(OPERATION, policy.validators.sessionEvent, reading.wire, 200, null);
295
+ } catch (error) {
296
+ if (!(error instanceof ApiError)) throw error;
297
+ options.onError?.(error);
298
+ return false;
299
+ }
300
+ delivered.add(reading.frame.id);
301
+ if (delivered.size > DEDUPE_WINDOW) delivered.delete(delivered.values().next().value);
302
+ backoff = initialMs;
303
+ options.onEvent(reading.frame);
304
+ return true;
305
+ };
306
+ /** One connection; resolves to the wait before the next, or null to end. */
307
+ const connect = async () => {
308
+ if (signal.aborted) return null;
309
+ const attempt = new AbortController();
310
+ const stop = () => attempt.abort();
311
+ signal.addEventListener("abort", stop, { once: true });
312
+ let idle;
313
+ const touch = () => {
314
+ clearTimeout(idle);
315
+ idle = setTimeout(stop, idleMs);
316
+ };
317
+ touch();
318
+ try {
319
+ const open = () => client.GET("/v1/sessions/{id}/events", {
320
+ params: {
321
+ path: { id: sessionId },
322
+ header: cursor ? { "Last-Event-ID": cursor } : {}
323
+ },
324
+ headers: { accept: "text/event-stream" },
325
+ parseAs: "stream",
326
+ signal: attempt.signal
327
+ });
328
+ let result;
329
+ try {
330
+ result = await open();
331
+ } catch {
332
+ return signal.aborted ? null : backoff;
333
+ }
334
+ const { data: body, error, response } = result;
335
+ if (!response.ok) {
336
+ const retryAfter = retryAfterSeconds(response);
337
+ if (response.status === 429 || response.status === 503) return retryAfter ? retryAfter * 1e3 : backoff;
338
+ if (response.status >= 500 || response.status === 408) return backoff;
339
+ options.onError?.(errorFromResponse$1(response, error));
340
+ return null;
341
+ }
342
+ if (!body) return backoff;
343
+ options.onState?.("open");
344
+ const reader = body.getReader();
345
+ const decoder = new TextDecoder();
346
+ const parser = createFrameParser();
347
+ for (;;) {
348
+ let chunk;
349
+ try {
350
+ chunk = await reader.read();
351
+ } catch {
352
+ break;
353
+ }
354
+ if (chunk.done) break;
355
+ touch();
356
+ for (const raw of parser.push(decoder.decode(chunk.value, { stream: true }))) if (signal.aborted || !receive(raw)) {
357
+ await reader.cancel().catch(() => {});
358
+ return null;
359
+ }
360
+ }
361
+ await reader.cancel().catch(() => {});
362
+ return signal.aborted ? null : backoff;
363
+ } finally {
364
+ clearTimeout(idle);
365
+ signal.removeEventListener("abort", stop);
366
+ }
367
+ };
368
+ try {
369
+ while (!signal.aborted) {
370
+ options.onState?.("connecting");
371
+ const wait = await connect();
372
+ if (wait === null) return;
373
+ backoff = Math.min(backoff * 2, maxMs);
374
+ await sleep(wait, signal);
375
+ }
376
+ } finally {
377
+ options.onState?.("closed");
378
+ }
379
+ }
380
+ function sleep(ms, signal) {
381
+ return new Promise((resolve) => {
382
+ if (signal.aborted) return resolve();
383
+ const done = () => {
384
+ clearTimeout(timer);
385
+ signal.removeEventListener("abort", done);
386
+ resolve();
387
+ };
388
+ const timer = setTimeout(done, ms);
389
+ signal.addEventListener("abort", done, { once: true });
390
+ });
391
+ }
392
+ // src/core/errors.ts
393
+ /** One class per failure mechanism, never per code: branch on `code`. */
394
+ var WerftError = class extends Error {
395
+ code;
396
+ attempts;
397
+ /** Set on every error from a keyed command, aborts included. Retry with this key, not a new one. */
398
+ idempotencyKey;
399
+ constructor(code, message, init = {}) {
400
+ super(message, init.cause === void 0 ? void 0 : { cause: init.cause });
401
+ this.name = new.target.name;
402
+ this.code = code;
403
+ this.attempts = init.attempts ?? 1;
404
+ this.idempotencyKey = init.idempotencyKey;
405
+ }
406
+ };
407
+ /** The server answered with an error. `code` is its `error.code` as sent. */
408
+ var APIError = class extends WerftError {
409
+ status;
410
+ retryable;
411
+ requestId;
412
+ details;
413
+ /** Retry-After in milliseconds, when the response had one. */
414
+ retryAfterMs;
415
+ constructor(status, code, message, init = {}) {
416
+ super(code, message, init);
417
+ this.status = status;
418
+ this.retryable = init.retryable ?? false;
419
+ this.requestId = init.requestId ?? null;
420
+ this.details = init.details ?? null;
421
+ this.retryAfterMs = init.retryAfterMs;
422
+ }
423
+ };
424
+ /** No response was read: the command may or may not have run. */
425
+ var ConnectionError = class extends WerftError {
426
+ constructor(message, init = {}) {
427
+ super("NETWORK_ERROR", message, init);
428
+ }
429
+ };
430
+ var TimeoutError = class extends WerftError {
431
+ constructor(message, init = {}) {
432
+ super("TIMEOUT", message, init);
433
+ }
434
+ };
435
+ /** The caller's signal aborted. `name` is "AbortError" and `cause` the signal's reason. */
436
+ var AbortedError = class extends WerftError {
437
+ constructor(reason, init = {}) {
438
+ super("ABORTED", "The request was aborted", {
439
+ ...init,
440
+ cause: reason
441
+ });
442
+ this.name = "AbortError";
443
+ }
444
+ };
445
+ /** waitForTurn only: the session left `active` and the turn cannot end until someone acts. */
446
+ var TurnBlockedError = class extends WerftError {
447
+ details;
448
+ constructor(admissionState) {
449
+ super("TURN_BLOCKED", `The session is ${admissionState}; the turn cannot finish until it is acted on`);
450
+ this.details = { admission_state: admissionState };
451
+ }
452
+ };
453
+ function isApiError(error, code) {
454
+ return error instanceof APIError && (code === void 0 || error.code === code);
455
+ }
456
+ // src/core/request.ts
457
+ const RESERVED_HEADERS = /* @__PURE__ */ new Set(["authorization", "idempotency-key"]);
458
+ function checkHeaders(headers) {
459
+ for (const name of Object.keys(headers ?? {})) if (RESERVED_HEADERS.has(name.toLowerCase())) throw new TypeError(`headers cannot set ${name}`);
460
+ return { ...headers };
461
+ }
462
+ /** The one place for deadlines, retries, the Idempotency-Key and error mapping. */
463
+ async function call(policy, kind, attempt, options = {}, defaultKey) {
464
+ const headers = checkHeaders(options.headers);
465
+ const key = kind === "keyed" ? options.idempotencyKey ?? defaultKey ?? crypto.randomUUID() : void 0;
466
+ const { signal } = options;
467
+ const maxRetries = options.maxRetries ?? policy.maxRetries;
468
+ const timeoutMs = options.timeoutMs ?? policy.timeoutMs;
469
+ const sleep = policy.sleep ?? abortableSleep;
470
+ const random = policy.random ?? Math.random;
471
+ for (let retry = 0;; retry++) {
472
+ const tags = {
473
+ attempts: retry + 1,
474
+ idempotencyKey: key
475
+ };
476
+ if (signal?.aborted) throw new AbortedError(signal.reason, {
477
+ ...tags,
478
+ attempts: retry
479
+ });
480
+ const deadline = new AbortController();
481
+ const timer = setTimeout(() => deadline.abort(), timeoutMs);
482
+ let failure;
483
+ let retryable;
484
+ try {
485
+ const { data, error, response } = await attempt({
486
+ signal: signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal,
487
+ headers,
488
+ idempotencyKey: key ?? ""
489
+ });
490
+ if (response.ok) return data;
491
+ failure = errorFromResponse(response, error, tags);
492
+ retryable = isRetryable(response.status, error, kind);
493
+ } catch (cause) {
494
+ if (signal?.aborted) throw new AbortedError(signal.reason, tags);
495
+ failure = deadline.signal.aborted ? new TimeoutError(`No response within ${timeoutMs} ms`, tags) : new ConnectionError(cause instanceof Error ? cause.message : String(cause), {
496
+ ...tags,
497
+ cause
498
+ });
499
+ retryable = kind !== "unkeyed";
500
+ } finally {
501
+ clearTimeout(timer);
502
+ }
503
+ if (!retryable || retry >= maxRetries) throw failure;
504
+ let wait = random() * Math.min(500 * 2 ** retry, 8e3, policy.maxRetryDelayMs);
505
+ if (failure instanceof APIError && failure.retryAfterMs !== void 0) {
506
+ if (failure.retryAfterMs > policy.maxRetryDelayMs) throw failure;
507
+ wait = failure.retryAfterMs;
508
+ }
509
+ await sleep(wait, signal);
510
+ if (signal?.aborted) throw new AbortedError(signal.reason, tags);
511
+ }
512
+ }
513
+ function apiErrorBody(body) {
514
+ const error = body?.error;
515
+ return typeof error?.code === "string" ? error : void 0;
516
+ }
517
+ /**
518
+ * 409 is always a state conflict and 500 a server bug: neither is retried
519
+ * whatever the body says. A 502 or 504 without the API's error body came
520
+ * from a proxy in front of it, so the API may have run the call: like a
521
+ * lost response, it is retried only for reads and keyed commands.
522
+ */
523
+ function isRetryable(status, body, kind) {
524
+ if (status === 409 || status === 500) return false;
525
+ const error = apiErrorBody(body);
526
+ if (error) return error.retryable === true;
527
+ return kind !== "unkeyed" && (status === 502 || status === 504);
528
+ }
529
+ /** A failure `call` would have retried: worth another try on the next poll. */
530
+ function isTransient(error) {
531
+ if (error instanceof ConnectionError || error instanceof TimeoutError) return true;
532
+ if (!(error instanceof APIError)) return false;
533
+ if (error.status === 409 || error.status === 500) return false;
534
+ return error.retryable || (error.status === 502 || error.status === 504) && error.code === "UNKNOWN";
535
+ }
536
+ function retryAfterMs(response) {
537
+ const header = response.headers.get("retry-after");
538
+ if (header === null || header.trim() === "") return void 0;
539
+ const seconds = Number(header);
540
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
541
+ }
542
+ const text = (value) => typeof value === "string" ? value : void 0;
543
+ function errorFromResponse(response, body, tags) {
544
+ const error = apiErrorBody(body);
545
+ return new APIError(response.status, text(error?.code) ?? "UNKNOWN", text(error?.message) ?? (response.statusText || `HTTP ${response.status}`), {
546
+ ...tags,
547
+ retryable: error?.retryable === true,
548
+ requestId: text(error?.request_id) ?? response.headers.get("x-request-id"),
549
+ details: error?.details ?? null,
550
+ retryAfterMs: retryAfterMs(response)
551
+ });
552
+ }
553
+ function abortableSleep(ms, signal) {
554
+ return new Promise((resolve) => {
555
+ if (signal?.aborted) return resolve();
556
+ const done = () => {
557
+ clearTimeout(timer);
558
+ signal?.removeEventListener("abort", done);
559
+ resolve();
560
+ };
561
+ const timer = setTimeout(done, ms);
562
+ signal?.addEventListener("abort", done, { once: true });
563
+ });
564
+ }
565
+ // src/core/page.ts
566
+ function page(fetch, body, options) {
567
+ return {
568
+ items: body.items,
569
+ next_cursor: body.next_cursor,
570
+ async nextPage(overrides) {
571
+ if (body.next_cursor === null) return null;
572
+ const merged = {
573
+ ...options,
574
+ ...overrides
575
+ };
576
+ return page(fetch, await fetch(body.next_cursor, merged), merged);
577
+ }
578
+ };
579
+ }
580
+ function paginate(fetch, options) {
581
+ const first = fetch(void 0, options).then((body) => page(fetch, body, options));
582
+ return Object.assign(first, { async *[Symbol.asyncIterator]() {
583
+ let current = await first;
584
+ while (current) {
585
+ yield* current.items;
586
+ current = await current.nextPage();
587
+ }
588
+ } });
589
+ }
590
+ // src/core/stream.ts
591
+ const MAX_BUFFERED_EVENTS = 1e4;
592
+ function toEvent(frame) {
593
+ return {
594
+ id: frame.id,
595
+ turn_id: frame.turnId,
596
+ attempt_id: frame.attemptId,
597
+ occurred_at: frame.occurredAt,
598
+ kind: frame.payload.event,
599
+ payload: frame.payload.data
600
+ };
601
+ }
602
+ function fromClientError(error) {
603
+ return new APIError(error.status, error.code, error.message, {
604
+ retryable: error.retryable === true,
605
+ requestId: error.requestId,
606
+ details: error.details,
607
+ retryAfterMs: error.retryAfterSeconds === null ? void 0 : error.retryAfterSeconds * 1e3
608
+ });
609
+ }
610
+ function openEventStream(api, sessionId, options = {}) {
611
+ const stop = new AbortController();
612
+ const signal = options.signal ? AbortSignal.any([options.signal, stop.signal]) : stop.signal;
613
+ let lastEventId = options.lastEventId || void 0;
614
+ let started = false;
615
+ const queue = [];
616
+ let failure;
617
+ let ended = false;
618
+ let closed = false;
619
+ let wake;
620
+ const notify = () => {
621
+ wake?.();
622
+ wake = void 0;
623
+ };
624
+ const start = () => {
625
+ started = true;
626
+ signal.addEventListener("abort", () => {
627
+ ended = true;
628
+ notify();
629
+ }, { once: true });
630
+ subscribeSessionEvents(api, sessionId, {
631
+ signal,
632
+ ...lastEventId ? { lastEventId } : {},
633
+ onEvent(frame) {
634
+ if (failure !== void 0) return;
635
+ if (queue.length >= 1e4) {
636
+ failure = new WerftError("STREAM_OVERFLOW", `More than ${MAX_BUFFERED_EVENTS} events waited to be read; reopen the stream from lastEventId`);
637
+ stop.abort();
638
+ return;
639
+ }
640
+ queue.push(toEvent(frame));
641
+ notify();
642
+ },
643
+ onError(error) {
644
+ failure = fromClientError(error);
645
+ }
646
+ }).catch((error) => {
647
+ failure = error;
648
+ }).finally(() => {
649
+ ended = true;
650
+ notify();
651
+ });
652
+ };
653
+ return {
654
+ get lastEventId() {
655
+ return lastEventId;
656
+ },
657
+ close() {
658
+ closed = true;
659
+ stop.abort();
660
+ },
661
+ async *[Symbol.asyncIterator]() {
662
+ if (started) throw new TypeError("an event stream is read once");
663
+ start();
664
+ try {
665
+ for (;;) {
666
+ if (closed || options.signal?.aborted) return;
667
+ const event = queue.shift();
668
+ if (event) {
669
+ lastEventId = event.id;
670
+ yield event;
671
+ continue;
672
+ }
673
+ if (ended) {
674
+ if (failure !== void 0) throw failure;
675
+ return;
676
+ }
677
+ await new Promise((resolve) => {
678
+ wake = resolve;
679
+ });
680
+ }
681
+ } finally {
682
+ stop.abort();
683
+ }
684
+ }
685
+ };
686
+ }
687
+ // src/resources/close.ts
688
+ /** Admission while the worker drains for a close; `closed` follows. */
689
+ const SESSION_ADMISSION_CLOSING = "closing";
690
+ /** A message or control sent while the session is `closing`. */
691
+ const SESSION_CLOSING = "SESSION_CLOSING";
692
+ /** Attention on a close whose drain has run over 60 seconds. Nothing is killed for it. */
693
+ const CLOSE_BLOCKED = "CLOSE_BLOCKED";
694
+ function isClosing(session) {
695
+ return session.admission_state === SESSION_ADMISSION_CLOSING;
696
+ }
697
+ function closeBlocked(session) {
698
+ const attention = session.attention;
699
+ return attention?.code === "CLOSE_BLOCKED" ? attention : null;
700
+ }
701
+ /** The typed result of a succeeded close receipt; null for any other receipt. */
702
+ function closeResult(receipt) {
703
+ if (receipt.operation !== "close" || receipt.status !== "succeeded") return null;
704
+ const result = receipt.result;
705
+ if (typeof result?.resulting_admission_state !== "string" || typeof result.cancelled_turn_count !== "number" || result.checkpoint_revision !== null && typeof result.checkpoint_revision !== "number") return null;
706
+ return result;
707
+ }
708
+ /**
709
+ * Ends a session whose work is done; the running turn finishes first. A
710
+ * caller closing its own session from inside a turn must not wait on the
711
+ * receipt: the drain waits for that turn to end.
712
+ */
713
+ function closeSession(context, id, params, options) {
714
+ return call(context.policy, "keyed", ({ signal, headers, idempotencyKey }) => context.api.POST("/v1/sessions/{id}/close", {
715
+ params: {
716
+ path: { id },
717
+ header: { "Idempotency-Key": idempotencyKey }
718
+ },
719
+ body: params,
720
+ signal,
721
+ headers
722
+ }), options);
723
+ }
724
+ // src/resources/sessions.ts
725
+ function sessions(context) {
726
+ const { api, policy } = context;
727
+ const header = (idempotencyKey) => ({ "Idempotency-Key": idempotencyKey });
728
+ const approve = (id, pushRequestId, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/push-requests/{push_request_id}/approve", {
729
+ params: {
730
+ path: {
731
+ id,
732
+ push_request_id: pushRequestId
733
+ },
734
+ header: header(idempotencyKey)
735
+ },
736
+ body: params,
737
+ signal,
738
+ headers
739
+ }), options);
740
+ const reject = (id, pushRequestId, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/push-requests/{push_request_id}/reject", {
741
+ params: {
742
+ path: {
743
+ id,
744
+ push_request_id: pushRequestId
745
+ },
746
+ header: header(idempotencyKey)
747
+ },
748
+ body: params,
749
+ signal,
750
+ headers
751
+ }), options);
752
+ return {
753
+ create: (params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions", {
754
+ params: { header: header(idempotencyKey) },
755
+ body: params,
756
+ signal,
757
+ headers
758
+ }), options),
759
+ get: (id, options) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}", {
760
+ params: { path: { id } },
761
+ signal,
762
+ headers
763
+ }), options),
764
+ list: (params, options) => paginate((cursor, pageOptions) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions", {
765
+ params: { query: {
766
+ ...params,
767
+ ...cursor ? { cursor } : {}
768
+ } },
769
+ signal,
770
+ headers
771
+ }), pageOptions), options),
772
+ usage: (id, options) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/usage", {
773
+ params: { path: { id } },
774
+ signal,
775
+ headers
776
+ }), options),
777
+ share: (id, options) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/share", {
778
+ params: { path: { id } },
779
+ signal,
780
+ headers
781
+ }), options),
782
+ interrupt: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/interrupt", {
783
+ params: {
784
+ path: { id },
785
+ header: header(idempotencyKey)
786
+ },
787
+ body: params,
788
+ signal,
789
+ headers
790
+ }), options),
791
+ pause: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/pause", {
792
+ params: {
793
+ path: { id },
794
+ header: header(idempotencyKey)
795
+ },
796
+ body: params,
797
+ signal,
798
+ headers
799
+ }), options),
800
+ resume: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/resume", {
801
+ params: {
802
+ path: { id },
803
+ header: header(idempotencyKey)
804
+ },
805
+ body: params,
806
+ signal,
807
+ headers
808
+ }), options),
809
+ terminate: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/terminate", {
810
+ params: {
811
+ path: { id },
812
+ header: header(idempotencyKey)
813
+ },
814
+ body: params,
815
+ signal,
816
+ headers
817
+ }), options),
818
+ close: (id, params, options) => closeSession(context, id, params, options),
819
+ changeProfile: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/profile", {
820
+ params: {
821
+ path: { id },
822
+ header: header(idempotencyKey)
823
+ },
824
+ body: params,
825
+ signal,
826
+ headers
827
+ }), options),
828
+ recoveryDecisions: { create: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/recovery-decisions", {
829
+ params: {
830
+ path: { id },
831
+ header: header(idempotencyKey)
832
+ },
833
+ body: params,
834
+ signal,
835
+ headers
836
+ }), options) },
837
+ messages: { send: (id, params, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/messages", {
838
+ params: {
839
+ path: { id },
840
+ header: header(idempotencyKey)
841
+ },
842
+ body: {
843
+ mode: "enqueue",
844
+ ...params
845
+ },
846
+ signal,
847
+ headers
848
+ }), options) },
849
+ turns: {
850
+ get: (id, turnId, options) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/turns/{turn_id}", {
851
+ params: { path: {
852
+ id,
853
+ turn_id: turnId
854
+ } },
855
+ signal,
856
+ headers
857
+ }), options),
858
+ list: (id, params, options) => paginate((cursor, pageOptions) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/turns", {
859
+ params: {
860
+ path: { id },
861
+ query: {
862
+ ...params,
863
+ ...cursor ? { cursor } : {}
864
+ }
865
+ },
866
+ signal,
867
+ headers
868
+ }), pageOptions), options)
869
+ },
870
+ events: { stream: (id, options) => openEventStream(api, id, options) },
871
+ requests: {
872
+ list: async (id, options) => (await call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/pending-requests", {
873
+ params: { path: { id } },
874
+ signal,
875
+ headers
876
+ }), options)).items,
877
+ answer: (id, body, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/answers", {
878
+ params: {
879
+ path: { id },
880
+ header: header(idempotencyKey)
881
+ },
882
+ body,
883
+ signal,
884
+ headers
885
+ }), options, `answer:${body.request_id}`)
886
+ },
887
+ pushRequests: {
888
+ create: (id, options) => call(policy, "keyed", ({ signal, headers, idempotencyKey }) => api.POST("/v1/sessions/{id}/push-requests", {
889
+ params: {
890
+ path: { id },
891
+ header: header(idempotencyKey)
892
+ },
893
+ body: {},
894
+ signal,
895
+ headers
896
+ }), options),
897
+ list: async (id, options) => (await call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/push-requests", {
898
+ params: { path: { id } },
899
+ signal,
900
+ headers
901
+ }), options)).items,
902
+ get: (id, pushRequestId, options) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/sessions/{id}/push-requests/{push_request_id}", {
903
+ params: { path: {
904
+ id,
905
+ push_request_id: pushRequestId
906
+ } },
907
+ signal,
908
+ headers
909
+ }), options),
910
+ approve,
911
+ reject,
912
+ openPullRequest: (id, pushRequestId, params, options) => call(policy, "unkeyed", ({ signal, headers }) => api.POST("/v1/sessions/{id}/push-requests/{push_request_id}/pull-request", {
913
+ params: { path: {
914
+ id,
915
+ push_request_id: pushRequestId
916
+ } },
917
+ body: params ?? {},
918
+ signal,
919
+ headers
920
+ }), options),
921
+ commentPullRequest: (id, pushRequestId, params, options) => call(policy, "unkeyed", ({ signal, headers }) => api.POST("/v1/sessions/{id}/push-requests/{push_request_id}/pull-request/comments", {
922
+ params: { path: {
923
+ id,
924
+ push_request_id: pushRequestId
925
+ } },
926
+ body: params,
927
+ signal,
928
+ headers
929
+ }), options)
930
+ }
931
+ };
932
+ }
933
+ // src/client.ts
934
+ const settings = /* @__PURE__ */ new WeakMap();
935
+ function environment() {
936
+ return globalThis.process?.env ?? {};
937
+ }
938
+ function isLoopback(hostname) {
939
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "[::1]" || /^127(\.\d{1,3}){3}$/.test(hostname);
940
+ }
941
+ function count(name, value, fallback) {
942
+ if (value === void 0) return fallback;
943
+ if (!Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
944
+ return value;
945
+ }
946
+ var Werft = class Werft {
947
+ sessions;
948
+ receipts;
949
+ /** This key's owner and scopes. */
950
+ auth;
951
+ limits;
952
+ profiles;
953
+ root;
954
+ constructor(options = {}) {
955
+ const env = environment();
956
+ const apiKey = options.apiKey ?? env.WERFT_API_KEY;
957
+ const baseUrl = options.baseUrl ?? env.WERFT_BASE_URL;
958
+ if (!apiKey) throw new TypeError("No API key: pass apiKey or set WERFT_API_KEY");
959
+ if (!baseUrl) throw new TypeError("No base URL: pass baseUrl or set WERFT_BASE_URL");
960
+ if (typeof globalThis.window !== "undefined" && !options.dangerouslyAllowBrowser) throw new Error("Werft runs on servers and scripts: an API key in a browser page leaks. Set dangerouslyAllowBrowser to override.");
961
+ let url;
962
+ try {
963
+ url = new URL(baseUrl);
964
+ } catch {
965
+ throw new TypeError("baseUrl is not a URL");
966
+ }
967
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new TypeError("baseUrl must be http or https");
968
+ if (url.protocol === "http:" && !isLoopback(url.hostname) && !options.allowInsecureHttp) throw new Error("baseUrl is plain http to a remote host: the API key would cross the wire in clear text. Use https or set allowInsecureHttp.");
969
+ const policy = {
970
+ maxRetries: count("maxRetries", options.maxRetries, 2),
971
+ maxRetryDelayMs: count("maxRetryDelayMs", options.maxRetryDelayMs, 6e4),
972
+ timeoutMs: count("timeoutMs", options.timeoutMs, 6e4)
973
+ };
974
+ const api = createApiClient({
975
+ baseUrl: url.href.replace(/\/+$/, ""),
976
+ credentials: "omit",
977
+ headers: {
978
+ ...checkHeaders(options.defaultHeaders),
979
+ authorization: `Bearer ${apiKey}`
980
+ },
981
+ ...options.fetch ? { fetch: options.fetch } : {}
982
+ });
983
+ const context = {
984
+ api,
985
+ policy
986
+ };
987
+ settings.set(this, {
988
+ ...options,
989
+ apiKey,
990
+ baseUrl
991
+ });
992
+ this.sessions = sessions(context);
993
+ this.receipts = { get: (id, o) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/receipts/{id}", {
994
+ params: { path: { id } },
995
+ signal,
996
+ headers
997
+ }), o) };
998
+ this.auth = { me: (o) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/auth/me", {
999
+ signal,
1000
+ headers
1001
+ }), o) };
1002
+ this.limits = { get: async (o) => {
1003
+ const limits = await call(policy, "read", ({ signal, headers }) => api.GET("/v1/limits", {
1004
+ params: { query: { scope: "installation" } },
1005
+ signal,
1006
+ headers
1007
+ }), o);
1008
+ if (limits.scope !== "installation") throw new TypeError(`unexpected limits scope ${limits.scope}`);
1009
+ return limits;
1010
+ } };
1011
+ this.profiles = { list: (o) => call(policy, "read", ({ signal, headers }) => api.GET("/v1/profiles", {
1012
+ signal,
1013
+ headers
1014
+ }), o) };
1015
+ this.root = { get: (o) => call(policy, "read", ({ signal, headers }) => api.GET("/v1", {
1016
+ signal,
1017
+ headers
1018
+ }), o) };
1019
+ }
1020
+ /** A second client with some options changed, e.g. an approve-scoped key. */
1021
+ withOptions(overrides) {
1022
+ return new Werft({
1023
+ ...settings.get(this),
1024
+ ...overrides
1025
+ });
1026
+ }
1027
+ };
1028
+ // src/helpers.ts
1029
+ const TERMINAL_TURN = /* @__PURE__ */ new Set([
1030
+ "completed",
1031
+ "failed",
1032
+ "interrupted",
1033
+ "cancelled",
1034
+ "outcome_unknown"
1035
+ ]);
1036
+ const BLOCKED_ADMISSION = /* @__PURE__ */ new Set([
1037
+ "paused",
1038
+ "stopped",
1039
+ "recovery_required",
1040
+ "closed"
1041
+ ]);
1042
+ const OPEN_PUSH = /* @__PURE__ */ new Set([
1043
+ "requested",
1044
+ "approved",
1045
+ "pushing"
1046
+ ]);
1047
+ const ended = (turn) => TERMINAL_TURN.has(turn.status) || turn.ended_at !== null;
1048
+ const never = new AbortController().signal;
1049
+ function aborted(signal) {
1050
+ if (signal?.aborted) throw new AbortedError(signal.reason);
1051
+ }
1052
+ const optionsOf = (signal) => signal ? { signal } : {};
1053
+ async function decide(request, handlers, signal) {
1054
+ if (request.kind === "permission" && handlers.permission) {
1055
+ const answer = await handlers.permission(request, { signal });
1056
+ if (!answer) return void 0;
1057
+ const { action } = answer;
1058
+ if (action === "deny" && !answer.reason?.trim()) throw new TypeError("a denial needs a reason the model can act on");
1059
+ return {
1060
+ request_id: request.request_id,
1061
+ kind: "permission",
1062
+ action,
1063
+ ...answer.reason ? { reason: answer.reason } : {}
1064
+ };
1065
+ }
1066
+ if (request.kind === "question" && handlers.question) {
1067
+ const answer = await handlers.question(request, { signal });
1068
+ if (!answer) return void 0;
1069
+ return {
1070
+ request_id: request.request_id,
1071
+ kind: "question",
1072
+ answers: answer.answers
1073
+ };
1074
+ }
1075
+ }
1076
+ async function answerOne(client, sessionId, request, handlers, signal) {
1077
+ const { request_id } = request;
1078
+ const body = await decide(request, handlers, signal ?? never);
1079
+ if (!body) return {
1080
+ request_id,
1081
+ outcome: "left_open"
1082
+ };
1083
+ try {
1084
+ return {
1085
+ request_id,
1086
+ outcome: "accepted",
1087
+ receipt: await client.sessions.requests.answer(sessionId, body, optionsOf(signal))
1088
+ };
1089
+ } catch (error) {
1090
+ if (isApiError(error, "REQUEST_EXPIRED")) return {
1091
+ request_id,
1092
+ outcome: "too_late",
1093
+ code: "REQUEST_EXPIRED"
1094
+ };
1095
+ if (isApiError(error, "REQUEST_STALE")) return {
1096
+ request_id,
1097
+ outcome: "too_late",
1098
+ code: "REQUEST_STALE"
1099
+ };
1100
+ if (isApiError(error, "IDEMPOTENCY_CONFLICT")) return {
1101
+ request_id,
1102
+ outcome: "conflict"
1103
+ };
1104
+ throw error;
1105
+ }
1106
+ }
1107
+ const oldestFirst = (a, b) => a.created_at.localeCompare(b.created_at);
1108
+ /** Hands every open request of the session, oldest first, to its handler once. */
1109
+ async function answerPending(client, sessionId, handlers, options = {}) {
1110
+ const requests = await client.sessions.requests.list(sessionId, optionsOf(options.signal));
1111
+ const outcomes = [];
1112
+ for (const request of [...requests].sort(oldestFirst)) outcomes.push(await answerOne(client, sessionId, request, handlers, options.signal));
1113
+ return outcomes;
1114
+ }
1115
+ /** A wait that the timer, an event or the signal ends, whichever comes first. */
1116
+ function waker(signal) {
1117
+ let pending = false;
1118
+ let holdMs = 0;
1119
+ let wakeNow;
1120
+ return {
1121
+ wake() {
1122
+ if (wakeNow) wakeNow();
1123
+ else pending = true;
1124
+ },
1125
+ /** The server's Retry-After: the next nap lasts at least this, and no event cuts it short. */
1126
+ holdOff(ms) {
1127
+ holdMs = Math.max(holdMs, ms);
1128
+ },
1129
+ nap(ms) {
1130
+ const hold = holdMs;
1131
+ holdMs = 0;
1132
+ if (signal?.aborted || pending && hold === 0) {
1133
+ pending = false;
1134
+ return Promise.resolve();
1135
+ }
1136
+ pending = false;
1137
+ return new Promise((resolve) => {
1138
+ const done = () => {
1139
+ clearTimeout(timer);
1140
+ signal?.removeEventListener("abort", done);
1141
+ wakeNow = void 0;
1142
+ resolve();
1143
+ };
1144
+ const timer = setTimeout(done, Math.max(ms, hold));
1145
+ signal?.addEventListener("abort", done, { once: true });
1146
+ if (hold === 0) wakeNow = done;
1147
+ });
1148
+ }
1149
+ };
1150
+ }
1151
+ /** Retryable failures wait for the next poll; any other ends the wait. */
1152
+ async function orNull(read, timer) {
1153
+ try {
1154
+ return await read;
1155
+ } catch (error) {
1156
+ if (!isTransient(error)) throw error;
1157
+ if (error instanceof APIError && error.retryAfterMs !== void 0) timer.holdOff(error.retryAfterMs);
1158
+ return null;
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Resolves with the turn once GET turn shows it ended; a `failed` turn is
1163
+ * data, not an error. One loop reads the turn, answers new pending requests
1164
+ * of the session (not only this turn's: a queued turn waits on the one
1165
+ * ahead) and checks the session is not stuck, then sleeps.
1166
+ */
1167
+ async function waitForTurn(client, sessionId, turnId, options = {}) {
1168
+ const { signal } = options;
1169
+ const interval = options.pollIntervalMs ?? 2e3;
1170
+ const handed = /* @__PURE__ */ new Set();
1171
+ const timer = waker(signal);
1172
+ const streamStop = new AbortController();
1173
+ if (options.events) {
1174
+ const stream = client.sessions.events.stream(sessionId, {
1175
+ signal: signal ? AbortSignal.any([signal, streamStop.signal]) : streamStop.signal,
1176
+ ...options.events.lastEventId ? { lastEventId: options.events.lastEventId } : {}
1177
+ });
1178
+ const onEvent = options.events.onEvent;
1179
+ (async () => {
1180
+ try {
1181
+ for await (const event of stream) {
1182
+ onEvent?.(event);
1183
+ timer.wake();
1184
+ }
1185
+ } catch {}
1186
+ })();
1187
+ }
1188
+ try {
1189
+ for (;;) {
1190
+ aborted(signal);
1191
+ const turn = await orNull(client.sessions.turns.get(sessionId, turnId, optionsOf(signal)), timer);
1192
+ if (turn && ended(turn)) return turn;
1193
+ if (options.onRequest) {
1194
+ const requests = await orNull(client.sessions.requests.list(sessionId, optionsOf(signal)), timer);
1195
+ for (const request of [...requests ?? []].sort(oldestFirst)) {
1196
+ if (handed.has(request.request_id)) continue;
1197
+ handed.add(request.request_id);
1198
+ const outcome = await answerOne(client, sessionId, request, options.onRequest, signal);
1199
+ options.onAnswer?.(outcome);
1200
+ }
1201
+ }
1202
+ const session = await orNull(client.sessions.get(sessionId, optionsOf(signal)), timer);
1203
+ if (turn && session && BLOCKED_ADMISSION.has(session.admission_state)) {
1204
+ const again = await orNull(client.sessions.turns.get(sessionId, turnId, optionsOf(signal)), timer);
1205
+ if (again && ended(again)) return again;
1206
+ if (again) throw new TurnBlockedError(session.admission_state);
1207
+ }
1208
+ await timer.nap(interval);
1209
+ }
1210
+ } finally {
1211
+ streamStop.abort();
1212
+ }
1213
+ }
1214
+ /** Polls the receipt until it leaves `accepted`. */
1215
+ async function waitForReceipt(client, receiptId, options = {}) {
1216
+ const { signal } = options;
1217
+ const timer = waker(signal);
1218
+ for (;;) {
1219
+ aborted(signal);
1220
+ const receipt = await orNull(client.receipts.get(receiptId, optionsOf(signal)), timer);
1221
+ if (receipt && receipt.status !== "accepted") return receipt;
1222
+ await timer.nap(options.pollIntervalMs ?? 1e3);
1223
+ }
1224
+ }
1225
+ /** Polls until the push request is pushed, failed or rejected. */
1226
+ async function waitForPushRequest(client, sessionId, pushRequestId, options = {}) {
1227
+ const { signal } = options;
1228
+ const timer = waker(signal);
1229
+ for (;;) {
1230
+ aborted(signal);
1231
+ const push = await orNull(client.sessions.pushRequests.get(sessionId, pushRequestId, optionsOf(signal)), timer);
1232
+ if (push && !OPEN_PUSH.has(push.state)) return push;
1233
+ await timer.nap(options.pollIntervalMs ?? 2e3);
1234
+ }
1235
+ }
1236
+ // src/version.ts
1237
+ const SDK_VERSION = "0.1.2";
1238
+ /** info.version of the spec the types were generated from. Not checked at runtime: the API does not report one. */
1239
+ const API_SPEC_VERSION = "0.1.0-alpha";
1240
+ const SUPPORTED_SSE_SCHEMA_VERSIONS = [1];
1241
+ export { APIError, API_SPEC_VERSION, AbortedError, CLOSE_BLOCKED, ConnectionError, SDK_VERSION, SESSION_ADMISSION_CLOSING, SESSION_CLOSING, SUPPORTED_SSE_SCHEMA_VERSIONS, TimeoutError, TurnBlockedError, Werft, WerftError, answerPending, closeBlocked, closeResult, isApiError, isClosing, waitForPushRequest, waitForReceipt, waitForTurn };