ai 7.0.136 → 7.0.138

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 (37) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/index.d.ts +31 -6
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +116 -77
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +73 -4
  7. package/dist/internal/index.d.ts.map +1 -1
  8. package/dist/internal/index.js +44 -17
  9. package/dist/internal/index.js.map +1 -1
  10. package/docs/03-ai-sdk-core/05-generating-text.mdx +70 -2
  11. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +14 -1
  12. package/docs/04-ai-sdk-ui/03-chatbot-resume-streams.mdx +23 -1
  13. package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +6 -0
  14. package/docs/04-ai-sdk-ui/21-error-handling.mdx +34 -0
  15. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +4 -2
  16. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +11 -1
  17. package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +8 -1
  18. package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +2 -2
  19. package/package.json +12 -12
  20. package/src/decide/validate-decision.ts +2 -1
  21. package/src/generate-text/content-part.ts +7 -1
  22. package/src/generate-text/is-output-chunk.ts +59 -0
  23. package/src/generate-text/stream-text-result.ts +2 -0
  24. package/src/generate-text/stream-text.ts +7 -57
  25. package/src/global.ts +23 -4
  26. package/src/middleware/simulate-streaming-middleware.ts +8 -2
  27. package/src/prompt/convert-to-language-model-prompt.ts +0 -42
  28. package/src/types/citation.ts +31 -0
  29. package/src/types/index.ts +1 -0
  30. package/src/ui/chat.ts +63 -13
  31. package/src/ui/default-chat-transport.ts +14 -1
  32. package/src/ui/process-ui-message-stream.ts +23 -11
  33. package/src/ui/ui-messages.ts +4 -0
  34. package/src/ui/validate-ui-messages.ts +2 -0
  35. package/src/ui-message-stream/read-ui-message-stream.ts +1 -0
  36. package/src/ui-message-stream/to-ui-message-chunk.ts +1 -0
  37. package/src/ui-message-stream/ui-message-chunks.ts +3 -0
@@ -54,38 +54,6 @@ export async function convertToLanguageModelPrompt({
54
54
  supportedUrls,
55
55
  );
56
56
 
57
- const approvalIdToToolCallId = new Map<string, string>();
58
- for (const message of prompt.messages) {
59
- if (message.role === 'assistant' && Array.isArray(message.content)) {
60
- for (const part of message.content) {
61
- if (
62
- part.type === 'tool-approval-request' &&
63
- 'approvalId' in part &&
64
- 'toolCallId' in part
65
- ) {
66
- approvalIdToToolCallId.set(
67
- part.approvalId as string,
68
- part.toolCallId as string,
69
- );
70
- }
71
- }
72
- }
73
- }
74
-
75
- const approvedToolCallIds = new Set<string>();
76
- for (const message of prompt.messages) {
77
- if (message.role === 'tool') {
78
- for (const part of message.content) {
79
- if (part.type === 'tool-approval-response') {
80
- const toolCallId = approvalIdToToolCallId.get(part.approvalId);
81
- if (toolCallId) {
82
- approvedToolCallIds.add(toolCallId);
83
- }
84
- }
85
- }
86
- }
87
- }
88
-
89
57
  const messages = [
90
58
  ...(prompt.instructions != null
91
59
  ? typeof prompt.instructions === 'string'
@@ -157,11 +125,6 @@ export async function convertToLanguageModelPrompt({
157
125
  }
158
126
  case 'user':
159
127
  case 'system':
160
- // remove approved tool calls from the set before checking:
161
- for (const id of approvedToolCallIds) {
162
- toolCallIds.delete(id);
163
- }
164
-
165
128
  if (toolCallIds.size > 0) {
166
129
  throw new MissingToolResultsError({
167
130
  toolCallIds: Array.from(toolCallIds),
@@ -171,11 +134,6 @@ export async function convertToLanguageModelPrompt({
171
134
  }
172
135
  }
173
136
 
174
- // remove approved tool calls from the set before checking:
175
- for (const id of approvedToolCallIds) {
176
- toolCallIds.delete(id);
177
- }
178
-
179
137
  if (toolCallIds.size > 0) {
180
138
  throw new MissingToolResultsError({ toolCallIds: Array.from(toolCallIds) });
181
139
  }
@@ -0,0 +1,31 @@
1
+ import type { LanguageModelV4Citation } from '@ai-sdk/provider';
2
+ import { z, type ZodType } from '../util/zod';
3
+ import { providerMetadataSchema } from './provider-metadata';
4
+
5
+ /** A reference supporting a generated text part. */
6
+ export type Citation = LanguageModelV4Citation;
7
+
8
+ export const citationSchema: ZodType<Citation> = z.object({
9
+ source: z.discriminatedUnion('sourceType', [
10
+ z.object({
11
+ type: z.literal('source'),
12
+ sourceType: z.literal('url'),
13
+ id: z.string(),
14
+ url: z.string(),
15
+ title: z.string().optional(),
16
+ providerMetadata: providerMetadataSchema.optional(),
17
+ }),
18
+ z.object({
19
+ type: z.literal('source'),
20
+ sourceType: z.literal('document'),
21
+ id: z.string(),
22
+ mediaType: z.string(),
23
+ title: z.string(),
24
+ filename: z.string().optional(),
25
+ providerMetadata: providerMetadataSchema.optional(),
26
+ }),
27
+ ]),
28
+ startIndex: z.number().int().nonnegative().optional(),
29
+ endIndex: z.number().int().nonnegative().optional(),
30
+ citedText: z.string().optional(),
31
+ });
@@ -1,4 +1,5 @@
1
1
  export type { JSONSchema7 } from '@ai-sdk/provider';
2
+ export type { Citation } from './citation';
2
3
  export type { Embedding, EmbeddingModel } from './embedding-model';
3
4
  export type { EmbeddingModelMiddleware } from './embedding-model-middleware';
4
5
  export type { ImageModel, ImageModelProviderMetadata } from './image-model';
package/src/ui/chat.ts CHANGED
@@ -541,6 +541,33 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
541
541
  await this.makeRequest({ trigger: 'resume-stream', ...options });
542
542
  };
543
543
 
544
+ /**
545
+ * Resume the stream if the latest response ended with a resumable network
546
+ * disconnect. If a response is still active, wait for it to settle first so
547
+ * disconnects reported shortly after the caller's trigger are handled.
548
+ *
549
+ * @internal
550
+ */
551
+ '~resumeStreamIfDisconnected' = async ({
552
+ shouldResume = () => true,
553
+ }: {
554
+ shouldResume?: () => boolean;
555
+ } = {}): Promise<void> => {
556
+ const activeResponse = this.activeResponse;
557
+
558
+ if (this.resumableStreamState == null && activeResponse != null) {
559
+ await activeResponse.completionPromise;
560
+
561
+ if (this.resumableStreamState !== activeResponse.state) {
562
+ return;
563
+ }
564
+ }
565
+
566
+ if (this.resumableStreamState != null && shouldResume()) {
567
+ await this.resumeStream();
568
+ }
569
+ };
570
+
544
571
  /**
545
572
  * Clear the error state and set the status to ready if the chat is in an error state.
546
573
  */
@@ -635,7 +662,6 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
635
662
  }) =>
636
663
  this.jobExecutor.run(async () => {
637
664
  const messages = this.state.messages;
638
- const lastMessage = messages[messages.length - 1];
639
665
 
640
666
  const updatePart = (
641
667
  part: UIMessagePart<UIDataTypes, UITools>,
@@ -675,11 +701,21 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
675
701
  };
676
702
  };
677
703
 
678
- // update the message to trigger an immediate UI update
679
- this.state.replaceMessage(messages.length - 1, {
680
- ...lastMessage,
681
- parts: lastMessage.parts.map(updatePart),
682
- });
704
+ const messageIndex = messages.findIndex(message =>
705
+ message.parts.some(
706
+ part => isToolUIPart(part) && part.toolCallId === toolCallId,
707
+ ),
708
+ );
709
+
710
+ if (messageIndex !== -1) {
711
+ const message = messages[messageIndex];
712
+
713
+ // update the message to trigger an immediate UI update
714
+ this.state.replaceMessage(messageIndex, {
715
+ ...message,
716
+ parts: message.parts.map(updatePart),
717
+ });
718
+ }
683
719
 
684
720
  // update the active response if it exists
685
721
  if (this.activeResponse) {
@@ -689,6 +725,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
689
725
 
690
726
  // automatically send the message if the sendAutomaticallyWhen function returns true
691
727
  if (
728
+ messageIndex !== -1 &&
692
729
  this.status !== 'streaming' &&
693
730
  this.status !== 'submitted' &&
694
731
  this.sendAutomaticallyWhen
@@ -701,7 +738,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
701
738
  () =>
702
739
  this.makeRequest({
703
740
  trigger: 'submit-message',
704
- messageId: this.lastMessage?.id,
741
+ messageId: messages[messageIndex].id,
705
742
  ...options,
706
743
  }),
707
744
  shouldSend,
@@ -938,6 +975,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
938
975
  responseMessageIndex === -1
939
976
  ? lastMessage
940
977
  : this.state.messages[responseMessageIndex];
978
+ const originalResponseMessageId = responseMessage?.id;
941
979
  // The continued stream can start with input deltas or a tool result.
942
980
  // Keep unfinished tool parts so result chunks can find their tool call.
943
981
  const resumableResponseMessage =
@@ -1028,16 +1066,28 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
1028
1066
  this.setStatus({ status: 'streaming' });
1029
1067
  }
1030
1068
 
1031
- if (usesEarlierAssistantMessage) {
1069
+ const existingMessageIndex = this.state.messages.findLastIndex(
1070
+ message => message.id === response.state.message.id,
1071
+ );
1072
+
1073
+ if (existingMessageIndex !== -1) {
1032
1074
  this.state.replaceMessage(
1033
- responseMessageIndex,
1075
+ existingMessageIndex,
1034
1076
  response.state.message,
1035
1077
  );
1036
- } else if (response.state.message.id === this.lastMessage?.id) {
1037
- this.state.replaceMessage(
1038
- this.state.messages.length - 1,
1039
- response.state.message,
1078
+ } else if (usesEarlierAssistantMessage) {
1079
+ const originalMessageIndex = this.state.messages.findLastIndex(
1080
+ message => message.id === originalResponseMessageId,
1040
1081
  );
1082
+
1083
+ if (originalMessageIndex !== -1) {
1084
+ this.state.replaceMessage(
1085
+ originalMessageIndex,
1086
+ response.state.message,
1087
+ );
1088
+ } else {
1089
+ this.state.pushMessage(response.state.message);
1090
+ }
1041
1091
  } else {
1042
1092
  this.state.pushMessage(response.state.message);
1043
1093
  }
@@ -12,8 +12,21 @@ import type { UIMessage } from './ui-messages';
12
12
  export class DefaultChatTransport<
13
13
  UI_MESSAGE extends UIMessage,
14
14
  > extends HttpChatTransport<UI_MESSAGE> {
15
- constructor(options: HttpChatTransportInitOptions<UI_MESSAGE> = {}) {
15
+ readonly resumeStreamIsReplay: boolean;
16
+
17
+ constructor({
18
+ resumeStreamIsReplay = true,
19
+ ...options
20
+ }: HttpChatTransportInitOptions<UI_MESSAGE> & {
21
+ /**
22
+ * Whether resumed streams replay the response from the beginning.
23
+ * Defaults to true. Set to false for endpoints that only continue the
24
+ * interrupted response, even when they emit a start chunk.
25
+ */
26
+ resumeStreamIsReplay?: boolean;
27
+ } = {}) {
16
28
  super(options);
29
+ this.resumeStreamIsReplay = resumeStreamIsReplay;
17
30
  }
18
31
 
19
32
  protected processResponseStream(
@@ -143,6 +143,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
143
143
  // A transport can replay a response from the beginning using the same ID.
144
144
  resetStateOnFirstMessageStart?: boolean;
145
145
  }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> {
146
+ let isFirstMessageStart = true;
147
+
146
148
  return stream.pipeThrough(
147
149
  new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
148
150
  async transform(chunk, controller) {
@@ -508,6 +510,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
508
510
  });
509
511
  }
510
512
  textPart.state = 'done';
513
+ if (chunk.citations != null) {
514
+ textPart.citations = chunk.citations;
515
+ }
511
516
  textPart.providerMetadata =
512
517
  chunk.providerMetadata ?? textPart.providerMetadata;
513
518
  delete state.activeTextParts[chunk.id];
@@ -959,20 +964,23 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
959
964
  }
960
965
 
961
966
  case 'start': {
962
- if (
963
- chunk.messageId != null &&
964
- (resetStateOnFirstMessageStart ||
965
- (resetStateOnMessageIdChange &&
966
- chunk.messageId !== state.message.id))
967
- ) {
968
- // Start the separate response with empty parts and metadata.
969
- // Reset the active-part maps too, so earlier chunks stay with
970
- // the previous response. Keep the state object for its callers.
967
+ const shouldReset =
968
+ (resetStateOnFirstMessageStart && isFirstMessageStart) ||
969
+ (resetStateOnMessageIdChange &&
970
+ chunk.messageId != null &&
971
+ chunk.messageId !== state.message.id);
972
+ isFirstMessageStart = false;
973
+
974
+ if (shouldReset) {
975
+ // Rebuild a replay or separate response from empty state. Only
976
+ // reset a replay once: merged streams can emit more starts.
977
+ // Keep the state object for its callers and preserve the ID
978
+ // when the replay does not include one.
971
979
  Object.assign(
972
980
  state,
973
981
  createStreamingUIMessageState({
974
982
  lastMessage: undefined,
975
- messageId: chunk.messageId,
983
+ messageId: chunk.messageId ?? state.message.id,
976
984
  }),
977
985
  { finishReason: undefined },
978
986
  );
@@ -984,7 +992,11 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
984
992
 
985
993
  await updateMessageMetadata(chunk.messageMetadata);
986
994
 
987
- if (chunk.messageId != null || chunk.messageMetadata != null) {
995
+ if (
996
+ shouldReset ||
997
+ chunk.messageId != null ||
998
+ chunk.messageMetadata != null
999
+ ) {
988
1000
  write({ updateStatus: false });
989
1001
  }
990
1002
  break;
@@ -1,3 +1,4 @@
1
+ import type { Citation } from '../types/citation';
1
2
  import type { JSONObject } from '@ai-sdk/provider';
2
3
  import type {
3
4
  InferToolInput,
@@ -96,6 +97,9 @@ export type UIMessagePart<
96
97
  export type TextUIPart = {
97
98
  type: 'text';
98
99
 
100
+ /** References supporting this text, separate from retrieved sources. */
101
+ citations?: Array<Citation>;
102
+
99
103
  /**
100
104
  * The text content.
101
105
  */
@@ -1,3 +1,4 @@
1
+ import { citationSchema } from '../types/citation';
1
2
  import { TypeValidationError, type JSONObject } from '@ai-sdk/provider';
2
3
  import {
3
4
  lazySchema,
@@ -95,6 +96,7 @@ const uiMessagesSchema = lazySchema(() => {
95
96
  z.object({
96
97
  type: z.literal('text'),
97
98
  text: z.string(),
99
+ citations: z.array(citationSchema).optional(),
98
100
  state: z.enum(['streaming', 'done']).optional(),
99
101
  providerMetadata: providerMetadataSchema.optional(),
100
102
  }),
@@ -95,6 +95,7 @@ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({
95
95
  consumeStream({
96
96
  stream: processUIMessageStream({
97
97
  stream,
98
+ resetStateOnMessageIdChange: true,
98
99
  runUpdateMessageJob(
99
100
  job: (options: {
100
101
  state: StreamingUIMessageState<UI_MESSAGE>;
@@ -89,6 +89,7 @@ export function toUIMessageChunk<
89
89
  return {
90
90
  type: 'text-end',
91
91
  id: part.id,
92
+ ...(part.citations != null ? { citations: part.citations } : {}),
92
93
  ...(part.providerMetadata != null
93
94
  ? { providerMetadata: part.providerMetadata }
94
95
  : {}),
@@ -1,3 +1,4 @@
1
+ import { citationSchema, type Citation } from '../types/citation';
1
2
  import type { JSONObject } from '@ai-sdk/provider';
2
3
  import {
3
4
  providerMetadataSchema,
@@ -37,6 +38,7 @@ export const uiMessageChunkSchema = lazySchema(() =>
37
38
  z.looseObject({
38
39
  type: z.literal('text-end'),
39
40
  id: z.string(),
41
+ citations: z.array(citationSchema).optional(),
40
42
  providerMetadata: providerMetadataSchema.optional(),
41
43
  }),
42
44
  z.looseObject({
@@ -246,6 +248,7 @@ export type UIMessageChunk<
246
248
  }
247
249
  | {
248
250
  type: 'text-end';
251
+ citations?: Array<Citation>;
249
252
  id: string;
250
253
  providerMetadata?: ProviderMetadata;
251
254
  }