@ai-sdk/harness 1.0.37 → 1.0.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@ai-sdk/provider": "4.0.3",
48
- "@ai-sdk/provider-utils": "5.0.11",
49
- "ai": "7.0.32"
48
+ "@ai-sdk/provider-utils": "5.0.12",
49
+ "ai": "7.0.34"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -460,6 +460,25 @@ export class HarnessStreamTextResult<
460
460
  this.fullStreamController.close();
461
461
  }
462
462
 
463
+ /**
464
+ * Settle the turn as user-aborted: emit a final `abort` part — matching
465
+ * `streamText`'s abort contract — and close the stream, instead of
466
+ * surfacing an `error` part. `toUIMessageStream` consumers then observe
467
+ * an `abort` chunk (and `isAborted: true`) rather than a spurious
468
+ * `onError`. The delayed promise accessors still reject with the
469
+ * underlying abort error so awaiting consumers do not hang. Idempotent.
470
+ */
471
+ abort(input: { error: unknown; reason?: string }): void {
472
+ if (this.settled) return;
473
+ this.settled = true;
474
+ this.fullStreamController.enqueue({
475
+ type: 'abort',
476
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
477
+ } as TextStreamPart<TOOLS>);
478
+ this.fullStreamController.close();
479
+ this.rejectDelayedPromises(input.error);
480
+ }
481
+
463
482
  /**
464
483
  * Surface a fatal error as a stream `error` part + reject every delayed
465
484
  * promise so awaiting consumers stop hanging. Idempotent.
@@ -472,6 +491,10 @@ export class HarnessStreamTextResult<
472
491
  error,
473
492
  } as TextStreamPart<TOOLS>);
474
493
  this.fullStreamController.close();
494
+ this.rejectDelayedPromises(error);
495
+ }
496
+
497
+ private rejectDelayedPromises(error: unknown): void {
475
498
  for (const dp of [
476
499
  this._content,
477
500
  this._text,
@@ -605,6 +628,7 @@ export class HarnessStreamTextResult<
605
628
  toUIMessageStream<UI_MESSAGE extends UIMessage>({
606
629
  originalMessages,
607
630
  generateMessageId,
631
+ onEnd,
608
632
  onFinish,
609
633
  messageMetadata,
610
634
  sendReasoning,
@@ -621,6 +645,7 @@ export class HarnessStreamTextResult<
621
645
  tools: this.tools,
622
646
  originalMessages,
623
647
  generateMessageId,
648
+ onEnd,
624
649
  onFinish,
625
650
  messageMetadata,
626
651
  sendReasoning,
@@ -643,6 +668,7 @@ export class HarnessStreamTextResult<
643
668
  toUIMessageStreamResponse<UI_MESSAGE extends UIMessage>({
644
669
  originalMessages,
645
670
  generateMessageId,
671
+ onEnd,
646
672
  onFinish,
647
673
  messageMetadata,
648
674
  sendReasoning,
@@ -660,6 +686,7 @@ export class HarnessStreamTextResult<
660
686
  stream: this.toUIMessageStream<UI_MESSAGE>({
661
687
  originalMessages,
662
688
  generateMessageId,
689
+ onEnd,
663
690
  onFinish,
664
691
  messageMetadata,
665
692
  sendReasoning,
@@ -21,10 +21,11 @@ import {
21
21
  type Experimental_SandboxSession as SandboxSession,
22
22
  type ToolSet,
23
23
  } from '@ai-sdk/provider-utils';
24
- import type {
25
- LanguageModelV4FinishReason,
26
- LanguageModelV4ToolCall,
27
- LanguageModelV4Usage,
24
+ import {
25
+ getErrorMessage,
26
+ type LanguageModelV4FinishReason,
27
+ type LanguageModelV4ToolCall,
28
+ type LanguageModelV4Usage,
28
29
  } from '@ai-sdk/provider';
29
30
  import { parseToolCall } from 'ai/internal';
30
31
  import type {
@@ -124,6 +125,29 @@ export function runPrompt<
124
125
  runtimeContext: input.runtimeContext,
125
126
  });
126
127
 
128
+ /*
129
+ * Settle a failed turn. When the caller's own `abortSignal` has fired, the
130
+ * failure is a user-initiated stop, not an error: surface it as an `abort`
131
+ * stream part — matching `streamText`'s abort contract — so
132
+ * `toUIMessageStream` consumers observe an `abort` chunk and
133
+ * `isAborted: true` instead of a spurious `onError`. Every other failure
134
+ * stays a real `error` part. Both outcomes notify `onTurnFailed` so the
135
+ * session's turn tracking returns to idle and the session stays usable.
136
+ */
137
+ const settleFailure = (err: unknown) => {
138
+ input.onTurnFailed?.();
139
+ if (input.abortSignal?.aborted) {
140
+ result.abort({
141
+ error: err,
142
+ ...(input.abortSignal.reason !== undefined
143
+ ? { reason: getErrorMessage(input.abortSignal.reason) }
144
+ : {}),
145
+ });
146
+ return;
147
+ }
148
+ result.fail(err);
149
+ };
150
+
127
151
  const done = (async () => {
128
152
  let bridge: Awaited<ReturnType<typeof toHarnessStream>>;
129
153
  try {
@@ -159,8 +183,7 @@ export function runPrompt<
159
183
  context: 'failed to start harness turn',
160
184
  error: err,
161
185
  });
162
- input.onTurnFailed?.();
163
- result.fail(err);
186
+ settleFailure(err);
164
187
  return;
165
188
  }
166
189
 
@@ -531,6 +554,28 @@ export function runPrompt<
531
554
  }
532
555
  }
533
556
 
557
+ /*
558
+ * Settle failures before the translate-and-forward step below: the
559
+ * translated `error` part must not reach the consumer stream, or the
560
+ * turn surfaces BOTH a forwarded `error` part and the settle-owned
561
+ * terminal part (an `abort` part for user stops via `settleFailure`,
562
+ * or a second `error` part from `fail`).
563
+ */
564
+ if (value.type === 'error' && displayValue.type === 'error') {
565
+ // Telemetry and stderr diagnostics keep the raw error (absolute
566
+ // paths help debugging); the consumer-facing settle uses the
567
+ // workDir-stripped one, like every other forwarded part.
568
+ telemetry.error(value.error);
569
+ logBridgeError({
570
+ harnessId: input.harness.harnessId,
571
+ sessionId: input.session.sessionId,
572
+ context: 'harness stream error',
573
+ error: value.error,
574
+ });
575
+ settleFailure(displayValue.error);
576
+ return;
577
+ }
578
+
534
579
  // Forward to consumer as soon as possible.
535
580
  for (const part of translateStreamPart<TOOLS>(displayValue)) {
536
581
  result.enqueue(part);
@@ -799,19 +844,6 @@ export function runPrompt<
799
844
  }
800
845
  telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
801
846
  }
802
-
803
- if (value.type === 'error') {
804
- telemetry.error(value.error);
805
- logBridgeError({
806
- harnessId: input.harness.harnessId,
807
- sessionId: input.session.sessionId,
808
- context: 'harness stream error',
809
- error: value.error,
810
- });
811
- input.onTurnFailed?.();
812
- result.fail(value.error);
813
- return;
814
- }
815
847
  }
816
848
  if (finalFinish != null) {
817
849
  input.onTurnFinished?.();
@@ -835,8 +867,7 @@ export function runPrompt<
835
867
  context: 'harness turn failed',
836
868
  error: err,
837
869
  });
838
- input.onTurnFailed?.();
839
- result.fail(err);
870
+ settleFailure(err);
840
871
  } finally {
841
872
  reader.releaseLock();
842
873
  }