@assistant-ui/core 0.3.9 → 0.3.11

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 (40) hide show
  1. package/dist/model-context/frame/provider.js +1 -3
  2. package/dist/model-context/frame/provider.js.map +1 -1
  3. package/dist/model-context/types.js +4 -2
  4. package/dist/model-context/types.js.map +1 -1
  5. package/dist/react/AssistantProvider.js +5 -4
  6. package/dist/react/AssistantProvider.js.map +1 -1
  7. package/dist/react/adapters/LocalStorageThreadListAdapter.js +2 -1
  8. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  9. package/dist/react/client/Interactables.js +12 -10
  10. package/dist/react/client/Interactables.js.map +1 -1
  11. package/dist/react/model-context/useInteractable.js.map +1 -1
  12. package/dist/react/primitives/message/MessageGroupedParts.js +11 -8
  13. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  14. package/dist/react/primitives/message/MessageParts.js +27 -22
  15. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  16. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  17. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +22 -14
  18. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  19. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  20. package/dist/react/runtimes/external-message-converter.js +18 -3
  21. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  22. package/dist/react/runtimes/useLocalRuntime.js +2 -1
  23. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  24. package/dist/runtime/api/message-runtime.js +4 -2
  25. package/dist/runtime/api/message-runtime.js.map +1 -1
  26. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -4
  27. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  28. package/dist/runtimes/local/local-thread-runtime-core.js +9 -6
  29. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  30. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +31 -28
  31. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  32. package/dist/subscribable/subscribable.js +6 -4
  33. package/dist/subscribable/subscribable.js.map +1 -1
  34. package/package.json +10 -10
  35. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +49 -0
  36. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +11 -2
  37. package/src/react/runtimes/external-message-converter.test.ts +71 -0
  38. package/src/react/runtimes/external-message-converter.test.tsx +9 -0
  39. package/src/react/runtimes/external-message-converter.ts +35 -2
  40. package/src/runtimes/local/local-thread-runtime-core.test.ts +18 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/core",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Framework-agnostic core runtime for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -59,8 +59,8 @@
59
59
  ],
60
60
  "sideEffects": false,
61
61
  "dependencies": {
62
- "assistant-stream": "^0.3.35",
63
- "nanoid": "^6.0.0"
62
+ "assistant-stream": "^0.3.37",
63
+ "nanoid": "^6.0.1"
64
64
  },
65
65
  "optionalDevDependencies": {
66
66
  "@assistant-ui/x-generative-compiler": ">=0.0.3"
@@ -89,17 +89,17 @@
89
89
  },
90
90
  "devDependencies": {
91
91
  "@testing-library/react": "^16.3.2",
92
- "@types/react": "^19.2.17",
93
- "jsdom": "^29.1.1",
92
+ "@types/react": "^19.2.18",
93
+ "jsdom": "^30.0.1",
94
94
  "react": "^19.2.8",
95
95
  "react-dom": "^19.2.8",
96
96
  "vitest": "^4.1.10",
97
97
  "zustand": "^5.0.14",
98
- "@assistant-ui/tap": "0.9.10",
99
- "@assistant-ui/vite": "0.0.11",
100
- "@assistant-ui/store": "0.3.7",
101
- "@assistant-ui/x-buildutils": "0.0.21",
102
- "assistant-cloud": "0.1.38"
98
+ "@assistant-ui/store": "0.3.8",
99
+ "@assistant-ui/tap": "0.9.11",
100
+ "@assistant-ui/vite": "0.0.12",
101
+ "@assistant-ui/x-buildutils": "0.0.22",
102
+ "assistant-cloud": "0.1.39"
103
103
  },
104
104
  "publishConfig": {
105
105
  "access": "public",
@@ -0,0 +1,49 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+ import type { AssistantCloud } from "assistant-cloud";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { useCloudThreadListAdapter } from "./useCloudThreadListAdapter";
7
+
8
+ const makeThread = (id: string) => ({
9
+ id,
10
+ title: id,
11
+ is_archived: false,
12
+ external_id: null,
13
+ metadata: null,
14
+ last_message_at: new Date(0),
15
+ created_at: new Date(0),
16
+ updated_at: new Date(0),
17
+ project_id: "project-1",
18
+ workspace_id: "workspace-1",
19
+ });
20
+
21
+ describe("useCloudThreadListAdapter", () => {
22
+ it("exposes full Cloud pages through the remote thread cursor", async () => {
23
+ const firstPage = Array.from({ length: 20 }, (_, index) =>
24
+ makeThread(`thread-${index + 1}`),
25
+ );
26
+ const secondPage = [makeThread("thread-21")];
27
+ const list = vi
28
+ .fn()
29
+ .mockResolvedValueOnce({ threads: firstPage })
30
+ .mockResolvedValueOnce({ threads: secondPage });
31
+ const cloud = { threads: { list } } as unknown as AssistantCloud;
32
+ const { result } = renderHook(() => useCloudThreadListAdapter({ cloud }));
33
+
34
+ const first = await result.current.list();
35
+ expect(list).toHaveBeenNthCalledWith(1, { limit: 20 });
36
+ expect(first.threads).toHaveLength(20);
37
+ expect(first.nextCursor).toBe("thread-20");
38
+
39
+ const second = await result.current.list({ after: first.nextCursor });
40
+ expect(list).toHaveBeenNthCalledWith(2, {
41
+ limit: 20,
42
+ after: "thread-20",
43
+ });
44
+ expect(second.threads.map((thread) => thread.remoteId)).toEqual([
45
+ "thread-21",
46
+ ]);
47
+ expect(second.nextCursor).toBeUndefined();
48
+ });
49
+ });
@@ -37,6 +37,8 @@ const autoCloud = baseUrl
37
37
  ? new AssistantCloud({ baseUrl, anonymous: true })
38
38
  : undefined;
39
39
 
40
+ const CLOUD_THREAD_PAGE_SIZE = 20;
41
+
40
42
  export const useCloudThreadListAdapter = (
41
43
  adapter: CloudThreadListAdapterOptions,
42
44
  ): RemoteThreadListAdapter => {
@@ -88,8 +90,11 @@ export const useCloudThreadListAdapter = (
88
90
  }
89
91
 
90
92
  return {
91
- list: async () => {
92
- const { threads } = await cloud.threads.list();
93
+ list: async ({ after } = {}) => {
94
+ const { threads } = await cloud.threads.list({
95
+ limit: CLOUD_THREAD_PAGE_SIZE,
96
+ ...(after ? { after } : {}),
97
+ });
93
98
  return {
94
99
  threads: threads.map((t) => ({
95
100
  status: t.is_archived ? "archived" : "regular",
@@ -101,6 +106,10 @@ export const useCloudThreadListAdapter = (
101
106
  externalId: t.external_id ?? undefined,
102
107
  custom: toCustom(t.metadata),
103
108
  })),
109
+ nextCursor:
110
+ threads.length === CLOUD_THREAD_PAGE_SIZE
111
+ ? threads.at(-1)?.id
112
+ : undefined,
104
113
  };
105
114
  },
106
115
 
@@ -343,4 +343,75 @@ describe("convertExternalMessages", () => {
343
343
  expect(result[0]!.role).toBe("user");
344
344
  });
345
345
  });
346
+
347
+ describe("invalid converter output", () => {
348
+ it("throws a descriptive error when the callback returns undefined", () => {
349
+ const messages = [{ id: "m1", type: "remove" }];
350
+ const callback = (() =>
351
+ undefined) as unknown as useExternalMessageConverter.Callback<
352
+ (typeof messages)[number]
353
+ >;
354
+
355
+ expect(() =>
356
+ convertExternalMessages(messages, callback, false, {}),
357
+ ).toThrowError(
358
+ /returned an invalid message \(undefined\) for input \{"id":"m1","type":"remove"\}/,
359
+ );
360
+ });
361
+
362
+ it("throws a descriptive error when the callback returns an array containing undefined", () => {
363
+ const messages = [{ id: "m1", role: "user" as const, content: "hi" }];
364
+ const callback = ((msg: (typeof messages)[number]) => [
365
+ msg,
366
+ undefined,
367
+ ]) as unknown as useExternalMessageConverter.Callback<
368
+ (typeof messages)[number]
369
+ >;
370
+
371
+ expect(() =>
372
+ convertExternalMessages(messages, callback, false, {}),
373
+ ).toThrowError(/returned an invalid message \(undefined\)/);
374
+ });
375
+
376
+ it("throws a descriptive error when the callback returns an unsupported role", () => {
377
+ const messages = [{ id: "m1", role: "user" as const, content: "hi" }];
378
+ const callback = (() => ({
379
+ role: "other",
380
+ content: "hi",
381
+ })) as unknown as useExternalMessageConverter.Callback<
382
+ (typeof messages)[number]
383
+ >;
384
+
385
+ expect(() =>
386
+ convertExternalMessages(messages, callback, false, {}),
387
+ ).toThrowError(/returned an invalid message \(\{"role":"other"/);
388
+ });
389
+
390
+ it("throws a descriptive error when the callback returns a message without content", () => {
391
+ const messages = [{ id: "m1", role: "user" as const, content: "hi" }];
392
+ const callback = (() => ({
393
+ role: "user",
394
+ })) as unknown as useExternalMessageConverter.Callback<
395
+ (typeof messages)[number]
396
+ >;
397
+
398
+ expect(() =>
399
+ convertExternalMessages(messages, callback, false, {}),
400
+ ).toThrowError(/returned an invalid message \(\{"role":"user"\}\)/);
401
+ });
402
+
403
+ it("tolerates a tool message without toolCallId as an orphaned tool result", () => {
404
+ const messages = [{ id: "m1", role: "user" as const, content: "hi" }];
405
+ const callback = ((msg: (typeof messages)[number]) => [
406
+ msg,
407
+ { role: "tool", result: "ok" },
408
+ ]) as unknown as useExternalMessageConverter.Callback<
409
+ (typeof messages)[number]
410
+ >;
411
+
412
+ const result = convertExternalMessages(messages, callback, false, {});
413
+ expect(result[0]!.role).toBe("user");
414
+ expect(result[1]!.content).toHaveLength(0);
415
+ });
416
+ });
346
417
  });
@@ -100,6 +100,15 @@ describe("useExternalMessageConverter", () => {
100
100
  });
101
101
  });
102
102
 
103
+ it("throws the descriptive converter error when the callback returns an invalid message", () => {
104
+ const bad = (() =>
105
+ undefined) as unknown as useExternalMessageConverter.Callback<TestMessage>;
106
+
107
+ expect(() => renderConverter({ callback: bad })).toThrowError(
108
+ /External message converter: the converter callback returned an invalid message \(undefined\) for input \{"id":"u1"/,
109
+ );
110
+ });
111
+
103
112
  it.each([
104
113
  ["false", false],
105
114
  ["zero", 0],
@@ -76,6 +76,39 @@ type CallbackResult<T> = {
76
76
  outputs: useExternalMessageConverter.Message[];
77
77
  };
78
78
 
79
+ const stringifyForError = (value: unknown) => {
80
+ let text;
81
+ try {
82
+ text = value instanceof Error ? String(value) : JSON.stringify(value);
83
+ } catch {
84
+ /* circular */
85
+ }
86
+ text ??= String(value);
87
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text;
88
+ };
89
+
90
+ const toCallbackOutputs = (
91
+ output:
92
+ | useExternalMessageConverter.Message
93
+ | useExternalMessageConverter.Message[],
94
+ input: unknown,
95
+ ): useExternalMessageConverter.Message[] => {
96
+ const outputs = Array.isArray(output) ? output : [output];
97
+ for (const o of outputs) {
98
+ const valid =
99
+ typeof o === "object" &&
100
+ o !== null &&
101
+ (o.role === "tool" ||
102
+ ((o.role === "assistant" || o.role === "user" || o.role === "system") &&
103
+ (typeof o.content === "string" || Array.isArray(o.content))));
104
+ if (!valid)
105
+ throw new Error(
106
+ `External message converter: the converter callback returned an invalid message (${stringifyForError(o)}) for input ${stringifyForError(input)}. Return an empty array to skip a message.`,
107
+ );
108
+ }
109
+ return outputs;
110
+ };
111
+
79
112
  type CallbackCacheEntry<T> = CallbackResult<T> & {
80
113
  metadata: useExternalMessageConverter.Metadata;
81
114
  callback: useExternalMessageConverter.Callback<T>;
@@ -354,7 +387,7 @@ export const convertExternalMessages = <T extends WeakKey>(
354
387
  const callbackResults: CallbackResult<T>[] = [];
355
388
  for (const message of messages) {
356
389
  const output = callback(message, metadata);
357
- const outputs = Array.isArray(output) ? output : [output];
390
+ const outputs = toCallbackOutputs(output, message);
358
391
  const result = { input: message, outputs };
359
392
  callbackResults.push(result);
360
393
  }
@@ -444,7 +477,7 @@ export const useExternalMessageConverter = <T extends WeakKey>({
444
477
  result.callback !== state.callback
445
478
  ) {
446
479
  const output = state.callback(message, state.metadata);
447
- const outputs = Array.isArray(output) ? output : [output];
480
+ const outputs = toCallbackOutputs(output, message);
448
481
  result = {
449
482
  input: message,
450
483
  outputs,
@@ -1186,12 +1186,10 @@ describe("LocalThreadRuntimeCore runs", () => {
1186
1186
  };
1187
1187
 
1188
1188
  it("appends the user message and the adapter result", async () => {
1189
- const run = vi.fn(
1190
- async (): Promise<ChatModelRunResult> => ({
1191
- content: [{ type: "text", text: "Hello!" }],
1192
- status: { type: "complete", reason: "stop" },
1193
- }),
1194
- );
1189
+ const run = vi.fn(async (): Promise<ChatModelRunResult> => ({
1190
+ content: [{ type: "text", text: "Hello!" }],
1191
+ status: { type: "complete", reason: "stop" },
1192
+ }));
1195
1193
  const thread = createPlainThread({ run });
1196
1194
 
1197
1195
  await thread.append(userMessage("Hi"));
@@ -1247,23 +1245,21 @@ describe("LocalThreadRuntimeCore runs", () => {
1247
1245
  });
1248
1246
 
1249
1247
  it("does not run again after a tool result once maxSteps is reached", async () => {
1250
- const run = vi.fn(
1251
- async (): Promise<ChatModelRunResult> => ({
1252
- content: [
1253
- {
1254
- type: "tool-call",
1255
- toolCallId: "tc1",
1256
- toolName: "myTool",
1257
- args: {},
1258
- argsText: "{}",
1259
- },
1260
- ],
1261
- status: { type: "requires-action", reason: "tool-calls" },
1262
- metadata: {
1263
- steps: [{ usage: { promptTokens: 10, completionTokens: 5 } }],
1248
+ const run = vi.fn(async (): Promise<ChatModelRunResult> => ({
1249
+ content: [
1250
+ {
1251
+ type: "tool-call",
1252
+ toolCallId: "tc1",
1253
+ toolName: "myTool",
1254
+ args: {},
1255
+ argsText: "{}",
1264
1256
  },
1265
- }),
1266
- );
1257
+ ],
1258
+ status: { type: "requires-action", reason: "tool-calls" },
1259
+ metadata: {
1260
+ steps: [{ usage: { promptTokens: 10, completionTokens: 5 } }],
1261
+ },
1262
+ }));
1267
1263
  const thread = createPlainThread({ run }, { maxSteps: 1 });
1268
1264
 
1269
1265
  await thread.append(userMessage("Tool call"));