@tambo-ai/react 0.69.0 → 0.70.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 (202) hide show
  1. package/README.md +7 -7
  2. package/dist/hooks/use-tambo-threads.test.js.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/mcp/index.d.ts +4 -5
  7. package/dist/mcp/index.d.ts.map +1 -1
  8. package/dist/mcp/index.js +4 -5
  9. package/dist/mcp/index.js.map +1 -1
  10. package/dist/model/component-metadata.d.ts +88 -241
  11. package/dist/model/component-metadata.d.ts.map +1 -1
  12. package/dist/model/component-metadata.js.map +1 -1
  13. package/dist/model/mcp-server-info.d.ts +3 -3
  14. package/dist/model/mcp-server-info.js.map +1 -1
  15. package/dist/providers/hooks/use-tambo-session-token.test.js.map +1 -1
  16. package/dist/providers/tambo-component-provider.d.ts +2 -2
  17. package/dist/providers/tambo-component-provider.d.ts.map +1 -1
  18. package/dist/providers/tambo-component-provider.js.map +1 -1
  19. package/dist/providers/tambo-interactable-provider.d.ts +1 -1
  20. package/dist/providers/tambo-registry-provider.d.ts +4 -4
  21. package/dist/providers/tambo-registry-provider.d.ts.map +1 -1
  22. package/dist/providers/tambo-registry-provider.js +11 -8
  23. package/dist/providers/tambo-registry-provider.js.map +1 -1
  24. package/dist/providers/tambo-registry-provider.test.js +31 -0
  25. package/dist/providers/tambo-registry-provider.test.js.map +1 -1
  26. package/dist/providers/tambo-registry-schema-compat.test.js +42 -52
  27. package/dist/providers/tambo-registry-schema-compat.test.js.map +1 -1
  28. package/dist/providers/tambo-stubs.d.ts +2 -2
  29. package/dist/providers/tambo-stubs.d.ts.map +1 -1
  30. package/dist/providers/tambo-stubs.js.map +1 -1
  31. package/dist/providers/tambo-thread-provider-initial-messages.test.js.map +1 -1
  32. package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
  33. package/dist/providers/tambo-thread-provider.js +110 -142
  34. package/dist/providers/tambo-thread-provider.js.map +1 -1
  35. package/dist/providers/tambo-thread-provider.test.js +362 -445
  36. package/dist/providers/tambo-thread-provider.test.js.map +1 -1
  37. package/dist/schema/index.d.ts +1 -2
  38. package/dist/schema/index.d.ts.map +1 -1
  39. package/dist/schema/index.js +1 -5
  40. package/dist/schema/index.js.map +1 -1
  41. package/dist/schema/schema.d.ts +7 -24
  42. package/dist/schema/schema.d.ts.map +1 -1
  43. package/dist/schema/schema.js +34 -105
  44. package/dist/schema/schema.js.map +1 -1
  45. package/dist/schema/schema.test.js +26 -124
  46. package/dist/schema/schema.test.js.map +1 -1
  47. package/dist/testing/tools.d.ts +2 -12
  48. package/dist/testing/tools.d.ts.map +1 -1
  49. package/dist/testing/tools.js +1 -20
  50. package/dist/testing/tools.js.map +1 -1
  51. package/dist/testing/types.d.ts +2 -2
  52. package/dist/testing/types.d.ts.map +1 -1
  53. package/dist/testing/types.js.map +1 -1
  54. package/dist/util/registry-validators.d.ts +2 -2
  55. package/dist/util/registry-validators.d.ts.map +1 -1
  56. package/dist/util/registry-validators.js +37 -17
  57. package/dist/util/registry-validators.js.map +1 -1
  58. package/dist/util/registry-validators.test.js +64 -25
  59. package/dist/util/registry-validators.test.js.map +1 -1
  60. package/dist/util/registry.d.ts +4 -10
  61. package/dist/util/registry.d.ts.map +1 -1
  62. package/dist/util/registry.js +6 -22
  63. package/dist/util/registry.js.map +1 -1
  64. package/dist/util/registry.test.js +1 -47
  65. package/dist/util/registry.test.js.map +1 -1
  66. package/dist/util/tool-caller.d.ts +2 -2
  67. package/dist/util/tool-caller.d.ts.map +1 -1
  68. package/dist/util/tool-caller.js +5 -12
  69. package/dist/util/tool-caller.js.map +1 -1
  70. package/dist/v1/index.d.ts +35 -0
  71. package/dist/v1/index.d.ts.map +1 -0
  72. package/dist/v1/index.js +47 -0
  73. package/dist/v1/index.js.map +1 -0
  74. package/dist/v1/types/component.d.ts +47 -0
  75. package/dist/v1/types/component.d.ts.map +1 -0
  76. package/dist/v1/types/component.js +11 -0
  77. package/dist/v1/types/component.js.map +1 -0
  78. package/dist/v1/types/event.d.ts +63 -0
  79. package/dist/v1/types/event.d.ts.map +1 -0
  80. package/dist/v1/types/event.js +9 -0
  81. package/dist/v1/types/event.js.map +1 -0
  82. package/dist/v1/types/message.d.ts +39 -0
  83. package/dist/v1/types/message.d.ts.map +1 -0
  84. package/dist/v1/types/message.js +10 -0
  85. package/dist/v1/types/message.js.map +1 -0
  86. package/dist/v1/types/thread.d.ts +54 -0
  87. package/dist/v1/types/thread.d.ts.map +1 -0
  88. package/dist/v1/types/thread.js +9 -0
  89. package/dist/v1/types/thread.js.map +1 -0
  90. package/dist/v1/types/tool.d.ts +52 -0
  91. package/dist/v1/types/tool.d.ts.map +1 -0
  92. package/dist/v1/types/tool.js +11 -0
  93. package/dist/v1/types/tool.js.map +1 -0
  94. package/esm/hooks/use-tambo-threads.test.js.map +1 -1
  95. package/esm/index.d.ts +1 -1
  96. package/esm/index.d.ts.map +1 -1
  97. package/esm/index.js.map +1 -1
  98. package/esm/mcp/index.d.ts +4 -5
  99. package/esm/mcp/index.d.ts.map +1 -1
  100. package/esm/mcp/index.js +4 -5
  101. package/esm/mcp/index.js.map +1 -1
  102. package/esm/model/component-metadata.d.ts +88 -241
  103. package/esm/model/component-metadata.d.ts.map +1 -1
  104. package/esm/model/component-metadata.js.map +1 -1
  105. package/esm/model/mcp-server-info.d.ts +3 -3
  106. package/esm/model/mcp-server-info.js.map +1 -1
  107. package/esm/providers/hooks/use-tambo-session-token.test.js.map +1 -1
  108. package/esm/providers/tambo-component-provider.d.ts +2 -2
  109. package/esm/providers/tambo-component-provider.d.ts.map +1 -1
  110. package/esm/providers/tambo-component-provider.js.map +1 -1
  111. package/esm/providers/tambo-interactable-provider.d.ts +1 -1
  112. package/esm/providers/tambo-registry-provider.d.ts +4 -4
  113. package/esm/providers/tambo-registry-provider.d.ts.map +1 -1
  114. package/esm/providers/tambo-registry-provider.js +11 -8
  115. package/esm/providers/tambo-registry-provider.js.map +1 -1
  116. package/esm/providers/tambo-registry-provider.test.js +31 -0
  117. package/esm/providers/tambo-registry-provider.test.js.map +1 -1
  118. package/esm/providers/tambo-registry-schema-compat.test.js +42 -52
  119. package/esm/providers/tambo-registry-schema-compat.test.js.map +1 -1
  120. package/esm/providers/tambo-stubs.d.ts +2 -2
  121. package/esm/providers/tambo-stubs.d.ts.map +1 -1
  122. package/esm/providers/tambo-stubs.js.map +1 -1
  123. package/esm/providers/tambo-thread-provider-initial-messages.test.js.map +1 -1
  124. package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
  125. package/esm/providers/tambo-thread-provider.js +110 -142
  126. package/esm/providers/tambo-thread-provider.js.map +1 -1
  127. package/esm/providers/tambo-thread-provider.test.js +329 -445
  128. package/esm/providers/tambo-thread-provider.test.js.map +1 -1
  129. package/esm/schema/index.d.ts +1 -2
  130. package/esm/schema/index.d.ts.map +1 -1
  131. package/esm/schema/index.js +1 -2
  132. package/esm/schema/index.js.map +1 -1
  133. package/esm/schema/schema.d.ts +7 -24
  134. package/esm/schema/schema.d.ts.map +1 -1
  135. package/esm/schema/schema.js +34 -103
  136. package/esm/schema/schema.js.map +1 -1
  137. package/esm/schema/schema.test.js +27 -125
  138. package/esm/schema/schema.test.js.map +1 -1
  139. package/esm/testing/tools.d.ts +2 -12
  140. package/esm/testing/tools.d.ts.map +1 -1
  141. package/esm/testing/tools.js +2 -20
  142. package/esm/testing/tools.js.map +1 -1
  143. package/esm/testing/types.d.ts +2 -2
  144. package/esm/testing/types.d.ts.map +1 -1
  145. package/esm/testing/types.js.map +1 -1
  146. package/esm/util/registry-validators.d.ts +2 -2
  147. package/esm/util/registry-validators.d.ts.map +1 -1
  148. package/esm/util/registry-validators.js +38 -18
  149. package/esm/util/registry-validators.js.map +1 -1
  150. package/esm/util/registry-validators.test.js +64 -25
  151. package/esm/util/registry-validators.test.js.map +1 -1
  152. package/esm/util/registry.d.ts +4 -10
  153. package/esm/util/registry.d.ts.map +1 -1
  154. package/esm/util/registry.js +7 -22
  155. package/esm/util/registry.js.map +1 -1
  156. package/esm/util/registry.test.js +3 -49
  157. package/esm/util/registry.test.js.map +1 -1
  158. package/esm/util/tool-caller.d.ts +2 -2
  159. package/esm/util/tool-caller.d.ts.map +1 -1
  160. package/esm/util/tool-caller.js +5 -12
  161. package/esm/util/tool-caller.js.map +1 -1
  162. package/esm/v1/index.d.ts +35 -0
  163. package/esm/v1/index.d.ts.map +1 -0
  164. package/esm/v1/index.js +46 -0
  165. package/esm/v1/index.js.map +1 -0
  166. package/esm/v1/types/component.d.ts +47 -0
  167. package/esm/v1/types/component.d.ts.map +1 -0
  168. package/esm/v1/types/component.js +10 -0
  169. package/esm/v1/types/component.js.map +1 -0
  170. package/esm/v1/types/event.d.ts +63 -0
  171. package/esm/v1/types/event.d.ts.map +1 -0
  172. package/esm/v1/types/event.js +8 -0
  173. package/esm/v1/types/event.js.map +1 -0
  174. package/esm/v1/types/message.d.ts +39 -0
  175. package/esm/v1/types/message.d.ts.map +1 -0
  176. package/esm/v1/types/message.js +9 -0
  177. package/esm/v1/types/message.js.map +1 -0
  178. package/esm/v1/types/thread.d.ts +54 -0
  179. package/esm/v1/types/thread.d.ts.map +1 -0
  180. package/esm/v1/types/thread.js +8 -0
  181. package/esm/v1/types/thread.js.map +1 -0
  182. package/esm/v1/types/tool.d.ts +52 -0
  183. package/esm/v1/types/tool.d.ts.map +1 -0
  184. package/esm/v1/types/tool.js +10 -0
  185. package/esm/v1/types/tool.js.map +1 -0
  186. package/package.json +18 -8
  187. package/dist/schema/zod.d.ts +0 -57
  188. package/dist/schema/zod.d.ts.map +0 -1
  189. package/dist/schema/zod.js +0 -191
  190. package/dist/schema/zod.js.map +0 -1
  191. package/dist/schema/zod.test.d.ts +0 -2
  192. package/dist/schema/zod.test.d.ts.map +0 -1
  193. package/dist/schema/zod.test.js +0 -663
  194. package/dist/schema/zod.test.js.map +0 -1
  195. package/esm/schema/zod.d.ts +0 -57
  196. package/esm/schema/zod.d.ts.map +0 -1
  197. package/esm/schema/zod.js +0 -180
  198. package/esm/schema/zod.js.map +0 -1
  199. package/esm/schema/zod.test.d.ts +0 -2
  200. package/esm/schema/zod.test.d.ts.map +0 -1
  201. package/esm/schema/zod.test.js +0 -628
  202. package/esm/schema/zod.test.js.map +0 -1
@@ -1,9 +1,42 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const typescript_sdk_1 = require("@tambo-ai/typescript-sdk");
39
+ const typescript_sdk_1 = __importStar(require("@tambo-ai/typescript-sdk"));
7
40
  const react_1 = require("@testing-library/react");
8
41
  const react_2 = __importDefault(require("react"));
9
42
  const v4_1 = require("zod/v4");
@@ -28,9 +61,14 @@ jest.mock("./tambo-client-provider", () => {
28
61
  TamboClientContext: react_2.default.createContext(undefined),
29
62
  };
30
63
  });
31
- jest.mock("@tambo-ai/typescript-sdk", () => ({
32
- advanceStream: jest.fn(),
33
- }));
64
+ jest.mock("@tambo-ai/typescript-sdk", () => {
65
+ const actual = jest.requireActual("@tambo-ai/typescript-sdk");
66
+ return {
67
+ __esModule: true,
68
+ ...actual,
69
+ advanceStream: jest.fn(),
70
+ };
71
+ });
34
72
  // Mock the getCustomContext
35
73
  jest.mock("../util/registry", () => ({
36
74
  ...jest.requireActual("../util/registry"),
@@ -73,26 +111,9 @@ const createMockAdvanceResponse = (overrides = {}) => ({
73
111
  });
74
112
  describe("TamboThreadProvider", () => {
75
113
  const mockThread = createMockThread();
76
- const mockThreadsApi = {
77
- messages: {
78
- create: jest.fn(),
79
- },
80
- retrieve: jest.fn(),
81
- advance: jest.fn(),
82
- advanceByID: jest.fn(),
83
- generateName: jest.fn(),
84
- };
85
- const mockProjectsApi = {
86
- getCurrent: jest.fn(),
87
- };
88
- const mockBeta = {
89
- threads: mockThreadsApi,
90
- projects: mockProjectsApi,
91
- };
92
- const mockTamboAI = {
93
- apiKey: "",
94
- beta: mockBeta,
95
- };
114
+ let mockTamboAI;
115
+ let mockThreadsApi;
116
+ let mockProjectsApi;
96
117
  let mockQueryClient;
97
118
  const mockRegistry = [
98
119
  {
@@ -144,8 +165,19 @@ describe("TamboThreadProvider", () => {
144
165
  };
145
166
  // Default wrapper for most tests
146
167
  const Wrapper = createWrapper();
168
+ afterEach(() => {
169
+ jest.restoreAllMocks();
170
+ });
147
171
  beforeEach(() => {
148
172
  jest.clearAllMocks();
173
+ mockTamboAI = new typescript_sdk_1.default({
174
+ apiKey: "",
175
+ fetch: () => {
176
+ throw new Error("Unexpected network call in test");
177
+ },
178
+ });
179
+ mockThreadsApi = mockTamboAI.beta.threads;
180
+ mockProjectsApi = mockTamboAI.beta.projects;
149
181
  // Setup mock query client
150
182
  mockQueryClient = {
151
183
  invalidateQueries: jest.fn().mockResolvedValue(undefined),
@@ -154,21 +186,22 @@ describe("TamboThreadProvider", () => {
154
186
  jest
155
187
  .mocked(tambo_client_provider_1.useTamboQueryClient)
156
188
  .mockReturnValue(mockQueryClient);
157
- jest.mocked(mockThreadsApi.retrieve).mockResolvedValue(mockThread);
189
+ jest.spyOn(mockThreadsApi, "retrieve").mockResolvedValue(mockThread);
158
190
  jest
159
- .mocked(mockThreadsApi.messages.create)
191
+ .spyOn(mockThreadsApi.messages, "create")
160
192
  .mockResolvedValue(createMockMessage());
161
193
  jest
162
- .mocked(mockThreadsApi.advance)
194
+ .spyOn(mockThreadsApi, "advance")
163
195
  .mockResolvedValue(createMockAdvanceResponse());
164
196
  jest
165
- .mocked(mockThreadsApi.advanceByID)
197
+ .spyOn(mockThreadsApi, "advanceByID")
166
198
  .mockResolvedValue(createMockAdvanceResponse());
167
- jest.mocked(mockThreadsApi.generateName).mockResolvedValue({
199
+ jest.spyOn(mockThreadsApi, "generateName").mockResolvedValue({
168
200
  ...mockThread,
169
201
  name: "Generated Thread Name",
170
202
  });
171
- jest.mocked(mockProjectsApi.getCurrent).mockResolvedValue({
203
+ jest.spyOn(mockThreadsApi, "update").mockResolvedValue({});
204
+ jest.spyOn(mockProjectsApi, "getCurrent").mockResolvedValue({
172
205
  id: "test-project-id",
173
206
  name: "Test Project",
174
207
  isTokenRequired: false,
@@ -236,7 +269,7 @@ describe("TamboThreadProvider", () => {
236
269
  });
237
270
  });
238
271
  it("should send a message and update thread state", async () => {
239
- const mockAdvanceResponse = {
272
+ const mockStreamResponse = {
240
273
  responseMessageDto: {
241
274
  id: "response-1",
242
275
  content: [{ type: "text", text: "Response" }],
@@ -249,14 +282,17 @@ describe("TamboThreadProvider", () => {
249
282
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
250
283
  mcpAccessToken: "test-mcp-access-token",
251
284
  };
252
- jest
253
- .mocked(mockThreadsApi.advanceByID)
254
- .mockResolvedValue(mockAdvanceResponse);
285
+ const mockAsyncIterator = {
286
+ [Symbol.asyncIterator]: async function* () {
287
+ yield mockStreamResponse;
288
+ },
289
+ };
290
+ jest.mocked(typescript_sdk_1.advanceStream).mockResolvedValue(mockAsyncIterator);
255
291
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), { wrapper: Wrapper });
256
292
  await (0, react_1.act)(async () => {
257
293
  await result.current.sendThreadMessage("Hello", {
258
294
  threadId: "test-thread-1",
259
- streamResponse: false,
295
+ streamResponse: true,
260
296
  additionalContext: {
261
297
  custom: {
262
298
  message: "additional instructions",
@@ -264,7 +300,7 @@ describe("TamboThreadProvider", () => {
264
300
  },
265
301
  });
266
302
  });
267
- expect(mockThreadsApi.advanceByID).toHaveBeenCalledWith("test-thread-1", {
303
+ expect(typescript_sdk_1.advanceStream).toHaveBeenCalledWith(expect.anything(), {
268
304
  messageToAppend: {
269
305
  content: [{ type: "text", text: "Hello" }],
270
306
  role: "user",
@@ -278,7 +314,7 @@ describe("TamboThreadProvider", () => {
278
314
  contextKey: undefined,
279
315
  clientTools: [],
280
316
  toolCallCounts: {},
281
- });
317
+ }, "test-thread-1");
282
318
  expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.COMPLETE);
283
319
  });
284
320
  it("should handle streaming responses", async () => {
@@ -313,7 +349,7 @@ describe("TamboThreadProvider", () => {
313
349
  expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.COMPLETE);
314
350
  });
315
351
  it("should handle tool calls during message processing.", async () => {
316
- const mockToolCallResponse = {
352
+ const mockToolCallChunk = {
317
353
  responseMessageDto: {
318
354
  id: "tool-call-1",
319
355
  content: [{ type: "text", text: "Tool response" }],
@@ -329,10 +365,7 @@ describe("TamboThreadProvider", () => {
329
365
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
330
366
  mcpAccessToken: "test-mcp-access-token",
331
367
  };
332
- jest
333
- .mocked(mockThreadsApi.advanceByID)
334
- .mockResolvedValueOnce(mockToolCallResponse)
335
- .mockResolvedValueOnce({
368
+ const mockFinalChunk = {
336
369
  responseMessageDto: {
337
370
  id: "advance-response2",
338
371
  content: [{ type: "text", text: "response 2" }],
@@ -343,12 +376,26 @@ describe("TamboThreadProvider", () => {
343
376
  },
344
377
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
345
378
  mcpAccessToken: "test-mcp-access-token",
346
- });
379
+ };
380
+ const mockAsyncIterator = {
381
+ [Symbol.asyncIterator]: async function* () {
382
+ yield mockToolCallChunk;
383
+ },
384
+ };
385
+ const mockAsyncIterator2 = {
386
+ [Symbol.asyncIterator]: async function* () {
387
+ yield mockFinalChunk;
388
+ },
389
+ };
390
+ jest
391
+ .mocked(typescript_sdk_1.advanceStream)
392
+ .mockResolvedValueOnce(mockAsyncIterator)
393
+ .mockResolvedValueOnce(mockAsyncIterator2);
347
394
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), { wrapper: Wrapper });
348
395
  await (0, react_1.act)(async () => {
349
396
  await result.current.sendThreadMessage("Use tool", {
350
397
  threadId: "test-thread-1",
351
- streamResponse: false,
398
+ streamResponse: true,
352
399
  });
353
400
  });
354
401
  expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.COMPLETE);
@@ -361,7 +408,7 @@ describe("TamboThreadProvider", () => {
361
408
  const mockOnCallUnregisteredTool = jest
362
409
  .fn()
363
410
  .mockResolvedValue("unregistered-tool-result");
364
- const mockUnregisteredToolCallResponse = {
411
+ const mockUnregisteredToolCallChunk = {
365
412
  responseMessageDto: {
366
413
  id: "unregistered-tool-call-1",
367
414
  content: [{ type: "text", text: "Unregistered tool response" }],
@@ -379,10 +426,7 @@ describe("TamboThreadProvider", () => {
379
426
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
380
427
  mcpAccessToken: "test-mcp-access-token",
381
428
  };
382
- jest
383
- .mocked(mockThreadsApi.advanceByID)
384
- .mockResolvedValueOnce(mockUnregisteredToolCallResponse)
385
- .mockResolvedValueOnce({
429
+ const mockFinalChunk = {
386
430
  responseMessageDto: {
387
431
  id: "advance-response2",
388
432
  content: [{ type: "text", text: "response 2" }],
@@ -393,7 +437,21 @@ describe("TamboThreadProvider", () => {
393
437
  },
394
438
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
395
439
  mcpAccessToken: "test-mcp-access-token",
396
- });
440
+ };
441
+ const mockAsyncIterator = {
442
+ [Symbol.asyncIterator]: async function* () {
443
+ yield mockUnregisteredToolCallChunk;
444
+ },
445
+ };
446
+ const mockAsyncIterator2 = {
447
+ [Symbol.asyncIterator]: async function* () {
448
+ yield mockFinalChunk;
449
+ },
450
+ };
451
+ jest
452
+ .mocked(typescript_sdk_1.advanceStream)
453
+ .mockResolvedValueOnce(mockAsyncIterator)
454
+ .mockResolvedValueOnce(mockAsyncIterator2);
397
455
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
398
456
  wrapper: createWrapper({
399
457
  onCallUnregisteredTool: mockOnCallUnregisteredTool,
@@ -402,14 +460,14 @@ describe("TamboThreadProvider", () => {
402
460
  await (0, react_1.act)(async () => {
403
461
  await result.current.sendThreadMessage("Use unregistered tool", {
404
462
  threadId: "test-thread-1",
405
- streamResponse: false,
463
+ streamResponse: true,
406
464
  });
407
465
  });
408
466
  expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.COMPLETE);
409
467
  expect(mockOnCallUnregisteredTool).toHaveBeenCalledWith("unregistered-tool", [{ parameterName: "input", parameterValue: "test-input" }]);
410
468
  });
411
469
  it("should handle unregistered tool calls without onCallUnregisteredTool", async () => {
412
- const mockUnregisteredToolCallResponse = {
470
+ const mockUnregisteredToolCallChunk = {
413
471
  responseMessageDto: {
414
472
  id: "unregistered-tool-call-1",
415
473
  content: [{ type: "text", text: "Unregistered tool response" }],
@@ -427,10 +485,7 @@ describe("TamboThreadProvider", () => {
427
485
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
428
486
  mcpAccessToken: "test-mcp-access-token",
429
487
  };
430
- jest
431
- .mocked(mockThreadsApi.advanceByID)
432
- .mockResolvedValueOnce(mockUnregisteredToolCallResponse)
433
- .mockResolvedValueOnce({
488
+ const mockFinalChunk = {
434
489
  responseMessageDto: {
435
490
  id: "advance-response2",
436
491
  content: [{ type: "text", text: "response 2" }],
@@ -441,12 +496,26 @@ describe("TamboThreadProvider", () => {
441
496
  },
442
497
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
443
498
  mcpAccessToken: "test-mcp-access-token",
444
- });
499
+ };
500
+ const mockAsyncIterator = {
501
+ [Symbol.asyncIterator]: async function* () {
502
+ yield mockUnregisteredToolCallChunk;
503
+ },
504
+ };
505
+ const mockAsyncIterator2 = {
506
+ [Symbol.asyncIterator]: async function* () {
507
+ yield mockFinalChunk;
508
+ },
509
+ };
510
+ jest
511
+ .mocked(typescript_sdk_1.advanceStream)
512
+ .mockResolvedValueOnce(mockAsyncIterator)
513
+ .mockResolvedValueOnce(mockAsyncIterator2);
445
514
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), { wrapper: Wrapper });
446
515
  await (0, react_1.act)(async () => {
447
516
  await result.current.sendThreadMessage("Use unregistered tool", {
448
517
  threadId: "test-thread-1",
449
- streamResponse: false,
518
+ streamResponse: true,
450
519
  });
451
520
  });
452
521
  expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.COMPLETE);
@@ -507,77 +576,16 @@ describe("TamboThreadProvider", () => {
507
576
  expect(mockThreadsApi.advance).not.toHaveBeenCalled();
508
577
  expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
509
578
  });
510
- it("should call advanceById when streamResponse=false for existing thread", async () => {
511
- // Use wrapper with streaming=true to show that explicit streamResponse=false overrides provider setting
579
+ it("should throw error when streamResponse=false (non-streaming not supported)", async () => {
512
580
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
513
581
  wrapper: createWrapper({ streaming: true }),
514
582
  });
515
583
  await (0, react_1.act)(async () => {
516
- await result.current.sendThreadMessage("Hello non-streaming", {
584
+ await expect(result.current.sendThreadMessage("Hello non-streaming", {
517
585
  threadId: "test-thread-1",
518
586
  streamResponse: false,
519
- additionalContext: {
520
- custom: {
521
- message: "additional instructions",
522
- },
523
- },
524
- });
587
+ })).rejects.toThrow();
525
588
  });
526
- expect(mockThreadsApi.advanceByID).toHaveBeenCalledWith("test-thread-1", {
527
- messageToAppend: {
528
- content: [{ type: "text", text: "Hello non-streaming" }],
529
- role: "user",
530
- additionalContext: {
531
- custom: {
532
- message: "additional instructions",
533
- },
534
- },
535
- },
536
- availableComponents: (0, tools_1.serializeRegistry)(mockRegistry),
537
- contextKey: undefined,
538
- clientTools: [],
539
- forceToolChoice: undefined,
540
- toolCallCounts: {},
541
- });
542
- // Should not call advance or advanceStream
543
- expect(mockThreadsApi.advance).not.toHaveBeenCalled();
544
- expect(typescript_sdk_1.advanceStream).not.toHaveBeenCalled();
545
- });
546
- it("should call advanceById when streamResponse is undefined and provider streaming=false", async () => {
547
- // Use wrapper with streaming=false to test that undefined streamResponse respects provider setting
548
- const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
549
- wrapper: createWrapper({ streaming: false }),
550
- });
551
- await (0, react_1.act)(async () => {
552
- await result.current.sendThreadMessage("Hello default", {
553
- threadId: "test-thread-1",
554
- // streamResponse is undefined, should use provider's streaming=false
555
- additionalContext: {
556
- custom: {
557
- message: "additional instructions",
558
- },
559
- },
560
- });
561
- });
562
- expect(mockThreadsApi.advanceByID).toHaveBeenCalledWith("test-thread-1", {
563
- messageToAppend: {
564
- content: [{ type: "text", text: "Hello default" }],
565
- role: "user",
566
- additionalContext: {
567
- custom: {
568
- message: "additional instructions",
569
- },
570
- },
571
- },
572
- availableComponents: (0, tools_1.serializeRegistry)(mockRegistry),
573
- contextKey: undefined,
574
- clientTools: [],
575
- forceToolChoice: undefined,
576
- toolCallCounts: {},
577
- });
578
- // Should not call advance or advanceStream
579
- expect(mockThreadsApi.advance).not.toHaveBeenCalled();
580
- expect(typescript_sdk_1.advanceStream).not.toHaveBeenCalled();
581
589
  });
582
590
  it("should call advanceStream when streamResponse is undefined and provider streaming=true (default)", async () => {
583
591
  const mockStreamResponse = {
@@ -633,44 +641,6 @@ describe("TamboThreadProvider", () => {
633
641
  expect(mockThreadsApi.advance).not.toHaveBeenCalled();
634
642
  expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
635
643
  });
636
- it("should call advance when streamResponse=false for placeholder thread", async () => {
637
- // Use wrapper with streaming=true to show that explicit streamResponse=false overrides provider setting
638
- const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
639
- wrapper: createWrapper({ streaming: true }),
640
- });
641
- // Start with placeholder thread (which is the default state)
642
- expect(result.current.thread.id).toBe("placeholder");
643
- await (0, react_1.act)(async () => {
644
- await result.current.sendThreadMessage("Hello new thread", {
645
- threadId: "placeholder",
646
- streamResponse: false,
647
- additionalContext: {
648
- custom: {
649
- message: "additional instructions",
650
- },
651
- },
652
- });
653
- });
654
- expect(mockThreadsApi.advance).toHaveBeenCalledWith({
655
- messageToAppend: {
656
- content: [{ type: "text", text: "Hello new thread" }],
657
- role: "user",
658
- additionalContext: {
659
- custom: {
660
- message: "additional instructions",
661
- },
662
- },
663
- },
664
- availableComponents: (0, tools_1.serializeRegistry)(mockRegistry),
665
- contextKey: undefined,
666
- clientTools: [],
667
- forceToolChoice: undefined,
668
- toolCallCounts: {},
669
- });
670
- // Should not call advanceById or advanceStream
671
- expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
672
- expect(typescript_sdk_1.advanceStream).not.toHaveBeenCalled();
673
- });
674
644
  it("should call advanceStream when streamResponse=true for placeholder thread", async () => {
675
645
  const mockStreamResponse = {
676
646
  responseMessageDto: {
@@ -727,26 +697,96 @@ describe("TamboThreadProvider", () => {
727
697
  expect(mockThreadsApi.advance).not.toHaveBeenCalled();
728
698
  expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
729
699
  });
730
- });
731
- describe("error handling", () => {
732
- it("should set generation stage to ERROR when non-streaming sendThreadMessage fails", async () => {
733
- const testError = new Error("API call failed");
734
- // Mock advanceById to throw an error
735
- jest.mocked(mockThreadsApi.advanceByID).mockRejectedValue(testError);
700
+ it("should handle multiple sequential messages during streaming (server tool scenario)", async () => {
701
+ // This test verifies the fix for the bug where the second message doesn't render
702
+ // during server tool response streaming. The scenario:
703
+ // 1. First message: "I will call the tool..." with statusMessage
704
+ // 2. Second message: The tool result response streaming in
705
+ // First message - tool announcement (server tools don't have componentName set during streaming)
706
+ const mockFirstMessage = {
707
+ responseMessageDto: {
708
+ id: "msg-first",
709
+ content: [{ type: "text", text: "I will search the docs..." }],
710
+ role: "assistant",
711
+ threadId: "test-thread-1",
712
+ component: {
713
+ componentName: "",
714
+ componentState: {},
715
+ message: "",
716
+ props: {},
717
+ statusMessage: "searching the Tambo docs...",
718
+ },
719
+ componentState: {},
720
+ createdAt: new Date().toISOString(),
721
+ },
722
+ generationStage: generate_component_response_1.GenerationStage.STREAMING_RESPONSE,
723
+ mcpAccessToken: "test-mcp-access-token",
724
+ };
725
+ // Second message - tool result (different ID!)
726
+ const mockSecondMessageChunk1 = {
727
+ responseMessageDto: {
728
+ id: "msg-second",
729
+ content: [{ type: "text", text: "Here's what I found..." }],
730
+ role: "assistant",
731
+ threadId: "test-thread-1",
732
+ componentState: {},
733
+ createdAt: new Date().toISOString(),
734
+ },
735
+ generationStage: generate_component_response_1.GenerationStage.STREAMING_RESPONSE,
736
+ mcpAccessToken: "test-mcp-access-token",
737
+ };
738
+ const mockSecondMessageChunk2 = {
739
+ responseMessageDto: {
740
+ id: "msg-second",
741
+ content: [
742
+ {
743
+ type: "text",
744
+ text: "Here's what I found in the documentation about that topic.",
745
+ },
746
+ ],
747
+ role: "assistant",
748
+ threadId: "test-thread-1",
749
+ componentState: {},
750
+ createdAt: new Date().toISOString(),
751
+ },
752
+ generationStage: generate_component_response_1.GenerationStage.COMPLETE,
753
+ mcpAccessToken: "test-mcp-access-token",
754
+ };
755
+ const mockAsyncIterator = {
756
+ [Symbol.asyncIterator]: async function* () {
757
+ yield mockFirstMessage;
758
+ yield mockSecondMessageChunk1;
759
+ yield mockSecondMessageChunk2;
760
+ },
761
+ };
762
+ jest.mocked(typescript_sdk_1.advanceStream).mockResolvedValue(mockAsyncIterator);
736
763
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
737
- wrapper: Wrapper,
764
+ wrapper: createWrapper({ streaming: true }),
738
765
  });
739
- // Expect the error to be thrown
740
766
  await (0, react_1.act)(async () => {
741
- await result.current.switchCurrentThread("test-thread-1");
742
- await expect(result.current.sendThreadMessage("Hello", {
767
+ await result.current.sendThreadMessage("Search the docs", {
743
768
  threadId: "test-thread-1",
744
- streamResponse: false,
745
- })).rejects.toThrow("API call failed");
769
+ streamResponse: true,
770
+ });
746
771
  });
747
- // Verify generation stage is set to ERROR
748
- expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.ERROR);
772
+ // Thread should have 3 messages: user message + 2 assistant messages
773
+ expect(result.current.thread.messages).toHaveLength(3);
774
+ // Filter to assistant messages only
775
+ const assistantMessages = result.current.thread.messages.filter((m) => m.role === "assistant");
776
+ expect(assistantMessages).toHaveLength(2);
777
+ // First assistant message should have the tool status
778
+ const firstMsg = result.current.thread.messages.find((m) => m.id === "msg-first");
779
+ expect(firstMsg).toBeDefined();
780
+ expect(firstMsg?.content[0]?.text).toContain("search the docs");
781
+ // Second assistant message should have the final content
782
+ const secondMsg = result.current.thread.messages.find((m) => m.id === "msg-second");
783
+ expect(secondMsg).toBeDefined();
784
+ expect(secondMsg?.content[0]?.text).toContain("what I found in the documentation");
785
+ // Generation should be complete
786
+ expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.COMPLETE);
749
787
  });
788
+ });
789
+ describe("error handling", () => {
750
790
  it("should set generation stage to ERROR when streaming sendThreadMessage fails", async () => {
751
791
  const testError = new Error("Streaming API call failed");
752
792
  // Mock advanceStream to throw an error
@@ -765,24 +805,85 @@ describe("TamboThreadProvider", () => {
765
805
  // Verify generation stage is set to ERROR
766
806
  expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.ERROR);
767
807
  });
768
- it("should set generation stage to ERROR when advance API call fails for placeholder thread", async () => {
769
- const testError = new Error("Advance API call failed");
770
- // Mock advance to throw an error
771
- jest.mocked(mockThreadsApi.advance).mockRejectedValue(testError);
808
+ it("should rollback optimistic user message when sendThreadMessage fails", async () => {
809
+ const testError = new Error("API call failed");
810
+ jest.mocked(typescript_sdk_1.advanceStream).mockRejectedValue(testError);
772
811
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
773
812
  wrapper: Wrapper,
774
813
  });
775
- // Start with placeholder thread (which is the default state)
776
- expect(result.current.thread.id).toBe("placeholder");
777
- // Expect the error to be thrown
814
+ await (0, react_1.act)(async () => {
815
+ await result.current.switchCurrentThread("test-thread-1");
816
+ });
817
+ const initialMessageCount = result.current.thread.messages.length;
778
818
  await (0, react_1.act)(async () => {
779
819
  await expect(result.current.sendThreadMessage("Hello", {
780
- threadId: "placeholder",
781
- streamResponse: false,
782
- })).rejects.toThrow("Advance API call failed");
820
+ threadId: "test-thread-1",
821
+ streamResponse: true,
822
+ })).rejects.toThrow("API call failed");
783
823
  });
784
- // Verify generation stage is set to ERROR
785
- expect(result.current.generationStage).toBe(generate_component_response_1.GenerationStage.ERROR);
824
+ // Verify user message was rolled back
825
+ expect(result.current.thread.messages.length).toBe(initialMessageCount);
826
+ });
827
+ it("should rollback optimistic message when addThreadMessage fails", async () => {
828
+ const testError = new Error("Create message failed");
829
+ jest.mocked(mockThreadsApi.messages.create).mockRejectedValue(testError);
830
+ const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
831
+ wrapper: Wrapper,
832
+ });
833
+ await (0, react_1.act)(async () => {
834
+ await result.current.switchCurrentThread("test-thread-1");
835
+ });
836
+ const initialMessageCount = result.current.thread.messages.length;
837
+ const newMessage = createMockMessage({ threadId: "test-thread-1" });
838
+ await (0, react_1.act)(async () => {
839
+ await expect(result.current.addThreadMessage(newMessage, true)).rejects.toThrow("Create message failed");
840
+ });
841
+ // Verify message was rolled back
842
+ expect(result.current.thread.messages.length).toBe(initialMessageCount);
843
+ });
844
+ it("should rollback optimistic update when updateThreadMessage fails", async () => {
845
+ const testError = new Error("Update message failed");
846
+ jest.mocked(mockThreadsApi.messages.create).mockRejectedValue(testError);
847
+ const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
848
+ wrapper: Wrapper,
849
+ });
850
+ await (0, react_1.act)(async () => {
851
+ await result.current.switchCurrentThread("test-thread-1");
852
+ });
853
+ const existingMessage = createMockMessage({
854
+ id: "existing-msg",
855
+ threadId: "test-thread-1",
856
+ content: [{ type: "text", text: "Old content" }],
857
+ });
858
+ await (0, react_1.act)(async () => {
859
+ await result.current.addThreadMessage(existingMessage, false);
860
+ });
861
+ const initialMessageCount = result.current.thread.messages.length;
862
+ await (0, react_1.act)(async () => {
863
+ await expect(result.current.updateThreadMessage("existing-msg", {
864
+ threadId: "test-thread-1",
865
+ content: [{ type: "text", text: "New content" }],
866
+ role: "assistant",
867
+ }, true)).rejects.toThrow("Update message failed");
868
+ });
869
+ // Verify message was rolled back
870
+ expect(result.current.thread.messages.length).toBe(initialMessageCount - 1);
871
+ });
872
+ it("should rollback optimistic name update when updateThreadName fails", async () => {
873
+ const testError = new Error("Update name failed");
874
+ jest.mocked(mockThreadsApi.update).mockRejectedValue(testError);
875
+ const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
876
+ wrapper: Wrapper,
877
+ });
878
+ await (0, react_1.act)(async () => {
879
+ await result.current.switchCurrentThread("test-thread-1");
880
+ });
881
+ const initialName = result.current.thread.name;
882
+ await (0, react_1.act)(async () => {
883
+ await expect(result.current.updateThreadName("New Name", "test-thread-1")).rejects.toThrow("Update name failed");
884
+ });
885
+ // Verify name was rolled back
886
+ expect(result.current.thread.name).toBe(initialName);
786
887
  });
787
888
  });
788
889
  describe("refetch threads list behavior", () => {
@@ -790,8 +891,8 @@ describe("TamboThreadProvider", () => {
790
891
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
791
892
  wrapper: Wrapper,
792
893
  });
793
- // Mock the advance response to return a new thread ID
794
- const mockAdvanceResponse = {
894
+ // Mock the stream response to return a new thread ID
895
+ const mockStreamResponse = {
795
896
  responseMessageDto: {
796
897
  id: "response-1",
797
898
  content: [{ type: "text", text: "Response" }],
@@ -804,16 +905,19 @@ describe("TamboThreadProvider", () => {
804
905
  generationStage: generate_component_response_1.GenerationStage.COMPLETE,
805
906
  mcpAccessToken: "test-mcp-access-token",
806
907
  };
807
- jest
808
- .mocked(mockThreadsApi.advance)
809
- .mockResolvedValue(mockAdvanceResponse);
908
+ const mockAsyncIterator = {
909
+ [Symbol.asyncIterator]: async function* () {
910
+ yield mockStreamResponse;
911
+ },
912
+ };
913
+ jest.mocked(typescript_sdk_1.advanceStream).mockResolvedValue(mockAsyncIterator);
810
914
  // Start with placeholder thread
811
915
  expect(result.current.thread.id).toBe("placeholder");
812
916
  // Send a message which will create a new thread with contextKey
813
917
  await (0, react_1.act)(async () => {
814
918
  await result.current.sendThreadMessage("Hello", {
815
919
  threadId: "placeholder",
816
- streamResponse: false,
920
+ streamResponse: true,
817
921
  contextKey: "test-context-key",
818
922
  });
819
923
  });
@@ -851,93 +955,6 @@ describe("TamboThreadProvider", () => {
851
955
  });
852
956
  });
853
957
  describe("transformToContent", () => {
854
- it("should use custom transformToContent when provided (non-streaming)", async () => {
855
- const mockTransformToContent = jest.fn().mockReturnValue([
856
- { type: "text", text: "Custom transformed content" },
857
- {
858
- type: "image_url",
859
- image_url: { url: "https://example.com/image.png" },
860
- },
861
- ]);
862
- const customToolRegistry = [
863
- {
864
- name: "TestComponent",
865
- component: () => react_2.default.createElement("div", null, "Test"),
866
- description: "Test",
867
- propsSchema: v4_1.z.object({ test: v4_1.z.string() }),
868
- associatedTools: [
869
- {
870
- name: "custom-tool",
871
- tool: jest.fn().mockResolvedValue({ data: "tool result" }),
872
- description: "Tool with custom transform",
873
- inputSchema: v4_1.z.object({ input: v4_1.z.string() }),
874
- outputSchema: v4_1.z.object({ data: v4_1.z.string() }),
875
- transformToContent: mockTransformToContent,
876
- },
877
- ],
878
- },
879
- ];
880
- const mockToolCallResponse = {
881
- responseMessageDto: {
882
- id: "tool-call-1",
883
- content: [{ type: "text", text: "Tool response" }],
884
- role: "tool",
885
- threadId: "test-thread-1",
886
- toolCallRequest: {
887
- toolName: "custom-tool",
888
- parameters: [{ parameterName: "input", parameterValue: "test" }],
889
- },
890
- componentState: {},
891
- createdAt: new Date().toISOString(),
892
- },
893
- generationStage: generate_component_response_1.GenerationStage.COMPLETE,
894
- mcpAccessToken: "test-mcp-access-token",
895
- };
896
- jest
897
- .mocked(mockThreadsApi.advanceByID)
898
- .mockResolvedValueOnce(mockToolCallResponse)
899
- .mockResolvedValueOnce({
900
- responseMessageDto: {
901
- id: "final-response",
902
- content: [{ type: "text", text: "Final response" }],
903
- role: "assistant",
904
- threadId: "test-thread-1",
905
- componentState: {},
906
- createdAt: new Date().toISOString(),
907
- },
908
- generationStage: generate_component_response_1.GenerationStage.COMPLETE,
909
- mcpAccessToken: "test-mcp-access-token",
910
- });
911
- const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
912
- wrapper: createWrapper({ components: customToolRegistry }),
913
- });
914
- await (0, react_1.act)(async () => {
915
- await result.current.sendThreadMessage("Use custom tool", {
916
- threadId: "test-thread-1",
917
- streamResponse: false,
918
- });
919
- });
920
- // Verify the tool was called with single object arg (new inputSchema interface)
921
- expect(customToolRegistry[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith({ input: "test" });
922
- // Verify transformToContent was called with the tool result
923
- expect(mockTransformToContent).toHaveBeenCalledWith({
924
- data: "tool result",
925
- });
926
- // Verify the second advance call included the transformed content
927
- expect(mockThreadsApi.advanceByID).toHaveBeenCalledTimes(2);
928
- expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
929
- messageToAppend: expect.objectContaining({
930
- content: [
931
- { type: "text", text: "Custom transformed content" },
932
- {
933
- type: "image_url",
934
- image_url: { url: "https://example.com/image.png" },
935
- },
936
- ],
937
- role: "tool",
938
- }),
939
- }));
940
- });
941
958
  it("should use custom async transformToContent when provided (streaming)", async () => {
942
959
  const mockTransformToContent = jest
943
960
  .fn()
@@ -1034,169 +1051,6 @@ describe("TamboThreadProvider", () => {
1034
1051
  }),
1035
1052
  }), "test-thread-1");
1036
1053
  });
1037
- it("should fallback to stringified text when transformToContent is not provided", async () => {
1038
- const toolWithoutTransform = [
1039
- {
1040
- name: "TestComponent",
1041
- component: () => react_2.default.createElement("div", null, "Test"),
1042
- description: "Test",
1043
- propsSchema: v4_1.z.object({ test: v4_1.z.string() }),
1044
- associatedTools: [
1045
- {
1046
- name: "no-transform-tool",
1047
- tool: jest
1048
- .fn()
1049
- .mockResolvedValue({ complex: "data", nested: { value: 42 } }),
1050
- description: "Tool without custom transform",
1051
- inputSchema: v4_1.z.object({ input: v4_1.z.string() }),
1052
- outputSchema: v4_1.z.object({
1053
- complex: v4_1.z.string(),
1054
- nested: v4_1.z.object({ value: v4_1.z.number() }),
1055
- }),
1056
- // No transformToContent provided
1057
- },
1058
- ],
1059
- },
1060
- ];
1061
- const mockToolCallResponse = {
1062
- responseMessageDto: {
1063
- id: "tool-call-1",
1064
- content: [{ type: "text", text: "Tool call" }],
1065
- role: "tool",
1066
- threadId: "test-thread-1",
1067
- toolCallRequest: {
1068
- toolName: "no-transform-tool",
1069
- parameters: [{ parameterName: "input", parameterValue: "test" }],
1070
- },
1071
- componentState: {},
1072
- createdAt: new Date().toISOString(),
1073
- },
1074
- generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1075
- mcpAccessToken: "test-mcp-access-token",
1076
- };
1077
- jest
1078
- .mocked(mockThreadsApi.advanceByID)
1079
- .mockResolvedValueOnce(mockToolCallResponse)
1080
- .mockResolvedValueOnce({
1081
- responseMessageDto: {
1082
- id: "final-response",
1083
- content: [{ type: "text", text: "Final response" }],
1084
- role: "assistant",
1085
- threadId: "test-thread-1",
1086
- componentState: {},
1087
- createdAt: new Date().toISOString(),
1088
- },
1089
- generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1090
- mcpAccessToken: "test-mcp-access-token",
1091
- });
1092
- const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
1093
- wrapper: createWrapper({ components: toolWithoutTransform }),
1094
- });
1095
- await (0, react_1.act)(async () => {
1096
- await result.current.sendThreadMessage("Use tool without transform", {
1097
- threadId: "test-thread-1",
1098
- streamResponse: false,
1099
- });
1100
- });
1101
- // Verify the tool was called with single object arg (new inputSchema interface)
1102
- expect(toolWithoutTransform[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith({ input: "test" });
1103
- // Verify the second advance call used stringified content
1104
- expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
1105
- messageToAppend: expect.objectContaining({
1106
- content: [
1107
- {
1108
- type: "text",
1109
- text: '{"complex":"data","nested":{"value":42}}',
1110
- },
1111
- ],
1112
- role: "tool",
1113
- }),
1114
- }));
1115
- });
1116
- it("should always return text for error responses even with transformToContent", async () => {
1117
- const mockTransformToContent = jest.fn().mockReturnValue([
1118
- {
1119
- type: "image_url",
1120
- image_url: { url: "https://example.com/error.png" },
1121
- },
1122
- ]);
1123
- const toolWithTransform = [
1124
- {
1125
- name: "TestComponent",
1126
- component: () => react_2.default.createElement("div", null, "Test"),
1127
- description: "Test",
1128
- propsSchema: v4_1.z.object({ test: v4_1.z.string() }),
1129
- associatedTools: [
1130
- {
1131
- name: "error-tool",
1132
- tool: jest
1133
- .fn()
1134
- .mockRejectedValue(new Error("Tool execution failed")),
1135
- description: "Tool that errors",
1136
- inputSchema: v4_1.z.object({ input: v4_1.z.string() }),
1137
- outputSchema: v4_1.z.string(),
1138
- transformToContent: mockTransformToContent,
1139
- },
1140
- ],
1141
- },
1142
- ];
1143
- const mockToolCallResponse = {
1144
- responseMessageDto: {
1145
- id: "tool-call-1",
1146
- content: [{ type: "text", text: "Tool call" }],
1147
- role: "tool",
1148
- threadId: "test-thread-1",
1149
- toolCallRequest: {
1150
- toolName: "error-tool",
1151
- parameters: [{ parameterName: "input", parameterValue: "test" }],
1152
- },
1153
- componentState: {},
1154
- createdAt: new Date().toISOString(),
1155
- },
1156
- generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1157
- mcpAccessToken: "test-mcp-access-token",
1158
- };
1159
- jest
1160
- .mocked(mockThreadsApi.advanceByID)
1161
- .mockResolvedValueOnce(mockToolCallResponse)
1162
- .mockResolvedValueOnce({
1163
- responseMessageDto: {
1164
- id: "final-response",
1165
- content: [{ type: "text", text: "Final response" }],
1166
- role: "assistant",
1167
- threadId: "test-thread-1",
1168
- componentState: {},
1169
- createdAt: new Date().toISOString(),
1170
- },
1171
- generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1172
- mcpAccessToken: "test-mcp-access-token",
1173
- });
1174
- const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
1175
- wrapper: createWrapper({ components: toolWithTransform }),
1176
- });
1177
- await (0, react_1.act)(async () => {
1178
- await result.current.sendThreadMessage("Use error tool", {
1179
- threadId: "test-thread-1",
1180
- streamResponse: false,
1181
- });
1182
- });
1183
- // Verify the tool was called with single object arg (new inputSchema interface)
1184
- expect(toolWithTransform[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith({ input: "test" });
1185
- // Verify transformToContent was NOT called for error responses
1186
- expect(mockTransformToContent).not.toHaveBeenCalled();
1187
- // Verify the second advance call used text content with the error message
1188
- expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
1189
- messageToAppend: expect.objectContaining({
1190
- content: [
1191
- expect.objectContaining({
1192
- type: "text",
1193
- // Error message should be in text format
1194
- }),
1195
- ],
1196
- role: "tool",
1197
- }),
1198
- }));
1199
- });
1200
1054
  });
1201
1055
  describe("tamboStreamableHint streaming behavior", () => {
1202
1056
  it("should call streamable tool during streaming when tamboStreamableHint is true", async () => {
@@ -1529,6 +1383,25 @@ describe("TamboThreadProvider", () => {
1529
1383
  });
1530
1384
  describe("auto-generate thread name", () => {
1531
1385
  it("should auto-generate thread name after reaching threshold", async () => {
1386
+ const mockStreamResponse = {
1387
+ responseMessageDto: {
1388
+ id: "response-1",
1389
+ content: [{ type: "text", text: "Response" }],
1390
+ role: "assistant",
1391
+ threadId: "test-thread-1",
1392
+ component: undefined,
1393
+ componentState: {},
1394
+ createdAt: new Date().toISOString(),
1395
+ },
1396
+ generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1397
+ mcpAccessToken: "test-mcp-access-token",
1398
+ };
1399
+ const mockAsyncIterator = {
1400
+ [Symbol.asyncIterator]: async function* () {
1401
+ yield mockStreamResponse;
1402
+ },
1403
+ };
1404
+ jest.mocked(typescript_sdk_1.advanceStream).mockResolvedValue(mockAsyncIterator);
1532
1405
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
1533
1406
  wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
1534
1407
  });
@@ -1560,13 +1433,34 @@ describe("TamboThreadProvider", () => {
1560
1433
  }), false);
1561
1434
  });
1562
1435
  await (0, react_1.act)(async () => {
1563
- await result.current.sendThreadMessage("Test message");
1436
+ await result.current.sendThreadMessage("Test message", {
1437
+ streamResponse: true,
1438
+ });
1564
1439
  });
1565
1440
  expect(mockThreadsApi.generateName).toHaveBeenCalledWith("test-thread-1");
1566
1441
  expect(result.current.thread.name).toBe("Generated Thread Name");
1567
1442
  expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(["threads", "test-project-id", undefined], expect.any(Function));
1568
1443
  });
1569
1444
  it("should NOT auto-generate when autoGenerateThreadName is false", async () => {
1445
+ const mockStreamResponse = {
1446
+ responseMessageDto: {
1447
+ id: "response-1",
1448
+ content: [{ type: "text", text: "Response" }],
1449
+ role: "assistant",
1450
+ threadId: "test-thread-1",
1451
+ component: undefined,
1452
+ componentState: {},
1453
+ createdAt: new Date().toISOString(),
1454
+ },
1455
+ generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1456
+ mcpAccessToken: "test-mcp-access-token",
1457
+ };
1458
+ const mockAsyncIterator = {
1459
+ [Symbol.asyncIterator]: async function* () {
1460
+ yield mockStreamResponse;
1461
+ },
1462
+ };
1463
+ jest.mocked(typescript_sdk_1.advanceStream).mockResolvedValue(mockAsyncIterator);
1570
1464
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
1571
1465
  wrapper: createWrapper({
1572
1466
  autoGenerateThreadName: false,
@@ -1598,12 +1492,33 @@ describe("TamboThreadProvider", () => {
1598
1492
  }), false);
1599
1493
  });
1600
1494
  await (0, react_1.act)(async () => {
1601
- await result.current.sendThreadMessage("Test message");
1495
+ await result.current.sendThreadMessage("Test message", {
1496
+ streamResponse: true,
1497
+ });
1602
1498
  });
1603
1499
  // Should NOT generate name because feature is disabled
1604
1500
  expect(mockThreadsApi.generateName).not.toHaveBeenCalled();
1605
1501
  });
1606
1502
  it("should NOT auto-generate when thread already has a name", async () => {
1503
+ const mockStreamResponse = {
1504
+ responseMessageDto: {
1505
+ id: "response-1",
1506
+ content: [{ type: "text", text: "Response" }],
1507
+ role: "assistant",
1508
+ threadId: "test-thread-1",
1509
+ component: undefined,
1510
+ componentState: {},
1511
+ createdAt: new Date().toISOString(),
1512
+ },
1513
+ generationStage: generate_component_response_1.GenerationStage.COMPLETE,
1514
+ mcpAccessToken: "test-mcp-access-token",
1515
+ };
1516
+ const mockAsyncIterator = {
1517
+ [Symbol.asyncIterator]: async function* () {
1518
+ yield mockStreamResponse;
1519
+ },
1520
+ };
1521
+ jest.mocked(typescript_sdk_1.advanceStream).mockResolvedValue(mockAsyncIterator);
1607
1522
  const { result } = (0, react_1.renderHook)(() => (0, tambo_thread_provider_1.useTamboThread)(), {
1608
1523
  wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
1609
1524
  });
@@ -1637,7 +1552,9 @@ describe("TamboThreadProvider", () => {
1637
1552
  expect(result.current.thread.messages).toHaveLength(2);
1638
1553
  // Send another message to reach threshold (3 messages total)
1639
1554
  await (0, react_1.act)(async () => {
1640
- await result.current.sendThreadMessage("Test message");
1555
+ await result.current.sendThreadMessage("Test message", {
1556
+ streamResponse: true,
1557
+ });
1641
1558
  });
1642
1559
  // Should NOT generate name because thread already has one
1643
1560
  expect(mockThreadsApi.generateName).not.toHaveBeenCalled();