@convex-dev/agent 0.1.4-alpha.0 → 0.1.5-alpha.0

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 (165) hide show
  1. package/README.md +24 -35
  2. package/dist/commonjs/client/createTool.d.ts +3 -5
  3. package/dist/commonjs/client/createTool.d.ts.map +1 -1
  4. package/dist/commonjs/client/createTool.js +1 -0
  5. package/dist/commonjs/client/createTool.js.map +1 -1
  6. package/dist/commonjs/client/index.d.ts +6 -6
  7. package/dist/commonjs/client/index.d.ts.map +1 -1
  8. package/dist/commonjs/client/index.js.map +1 -1
  9. package/dist/commonjs/client/streaming.d.ts +5 -5
  10. package/dist/commonjs/client/streaming.d.ts.map +1 -1
  11. package/dist/commonjs/client/streaming.js +3 -2
  12. package/dist/commonjs/client/streaming.js.map +1 -1
  13. package/dist/commonjs/component/files.d.ts +2 -2
  14. package/dist/commonjs/component/files.d.ts.map +1 -1
  15. package/dist/commonjs/component/files.js.map +1 -1
  16. package/dist/commonjs/component/messages.d.ts +24 -24
  17. package/dist/commonjs/component/messages.d.ts.map +1 -1
  18. package/dist/commonjs/component/messages.js +4 -5
  19. package/dist/commonjs/component/messages.js.map +1 -1
  20. package/dist/commonjs/component/schema.d.ts +45 -45
  21. package/dist/commonjs/component/schema.d.ts.map +1 -1
  22. package/dist/commonjs/component/schema.js.map +1 -1
  23. package/dist/commonjs/component/streams.d.ts.map +1 -1
  24. package/dist/commonjs/component/streams.js +4 -7
  25. package/dist/commonjs/component/streams.js.map +1 -1
  26. package/dist/commonjs/component/threads.d.ts +8 -8
  27. package/dist/commonjs/component/threads.js +3 -3
  28. package/dist/commonjs/component/threads.js.map +1 -1
  29. package/dist/commonjs/component/users.js +4 -4
  30. package/dist/commonjs/component/users.js.map +1 -1
  31. package/dist/commonjs/component/vector/index.d.ts +3 -3
  32. package/dist/commonjs/component/vector/index.d.ts.map +1 -1
  33. package/dist/commonjs/component/vector/index.js +4 -5
  34. package/dist/commonjs/component/vector/index.js.map +1 -1
  35. package/dist/commonjs/component/vector/tables.d.ts +5 -7
  36. package/dist/commonjs/component/vector/tables.d.ts.map +1 -1
  37. package/dist/commonjs/mapping.d.ts +2 -2
  38. package/dist/commonjs/mapping.d.ts.map +1 -1
  39. package/dist/commonjs/mapping.js.map +1 -1
  40. package/dist/commonjs/react/deltas.d.ts +29 -0
  41. package/dist/commonjs/react/deltas.d.ts.map +1 -0
  42. package/dist/commonjs/react/deltas.js +267 -0
  43. package/dist/commonjs/react/deltas.js.map +1 -0
  44. package/dist/commonjs/react/index.d.ts +4 -49
  45. package/dist/commonjs/react/index.d.ts.map +1 -1
  46. package/dist/commonjs/react/index.js +15 -446
  47. package/dist/commonjs/react/index.js.map +1 -1
  48. package/dist/commonjs/react/optimisticallySendMessage.d.ts +8 -0
  49. package/dist/commonjs/react/optimisticallySendMessage.d.ts.map +1 -0
  50. package/dist/commonjs/react/optimisticallySendMessage.js +40 -0
  51. package/dist/commonjs/react/optimisticallySendMessage.js.map +1 -0
  52. package/dist/commonjs/react/toUIMessages.d.ts +3 -3
  53. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  54. package/dist/commonjs/react/toUIMessages.js +4 -2
  55. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  56. package/dist/commonjs/react/types.d.ts +26 -0
  57. package/dist/commonjs/react/types.d.ts.map +1 -0
  58. package/dist/commonjs/react/types.js +2 -0
  59. package/dist/commonjs/react/types.js.map +1 -0
  60. package/dist/commonjs/react/useSmoothText.d.ts +20 -0
  61. package/dist/commonjs/react/useSmoothText.d.ts.map +1 -0
  62. package/dist/commonjs/react/useSmoothText.js +50 -0
  63. package/dist/commonjs/react/useSmoothText.js.map +1 -0
  64. package/dist/commonjs/shared.d.ts +2 -2
  65. package/dist/commonjs/shared.d.ts.map +1 -1
  66. package/dist/commonjs/validators.d.ts +1 -1
  67. package/dist/commonjs/validators.d.ts.map +1 -1
  68. package/dist/commonjs/validators.js +2 -2
  69. package/dist/commonjs/validators.js.map +1 -1
  70. package/dist/commonjs.tsbuildinfo +1 -0
  71. package/dist/esm/client/createTool.d.ts +3 -5
  72. package/dist/esm/client/createTool.d.ts.map +1 -1
  73. package/dist/esm/client/createTool.js +1 -0
  74. package/dist/esm/client/createTool.js.map +1 -1
  75. package/dist/esm/client/index.d.ts +6 -6
  76. package/dist/esm/client/index.d.ts.map +1 -1
  77. package/dist/esm/client/index.js.map +1 -1
  78. package/dist/esm/client/streaming.d.ts +5 -5
  79. package/dist/esm/client/streaming.d.ts.map +1 -1
  80. package/dist/esm/client/streaming.js +3 -2
  81. package/dist/esm/client/streaming.js.map +1 -1
  82. package/dist/esm/component/files.d.ts +2 -2
  83. package/dist/esm/component/files.d.ts.map +1 -1
  84. package/dist/esm/component/files.js.map +1 -1
  85. package/dist/esm/component/messages.d.ts +24 -24
  86. package/dist/esm/component/messages.d.ts.map +1 -1
  87. package/dist/esm/component/messages.js +4 -5
  88. package/dist/esm/component/messages.js.map +1 -1
  89. package/dist/esm/component/schema.d.ts +45 -45
  90. package/dist/esm/component/schema.d.ts.map +1 -1
  91. package/dist/esm/component/schema.js.map +1 -1
  92. package/dist/esm/component/streams.d.ts.map +1 -1
  93. package/dist/esm/component/streams.js +4 -7
  94. package/dist/esm/component/streams.js.map +1 -1
  95. package/dist/esm/component/threads.d.ts +8 -8
  96. package/dist/esm/component/threads.js +3 -3
  97. package/dist/esm/component/threads.js.map +1 -1
  98. package/dist/esm/component/users.js +4 -4
  99. package/dist/esm/component/users.js.map +1 -1
  100. package/dist/esm/component/vector/index.d.ts +3 -3
  101. package/dist/esm/component/vector/index.d.ts.map +1 -1
  102. package/dist/esm/component/vector/index.js +4 -5
  103. package/dist/esm/component/vector/index.js.map +1 -1
  104. package/dist/esm/component/vector/tables.d.ts +5 -7
  105. package/dist/esm/component/vector/tables.d.ts.map +1 -1
  106. package/dist/esm/mapping.d.ts +2 -2
  107. package/dist/esm/mapping.d.ts.map +1 -1
  108. package/dist/esm/mapping.js.map +1 -1
  109. package/dist/esm/react/deltas.d.ts +29 -0
  110. package/dist/esm/react/deltas.d.ts.map +1 -0
  111. package/dist/esm/react/deltas.js +267 -0
  112. package/dist/esm/react/deltas.js.map +1 -0
  113. package/dist/esm/react/index.d.ts +4 -49
  114. package/dist/esm/react/index.d.ts.map +1 -1
  115. package/dist/esm/react/index.js +15 -446
  116. package/dist/esm/react/index.js.map +1 -1
  117. package/dist/esm/react/optimisticallySendMessage.d.ts +8 -0
  118. package/dist/esm/react/optimisticallySendMessage.d.ts.map +1 -0
  119. package/dist/esm/react/optimisticallySendMessage.js +40 -0
  120. package/dist/esm/react/optimisticallySendMessage.js.map +1 -0
  121. package/dist/esm/react/toUIMessages.d.ts +3 -3
  122. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  123. package/dist/esm/react/toUIMessages.js +4 -2
  124. package/dist/esm/react/toUIMessages.js.map +1 -1
  125. package/dist/esm/react/types.d.ts +26 -0
  126. package/dist/esm/react/types.d.ts.map +1 -0
  127. package/dist/esm/react/types.js +2 -0
  128. package/dist/esm/react/types.js.map +1 -0
  129. package/dist/esm/react/useSmoothText.d.ts +20 -0
  130. package/dist/esm/react/useSmoothText.d.ts.map +1 -0
  131. package/dist/esm/react/useSmoothText.js +50 -0
  132. package/dist/esm/react/useSmoothText.js.map +1 -0
  133. package/dist/esm/shared.d.ts +2 -2
  134. package/dist/esm/shared.d.ts.map +1 -1
  135. package/dist/esm/validators.d.ts +1 -1
  136. package/dist/esm/validators.d.ts.map +1 -1
  137. package/dist/esm/validators.js +2 -2
  138. package/dist/esm/validators.js.map +1 -1
  139. package/dist/esm.tsbuildinfo +1 -0
  140. package/package.json +5 -1
  141. package/src/client/createTool.ts +3 -5
  142. package/src/client/index.test.ts +1 -1
  143. package/src/client/index.ts +9 -10
  144. package/src/client/streaming.ts +15 -5
  145. package/src/component/files.ts +2 -2
  146. package/src/component/messages.test.ts +4 -4
  147. package/src/component/messages.ts +15 -15
  148. package/src/component/schema.ts +1 -1
  149. package/src/component/streams.ts +8 -9
  150. package/src/component/threads.ts +6 -6
  151. package/src/component/users.ts +7 -7
  152. package/src/component/vector/index.ts +12 -8
  153. package/src/component/vector/tables.ts +10 -10
  154. package/src/mapping.ts +4 -4
  155. package/src/react/deltas.test.ts +298 -0
  156. package/src/react/deltas.ts +340 -0
  157. package/src/react/index.ts +26 -589
  158. package/src/react/optimisticallySendMessage.ts +46 -0
  159. package/src/react/toUIMessages.test.ts +301 -0
  160. package/src/react/toUIMessages.ts +9 -5
  161. package/src/react/types.ts +52 -0
  162. package/src/react/useSmoothText.ts +76 -0
  163. package/src/shared.ts +2 -2
  164. package/src/validators.test.ts +22 -22
  165. package/src/validators.ts +8 -2
@@ -1,7 +1,16 @@
1
- import { ChunkDetector, smoothStream, StreamTextTransform, ToolSet } from "ai";
2
- import { ProviderOptions, StreamDelta, TextStreamPart } from "../validators";
1
+ import {
2
+ type ChunkDetector,
3
+ smoothStream,
4
+ type StreamTextTransform,
5
+ type ToolSet,
6
+ } from "ai";
7
+ import type {
8
+ ProviderOptions,
9
+ StreamDelta,
10
+ TextStreamPart,
11
+ } from "../validators";
3
12
  import type { AgentComponent, MessageDoc } from "./index";
4
- import { RunActionCtx } from "./types";
13
+ import type { RunActionCtx } from "./types";
5
14
 
6
15
  export type StreamingOptions = {
7
16
  /**
@@ -9,7 +18,7 @@ export type StreamingOptions = {
9
18
  * Note: this is not a guarantee that every delta will be exactly one line.
10
19
  * E.g. if "line" is specified, it won't save any deltas until it encounters
11
20
  * a newline character.
12
- * Defaults to "word".
21
+ * Defaults to a regex that chunks by punctuation followed by whitespace.
13
22
  */
14
23
  chunking?: "word" | "line" | RegExp | ChunkDetector;
15
24
  /**
@@ -19,7 +28,8 @@ export type StreamingOptions = {
19
28
  throttleMs?: number;
20
29
  };
21
30
  export const DEFAULT_STREAMING_OPTIONS = {
22
- chunking: "word",
31
+ // This chunks by sentences / clauses. Punctuation followed by whitespace.
32
+ chunking: /[\p{P}\s]/u,
23
33
  throttleMs: 250,
24
34
  } satisfies StreamingOptions;
25
35
 
@@ -1,7 +1,7 @@
1
1
  import { paginator } from "convex-helpers/server/pagination";
2
- import { mutation, MutationCtx, query } from "./_generated/server.js";
2
+ import type { Id } from "./_generated/dataModel.js";
3
+ import { mutation, type MutationCtx, query } from "./_generated/server.js";
3
4
  import { schema, v } from "./schema.js";
4
- import { Id } from "./_generated/dataModel.js";
5
5
 
6
6
  export const addFile = mutation({
7
7
  args: {
@@ -1,12 +1,12 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import { describe, expect, test } from "vitest";
4
3
  import { convexTest } from "convex-test";
5
- import schema from "./schema.js";
4
+ import { describe, expect, test } from "vitest";
6
5
  import { api } from "./_generated/api.js";
7
- import { modules } from "./setup.test.js";
8
- import { Id } from "./_generated/dataModel.js";
6
+ import type { Id } from "./_generated/dataModel.js";
9
7
  import { getMaxMessage } from "./messages.js";
8
+ import schema from "./schema.js";
9
+ import { modules } from "./setup.test.js";
10
10
 
11
11
  describe("agent", () => {
12
12
  test("getMaxMessage works for threads", async () => {
@@ -1,6 +1,7 @@
1
1
  import { assert, omit } from "convex-helpers";
2
2
  import { mergedStream, stream } from "convex-helpers/server/stream";
3
- import { ObjectType } from "convex/values";
3
+ import { paginationOptsValidator } from "convex/server";
4
+ import type { ObjectType } from "convex/values";
4
5
  import {
5
6
  DEFAULT_MESSAGE_RANGE,
6
7
  DEFAULT_RECENT_MESSAGES,
@@ -8,38 +9,37 @@ import {
8
9
  isTool,
9
10
  } from "../shared.js";
10
11
  import {
11
- vPaginationResult,
12
12
  vMessageEmbeddings,
13
13
  vMessageStatus,
14
14
  vMessageWithMetadata,
15
+ vPaginationResult,
15
16
  vSearchOptions,
16
17
  vStepWithMessages,
17
18
  } from "../validators.js";
18
19
  import { api, internal } from "./_generated/api.js";
19
- import { Doc, Id } from "./_generated/dataModel.js";
20
+ import type { Doc, Id } from "./_generated/dataModel.js";
20
21
  import {
21
22
  action,
22
23
  internalQuery,
23
24
  mutation,
24
- MutationCtx,
25
+ type MutationCtx,
25
26
  query,
26
- QueryCtx,
27
+ type QueryCtx,
27
28
  } from "./_generated/server.js";
28
- import { schema, v } from "./schema.js";
29
+ import type { MessageDoc } from "./schema.js";
30
+ import { schema, v, vMessageDoc } from "./schema.js";
31
+ import {
32
+ getThread as _getThread,
33
+ listThreadsByUserId as _listThreadsByUserId,
34
+ updateThread as _updateThread,
35
+ } from "./threads.js";
29
36
  import { insertVector, searchVectors } from "./vector/index.js";
30
37
  import {
31
- VectorDimension,
38
+ type VectorDimension,
32
39
  VectorDimensions,
33
- VectorTableId,
40
+ type VectorTableId,
34
41
  vVectorId,
35
42
  } from "./vector/tables.js";
36
- import {
37
- listThreadsByUserId as _listThreadsByUserId,
38
- getThread as _getThread,
39
- updateThread as _updateThread,
40
- } from "./threads.js";
41
- import { paginationOptsValidator } from "convex/server";
42
- import { MessageDoc, vMessageDoc } from "./schema.js";
43
43
 
44
44
  /** @deprecated Use *.threads.listMessagesByThreadId instead. */
45
45
  export const listThreadsByUserId = _listThreadsByUserId;
@@ -1,5 +1,5 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
- import { Infer, v } from "convex/values";
2
+ import { type Infer, v } from "convex/values";
3
3
  import {
4
4
  vThreadStatus,
5
5
  vMessage,
@@ -1,15 +1,15 @@
1
+ import { omit, pick } from "convex-helpers";
1
2
  import { v } from "convex/values";
3
+ import { type StreamDelta, vStreamDelta, vStreamMessage } from "../validators";
4
+ import { internal } from "./_generated/api";
5
+ import type { Id } from "./_generated/dataModel";
2
6
  import {
3
7
  internalMutation,
4
8
  mutation,
5
- MutationCtx,
9
+ type MutationCtx,
6
10
  query,
7
11
  } from "./_generated/server";
8
- import { omit, pick } from "convex-helpers";
9
- import { StreamDelta, vStreamDelta, vStreamMessage } from "../validators";
10
12
  import schema from "./schema";
11
- import { internal } from "./_generated/api";
12
- import { Id } from "./_generated/dataModel";
13
13
 
14
14
  const MAX_DELTAS_PER_REQUEST = 1000;
15
15
  const MAX_DELTAS_PER_STREAM = 100;
@@ -51,10 +51,9 @@ export const listDeltas = query({
51
51
  );
52
52
  totalDeltas += streamDeltas.length;
53
53
  deltas.push(
54
- ...streamDeltas.map((d) => ({
55
- streamId: d.streamId.toString(),
56
- ...pick(d, ["start", "end", "parts"]),
57
- }))
54
+ ...streamDeltas.map((d) =>
55
+ pick(d, ["streamId", "start", "end", "parts"])
56
+ )
58
57
  );
59
58
  if (totalDeltas >= MAX_DELTAS_PER_REQUEST) {
60
59
  break;
@@ -1,21 +1,21 @@
1
1
  import { assert, omit, pick } from "convex-helpers";
2
2
  import { paginator } from "convex-helpers/server/pagination";
3
3
  import { partial } from "convex-helpers/validators";
4
- import { ObjectType } from "convex/values";
4
+ import { paginationOptsValidator } from "convex/server";
5
+ import type { ObjectType } from "convex/values";
6
+ import { type ThreadDoc, vThreadDoc } from "../client/index.js";
5
7
  import { vPaginationResult } from "../validators.js";
6
8
  import { api, internal } from "./_generated/api.js";
7
- import { Doc } from "./_generated/dataModel.js";
9
+ import type { Doc } from "./_generated/dataModel.js";
8
10
  import {
9
11
  action,
10
12
  internalMutation,
11
13
  mutation,
12
- MutationCtx,
14
+ type MutationCtx,
13
15
  query,
14
16
  } from "./_generated/server.js";
15
- import { schema, v } from "./schema.js";
16
- import { paginationOptsValidator } from "convex/server";
17
17
  import { deleteMessage } from "./messages.js";
18
- import { ThreadDoc, vThreadDoc } from "../client/index.js";
18
+ import { schema, v } from "./schema.js";
19
19
 
20
20
  function publicThreadOrNull(thread: Doc<"threads"> | null): ThreadDoc | null {
21
21
  if (thread === null) {
@@ -1,21 +1,21 @@
1
1
  import { paginator } from "convex-helpers/server/pagination";
2
+ import { stream } from "convex-helpers/server/stream";
2
3
  import { nullable } from "convex-helpers/validators";
3
- import { ObjectType } from "convex/values";
4
+ import { paginationOptsValidator } from "convex/server";
5
+ import type { ObjectType } from "convex/values";
6
+ import { vPaginationResult } from "../validators.js";
4
7
  import { internal } from "./_generated/api.js";
8
+ import type { Id } from "./_generated/dataModel.js";
5
9
  import {
6
10
  action,
7
11
  internalMutation,
8
12
  internalQuery,
9
13
  mutation,
10
- MutationCtx,
14
+ type MutationCtx,
11
15
  query,
12
16
  } from "./_generated/server.js";
13
- import { schema, v } from "./schema.js";
14
17
  import { deleteMessage } from "./messages.js";
15
- import { paginationOptsValidator } from "convex/server";
16
- import { stream } from "convex-helpers/server/stream";
17
- import { vPaginationResult } from "../validators.js";
18
- import { Id } from "./_generated/dataModel.js";
18
+ import { schema, v } from "./schema.js";
19
19
 
20
20
  // Note: it only searches for users with threads
21
21
  export const listUsersWithThreads = query({
@@ -1,18 +1,22 @@
1
1
  import { paginator } from "convex-helpers/server/pagination";
2
+ import { mergedStream, stream } from "convex-helpers/server/stream";
2
3
  import { v } from "convex/values";
3
- import { ActionCtx, mutation, MutationCtx, query } from "../_generated/server";
4
+ import type { Id } from "../_generated/dataModel.js";
4
5
  import {
5
- EmbeddingsWithoutDenormalizedFields,
6
+ type ActionCtx,
7
+ mutation,
8
+ type MutationCtx,
9
+ query,
10
+ } from "../_generated/server.js";
11
+ import schema from "../schema.js";
12
+ import {
13
+ type EmbeddingsWithoutDenormalizedFields,
6
14
  getVectorTableName,
7
- VectorDimension,
15
+ type VectorDimension,
8
16
  vEmbeddingsWithoutDenormalizedFields,
9
17
  vVectorDimension,
10
18
  vVectorId,
11
- } from "./tables";
12
- import schema from "../schema";
13
- import { mergedStream } from "convex-helpers/server/stream";
14
- import { stream } from "convex-helpers/server/stream";
15
- import { Id } from "../_generated/dataModel";
19
+ } from "./tables.js";
16
20
 
17
21
  export const paginate = query({
18
22
  args: {
@@ -2,20 +2,20 @@ import { omit } from "convex-helpers";
2
2
  import { literals } from "convex-helpers/validators";
3
3
  import {
4
4
  defineTable,
5
- GenericTableSearchIndexes,
6
- SchemaDefinition,
7
- TableDefinition,
5
+ type GenericTableSearchIndexes,
6
+ type SchemaDefinition,
7
+ type TableDefinition,
8
8
  } from "convex/server";
9
9
  import {
10
- GenericId,
11
- Infer,
12
- ObjectType,
10
+ type GenericId,
11
+ type Infer,
12
+ type ObjectType,
13
13
  v,
14
- VId,
15
- VObject,
16
- VUnion,
14
+ type VId,
15
+ type VObject,
16
+ type VUnion,
17
17
  } from "convex/values";
18
- import { QueryCtx } from "../_generated/server";
18
+ import type { QueryCtx } from "../_generated/server";
19
19
 
20
20
  // We only generate embeddings for non-tool, non-system messages
21
21
  const embeddings = {
package/src/mapping.ts CHANGED
@@ -1,19 +1,19 @@
1
+ import type { FileUIPart } from "@ai-sdk/ui-utils";
1
2
  import {
2
3
  convertToCoreMessages,
3
4
  coreMessageSchema,
4
- GenerateObjectResult,
5
+ type Message as AIMessage,
5
6
  type AssistantContent,
6
7
  type CoreMessage,
7
8
  type DataContent,
9
+ type GenerateObjectResult,
8
10
  type StepResult,
9
11
  type ToolContent,
10
12
  type ToolSet,
11
- type Message as AIMessage,
12
13
  type UserContent,
13
14
  } from "ai";
14
- import type { FileUIPart } from "@ai-sdk/ui-utils";
15
15
  import { assert } from "convex-helpers";
16
- import {
16
+ import type {
17
17
  MessageWithMetadata,
18
18
  Step,
19
19
  StepWithMessagesWithMetadata,
@@ -0,0 +1,298 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mergeDeltas, applyDeltasToStreamMessage } from "./deltas";
3
+ import type { StreamMessage, StreamDelta, TextStreamPart } from "../validators";
4
+
5
+ function makeStreamMessage(
6
+ streamId: string,
7
+ order: number,
8
+ stepOrder: number
9
+ ): StreamMessage {
10
+ return {
11
+ streamId,
12
+ order,
13
+ stepOrder,
14
+ } as StreamMessage;
15
+ }
16
+
17
+ function makeDelta(
18
+ streamId: string,
19
+ start: number,
20
+ end: number,
21
+ parts: TextStreamPart[]
22
+ ): StreamDelta {
23
+ return {
24
+ streamId,
25
+ start,
26
+ end,
27
+ parts,
28
+ };
29
+ }
30
+
31
+ describe("mergeDeltas", () => {
32
+ it("merges a single text-delta into a message", () => {
33
+ const streamId = "s1";
34
+ const streamMessages = [makeStreamMessage(streamId, 1, 0)];
35
+ const deltas = [
36
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
37
+ ];
38
+ const [messages, newStreams, changed] = mergeDeltas(
39
+ "thread1",
40
+ streamMessages,
41
+ [],
42
+ deltas
43
+ );
44
+ expect(messages).toHaveLength(1);
45
+ expect(messages[0].text).toBe("Hello");
46
+ expect(messages[0].message?.role).toBe("assistant");
47
+ expect(changed).toBe(true);
48
+ expect(newStreams[0].cursor).toBe(5);
49
+ });
50
+
51
+ it("merges multiple deltas for the same stream", () => {
52
+ const streamId = "s1";
53
+ const streamMessages = [makeStreamMessage(streamId, 1, 0)];
54
+ const deltas = [
55
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
56
+ makeDelta(streamId, 5, 11, [
57
+ { type: "text-delta", textDelta: " World!" },
58
+ ]),
59
+ ];
60
+ const [messages, newStreams, changed] = mergeDeltas(
61
+ "thread1",
62
+ streamMessages,
63
+ [],
64
+ deltas
65
+ );
66
+ expect(messages).toHaveLength(1);
67
+ expect(messages[0].text).toBe("Hello World!");
68
+ expect(changed).toBe(true);
69
+ expect(newStreams[0].cursor).toBe(11);
70
+ });
71
+
72
+ it("handles tool-call and tool-result parts", () => {
73
+ const streamId = "s2";
74
+ const streamMessages = [makeStreamMessage(streamId, 2, 0)];
75
+ const deltas = [
76
+ makeDelta(streamId, 0, 1, [
77
+ {
78
+ type: "tool-call",
79
+ toolCallId: "call1",
80
+ toolName: "myTool",
81
+ args: "",
82
+ },
83
+ ]),
84
+ makeDelta(streamId, 1, 2, [
85
+ {
86
+ type: "tool-result",
87
+ toolCallId: "call1",
88
+ toolName: "myTool",
89
+ result: "42",
90
+ },
91
+ ]),
92
+ ];
93
+ const [messages, _, changed] = mergeDeltas(
94
+ "thread1",
95
+ streamMessages,
96
+ [],
97
+ deltas
98
+ );
99
+ expect(messages).toHaveLength(2);
100
+ expect(messages[0].message?.role).toBe("assistant");
101
+ expect(messages[0].tool).toBe(true);
102
+ const content = messages[0].message?.content;
103
+ expect(content).toEqual([
104
+ {
105
+ type: "tool-call",
106
+ toolCallId: "call1",
107
+ toolName: "myTool",
108
+ args: "",
109
+ },
110
+ ]);
111
+ expect(messages[1].message?.role).toBe("tool");
112
+ expect(messages[1].tool).toBe(true);
113
+ expect(messages[1].message?.content).toEqual([
114
+ {
115
+ type: "tool-result",
116
+ toolCallId: "call1",
117
+ toolName: "myTool",
118
+ result: "42",
119
+ },
120
+ ]);
121
+ expect(changed).toBe(true);
122
+ });
123
+
124
+ it("returns changed=false if no new deltas", () => {
125
+ const streamId = "s3";
126
+ const streamMessages = [makeStreamMessage(streamId, 3, 0)];
127
+ const deltas: StreamDelta[] = [];
128
+ const [messages, newStreams, changed] = mergeDeltas(
129
+ "thread1",
130
+ streamMessages,
131
+ [],
132
+ deltas
133
+ );
134
+ expect(messages).toHaveLength(0);
135
+ expect(changed).toBe(false);
136
+ expect(newStreams[0].cursor).toBe(0);
137
+ });
138
+
139
+ it("handles multiple streams and sorts by order/stepOrder", () => {
140
+ const s1 = makeStreamMessage("s1", 1, 0);
141
+ const s2 = makeStreamMessage("s2", 2, 0);
142
+ const deltas = [
143
+ makeDelta("s2", 0, 3, [{ type: "text-delta", textDelta: "B" }]),
144
+ makeDelta("s1", 0, 3, [{ type: "text-delta", textDelta: "A" }]),
145
+ ];
146
+ const [messages, _, changed] = mergeDeltas("thread1", [s2, s1], [], deltas);
147
+ expect(messages).toHaveLength(2);
148
+ expect(messages[0].text).toBe("A");
149
+ expect(messages[1].text).toBe("B");
150
+ expect(changed).toBe(true);
151
+ // Sorted by order
152
+ expect(messages[0].order).toBe(1);
153
+ expect(messages[1].order).toBe(2);
154
+ });
155
+
156
+ it("does not duplicate text content when merging sequential text-deltas", () => {
157
+ const streamId = "s4";
158
+ const streamMessages = [makeStreamMessage(streamId, 4, 0)];
159
+ const deltas = [
160
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
161
+ makeDelta(streamId, 5, 11, [
162
+ { type: "text-delta", textDelta: " World!" },
163
+ ]),
164
+ makeDelta(streamId, 11, 12, [{ type: "text-delta", textDelta: "!" }]),
165
+ ];
166
+ const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
167
+ expect(messages).toHaveLength(1);
168
+ expect(messages[0].text).toBe("Hello World!!");
169
+ // There should only be one text part per message
170
+ const content = messages[0].message?.content;
171
+ if (Array.isArray(content)) {
172
+ const textParts = content.filter((p) => p.type === "text");
173
+ expect(textParts).toHaveLength(1);
174
+ expect(textParts[0].text).toBe("Hello World!!");
175
+ }
176
+ });
177
+
178
+ it("does not duplicate reasoning parts", () => {
179
+ const streamId = "s6";
180
+ const streamMessages = [makeStreamMessage(streamId, 6, 0)];
181
+ const deltas = [
182
+ makeDelta(streamId, 0, 1, [
183
+ { type: "reasoning", textDelta: "I'm thinking..." },
184
+ ]),
185
+ makeDelta(streamId, 1, 2, [
186
+ { type: "reasoning", textDelta: " Still thinking..." },
187
+ ]),
188
+ ];
189
+ const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
190
+ expect(messages).toHaveLength(1);
191
+ if (Array.isArray(messages[0].message?.content)) {
192
+ const reasoningParts = messages[0].message.content.filter(
193
+ (p) => p.type === "reasoning"
194
+ );
195
+ expect(reasoningParts).toHaveLength(1);
196
+ expect(reasoningParts[0].text).toBe("I'm thinking... Still thinking...");
197
+ }
198
+ });
199
+
200
+ it("applyDeltasToStreamMessage is idempotent and does not duplicate content", () => {
201
+ const streamId = "s7";
202
+ const streamMessage = makeStreamMessage(streamId, 7, 0);
203
+ const deltas = [
204
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
205
+ makeDelta(streamId, 5, 11, [
206
+ { type: "text-delta", textDelta: " World!" },
207
+ ]),
208
+ ];
209
+ // First call: apply both deltas
210
+ let [result, changed] = applyDeltasToStreamMessage(
211
+ "thread1",
212
+ streamMessage,
213
+ undefined,
214
+ deltas
215
+ );
216
+ expect(result.messages).toHaveLength(1);
217
+ expect(result.messages[0].text).toBe("Hello World!");
218
+ // Second call: re-apply the same deltas (should not duplicate)
219
+ [result, changed] = applyDeltasToStreamMessage(
220
+ "thread1",
221
+ streamMessage,
222
+ result,
223
+ deltas
224
+ );
225
+ expect(result.messages).toHaveLength(1);
226
+ expect(result.messages[0].text).toBe("Hello World!");
227
+ // Third call: add a new delta
228
+ const moreDeltas = [
229
+ ...deltas,
230
+ makeDelta(streamId, 11, 12, [{ type: "text-delta", textDelta: "!" }]),
231
+ ];
232
+ [result, changed] = applyDeltasToStreamMessage(
233
+ "thread1",
234
+ streamMessage,
235
+ result,
236
+ moreDeltas
237
+ );
238
+ expect(changed).toBe(true);
239
+ expect(result.messages).toHaveLength(1);
240
+ expect(result.messages[0].text).toBe("Hello World!!");
241
+ // Re-apply all deltas again (should still not duplicate)
242
+ [result, changed] = applyDeltasToStreamMessage(
243
+ "thread1",
244
+ streamMessage,
245
+ result,
246
+ moreDeltas
247
+ );
248
+ expect(changed).toBe(false);
249
+ expect(result.messages).toHaveLength(1);
250
+ expect(result.messages[0].text).toBe("Hello World!!");
251
+ });
252
+
253
+ it("mergeDeltas is pure and does not mutate inputs", () => {
254
+ const streamId = "s8";
255
+ const streamMessages = [makeStreamMessage(streamId, 8, 0)];
256
+ const deltas = [
257
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
258
+ makeDelta(streamId, 5, 11, [
259
+ { type: "text-delta", textDelta: " World!" },
260
+ ]),
261
+ ];
262
+ // Deep freeze inputs to catch mutation
263
+ function deepFreeze(obj: unknown): unknown {
264
+ if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
265
+ Object.freeze(obj);
266
+ for (const key of Object.keys(obj)) {
267
+ deepFreeze((obj as Record<string, unknown>)[key]);
268
+ }
269
+ }
270
+ return obj;
271
+ }
272
+ deepFreeze(streamMessages);
273
+ deepFreeze(deltas);
274
+ const [messages1, streams1, changed1] = mergeDeltas(
275
+ "thread1",
276
+ streamMessages,
277
+ [],
278
+ deltas
279
+ );
280
+ const [messages2, streams2, changed2] = mergeDeltas(
281
+ "thread1",
282
+ streamMessages,
283
+ [],
284
+ deltas
285
+ );
286
+ expect(messages1).toEqual(messages2);
287
+ expect(streams1).toEqual(streams2);
288
+ expect(changed1).toBe(changed2);
289
+ // Inputs should remain unchanged
290
+ expect(streamMessages).toEqual([makeStreamMessage(streamId, 8, 0)]);
291
+ expect(deltas).toEqual([
292
+ makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
293
+ makeDelta(streamId, 5, 11, [
294
+ { type: "text-delta", textDelta: " World!" },
295
+ ]),
296
+ ]);
297
+ });
298
+ });