@assistant-ui/core 0.2.5 → 0.2.7

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 (140) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.js +6 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/internal/duplicate-detection.d.ts +5 -0
  5. package/dist/internal/duplicate-detection.d.ts.map +1 -0
  6. package/dist/internal/duplicate-detection.js +11 -0
  7. package/dist/internal/duplicate-detection.js.map +1 -0
  8. package/dist/react/AssistantProvider.d.ts.map +1 -1
  9. package/dist/react/AssistantProvider.js.map +1 -1
  10. package/dist/react/index.d.ts +3 -2
  11. package/dist/react/index.js +2 -2
  12. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  13. package/dist/react/primitives/message/MessageGroupedParts.d.ts +25 -21
  14. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  15. package/dist/react/primitives/message/MessageGroupedParts.js +6 -7
  16. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  17. package/dist/react/primitives/message/MessageParts.d.ts +2 -1
  18. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  19. package/dist/react/primitives/message/MessageParts.js +9 -4
  20. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  21. package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
  22. package/dist/react/providers/TextMessagePartProvider.js +3 -0
  23. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  24. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -1
  25. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  26. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -1
  27. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  28. package/dist/react/runtimes/external-message-converter.d.ts +1 -1
  29. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  30. package/dist/react/runtimes/external-message-converter.js +7 -3
  31. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  32. package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
  33. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  34. package/dist/react/utils/groupParts.d.ts +40 -12
  35. package/dist/react/utils/groupParts.d.ts.map +1 -1
  36. package/dist/react/utils/groupParts.js +51 -9
  37. package/dist/react/utils/groupParts.js.map +1 -1
  38. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  39. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  40. package/dist/runtime/api/message-part-runtime.d.ts +8 -0
  41. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  42. package/dist/runtime/api/message-part-runtime.js +13 -0
  43. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  44. package/dist/runtime/api/thread-runtime.d.ts +2 -1
  45. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  46. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -1
  47. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  48. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  49. package/dist/runtime/interfaces/thread-runtime-core.d.ts +15 -1
  50. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  51. package/dist/runtime/utils/thread-message-like.d.ts +10 -0
  52. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  53. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  54. package/dist/runtimes/external-store/external-store-adapter.d.ts +33 -1
  55. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  56. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  57. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  58. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +27 -1
  59. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  60. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +98 -3
  61. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  62. package/dist/runtimes/local/local-thread-runtime-core.d.ts +2 -1
  63. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  64. package/dist/runtimes/local/local-thread-runtime-core.js +3 -0
  65. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  66. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
  67. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  68. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +3 -0
  69. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  70. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  71. package/dist/runtimes/remote-thread-list/empty-thread-core.js +3 -0
  72. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  73. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +168 -0
  74. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -0
  75. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +449 -0
  76. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -0
  77. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  78. package/dist/store/clients/thread-message-client.js +3 -0
  79. package/dist/store/clients/thread-message-client.js.map +1 -1
  80. package/dist/store/runtime-clients/message-part-runtime-client.js +1 -0
  81. package/dist/store/runtime-clients/message-part-runtime-client.js.map +1 -1
  82. package/dist/store/scopes/part.d.ts +7 -0
  83. package/dist/store/scopes/part.d.ts.map +1 -1
  84. package/dist/subscribable/subscribable.d.ts.map +1 -1
  85. package/dist/subscribable/subscribable.js.map +1 -1
  86. package/dist/types/message.d.ts +6 -0
  87. package/dist/types/message.d.ts.map +1 -1
  88. package/dist/types/message.js.map +1 -1
  89. package/package.json +4 -4
  90. package/src/adapters/index.ts +1 -4
  91. package/src/index.ts +11 -0
  92. package/src/internal/duplicate-detection.ts +26 -0
  93. package/src/react/AssistantProvider.tsx +2 -3
  94. package/src/react/index.ts +2 -6
  95. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -2
  96. package/src/react/primitives/message/MessageAttachments.test.tsx +1 -1
  97. package/src/react/primitives/message/MessageGroupedParts.tsx +38 -31
  98. package/src/react/primitives/message/MessageParts.tsx +14 -1
  99. package/src/react/providers/TextMessagePartProvider.tsx +3 -0
  100. package/src/react/runtimes/external-message-converter.ts +26 -13
  101. package/src/react/types/MessagePartComponentTypes.ts +8 -0
  102. package/src/react/utils/groupParts.ts +67 -22
  103. package/src/runtime/api/attachment-runtime.ts +1 -2
  104. package/src/runtime/api/message-part-runtime.ts +26 -0
  105. package/src/runtime/base/base-thread-runtime-core.ts +4 -0
  106. package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
  107. package/src/runtime/internal.ts +1 -4
  108. package/src/runtime/utils/thread-message-like.ts +7 -0
  109. package/src/runtimes/external-store/external-store-adapter.ts +37 -0
  110. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +1 -3
  111. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +168 -4
  112. package/src/runtimes/local/local-thread-runtime-core.ts +5 -0
  113. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +4 -0
  114. package/src/runtimes/remote-thread-list/empty-thread-core.ts +4 -0
  115. package/src/runtimes/tool-invocations/EDGE_CASES.md +194 -0
  116. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +1054 -0
  117. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +783 -0
  118. package/src/store/clients/thread-message-client.ts +3 -0
  119. package/src/store/runtime-clients/message-part-runtime-client.ts +2 -0
  120. package/src/store/scopes/part.ts +4 -0
  121. package/src/subscribable/subscribable.ts +3 -3
  122. package/src/tests/OptimisticState-delete-crash.test.ts +2 -0
  123. package/src/tests/OptimisticState-list-race.test.ts +2 -0
  124. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +5 -5
  125. package/src/tests/auiV0Encode.test.ts +1 -1
  126. package/src/tests/composer-can-send.test.ts +8 -4
  127. package/src/tests/duplicate-detection.test.ts +34 -0
  128. package/src/tests/external-store-thread-list-runtime-core.test.ts +1 -1
  129. package/src/tests/external-store-thread-runtime-core.test.ts +7 -6
  130. package/src/tests/groupParts.test.ts +118 -32
  131. package/src/tests/no-unsafe-process-env.test.ts +1 -0
  132. package/src/tests/remote-thread-list-isLoading.test.ts +2 -0
  133. package/src/tests/thread-message-like.test.ts +4 -1
  134. package/src/types/index.ts +1 -4
  135. package/src/types/message.ts +7 -0
  136. package/dist/react/runtimes/useToolInvocations.d.ts +0 -53
  137. package/dist/react/runtimes/useToolInvocations.d.ts.map +0 -1
  138. package/dist/react/runtimes/useToolInvocations.js +0 -380
  139. package/dist/react/runtimes/useToolInvocations.js.map +0 -1
  140. package/src/react/runtimes/useToolInvocations.ts +0 -694
@@ -38,6 +38,9 @@ const ThreadMessagePartClient = resource(
38
38
  resumeToolCall: () => {
39
39
  throw new Error("Not supported");
40
40
  },
41
+ respondToToolApproval: () => {
42
+ throw new Error("Not supported");
43
+ },
41
44
  };
42
45
  },
43
46
  );
@@ -11,6 +11,8 @@ export const MessagePartClient = resource(
11
11
  getState: () => state,
12
12
  addToolResult: (result) => runtime.addToolResult(result),
13
13
  resumeToolCall: (payload) => runtime.resumeToolCall(payload),
14
+ respondToToolApproval: (response) =>
15
+ runtime.respondToToolApproval(response),
14
16
  __internal_getRuntime: () => runtime,
15
17
  };
16
18
  },
@@ -26,6 +26,10 @@ export type PartMethods = {
26
26
  * This is useful when a tool has requested human input and is waiting for a response.
27
27
  */
28
28
  resumeToolCall(payload: unknown): void;
29
+ /**
30
+ * Respond to a server-side tool approval gate. The approval id is read from the part.
31
+ */
32
+ respondToToolApproval(response: { approved: boolean; reason?: string }): void;
29
33
  __internal_getRuntime?(): MessagePartRuntime;
30
34
  };
31
35
 
@@ -207,9 +207,9 @@ export class LazyMemoizeSubject<TState extends object, TPath>
207
207
  }
208
208
 
209
209
  export class NestedSubscriptionSubject<
210
- TState extends Subscribable | undefined,
211
- TPath,
212
- >
210
+ TState extends Subscribable | undefined,
211
+ TPath,
212
+ >
213
213
  extends BaseSubject
214
214
  implements
215
215
  SubscribableWithState<TState, TPath>,
@@ -24,6 +24,8 @@ const createThreadState = (
24
24
  const mappingId = createThreadMappingId(threadId);
25
25
  return {
26
26
  isLoading: false,
27
+ isLoadingMore: false,
28
+ cursor: undefined,
27
29
  newThreadId: undefined,
28
30
  threadIds: status === "regular" ? [threadId] : [],
29
31
  archivedThreadIds: status === "archived" ? [threadId] : [],
@@ -22,6 +22,8 @@ import { deferred } from "./remote-thread-list-test-helpers";
22
22
 
23
23
  const EMPTY_STATE: RemoteThreadState = {
24
24
  isLoading: false,
25
+ isLoadingMore: false,
26
+ cursor: undefined,
25
27
  newThreadId: undefined,
26
28
  threadIds: [],
27
29
  archivedThreadIds: [],
@@ -20,7 +20,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
20
20
 
21
21
  it("initial list response with nextCursor sets hasMore=true", async () => {
22
22
  const adapter = makeAdapter({
23
- list: vi.fn(async () => ({
23
+ list: vi.fn<ListFn>(async () => ({
24
24
  threads: [
25
25
  {
26
26
  status: "regular",
@@ -41,7 +41,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
41
41
 
42
42
  it("absent nextCursor leaves hasMore=false", async () => {
43
43
  const adapter = makeAdapter({
44
- list: vi.fn(async () => ({
44
+ list: vi.fn<ListFn>(async () => ({
45
45
  threads: [
46
46
  {
47
47
  status: "regular",
@@ -243,7 +243,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
243
243
 
244
244
  it("__internal_setOptions clears cursor and dedup handles on adapter swap, then refetches via the new adapter", async () => {
245
245
  const firstAdapter = makeAdapter({
246
- list: vi.fn(async () => ({
246
+ list: vi.fn<ListFn>(async () => ({
247
247
  threads: [{ status: "regular", remoteId: "old", externalId: "old" }],
248
248
  nextCursor: "old-cursor",
249
249
  })),
@@ -253,7 +253,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
253
253
  expect(core.threadIds).toEqual(["old"]);
254
254
  expect(core.hasMore).toBe(true);
255
255
 
256
- const secondList = vi.fn(async () => ({
256
+ const secondList = vi.fn<ListFn>(async () => ({
257
257
  threads: [{ status: "regular", remoteId: "new", externalId: "new" }],
258
258
  }));
259
259
  const secondAdapter = makeAdapter({ list: secondList });
@@ -290,7 +290,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
290
290
  const stale = core.loadMore();
291
291
 
292
292
  const secondAdapter = makeAdapter({
293
- list: vi.fn(async () => ({
293
+ list: vi.fn<ListFn>(async () => ({
294
294
  threads: [{ status: "regular", remoteId: "ignored", externalId: "x" }],
295
295
  })),
296
296
  });
@@ -9,7 +9,7 @@ describe("auiV0Encode", () => {
9
9
  role: "assistant",
10
10
  status: { type: "complete", reason: "stop" },
11
11
  metadata: {
12
- unstable_state: undefined,
12
+ unstable_state: null,
13
13
  unstable_annotations: [],
14
14
  unstable_data: [],
15
15
  steps: [],
@@ -102,10 +102,14 @@ describe("BaseComposerRuntimeCore.send", () => {
102
102
  describe("DefaultEditComposerRuntimeCore.canSend", () => {
103
103
  it("ignores runtime.isSendDisabled (thread-scoped flag does not block edits)", () => {
104
104
  const stub = makeRuntimeStub({ isSendDisabled: true });
105
- const composer = new DefaultEditComposerRuntimeCore(stub, () => {}, {
106
- parentId: null,
107
- message: makeUserMessage("seed"),
108
- });
105
+ const composer = new DefaultEditComposerRuntimeCore(
106
+ stub as unknown as ThreadRuntimeCore,
107
+ () => {},
108
+ {
109
+ parentId: null,
110
+ message: makeUserMessage("seed"),
111
+ },
112
+ );
109
113
 
110
114
  expect(composer.canSend).toBe(true);
111
115
  });
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { checkDuplicateCore } from "../internal/duplicate-detection";
3
+
4
+ const KEY = Symbol.for("@assistant-ui/core.loaded");
5
+
6
+ function reset(): void {
7
+ delete (globalThis as unknown as Record<symbol, unknown>)[KEY];
8
+ }
9
+
10
+ describe("checkDuplicateCore", () => {
11
+ let warn: ReturnType<typeof vi.spyOn>;
12
+
13
+ beforeEach(() => {
14
+ reset();
15
+ warn = vi.spyOn(console, "warn").mockImplementation(() => {});
16
+ });
17
+
18
+ afterEach(() => {
19
+ warn.mockRestore();
20
+ reset();
21
+ });
22
+
23
+ it("does not warn on the first load", () => {
24
+ checkDuplicateCore();
25
+ expect(warn).not.toHaveBeenCalled();
26
+ });
27
+
28
+ it("warns when a second copy registers in the same runtime", () => {
29
+ checkDuplicateCore();
30
+ checkDuplicateCore();
31
+ expect(warn).toHaveBeenCalledTimes(1);
32
+ expect(warn.mock.calls[0]![0]).toMatch(/npx assistant-ui doctor/);
33
+ });
34
+ });
@@ -184,7 +184,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
184
184
  const core = new ExternalStoreThreadListRuntimeCore(adapter, makeFactory());
185
185
  return new ThreadListRuntimeImpl(
186
186
  core,
187
- NoopThreadRuntime as unknown as Parameters<
187
+ NoopThreadRuntime as unknown as ConstructorParameters<
188
188
  typeof ThreadListRuntimeImpl
189
189
  >[1],
190
190
  );
@@ -8,12 +8,13 @@ const mockContextProvider: ModelContextProvider = {
8
8
  };
9
9
 
10
10
  const makeStore = (
11
- overrides?: Partial<ExternalStoreAdapter>,
12
- ): ExternalStoreAdapter => ({
13
- messages: [],
14
- onNew: vi.fn(),
15
- ...overrides,
16
- });
11
+ overrides?: Partial<ExternalStoreAdapter> | Record<string, unknown>,
12
+ ): ExternalStoreAdapter =>
13
+ ({
14
+ messages: [],
15
+ onNew: vi.fn(),
16
+ ...overrides,
17
+ }) as ExternalStoreAdapter;
17
18
 
18
19
  describe("ExternalStoreThreadRuntimeCore - state reference stability", () => {
19
20
  describe("capabilities", () => {
@@ -1,12 +1,13 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import type { PartState } from "../store/scopes/part";
2
3
  import {
3
4
  buildGroupTree,
4
- normalizeGroupKey,
5
+ GROUPBY_MEMO_KEY,
6
+ groupPartByType,
5
7
  type GroupNode,
6
8
  } from "../react/utils/groupParts";
7
9
 
8
- const asPaths = (keys: readonly (string | readonly string[] | null)[]) =>
9
- keys.map((k) => normalizeGroupKey(k));
10
+ const asPaths = (keys: readonly (readonly string[])[]) => keys;
10
11
 
11
12
  // Compact tree dump: "G:key#nodeKey[i,j]{...}" | "P:#nodeKey(i)"
12
13
  const dump = (nodes: readonly GroupNode[]): string =>
@@ -20,39 +21,23 @@ const dump = (nodes: readonly GroupNode[]): string =>
20
21
  })
21
22
  .join(",");
22
23
 
23
- describe("normalizeGroupKey", () => {
24
- it("maps null/undefined/[] to []", () => {
25
- expect(normalizeGroupKey(null)).toEqual([]);
26
- expect(normalizeGroupKey(undefined)).toEqual([]);
27
- expect(normalizeGroupKey([])).toEqual([]);
28
- });
29
-
30
- it("wraps a string into a single-element array", () => {
31
- expect(normalizeGroupKey("foo")).toEqual(["foo"]);
32
- });
33
-
34
- it("passes arrays through", () => {
35
- expect(normalizeGroupKey(["a", "b"])).toEqual(["a", "b"]);
36
- });
37
- });
38
-
39
24
  describe("buildGroupTree", () => {
40
25
  it("returns an empty list for no parts", () => {
41
26
  expect(buildGroupTree([])).toEqual([]);
42
27
  });
43
28
 
44
29
  it("emits one part leaf per ungrouped part (no coalescing)", () => {
45
- const tree = buildGroupTree(asPaths([null, null, null]));
30
+ const tree = buildGroupTree(asPaths([[], [], []]));
46
31
  expect(dump(tree)).toBe("P:#0(0),P:#1(1),P:#2(2)");
47
32
  });
48
33
 
49
34
  it("wraps adjacent same-key parts in one group with one part child each", () => {
50
- const tree = buildGroupTree(asPaths(["a", "a", "a"]));
35
+ const tree = buildGroupTree(asPaths([["a"], ["a"], ["a"]]));
51
36
  expect(dump(tree)).toBe("G:a#0[0,1,2]{P:#0.0(0),P:#0.1(1),P:#0.2(2)}");
52
37
  });
53
38
 
54
39
  it("splits non-adjacent runs of the same key into separate groups", () => {
55
- const tree = buildGroupTree(asPaths(["a", null, "a"]));
40
+ const tree = buildGroupTree(asPaths([["a"], [], ["a"]]));
56
41
  expect(dump(tree)).toBe("G:a#0[0]{P:#0.0(0)},P:#1(1),G:a#2[2]{P:#2.0(2)}");
57
42
  });
58
43
 
@@ -95,20 +80,121 @@ describe("buildGroupTree", () => {
95
80
  );
96
81
  });
97
82
 
98
- it("accepts strings and arrays interchangeably via normalizeGroupKey", () => {
99
- const tree = buildGroupTree([
100
- normalizeGroupKey("A"),
101
- normalizeGroupKey(["A"]),
102
- ]);
103
- expect(dump(tree)).toBe("G:A#0[0,1]{P:#0.0(0),P:#0.1(1)}");
104
- });
105
-
106
83
  it("assigns stable nodeKeys under append (existing keys do not shift)", () => {
107
- const before = buildGroupTree(asPaths([["A"], null]));
108
- const after = buildGroupTree(asPaths([["A"], null, ["B"]]));
84
+ const before = buildGroupTree(asPaths([["A"], []]));
85
+ const after = buildGroupTree(asPaths([["A"], [], ["B"]]));
109
86
 
110
87
  expect(before[0]!.nodeKey).toBe(after[0]!.nodeKey);
111
88
  expect(before[1]!.nodeKey).toBe(after[1]!.nodeKey);
112
89
  expect(after[2]!.nodeKey).toBe("2");
113
90
  });
114
91
  });
92
+
93
+ const part = (overrides: Partial<PartState>): PartState =>
94
+ ({
95
+ type: "text",
96
+ text: "",
97
+ status: { type: "complete" },
98
+ ...overrides,
99
+ }) as PartState;
100
+
101
+ describe("groupPartByType", () => {
102
+ it("maps part.type to the configured path", () => {
103
+ const fn = groupPartByType({
104
+ reasoning: ["group-thought", "group-reasoning"],
105
+ "tool-call": ["group-thought", "group-tool"],
106
+ });
107
+ expect(fn(part({ type: "reasoning" }))).toEqual([
108
+ "group-thought",
109
+ "group-reasoning",
110
+ ]);
111
+ expect(fn(part({ type: "tool-call" }))).toEqual([
112
+ "group-thought",
113
+ "group-tool",
114
+ ]);
115
+ });
116
+
117
+ it("returns [] for part types not in the map", () => {
118
+ const fn = groupPartByType({ reasoning: ["group-r"] });
119
+ expect(fn(part({ type: "text" }))).toEqual([]);
120
+ });
121
+
122
+ it("routes MCP-app tool calls through the 'mcp-app' entry when present", () => {
123
+ const fn = groupPartByType({
124
+ "tool-call": ["group-tool"],
125
+ "mcp-app": [],
126
+ });
127
+ const mcpApp = part({
128
+ type: "tool-call",
129
+ toolName: "render",
130
+ mcp: { app: { resourceUri: "ui://my-app" } },
131
+ } as Partial<PartState>);
132
+ const regular = part({
133
+ type: "tool-call",
134
+ toolName: "search",
135
+ } as Partial<PartState>);
136
+ expect(fn(mcpApp)).toEqual([]);
137
+ expect(fn(regular)).toEqual(["group-tool"]);
138
+ });
139
+
140
+ it("falls back to 'tool-call' for MCP-app parts when 'mcp-app' is absent", () => {
141
+ const fn = groupPartByType({ "tool-call": ["group-tool"] });
142
+ const mcpApp = part({
143
+ type: "tool-call",
144
+ toolName: "render",
145
+ mcp: { app: { resourceUri: "ui://x" } },
146
+ } as Partial<PartState>);
147
+ expect(fn(mcpApp)).toEqual(["group-tool"]);
148
+ });
149
+
150
+ it("does not route non-`ui://` tool calls through 'mcp-app'", () => {
151
+ const fn = groupPartByType({
152
+ "tool-call": ["group-tool"],
153
+ "mcp-app": ["group-mcp"],
154
+ });
155
+ const notMcp = part({
156
+ type: "tool-call",
157
+ toolName: "x",
158
+ mcp: { app: { resourceUri: "http://example.com" } },
159
+ } as Partial<PartState>);
160
+ expect(fn(notMcp)).toEqual(["group-tool"]);
161
+ });
162
+
163
+ it("tags the function with a GROUPBY_MEMO_KEY fingerprint", () => {
164
+ const fn = groupPartByType({ reasoning: ["group-r"] });
165
+ const memoKey = (fn as unknown as { [GROUPBY_MEMO_KEY]: string })[
166
+ GROUPBY_MEMO_KEY
167
+ ];
168
+ expect(memoKey).toMatch(/^groupPartByType:/);
169
+ });
170
+
171
+ it("produces the same fingerprint regardless of map key order", () => {
172
+ const a = groupPartByType({
173
+ reasoning: ["group-r"],
174
+ "tool-call": ["group-t"],
175
+ });
176
+ const b = groupPartByType({
177
+ "tool-call": ["group-t"],
178
+ reasoning: ["group-r"],
179
+ });
180
+ const keyA = (a as unknown as { [GROUPBY_MEMO_KEY]: string })[
181
+ GROUPBY_MEMO_KEY
182
+ ];
183
+ const keyB = (b as unknown as { [GROUPBY_MEMO_KEY]: string })[
184
+ GROUPBY_MEMO_KEY
185
+ ];
186
+ expect(keyA).toBe(keyB);
187
+ });
188
+
189
+ it("produces different fingerprints for different configs", () => {
190
+ const a = groupPartByType({ reasoning: ["group-r"] });
191
+ const b = groupPartByType({ reasoning: ["group-r2"] });
192
+ const keyA = (a as unknown as { [GROUPBY_MEMO_KEY]: string })[
193
+ GROUPBY_MEMO_KEY
194
+ ];
195
+ const keyB = (b as unknown as { [GROUPBY_MEMO_KEY]: string })[
196
+ GROUPBY_MEMO_KEY
197
+ ];
198
+ expect(keyA).not.toBe(keyB);
199
+ });
200
+ });
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { describe, expect, it } from "vitest";
2
3
  import { readFileSync, readdirSync, statSync } from "node:fs";
3
4
  import { resolve, relative, join } from "node:path";
@@ -26,6 +26,8 @@ type ListResult = {
26
26
 
27
27
  const INITIAL_STATE: RemoteThreadState = {
28
28
  isLoading: true,
29
+ isLoadingMore: false,
30
+ cursor: undefined,
29
31
  newThreadId: undefined,
30
32
  threadIds: [],
31
33
  archivedThreadIds: [],
@@ -2,7 +2,10 @@ import { describe, expect, it } from "vitest";
2
2
  import { fromThreadMessageLike } from "../runtime/utils/thread-message-like";
3
3
 
4
4
  const fallbackId = "test-id";
5
- const fallbackStatus = { type: "complete" as const };
5
+ const fallbackStatus = {
6
+ type: "complete" as const,
7
+ reason: "stop" as const,
8
+ };
6
9
 
7
10
  describe("fromThreadMessageLike", () => {
8
11
  describe("data-* prefixed types", () => {
@@ -49,7 +49,4 @@ export type {
49
49
  Unstable_DirectiveFormatter,
50
50
  } from "./directive";
51
51
 
52
- export type {
53
- Unstable_TriggerItem,
54
- Unstable_TriggerCategory,
55
- } from "./trigger";
52
+ export type { Unstable_TriggerItem, Unstable_TriggerCategory } from "./trigger";
@@ -165,6 +165,13 @@ export type ToolCallMessagePart<
165
165
  readonly modelContent?: readonly ToolModelContentPart[] | undefined;
166
166
  /** Human-input request that must be resolved before the run can continue. */
167
167
  readonly interrupt?: { type: "human"; payload: unknown };
168
+ /** Server-side approval gate. `approved === undefined` is the only state in which `respondToApproval` may be called. */
169
+ readonly approval?: {
170
+ readonly id: string;
171
+ readonly approved?: boolean;
172
+ readonly reason?: string;
173
+ readonly isAutomatic?: boolean;
174
+ };
168
175
  /** Parent message-part ID when this part belongs to a nested structure. */
169
176
  readonly parentId?: string;
170
177
  /**
@@ -1,53 +0,0 @@
1
- import { ThreadMessage } from "../../types/message.js";
2
- import { Tool, ToolModelContentPart } from "assistant-stream";
3
- import { ReadonlyJSONValue } from "assistant-stream/utils";
4
-
5
- //#region src/react/runtimes/useToolInvocations.d.ts
6
- type AssistantTransportState = {
7
- readonly messages: readonly ThreadMessage[];
8
- readonly state?: ReadonlyJSONValue;
9
- readonly isRunning: boolean;
10
- };
11
- type AddToolResultCommand = {
12
- readonly type: "add-tool-result";
13
- readonly toolCallId: string;
14
- readonly toolName: string;
15
- readonly result: ReadonlyJSONValue;
16
- readonly isError: boolean;
17
- readonly artifact?: ReadonlyJSONValue;
18
- readonly modelContent?: readonly ToolModelContentPart[];
19
- };
20
- type UseToolInvocationsParams = {
21
- state: AssistantTransportState;
22
- getTools: () => Record<string, Tool> | undefined;
23
- onResult: (command: AddToolResultCommand) => void;
24
- setToolStatuses: (updater: Record<string, ToolExecutionStatus> | ((prev: Record<string, ToolExecutionStatus>) => Record<string, ToolExecutionStatus>)) => void;
25
- };
26
- /**
27
- * Streaming execution state for a frontend tool.
28
- *
29
- * Custom runtime integrations use this to mirror in-flight tool calls while
30
- * `useToolInvocations` executes tools in the browser.
31
- */
32
- type ToolExecutionStatus = {
33
- /** The tool's execute function is currently running. */type: "executing";
34
- } | {
35
- /** The tool is waiting for a human input payload before continuing. */type: "interrupt"; /** Human input request emitted by the tool execution context. */
36
- payload: {
37
- type: "human";
38
- payload: unknown;
39
- };
40
- };
41
- declare function useToolInvocations({
42
- state,
43
- getTools,
44
- onResult,
45
- setToolStatuses
46
- }: UseToolInvocationsParams): {
47
- reset: () => void;
48
- abort: () => Promise<void>;
49
- resume: (toolCallId: string, payload: unknown) => void;
50
- };
51
- //#endregion
52
- export { AddToolResultCommand, AssistantTransportState, ToolExecutionStatus, useToolInvocations };
53
- //# sourceMappingURL=useToolInvocations.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useToolInvocations.d.ts","names":[],"sources":["../../../src/react/runtimes/useToolInvocations.ts"],"mappings":";;;;;KAkBY,uBAAA;EAAA,SACD,QAAA,WAAmB,aAAA;EAAA,SACnB,KAAA,GAAQ,iBAAiB;EAAA,SACzB,SAAA;AAAA;AAAA,KAGC,oBAAA;EAAA,SACD,IAAA;EAAA,SACA,UAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA,EAAQ,iBAAA;EAAA,SACR,OAAA;EAAA,SACA,QAAA,GAAW,iBAAA;EAAA,SACX,YAAA,YAAwB,oBAAA;AAAA;AAAA,KA2B9B,wBAAA;EACH,KAAA,EAAO,uBAAA;EACP,QAAA,QAAgB,MAAA,SAAe,IAAA;EAC/B,QAAA,GAAW,OAAA,EAAS,oBAAA;EACpB,eAAA,GACE,OAAA,EACI,MAAA,SAAe,mBAAA,MAEb,IAAA,EAAM,MAAA,SAAe,mBAAA,MAClB,MAAA,SAAe,mBAAA;AAAA;;;;;;;KAUhB,mBAAA;EA/CD,wDAkDL,IAAA;AAAA;EAjD6B,uEAqD7B,IAAA,eArDiD;EAuDjD,OAAA;IAAW,IAAA;IAAe,OAAA;EAAA;AAAA;AAAA,iBAyChB,kBAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA;AAAA,GACC,wBAAA;;eA+fiB,OAAA;+BAkCW,OAAA;AAAA"}