@jxsuite/ai 0.39.3 → 0.39.5
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/README.md +7 -7
- package/package.json +5 -5
- package/src/chat-state.ts +24 -19
- package/src/gateway/chat.ts +17 -1
- package/src/gateway/index.ts +1 -1
- package/src/gateway/normalize.ts +54 -53
- package/src/gateway/problem.ts +33 -21
- package/src/gateway/sse.ts +2 -2
- package/src/gateway/upstream-error.ts +50 -18
- package/src/harness/index.ts +1 -1
- package/src/harness/policy.ts +1 -1
- package/src/harness/run.ts +46 -27
- package/src/index.ts +1 -0
- package/src/messages/convert.ts +93 -38
- package/src/messages/openai.ts +45 -24
- package/src/messages/types.ts +3 -2
- package/src/streaming-client.ts +181 -111
- package/src/tools.ts +144 -52
package/src/streaming-client.ts
CHANGED
|
@@ -145,15 +145,18 @@ interface OpenAIChatRequestBody {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/** A tool-call fragment inside an OpenAI streaming `delta`. */
|
|
148
|
-
interface OpenAIToolCallDelta {
|
|
148
|
+
export interface OpenAIToolCallDelta {
|
|
149
149
|
index: number;
|
|
150
150
|
id?: string;
|
|
151
151
|
type?: string;
|
|
152
152
|
function?: { name?: string; arguments?: string };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A single OpenAI chat-completions streaming chunk (the JSON after `data: `). Exported as a type
|
|
157
|
+
* only, for the gateway's normalizer, which reads the same chunks.
|
|
158
|
+
*/
|
|
159
|
+
export interface OpenAIStreamChunk {
|
|
157
160
|
choices?: {
|
|
158
161
|
index?: number;
|
|
159
162
|
delta?: {
|
|
@@ -184,7 +187,7 @@ export interface OpenAIUsage {
|
|
|
184
187
|
* which is not the same as zero.
|
|
185
188
|
*/
|
|
186
189
|
export function usageEventFromOpenAI(usage?: OpenAIUsage | null): StreamUsageEvent | null {
|
|
187
|
-
if (!usage ||
|
|
190
|
+
if (!usage || !isCount(usage.prompt_tokens)) {
|
|
188
191
|
return null;
|
|
189
192
|
}
|
|
190
193
|
const event: StreamUsageEvent = {
|
|
@@ -193,11 +196,11 @@ export function usageEventFromOpenAI(usage?: OpenAIUsage | null): StreamUsageEve
|
|
|
193
196
|
outputTokens: usage.completion_tokens ?? 0,
|
|
194
197
|
};
|
|
195
198
|
const cached = usage.prompt_tokens_details?.cached_tokens;
|
|
196
|
-
if (
|
|
199
|
+
if (isCount(cached)) {
|
|
197
200
|
event.cachedInputTokens = cached;
|
|
198
201
|
}
|
|
199
202
|
const reasoning = usage.completion_tokens_details?.reasoning_tokens;
|
|
200
|
-
if (
|
|
203
|
+
if (isCount(reasoning)) {
|
|
201
204
|
event.reasoningTokens = reasoning;
|
|
202
205
|
}
|
|
203
206
|
return event;
|
|
@@ -215,58 +218,139 @@ function stopReasonFromOpenAI(reason: string | null | undefined): string | null
|
|
|
215
218
|
return null;
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
221
|
+
/**
|
|
222
|
+
* True when a figure the provider sent is a number. A chunk is upstream JSON, so a figure its
|
|
223
|
+
* interface calls a number is checked before it is reported.
|
|
224
|
+
*
|
|
225
|
+
* @param {unknown} value - A usage figure, as the provider sent it
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
function isCount(value: unknown): value is number {
|
|
229
|
+
return typeof value === "number";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* True when a member of a parsed body or chunk is a string, empty or not.
|
|
234
|
+
*
|
|
235
|
+
* @param {unknown} value - A member, as sent
|
|
236
|
+
* @returns {boolean}
|
|
237
|
+
*/
|
|
238
|
+
function isText(value: unknown): value is string {
|
|
239
|
+
return typeof value === "string";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* True when a member of a parsed error body is a non-empty string, the only `code` worth reading.
|
|
244
|
+
*
|
|
245
|
+
* @param {unknown} value - A `code` member, as sent
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
function isSentence(value: unknown): value is string {
|
|
249
|
+
return isText(value) && value !== "";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* True when a parsed value is a JSON object (or array) whose members may be probed with `in`; false
|
|
254
|
+
* for `null` and every primitive.
|
|
255
|
+
*
|
|
256
|
+
* @param {unknown} value - A parsed body, or one of its members
|
|
257
|
+
* @returns {boolean}
|
|
258
|
+
*/
|
|
259
|
+
function isMemberBag(value: unknown): value is object {
|
|
260
|
+
return typeof value === "object" && value !== null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* True when a parsed `errors` member is an array.
|
|
265
|
+
*
|
|
266
|
+
* @param {unknown} value - The parsed body's `errors` member
|
|
267
|
+
* @returns {boolean}
|
|
268
|
+
*/
|
|
269
|
+
function isEntryList(value: unknown): value is readonly unknown[] {
|
|
270
|
+
return Array.isArray(value);
|
|
236
271
|
}
|
|
237
272
|
|
|
238
273
|
/**
|
|
239
274
|
* Extract a human-readable message from an error body, falling back to the raw body (or `fallback`
|
|
240
275
|
* when the body is empty).
|
|
276
|
+
*
|
|
277
|
+
* The body is the proxy's flat `{ error: "..." }`, OpenAI's `{ error: { message } }`, or the
|
|
278
|
+
* array-shaped envelope Cloudflare's own REST API uses for a BYOK base URL pointed directly at it:
|
|
279
|
+
* `{ success: false, errors: [{ code, message }] }`, with no top-level `error` key at all. That
|
|
280
|
+
* last one is reachable only when `baseUrl` bypasses the Studio proxy, which normalizes the shape
|
|
281
|
+
* before it ever reaches a client.
|
|
241
282
|
*/
|
|
242
283
|
function extractErrorMessage(rawBody: string, fallback: string): string {
|
|
243
284
|
if (!rawBody) {
|
|
244
285
|
return fallback;
|
|
245
286
|
}
|
|
287
|
+
let parsed: unknown;
|
|
246
288
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
289
|
+
parsed = JSON.parse(rawBody);
|
|
290
|
+
} catch {
|
|
291
|
+
/* Not JSON — use the raw body. */
|
|
292
|
+
return rawBody;
|
|
293
|
+
}
|
|
294
|
+
// JSON `null`, a primitive or an array carries neither member, so it is the raw body.
|
|
295
|
+
if (isMemberBag(parsed)) {
|
|
296
|
+
if ("error" in parsed) {
|
|
297
|
+
const { error } = parsed;
|
|
298
|
+
if (isText(error)) {
|
|
299
|
+
return error;
|
|
300
|
+
}
|
|
301
|
+
if (isMemberBag(error) && "message" in error && Boolean(error.message)) {
|
|
302
|
+
return String(error.message);
|
|
255
303
|
}
|
|
256
304
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return message;
|
|
305
|
+
if ("errors" in parsed && isEntryList(parsed.errors)) {
|
|
306
|
+
const [first] = parsed.errors;
|
|
307
|
+
if (isMemberBag(first) && "message" in first && Boolean(first.message)) {
|
|
308
|
+
return String(first.message);
|
|
262
309
|
}
|
|
263
310
|
}
|
|
264
|
-
} catch {
|
|
265
|
-
/* Not JSON — use the raw body. */
|
|
266
311
|
}
|
|
267
312
|
return rawBody;
|
|
268
313
|
}
|
|
269
314
|
|
|
315
|
+
/**
|
|
316
|
+
* The proxy's machine code in an error body (`cf_reconnect_required` and friends), or "" when the
|
|
317
|
+
* body carries none.
|
|
318
|
+
*
|
|
319
|
+
* A backend that knows WHY it refused says so in `code`, beside its human `error`, and a client can
|
|
320
|
+
* only act on the reason it is given: the whole point of the code is that the Reconnect affordance
|
|
321
|
+
* appears without the user reading a sentence and deciding what it meant.
|
|
322
|
+
*/
|
|
323
|
+
function extractMachineCode(rawBody: string): string {
|
|
324
|
+
let parsed: unknown;
|
|
325
|
+
try {
|
|
326
|
+
parsed = JSON.parse(rawBody);
|
|
327
|
+
} catch {
|
|
328
|
+
/* Not JSON — there is no code. */
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
return isMemberBag(parsed) && "code" in parsed && isSentence(parsed.code) ? parsed.code : "";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The event for an exchange that failed: `done: cancelled` for an abort, otherwise an `error` event
|
|
336
|
+
* whose message is `prefix` and the failure's own message.
|
|
337
|
+
*
|
|
338
|
+
* @param {"Network error" | "Stream error"} prefix - Which half of the exchange failed
|
|
339
|
+
* @param {unknown} cause - What the request or the read rejected with
|
|
340
|
+
* @returns {StreamEvent}
|
|
341
|
+
*/
|
|
342
|
+
function failureEvent(prefix: "Network error" | "Stream error", cause: unknown): StreamEvent {
|
|
343
|
+
/* SAFETY: `fetch` and a body reader reject with an Error (a TypeError, or the abort's
|
|
344
|
+
AbortError DOMException), and the reads in a stream throw only TypeErrors. Only `name` and
|
|
345
|
+
`message` are read, so a custom non-null abort reason that is not an Error reads both as
|
|
346
|
+
undefined, as it always has. */
|
|
347
|
+
const failure = cause as Error;
|
|
348
|
+
if (failure.name === "AbortError") {
|
|
349
|
+
return { type: "done", stopReason: "cancelled" };
|
|
350
|
+
}
|
|
351
|
+
return { type: "error", message: `${prefix}: ${failure.message}` };
|
|
352
|
+
}
|
|
353
|
+
|
|
270
354
|
// ─── Type definitions ────────────────────────────────────────────────────────
|
|
271
355
|
|
|
272
356
|
/**
|
|
@@ -369,7 +453,7 @@ export function createOpenAIStreamingClient({
|
|
|
369
453
|
body.temperature = temperature;
|
|
370
454
|
}
|
|
371
455
|
|
|
372
|
-
if (tools
|
|
456
|
+
if (tools.length > 0) {
|
|
373
457
|
body.tools = tools;
|
|
374
458
|
body.tool_choice = "auto";
|
|
375
459
|
body.parallel_tool_calls = true;
|
|
@@ -389,14 +473,7 @@ export function createOpenAIStreamingClient({
|
|
|
389
473
|
signal,
|
|
390
474
|
});
|
|
391
475
|
} catch (error) {
|
|
392
|
-
|
|
393
|
-
yield { type: "done", stopReason: "cancelled" };
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
yield {
|
|
397
|
-
type: "error",
|
|
398
|
-
message: `Network error: ${(error as Error).message}`,
|
|
399
|
-
};
|
|
476
|
+
yield failureEvent("Network error", error);
|
|
400
477
|
return;
|
|
401
478
|
}
|
|
402
479
|
|
|
@@ -458,13 +535,8 @@ export function createOpenAIStreamingClient({
|
|
|
458
535
|
}
|
|
459
536
|
|
|
460
537
|
try {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (done) {
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
buffer += decoder.decode(value, { stream: true });
|
|
538
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
539
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
468
540
|
const lines = buffer.split("\n");
|
|
469
541
|
// Keep the last (potentially incomplete) line in the buffer
|
|
470
542
|
buffer = lines.pop() || "";
|
|
@@ -483,6 +555,10 @@ export function createOpenAIStreamingClient({
|
|
|
483
555
|
|
|
484
556
|
let parsed: OpenAIStreamChunk;
|
|
485
557
|
try {
|
|
558
|
+
/* SAFETY: the interface is the provider's wire shape, and this loop only reads it: each
|
|
559
|
+
member passes optional chaining, a truthiness test or a predicate before it shapes an
|
|
560
|
+
event, and a chunk too malformed to read (JSON `null`, a non-iterable `tool_calls`)
|
|
561
|
+
throws into the catch below, which reports it as a stream error. */
|
|
486
562
|
parsed = JSON.parse(dataStr) as OpenAIStreamChunk;
|
|
487
563
|
} catch {
|
|
488
564
|
continue; // Skip unparseable chunks
|
|
@@ -505,7 +581,7 @@ export function createOpenAIStreamingClient({
|
|
|
505
581
|
|
|
506
582
|
// Chain-of-thought, under either of the two names providers give it.
|
|
507
583
|
const reasoning = delta?.reasoning_content ?? delta?.reasoning;
|
|
508
|
-
if (
|
|
584
|
+
if (isText(reasoning) && reasoning) {
|
|
509
585
|
yield { type: "reasoning", content: reasoning };
|
|
510
586
|
}
|
|
511
587
|
|
|
@@ -553,14 +629,7 @@ export function createOpenAIStreamingClient({
|
|
|
553
629
|
yield* finish();
|
|
554
630
|
} catch (error) {
|
|
555
631
|
void reader.cancel();
|
|
556
|
-
|
|
557
|
-
yield { type: "done", stopReason: "cancelled" };
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
yield {
|
|
561
|
-
type: "error",
|
|
562
|
-
message: `Stream error: ${(error as Error).message}`,
|
|
563
|
-
};
|
|
632
|
+
yield failureEvent("Stream error", error);
|
|
564
633
|
}
|
|
565
634
|
}
|
|
566
635
|
|
|
@@ -570,22 +639,25 @@ export function createOpenAIStreamingClient({
|
|
|
570
639
|
// ─── Anthropic implementation (stub for v1) ──────────────────────────────────
|
|
571
640
|
|
|
572
641
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
* @param {object} _opts
|
|
577
|
-
* @param {string} _opts.baseUrl
|
|
578
|
-
* @param {string} _opts.apiKey
|
|
579
|
-
* @returns {StreamingClient}
|
|
642
|
+
* The credentials a provider endpoint is opened with: its base URL and the API key it takes, two
|
|
643
|
+
* fields that belong together. Studio keys its model listing on the same pair.
|
|
580
644
|
*/
|
|
581
|
-
export interface
|
|
645
|
+
export interface AiCredentials {
|
|
582
646
|
baseUrl: string;
|
|
583
647
|
apiKey: string;
|
|
584
648
|
}
|
|
585
649
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
650
|
+
/** The Anthropic client's options: the provider credentials and nothing more, for now. */
|
|
651
|
+
export type { AiCredentials as AnthropicStreamingClientOptions };
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Creates an Anthropic-compatible streaming client. Stub for v1 — throws on use. Full
|
|
655
|
+
* implementation in v2 handles Anthropic's content_block delta model.
|
|
656
|
+
*
|
|
657
|
+
* @param {AiCredentials} _opts
|
|
658
|
+
* @returns {StreamingClient}
|
|
659
|
+
*/
|
|
660
|
+
export function createAnthropicStreamingClient(_opts: AiCredentials): StreamingClient {
|
|
589
661
|
/**
|
|
590
662
|
* @yields {StreamEvent} Normalized stream events.
|
|
591
663
|
* @returns {AsyncGenerator<StreamEvent>}
|
|
@@ -631,6 +703,27 @@ export interface ProxyStreamingClientOptions {
|
|
|
631
703
|
sessionId?: string | undefined;
|
|
632
704
|
}
|
|
633
705
|
|
|
706
|
+
/**
|
|
707
|
+
* The headers a proxy request carries: always the content type, plus `X-Api-Key`, `X-Api-Base-URL`
|
|
708
|
+
* and the session header `AI_SESSION_HEADER` names, each only when the client was given its value.
|
|
709
|
+
* The optional names go through the index signature, as a `fetch` init's record takes them.
|
|
710
|
+
*/
|
|
711
|
+
interface ProxyRequestHeaders extends Record<string, string> {
|
|
712
|
+
"Content-Type": string;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* True when a proxy's `chatUrl` is a lookup to call rather than the URL itself.
|
|
717
|
+
*
|
|
718
|
+
* @param {ProxyStreamingClientOptions["chatUrl"]} chatUrl
|
|
719
|
+
* @returns {boolean}
|
|
720
|
+
*/
|
|
721
|
+
function isUrlLookup(
|
|
722
|
+
chatUrl: ProxyStreamingClientOptions["chatUrl"],
|
|
723
|
+
): chatUrl is () => string | Promise<string> {
|
|
724
|
+
return typeof chatUrl === "function";
|
|
725
|
+
}
|
|
726
|
+
|
|
634
727
|
export function createProxyStreamingClient({
|
|
635
728
|
chatUrl,
|
|
636
729
|
model = "gpt-4o",
|
|
@@ -655,7 +748,7 @@ export function createProxyStreamingClient({
|
|
|
655
748
|
systemPrompt: string,
|
|
656
749
|
signal: AbortSignal,
|
|
657
750
|
): AsyncGenerator<StreamEvent> {
|
|
658
|
-
const headers:
|
|
751
|
+
const headers: ProxyRequestHeaders = { "Content-Type": "application/json" };
|
|
659
752
|
// Client-supplied key (e.g. from Studio settings) — the proxy also falls back to OPENAI_API_KEY.
|
|
660
753
|
if (apiKey) {
|
|
661
754
|
headers["X-Api-Key"] = apiKey;
|
|
@@ -671,20 +764,20 @@ export function createProxyStreamingClient({
|
|
|
671
764
|
headers[AI_SESSION_HEADER] = sessionId;
|
|
672
765
|
}
|
|
673
766
|
|
|
674
|
-
resolvedUrl ??= Promise.resolve(
|
|
767
|
+
resolvedUrl ??= Promise.resolve(isUrlLookup(chatUrl) ? chatUrl() : chatUrl);
|
|
675
768
|
let url: string;
|
|
676
769
|
try {
|
|
677
770
|
url = await resolvedUrl;
|
|
678
771
|
} catch (error) {
|
|
679
772
|
// A lookup that failed after the turn was stopped is part of the stop, not an error.
|
|
680
|
-
if (signal
|
|
773
|
+
if (signal.aborted) {
|
|
681
774
|
yield { type: "done", stopReason: "cancelled" };
|
|
682
775
|
return;
|
|
683
776
|
}
|
|
684
777
|
throw error;
|
|
685
778
|
}
|
|
686
779
|
// Stopped while the URL was being resolved: nothing has been sent, and nothing will be.
|
|
687
|
-
if (signal
|
|
780
|
+
if (signal.aborted) {
|
|
688
781
|
yield { type: "done", stopReason: "cancelled" };
|
|
689
782
|
return;
|
|
690
783
|
}
|
|
@@ -698,14 +791,7 @@ export function createProxyStreamingClient({
|
|
|
698
791
|
signal,
|
|
699
792
|
});
|
|
700
793
|
} catch (error) {
|
|
701
|
-
|
|
702
|
-
yield { type: "done", stopReason: "cancelled" };
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
yield {
|
|
706
|
-
type: "error",
|
|
707
|
-
message: `Network error: ${(error as Error).message}`,
|
|
708
|
-
};
|
|
794
|
+
yield failureEvent("Network error", error);
|
|
709
795
|
return;
|
|
710
796
|
}
|
|
711
797
|
|
|
@@ -722,15 +808,7 @@ export function createProxyStreamingClient({
|
|
|
722
808
|
unconditionally, so a proxy answering 401 `{ code: "cf_reconnect_required" }` reached the
|
|
723
809
|
client as `"401"` — indistinguishable from a bad BYOK key, and the one reading that would
|
|
724
810
|
have put a Reconnect button on screen was thrown away at the only place it arrived. */
|
|
725
|
-
|
|
726
|
-
try {
|
|
727
|
-
const { code } = JSON.parse(errorBody) as ErrorResponseBody;
|
|
728
|
-
if (typeof code === "string" && code) {
|
|
729
|
-
machineCode = code;
|
|
730
|
-
}
|
|
731
|
-
} catch {
|
|
732
|
-
/* Not JSON — use the raw body. */
|
|
733
|
-
}
|
|
811
|
+
const machineCode = extractMachineCode(errorBody);
|
|
734
812
|
yield {
|
|
735
813
|
type: "error",
|
|
736
814
|
message: cleanMessage,
|
|
@@ -749,13 +827,8 @@ export function createProxyStreamingClient({
|
|
|
749
827
|
let buffer = "";
|
|
750
828
|
|
|
751
829
|
try {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (done) {
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
buffer += decoder.decode(value, { stream: true });
|
|
830
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
831
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
759
832
|
const lines = buffer.split("\n");
|
|
760
833
|
buffer = lines.pop() || "";
|
|
761
834
|
|
|
@@ -768,6 +841,10 @@ export function createProxyStreamingClient({
|
|
|
768
841
|
const dataStr = trimmed.slice(6);
|
|
769
842
|
let event: StreamEvent;
|
|
770
843
|
try {
|
|
844
|
+
/* SAFETY: the proxy is the chat route, whose every `data:` line is one StreamEvent
|
|
845
|
+
(`encodeSse` frames exactly that). The event is relayed as sent and only `type` is
|
|
846
|
+
read; a line of JSON `null` is relayed, then throws at that read into the catch
|
|
847
|
+
below. */
|
|
771
848
|
event = JSON.parse(dataStr) as StreamEvent;
|
|
772
849
|
} catch {
|
|
773
850
|
continue; // Skip unparseable chunks
|
|
@@ -782,14 +859,7 @@ export function createProxyStreamingClient({
|
|
|
782
859
|
}
|
|
783
860
|
} catch (error) {
|
|
784
861
|
void reader.cancel();
|
|
785
|
-
|
|
786
|
-
yield { type: "done", stopReason: "cancelled" };
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
yield {
|
|
790
|
-
type: "error",
|
|
791
|
-
message: `Stream error: ${(error as Error).message}`,
|
|
792
|
-
};
|
|
862
|
+
yield failureEvent("Stream error", error);
|
|
793
863
|
}
|
|
794
864
|
}
|
|
795
865
|
|