@ai-sdk/harness 1.0.149 → 1.0.150

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.149",
3
+ "version": "1.0.150",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@ai-sdk/provider": "4.0.27",
48
48
  "@ai-sdk/provider-utils": "5.0.59",
49
- "ai": "7.0.138"
49
+ "ai": "7.0.139"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@ai-sdk/otel": "1.0.138",
61
+ "@ai-sdk/otel": "1.0.139",
62
62
  "@opentelemetry/sdk-trace-base": "2.7.1",
63
63
  "@types/node": "22.19.19",
64
64
  "@types/ws": "^8.5.13",
@@ -22,6 +22,7 @@ import {
22
22
  createUIMessageStreamResponse,
23
23
  toUIMessageStream as toUIMessageStreamHelper,
24
24
  type CallWarning,
25
+ type Citation,
25
26
  type ContentPart,
26
27
  type FinishReason,
27
28
  type InferUIMessageChunk,
@@ -53,6 +54,19 @@ type EnrichedStreamPart<TOOLS extends ToolSet, PARTIAL_OUTPUT> = {
53
54
  partialOutput: PARTIAL_OUTPUT | undefined;
54
55
  };
55
56
 
57
+ type AccumulatedTextContent = {
58
+ type: 'text';
59
+ text: string;
60
+ citations?: Citation[];
61
+ providerMetadata?: ProviderMetadata;
62
+ };
63
+
64
+ type AccumulatedReasoningContent = {
65
+ type: 'reasoning';
66
+ text: string;
67
+ providerMetadata?: ProviderMetadata;
68
+ };
69
+
56
70
  /**
57
71
  * Concrete `StreamTextResult` implementation backed by a single
58
72
  * harness prompt turn.
@@ -149,6 +163,11 @@ export class HarnessStreamTextResult<
149
163
 
150
164
  private readonly stepsBuffer: StepResult<TOOLS, RUNTIME_CONTEXT>[] = [];
151
165
  private currentStepContent: ContentPart<TOOLS>[] = [];
166
+ private currentStepTextContent = new Map<string, AccumulatedTextContent>();
167
+ private currentStepReasoningContent = new Map<
168
+ string,
169
+ AccumulatedReasoningContent
170
+ >();
152
171
  private currentStepWarnings: CallWarning[] = [];
153
172
  private stepNumber = 0;
154
173
  private stepStarted = false;
@@ -261,6 +280,8 @@ export class HarnessStreamTextResult<
261
280
  */
262
281
  discardCurrentStepContent(): void {
263
282
  this.currentStepContent = [];
283
+ this.currentStepTextContent.clear();
284
+ this.currentStepReasoningContent.clear();
264
285
  this.currentStepWarnings = [];
265
286
  this.stepStarted = false;
266
287
  }
@@ -334,6 +355,8 @@ export class HarnessStreamTextResult<
334
355
 
335
356
  this.stepNumber += 1;
336
357
  this.currentStepContent = [];
358
+ this.currentStepTextContent.clear();
359
+ this.currentStepReasoningContent.clear();
337
360
  this.currentStepWarnings = [];
338
361
  this.stepStarted = false;
339
362
 
@@ -421,14 +444,12 @@ export class HarnessStreamTextResult<
421
444
  >,
422
445
  );
423
446
  this._text.resolve(finalStep.text);
424
- // Reasoning content parts are not yet derived from harness events; the
425
- // foundation surfaces an empty array. Adapters that emit reasoning
426
- // deltas can be wired up to produce real reasoning content in a later
427
- // pass.
428
447
  this._reasoning.resolve(
429
- [] as StreamProp<TOOLS, RUNTIME_CONTEXT, OUTPUT, 'reasoning'>,
448
+ finalStep.content.filter(
449
+ part => part.type === 'reasoning' || part.type === 'reasoning-file',
450
+ ) as StreamProp<TOOLS, RUNTIME_CONTEXT, OUTPUT, 'reasoning'>,
430
451
  );
431
- this._reasoningText.resolve(undefined);
452
+ this._reasoningText.resolve(finalStep.reasoningText);
432
453
  this._files.resolve(this.stepsBuffer.flatMap(s => s.files));
433
454
  this._sources.resolve(this.stepsBuffer.flatMap(s => s.sources));
434
455
  this._toolCalls.resolve(
@@ -491,10 +512,16 @@ export class HarnessStreamTextResult<
491
512
  this._response.resolve(finalStep.response);
492
513
  this._providerMetadata.resolve(this.finalProviderMetadata);
493
514
 
494
- const responseMessages = await toResponseMessages<TOOLS>({
495
- content: aggregatedContent,
496
- tools: this.tools,
497
- });
515
+ const responseMessages: Array<AssistantModelMessage | ToolModelMessage> =
516
+ [];
517
+ for (const step of this.stepsBuffer) {
518
+ responseMessages.push(
519
+ ...(await toResponseMessages<TOOLS>({
520
+ content: step.content,
521
+ tools: this.tools,
522
+ })),
523
+ );
524
+ }
498
525
  this._responseMessages.resolve(responseMessages);
499
526
 
500
527
  // Forward AI SDK finish event before closing.
@@ -755,6 +782,8 @@ export class HarnessStreamTextResult<
755
782
  toUIMessageStream<UI_MESSAGE extends UIMessage>({
756
783
  originalMessages,
757
784
  generateMessageId,
785
+ onStepEnd,
786
+ onStepFinish,
758
787
  onEnd,
759
788
  onFinish,
760
789
  messageMetadata,
@@ -772,6 +801,8 @@ export class HarnessStreamTextResult<
772
801
  tools: this.tools,
773
802
  originalMessages,
774
803
  generateMessageId,
804
+ onStepEnd,
805
+ onStepFinish,
775
806
  onEnd,
776
807
  onFinish,
777
808
  messageMetadata,
@@ -795,6 +826,8 @@ export class HarnessStreamTextResult<
795
826
  toUIMessageStreamResponse<UI_MESSAGE extends UIMessage>({
796
827
  originalMessages,
797
828
  generateMessageId,
829
+ onStepEnd,
830
+ onStepFinish,
798
831
  onEnd,
799
832
  onFinish,
800
833
  messageMetadata,
@@ -813,6 +846,8 @@ export class HarnessStreamTextResult<
813
846
  stream: this.toUIMessageStream<UI_MESSAGE>({
814
847
  originalMessages,
815
848
  generateMessageId,
849
+ onStepEnd,
850
+ onStepFinish,
816
851
  onEnd,
817
852
  onFinish,
818
853
  messageMetadata,
@@ -834,30 +869,88 @@ export class HarnessStreamTextResult<
834
869
 
835
870
  private appendToCurrentStepContent(part: TextStreamPart<TOOLS>): void {
836
871
  switch (part.type) {
872
+ case 'text-start': {
873
+ const content: AccumulatedTextContent = {
874
+ type: 'text',
875
+ text: '',
876
+ ...(part.providerMetadata != null
877
+ ? { providerMetadata: part.providerMetadata }
878
+ : {}),
879
+ };
880
+ this.currentStepTextContent.set(part.id, content);
881
+ this.currentStepContent.push(content);
882
+ return;
883
+ }
837
884
  case 'text-delta': {
838
- // Coalesce contiguous text-deltas with the same id into one text part.
839
- const last =
840
- this.currentStepContent[this.currentStepContent.length - 1];
841
- if (last && last.type === 'text') {
842
- (last as { text: string }).text += part.text;
843
- } else {
844
- this.currentStepContent.push({
885
+ let content = this.currentStepTextContent.get(part.id);
886
+ if (content == null) {
887
+ content = {
845
888
  type: 'text',
846
- text: part.text,
847
- } as ContentPart<TOOLS>);
889
+ text: '',
890
+ ...(part.providerMetadata != null
891
+ ? { providerMetadata: part.providerMetadata }
892
+ : {}),
893
+ };
894
+ this.currentStepTextContent.set(part.id, content);
895
+ this.currentStepContent.push(content);
848
896
  }
897
+ content.text += part.text;
898
+ if (part.providerMetadata != null) {
899
+ content.providerMetadata = part.providerMetadata;
900
+ }
901
+ return;
902
+ }
903
+ case 'text-end': {
904
+ const content = this.currentStepTextContent.get(part.id);
905
+ if (content != null) {
906
+ if (part.providerMetadata != null) {
907
+ content.providerMetadata = part.providerMetadata;
908
+ }
909
+ if (part.citations != null) {
910
+ content.citations = part.citations;
911
+ }
912
+ this.currentStepTextContent.delete(part.id);
913
+ }
914
+ return;
915
+ }
916
+ case 'reasoning-start': {
917
+ const content: AccumulatedReasoningContent = {
918
+ type: 'reasoning',
919
+ text: '',
920
+ ...(part.providerMetadata != null
921
+ ? { providerMetadata: part.providerMetadata }
922
+ : {}),
923
+ };
924
+ this.currentStepReasoningContent.set(part.id, content);
925
+ this.currentStepContent.push(content);
849
926
  return;
850
927
  }
851
928
  case 'reasoning-delta': {
852
- const last =
853
- this.currentStepContent[this.currentStepContent.length - 1];
854
- if (last && last.type === 'reasoning') {
855
- (last as { text: string }).text += part.text;
856
- } else {
857
- this.currentStepContent.push({
929
+ let content = this.currentStepReasoningContent.get(part.id);
930
+ if (content == null) {
931
+ content = {
858
932
  type: 'reasoning',
859
- text: part.text,
860
- } as ContentPart<TOOLS>);
933
+ text: '',
934
+ ...(part.providerMetadata != null
935
+ ? { providerMetadata: part.providerMetadata }
936
+ : {}),
937
+ };
938
+ this.currentStepReasoningContent.set(part.id, content);
939
+ this.currentStepContent.push(content);
940
+ }
941
+ content.text += part.text;
942
+ if (part.providerMetadata != null) {
943
+ content.providerMetadata = part.providerMetadata;
944
+ }
945
+ return;
946
+ }
947
+ case 'reasoning-end': {
948
+ const content = this.currentStepReasoningContent.get(part.id);
949
+ if (content != null) {
950
+ if (part.providerMetadata != null) {
951
+ content.providerMetadata = part.providerMetadata;
952
+ }
953
+ this.currentStepReasoningContent.delete(part.id);
861
954
  }
862
955
  return;
863
956
  }
@@ -303,12 +303,15 @@ export function runPrompt<
303
303
  >();
304
304
  /*
305
305
  * Host results are echoed back as `tool-result` events too, so the event
306
- * alone cannot say who ran the tool — classify by the originating
307
- * `tool-call`. Only an explicit `true` counts: `LanguageModelV4ToolCall`
308
- * defines an omitted flag as client-executed. A call missing from the map
309
- * arrived in an earlier slice, where nothing here can classify it.
306
+ * alone cannot provide the parsed input or say who ran the tool — restore
307
+ * both from the originating `tool-call`. Only an explicit `true` counts:
308
+ * `LanguageModelV4ToolCall` defines an omitted flag as client-executed. A
309
+ * call missing from the map arrived in an earlier slice, where nothing
310
+ * here can classify it.
310
311
  */
311
312
  const translateOptions = {
313
+ getToolCall: (toolCallId: string) =>
314
+ toolCallsByToolCallId.get(toolCallId),
312
315
  isProviderExecuted: (toolCallId: string): boolean => {
313
316
  const rawToolCall = rawToolCallsByToolCallId.get(toolCallId);
314
317
  return rawToolCall == null || rawToolCall.providerExecuted === true;
@@ -29,6 +29,17 @@ import { generateId, type ToolSet } from '@ai-sdk/provider-utils';
29
29
  export function translateStreamPart<TOOLS extends ToolSet>(
30
30
  event: HarnessV1StreamPart,
31
31
  options: {
32
+ /**
33
+ * Returns the parsed tool call that produced a result. Harness tool-result
34
+ * events only carry the output, so their input and execution origin must
35
+ * be restored from the originating call.
36
+ */
37
+ getToolCall?: (toolCallId: string) =>
38
+ | {
39
+ input: unknown;
40
+ providerExecuted?: boolean;
41
+ }
42
+ | undefined;
32
43
  /**
33
44
  * Whether the tool call that produced this event ran inside the harness
34
45
  * runtime. Host tools travel the same `tool-result` events and their
@@ -119,9 +130,12 @@ export function translateStreamPart<TOOLS extends ToolSet>(
119
130
  return [];
120
131
 
121
132
  case 'tool-result':
133
+ const toolCall = options.getToolCall?.(event.toolCallId);
122
134
  if (
123
135
  event.isError === true &&
124
- (options.isProviderExecuted?.(event.toolCallId) ?? true)
136
+ (toolCall?.providerExecuted ??
137
+ options.isProviderExecuted?.(event.toolCallId) ??
138
+ true)
125
139
  ) {
126
140
  /*
127
141
  * `providerExecuted` decides whether the message survives:
@@ -134,7 +148,7 @@ export function translateStreamPart<TOOLS extends ToolSet>(
134
148
  type: 'tool-error',
135
149
  toolCallId: event.toolCallId,
136
150
  toolName: event.toolName,
137
- input: undefined,
151
+ input: toolCall?.input,
138
152
  error: event.result,
139
153
  providerExecuted: true,
140
154
  ...(event.dynamic !== undefined ? { dynamic: event.dynamic } : {}),
@@ -150,8 +164,11 @@ export function translateStreamPart<TOOLS extends ToolSet>(
150
164
  type: 'tool-result',
151
165
  toolCallId: event.toolCallId,
152
166
  toolName: event.toolName,
153
- input: undefined,
167
+ input: toolCall?.input,
154
168
  output: event.result,
169
+ ...(toolCall?.providerExecuted !== undefined
170
+ ? { providerExecuted: toolCall.providerExecuted }
171
+ : {}),
155
172
  ...(event.preliminary !== undefined
156
173
  ? { preliminary: event.preliminary }
157
174
  : {}),