@assistant-ui/core 0.2.18 → 0.2.19

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 (228) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.js +3 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/interactable-composer-metadata.d.ts +105 -0
  7. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -0
  8. package/dist/model-context/interactable-composer-metadata.js +259 -0
  9. package/dist/model-context/interactable-composer-metadata.js.map +1 -0
  10. package/dist/model-context/registry.d.ts.map +1 -1
  11. package/dist/model-context/registry.js +1 -0
  12. package/dist/model-context/registry.js.map +1 -1
  13. package/dist/model-context/types.d.ts +6 -0
  14. package/dist/model-context/types.d.ts.map +1 -1
  15. package/dist/model-context/types.js +4 -0
  16. package/dist/model-context/types.js.map +1 -1
  17. package/dist/react/AssistantProvider.d.ts +1 -1
  18. package/dist/react/AssistantProvider.js +1 -1
  19. package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
  20. package/dist/react/RuntimeAdapter.js +1 -1
  21. package/dist/react/client/Interactables.d.ts +8 -2
  22. package/dist/react/client/Interactables.d.ts.map +1 -1
  23. package/dist/react/client/Interactables.js +190 -94
  24. package/dist/react/client/Interactables.js.map +1 -1
  25. package/dist/react/client/Tools.js +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts +9 -6
  27. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  28. package/dist/react/client/interactable-model-context.js +154 -32
  29. package/dist/react/client/interactable-model-context.js.map +1 -1
  30. package/dist/react/index.d.ts +15 -6
  31. package/dist/react/index.js +12 -5
  32. package/dist/react/interactables-legacy/Interactables.d.ts +12 -0
  33. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -0
  34. package/dist/react/interactables-legacy/Interactables.js +264 -0
  35. package/dist/react/interactables-legacy/Interactables.js.map +1 -0
  36. package/dist/react/interactables-legacy/interactable-model-context.d.ts +12 -0
  37. package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -0
  38. package/dist/react/interactables-legacy/interactable-model-context.js +54 -0
  39. package/dist/react/interactables-legacy/interactable-model-context.js.map +1 -0
  40. package/dist/react/interactables-legacy/scopes.d.ts +58 -0
  41. package/dist/react/interactables-legacy/scopes.d.ts.map +1 -0
  42. package/dist/react/interactables-legacy/scopes.js +0 -0
  43. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +31 -0
  44. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -0
  45. package/dist/react/{model-context → interactables-legacy}/useAssistantInteractable.js +6 -2
  46. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -0
  47. package/dist/react/interactables-legacy/useInteractableState.d.ts +21 -0
  48. package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -0
  49. package/dist/react/interactables-legacy/useInteractableState.js +84 -0
  50. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -0
  51. package/dist/react/model-context/interactableTool.d.ts +53 -0
  52. package/dist/react/model-context/interactableTool.d.ts.map +1 -0
  53. package/dist/react/model-context/interactableTool.js +188 -0
  54. package/dist/react/model-context/interactableTool.js.map +1 -0
  55. package/dist/react/model-context/useAssistantContext.js +1 -1
  56. package/dist/react/model-context/useAssistantDataUI.js +1 -1
  57. package/dist/react/model-context/useAssistantInstructions.js +1 -1
  58. package/dist/react/model-context/useAssistantTool.js +1 -1
  59. package/dist/react/model-context/useAssistantToolUI.js +1 -1
  60. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  61. package/dist/react/model-context/useInteractable.d.ts +73 -0
  62. package/dist/react/model-context/useInteractable.d.ts.map +1 -0
  63. package/dist/react/model-context/useInteractable.js +100 -0
  64. package/dist/react/model-context/useInteractable.js.map +1 -0
  65. package/dist/react/model-context/useInteractableState.d.ts +8 -5
  66. package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
  67. package/dist/react/model-context/useInteractableState.js +48 -48
  68. package/dist/react/model-context/useInteractableState.js.map +1 -1
  69. package/dist/react/model-context/useInteractableVersions.d.ts +19 -0
  70. package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -0
  71. package/dist/react/model-context/useInteractableVersions.js +51 -0
  72. package/dist/react/model-context/useInteractableVersions.js.map +1 -0
  73. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +15 -0
  74. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -0
  75. package/dist/react/primitive-hooks/useThreadMessageIds.js +26 -0
  76. package/dist/react/primitive-hooks/useThreadMessageIds.js.map +1 -0
  77. package/dist/react/primitive-hooks/useVoice.js +1 -1
  78. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  79. package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
  80. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  81. package/dist/react/primitives/generativeUI/GenerativeUI.js +1 -1
  82. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  83. package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
  84. package/dist/react/primitives/message/MessageParts.js +1 -1
  85. package/dist/react/primitives/message/MessageQuote.js +1 -1
  86. package/dist/react/primitives/part/PartMessages.d.ts +5 -2
  87. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  88. package/dist/react/primitives/part/PartMessages.js +8 -8
  89. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  90. package/dist/react/primitives/thread/ThreadMessages.d.ts +31 -1
  91. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  92. package/dist/react/primitives/thread/ThreadMessages.js +64 -2
  93. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  94. package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
  95. package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
  96. package/dist/react/providers/MessageByIdProvider.d.ts +9 -0
  97. package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -0
  98. package/dist/react/providers/MessageByIdProvider.js +57 -0
  99. package/dist/react/providers/MessageByIdProvider.js.map +1 -0
  100. package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
  101. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -1
  102. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
  103. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  104. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +16 -1
  105. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  106. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
  107. package/dist/react/runtimes/cloud/auiV0.js +1 -1
  108. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +1 -1
  109. package/dist/react/runtimes/createRuntimeExtras.d.ts +20 -0
  110. package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -0
  111. package/dist/react/runtimes/createRuntimeExtras.js +43 -0
  112. package/dist/react/runtimes/createRuntimeExtras.js.map +1 -0
  113. package/dist/react/runtimes/useLocalRuntime.js +1 -1
  114. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  115. package/dist/react/runtimes/useRemoteThreadListRuntime.js +6 -2
  116. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  117. package/dist/react/runtimes/useStreamingTiming.d.ts +26 -0
  118. package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -0
  119. package/dist/react/runtimes/useStreamingTiming.js +43 -0
  120. package/dist/react/runtimes/useStreamingTiming.js.map +1 -0
  121. package/dist/react/types/scopes/interactables.d.ts +40 -24
  122. package/dist/react/types/scopes/interactables.d.ts.map +1 -1
  123. package/dist/react/types/store-augmentation.d.ts +3 -1
  124. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  125. package/dist/react/utils/useJSONEqual.d.ts +11 -0
  126. package/dist/react/utils/useJSONEqual.d.ts.map +1 -0
  127. package/dist/react/utils/useJSONEqual.js +30 -0
  128. package/dist/react/utils/useJSONEqual.js.map +1 -0
  129. package/dist/runtime/base/base-composer-runtime-core.d.ts +5 -0
  130. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  131. package/dist/runtime/base/base-composer-runtime-core.js +13 -0
  132. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  133. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  134. package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -1
  135. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  136. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  137. package/dist/runtime/base/default-thread-composer-runtime-core.js +4 -1
  138. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  139. package/dist/runtime/utils/message-repository.d.ts +1 -0
  140. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  141. package/dist/runtime/utils/message-repository.js +6 -3
  142. package/dist/runtime/utils/message-repository.js.map +1 -1
  143. package/dist/runtime/utils/streaming-timing.d.ts +59 -0
  144. package/dist/runtime/utils/streaming-timing.d.ts.map +1 -0
  145. package/dist/runtime/utils/streaming-timing.js +76 -0
  146. package/dist/runtime/utils/streaming-timing.js.map +1 -0
  147. package/dist/runtime/utils/thread-message-like.js +2 -1
  148. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  149. package/dist/runtimes/external-store/external-store-adapter.d.ts +26 -1
  150. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  151. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +9 -0
  152. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +19 -0
  154. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  155. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  156. package/dist/runtimes/local/local-thread-runtime-core.js +1 -1
  157. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  158. package/dist/runtimes/remote-thread-list/types.d.ts +12 -0
  159. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  160. package/dist/store/clients/suggestions.js +1 -1
  161. package/dist/store/clients/thread-message-client.js +1 -1
  162. package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
  163. package/dist/store/runtime-clients/message-runtime-client.js +1 -1
  164. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  165. package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
  166. package/dist/tests/remote-thread-list-test-helpers.d.ts +1 -1
  167. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  168. package/dist/tests/remote-thread-list-test-helpers.js +3 -2
  169. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  170. package/package.json +9 -9
  171. package/src/index.ts +17 -0
  172. package/src/internal.ts +7 -0
  173. package/src/model-context/interactable-composer-metadata.test.ts +550 -0
  174. package/src/model-context/interactable-composer-metadata.ts +395 -0
  175. package/src/model-context/registry.ts +5 -0
  176. package/src/model-context/types.ts +12 -0
  177. package/src/react/client/Interactables.test.ts +358 -0
  178. package/src/react/client/Interactables.ts +339 -86
  179. package/src/react/client/interactable-model-context.test.ts +418 -0
  180. package/src/react/client/interactable-model-context.ts +252 -58
  181. package/src/react/index.ts +72 -3
  182. package/src/react/interactables-legacy/Interactables.ts +332 -0
  183. package/src/react/interactables-legacy/interactable-model-context.test.ts +54 -0
  184. package/src/react/interactables-legacy/interactable-model-context.ts +80 -0
  185. package/src/react/interactables-legacy/scopes.ts +66 -0
  186. package/src/react/{model-context → interactables-legacy}/useAssistantInteractable.ts +10 -1
  187. package/src/react/interactables-legacy/useInteractableState.ts +67 -0
  188. package/src/react/model-context/interactableTool.tsx +175 -0
  189. package/src/react/model-context/useInteractable.ts +233 -0
  190. package/src/react/model-context/useInteractableState.ts +31 -23
  191. package/src/react/model-context/useInteractableVersions.ts +48 -0
  192. package/src/react/primitive-hooks/useThreadMessageIds.ts +27 -0
  193. package/src/react/primitives/part/PartMessages.test.tsx +17 -0
  194. package/src/react/primitives/part/PartMessages.tsx +13 -8
  195. package/src/react/primitives/thread/ThreadMessages.tsx +67 -0
  196. package/src/react/providers/MessageByIdProvider.tsx +23 -0
  197. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +21 -1
  198. package/src/react/runtimes/createRuntimeExtras.test.ts +58 -0
  199. package/src/react/runtimes/createRuntimeExtras.ts +68 -0
  200. package/src/react/runtimes/createRuntimeExtras.use.test.ts +50 -0
  201. package/src/react/runtimes/useRemoteThreadListRuntime.ts +13 -1
  202. package/src/react/runtimes/useStreamingTiming.ts +68 -0
  203. package/src/react/types/scopes/interactables.ts +45 -23
  204. package/src/react/types/store-augmentation.ts +4 -2
  205. package/src/react/utils/useJSONEqual.ts +20 -0
  206. package/src/runtime/base/base-composer-runtime-core.ts +13 -0
  207. package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -1
  208. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  209. package/src/runtime/utils/message-repository.ts +11 -7
  210. package/src/runtime/utils/streaming-timing.test.ts +301 -0
  211. package/src/runtime/utils/streaming-timing.ts +161 -0
  212. package/src/runtime/utils/thread-message-like.ts +2 -1
  213. package/src/runtimes/external-store/external-store-adapter.ts +28 -0
  214. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +33 -1
  215. package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
  216. package/src/runtimes/remote-thread-list/types.ts +13 -0
  217. package/src/tests/MessageRepository.test.ts +2 -0
  218. package/src/tests/composer-can-send.test.ts +1 -0
  219. package/src/tests/default-edit-composer-runtime-core.test.ts +78 -1
  220. package/src/tests/default-thread-composer-runtime-core.test.ts +82 -0
  221. package/src/tests/external-store-thread-runtime-core.test.ts +337 -0
  222. package/src/tests/groupParts.test.ts +32 -0
  223. package/src/tests/remote-thread-list-reactive-threadId.test.ts +90 -0
  224. package/src/tests/remote-thread-list-test-helpers.ts +2 -1
  225. package/src/tests/thread-message-like.test.ts +50 -0
  226. package/dist/react/model-context/useAssistantInteractable.d.ts +0 -22
  227. package/dist/react/model-context/useAssistantInteractable.d.ts.map +0 -1
  228. package/dist/react/model-context/useAssistantInteractable.js.map +0 -1
@@ -0,0 +1,418 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mockGenerateId = vi.hoisted(() => vi.fn(() => "generated-id"));
4
+
5
+ vi.mock("../../utils/id", async (importOriginal) => ({
6
+ ...(await importOriginal<typeof import("../../utils/id")>()),
7
+ generateId: mockGenerateId,
8
+ }));
9
+
10
+ import {
11
+ buildInteractableModelContext,
12
+ type PartialJSONSchema,
13
+ } from "./interactable-model-context";
14
+ import type { Unstable_InteractableDefinition } from "../types/scopes/interactables";
15
+
16
+ const def = (
17
+ id: string,
18
+ name: string,
19
+ state: unknown = {},
20
+ ): Unstable_InteractableDefinition => ({
21
+ id,
22
+ name,
23
+ description: `desc of ${name}`,
24
+ stateSchema: { type: "object", properties: {} },
25
+ state,
26
+ initialState: state,
27
+ });
28
+
29
+ const partialNoteSchema = {
30
+ type: "object" as const,
31
+ properties: { title: { type: "string" as const } },
32
+ };
33
+
34
+ const partialTaskBoardSchema = {
35
+ type: "object" as const,
36
+ properties: {
37
+ tasks: {
38
+ type: "array" as const,
39
+ items: {
40
+ type: "object" as const,
41
+ properties: {
42
+ id: { type: "string" as const },
43
+ title: { type: "string" as const },
44
+ done: { type: "boolean" as const },
45
+ },
46
+ required: ["id", "title", "done"],
47
+ },
48
+ },
49
+ },
50
+ };
51
+
52
+ const build = (
53
+ definitions: Record<string, Unstable_InteractableDefinition>,
54
+ cache: Map<string, PartialJSONSchema> = new Map([["n1", partialNoteSchema]]),
55
+ ) => {
56
+ const setDefState = vi.fn(
57
+ (id: string, updater: (prev: unknown) => unknown) => {
58
+ const d = definitions[id];
59
+ if (d) definitions[id] = { ...d, state: updater(d.state) };
60
+ },
61
+ );
62
+ const ctx = buildInteractableModelContext(definitions, cache, setDefState);
63
+ return { ctx, setDefState };
64
+ };
65
+
66
+ beforeEach(() => {
67
+ mockGenerateId.mockReset();
68
+ mockGenerateId.mockReturnValue("generated-id");
69
+ });
70
+
71
+ describe("buildInteractableModelContext", () => {
72
+ it("returns undefined with no definitions", () => {
73
+ expect(build({}).ctx).toBeUndefined();
74
+ });
75
+
76
+ it("creates one tool per name regardless of instance count", () => {
77
+ const defs = {
78
+ n1: def("n1", "note"),
79
+ n2: def("n2", "note"),
80
+ b1: def("b1", "board"),
81
+ };
82
+ const { ctx } = build(defs);
83
+ expect(Object.keys(ctx!.tools).sort()).toEqual([
84
+ "update_board",
85
+ "update_note",
86
+ ]);
87
+ });
88
+
89
+ it("wraps the partial schema with a required id parameter", () => {
90
+ const { ctx } = build({ n1: def("n1", "note") });
91
+ const params = ctx!.tools["update_note"]!.parameters as {
92
+ properties: Record<string, unknown>;
93
+ required?: string[];
94
+ };
95
+ expect(Object.keys(params.properties)).toEqual(["id", "title"]);
96
+ expect(params.required).toEqual(["id"]);
97
+ });
98
+
99
+ it("keeps the reserved id parameter when the state schema also has id", () => {
100
+ const userIdProperty = { type: "number" as const };
101
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
102
+ try {
103
+ const { ctx } = build(
104
+ { n1: def("n1", "note") },
105
+ new Map([
106
+ [
107
+ "n1",
108
+ {
109
+ type: "object" as const,
110
+ properties: {
111
+ id: userIdProperty,
112
+ title: { type: "string" as const },
113
+ },
114
+ },
115
+ ],
116
+ ]),
117
+ );
118
+ const params = ctx!.tools["update_note"]!.parameters as {
119
+ properties: Record<string, { type: string }>;
120
+ required?: string[];
121
+ };
122
+
123
+ expect(Object.keys(params.properties)).toEqual(["id", "title"]);
124
+ expect(params.properties.id).toEqual({
125
+ type: "string",
126
+ description:
127
+ "The id of the instance to update, as shown in its state snapshot in the conversation.",
128
+ });
129
+ expect(params.properties.id).not.toBe(userIdProperty);
130
+ expect(params.required).toEqual(["id"]);
131
+ expect(warn).toHaveBeenCalledOnce();
132
+ } finally {
133
+ warn.mockRestore();
134
+ }
135
+ });
136
+
137
+ it("falls back to a permissive schema when the partial conversion failed", () => {
138
+ const { ctx } = build({ n1: def("n1", "note") }, new Map());
139
+ const params = ctx!.tools["update_note"]!.parameters as {
140
+ properties: Record<string, unknown>;
141
+ required?: string[];
142
+ additionalProperties?: boolean;
143
+ };
144
+ expect(params.required).toEqual(["id"]);
145
+ expect(params.additionalProperties).toBe(true);
146
+ });
147
+
148
+ it("exposes operation schemas for array fields", () => {
149
+ const { ctx } = build(
150
+ { b1: def("b1", "taskBoard") },
151
+ new Map([["b1", partialTaskBoardSchema]]),
152
+ );
153
+ const params = ctx!.tools["update_taskBoard"]!.parameters as {
154
+ properties: {
155
+ tasks: {
156
+ type: string;
157
+ properties: Record<string, unknown>;
158
+ };
159
+ };
160
+ };
161
+
162
+ expect(params.properties.tasks.type).toBe("object");
163
+ expect(Object.keys(params.properties.tasks.properties).sort()).toEqual([
164
+ "add",
165
+ "clear",
166
+ "remove",
167
+ "update",
168
+ ]);
169
+ });
170
+
171
+ it("omits id from add items but keeps it for update", () => {
172
+ const { ctx } = build(
173
+ { b1: def("b1", "taskBoard") },
174
+ new Map([["b1", partialTaskBoardSchema]]),
175
+ );
176
+ const tasks = (
177
+ ctx!.tools["update_taskBoard"]!.parameters as {
178
+ properties: {
179
+ tasks: {
180
+ properties: {
181
+ add: { items: { properties: object; required?: string[] } };
182
+ update: { items: { required?: string[] } };
183
+ };
184
+ };
185
+ };
186
+ }
187
+ ).properties.tasks.properties;
188
+
189
+ expect(Object.keys(tasks.add.items.properties)).not.toContain("id");
190
+ expect(tasks.add.items.required ?? []).not.toContain("id");
191
+ expect(tasks.update.items.required).toEqual(["id"]);
192
+ });
193
+
194
+ describe("execute", () => {
195
+ it("routes the partial update to the instance with the given id", async () => {
196
+ const defs = {
197
+ n1: def("n1", "note", { title: "a", color: "yellow" }),
198
+ n2: def("n2", "note", { title: "b", color: "blue" }),
199
+ };
200
+ const { ctx } = build(defs);
201
+ const result = await ctx!.tools["update_note"]!.execute!(
202
+ { id: "n2", title: "B!" },
203
+ {} as never,
204
+ );
205
+ expect(result).toEqual({ success: true, id: "n2" });
206
+ expect(defs.n2.state).toEqual({ title: "B!", color: "blue" });
207
+ expect(defs.n1.state).toEqual({ title: "a", color: "yellow" });
208
+ });
209
+
210
+ it("accepts an id-less call while exactly one instance exists", async () => {
211
+ const defs = { n1: def("n1", "note", { title: "a" }) };
212
+ const { ctx } = build(defs);
213
+ const result = await ctx!.tools["update_note"]!.execute!(
214
+ { title: "B" },
215
+ {} as never,
216
+ );
217
+ expect(result).toEqual({ success: true, id: "n1" });
218
+ expect(defs.n1.state).toEqual({ title: "B" });
219
+ });
220
+
221
+ it("mints an id for an added item that has none", async () => {
222
+ const defs = { b1: def("b1", "taskBoard", { tasks: [] }) };
223
+ const { ctx } = build(defs, new Map([["b1", partialTaskBoardSchema]]));
224
+ const result = await ctx!.tools["update_taskBoard"]!.execute!(
225
+ { id: "b1", tasks: { add: [{ title: "Write tests", done: false }] } },
226
+ {} as never,
227
+ );
228
+ expect(result).toEqual({
229
+ success: true,
230
+ id: "b1",
231
+ addedItemIds: { tasks: ["generated-id"] },
232
+ });
233
+ const tasks = (defs.b1.state as { tasks: Record<string, unknown>[] })
234
+ .tasks;
235
+ expect(tasks).toHaveLength(1);
236
+ expect(tasks[0]).toEqual({
237
+ title: "Write tests",
238
+ done: false,
239
+ id: "generated-id",
240
+ });
241
+ expect(mockGenerateId).toHaveBeenCalledOnce();
242
+ });
243
+
244
+ it("rejects an unknown id and lists valid ids", async () => {
245
+ const defs = { n1: def("n1", "note"), n2: def("n2", "note") };
246
+ const { ctx, setDefState } = build(defs);
247
+ const result = (await ctx!.tools["update_note"]!.execute!(
248
+ { id: "nope", title: "B" },
249
+ {} as never,
250
+ )) as { success: boolean; error?: string };
251
+ expect(result.success).toBe(false);
252
+ expect(result.error).toContain("n1");
253
+ expect(result.error).toContain("n2");
254
+ expect(setDefState).not.toHaveBeenCalled();
255
+ });
256
+
257
+ it("rejects an id-less call when multiple instances exist", async () => {
258
+ const defs = { n1: def("n1", "note"), n2: def("n2", "note") };
259
+ const { ctx, setDefState } = build(defs);
260
+ const result = (await ctx!.tools["update_note"]!.execute!(
261
+ { title: "B" },
262
+ {} as never,
263
+ )) as { success: boolean };
264
+ expect(result.success).toBe(false);
265
+ expect(setDefState).not.toHaveBeenCalled();
266
+ });
267
+
268
+ it("rejects an id that belongs to a different name", async () => {
269
+ const defs = { n1: def("n1", "note"), b1: def("b1", "board") };
270
+ const { ctx } = build(defs);
271
+ const result = (await ctx!.tools["update_note"]!.execute!(
272
+ { id: "b1", title: "B" },
273
+ {} as never,
274
+ )) as { success: boolean };
275
+ expect(result.success).toBe(false);
276
+ });
277
+
278
+ it("applies array operations", async () => {
279
+ const defs = {
280
+ b1: def("b1", "taskBoard", {
281
+ tasks: [
282
+ { id: "a", title: "A", done: false },
283
+ { id: "b", title: "B", done: false },
284
+ ],
285
+ }),
286
+ };
287
+ const { ctx } = build(defs, new Map([["b1", partialTaskBoardSchema]]));
288
+
289
+ await ctx!.tools["update_taskBoard"]!.execute!(
290
+ {
291
+ id: "b1",
292
+ tasks: {
293
+ update: [{ id: "a", done: true }],
294
+ remove: ["b"],
295
+ add: [{ id: "c", title: "C", done: false }],
296
+ },
297
+ },
298
+ {} as never,
299
+ );
300
+
301
+ expect(defs.b1.state).toEqual({
302
+ tasks: [
303
+ { id: "a", title: "A", done: true },
304
+ { id: "c", title: "C", done: false },
305
+ ],
306
+ });
307
+ });
308
+ });
309
+
310
+ describe("streamCall", () => {
311
+ const makeReader = (values: unknown[]) =>
312
+ ({
313
+ args: {
314
+ streamValues: async function* () {
315
+ yield* values;
316
+ },
317
+ },
318
+ }) as never;
319
+
320
+ it("applies partial values once the id is followed by state fields", async () => {
321
+ const defs = {
322
+ n1: def("n1", "note", { title: "a" }),
323
+ n2: def("n2", "note", { title: "b" }),
324
+ };
325
+ const { ctx, setDefState } = build(defs);
326
+ await ctx!.tools["update_note"]!.streamCall!(
327
+ makeReader([
328
+ { id: "n" },
329
+ { id: "n2" },
330
+ { id: "n2", title: "B" },
331
+ { id: "n2", title: "B!" },
332
+ ]),
333
+ {} as never,
334
+ );
335
+ expect(defs.n2.state).toEqual({ title: "B!" });
336
+ expect(defs.n1.state).toEqual({ title: "a" });
337
+ expect(setDefState).toHaveBeenCalledTimes(2);
338
+ });
339
+
340
+ it("does not route id-last prefix chunks to the wrong instance", async () => {
341
+ const defs = {
342
+ "note-1": def("note-1", "note", { title: "a" }),
343
+ "note-12": def("note-12", "note", { title: "b" }),
344
+ };
345
+ const { ctx, setDefState } = build(defs);
346
+ await ctx!.tools["update_note"]!.streamCall!(
347
+ makeReader([
348
+ { title: "B", id: "note-1" },
349
+ { title: "B", id: "note-12" },
350
+ ]),
351
+ {} as never,
352
+ );
353
+ expect(defs["note-1"].state).toEqual({ title: "a" });
354
+ expect(defs["note-12"].state).toEqual({ title: "b" });
355
+ expect(setDefState).not.toHaveBeenCalled();
356
+
357
+ const result = await ctx!.tools["update_note"]!.execute!(
358
+ { title: "B", id: "note-12" },
359
+ {} as never,
360
+ );
361
+ expect(result).toEqual({ success: true, id: "note-12" });
362
+ expect(defs["note-12"].state).toEqual({ title: "B" });
363
+ });
364
+
365
+ it("streams id-less values to a single instance", async () => {
366
+ const defs = { n1: def("n1", "note", { title: "a" }) };
367
+ const { ctx } = build(defs);
368
+ await ctx!.tools["update_note"]!.streamCall!(
369
+ makeReader([{ title: "B" }]),
370
+ {} as never,
371
+ );
372
+ expect(defs.n1.state).toEqual({ title: "B" });
373
+ });
374
+
375
+ it("streams array operations from the call baseline", async () => {
376
+ const defs = {
377
+ b1: def("b1", "taskBoard", {
378
+ tasks: [{ id: "a", title: "A", done: false }],
379
+ }),
380
+ };
381
+ const { ctx } = build(defs, new Map([["b1", partialTaskBoardSchema]]));
382
+ const context = { toolCallId: "call-1" } as never;
383
+
384
+ await ctx!.tools["update_taskBoard"]!.streamCall!(
385
+ makeReader([
386
+ { id: "b1", tasks: { add: [{ id: "b", title: "B" }] } },
387
+ {
388
+ id: "b1",
389
+ tasks: { add: [{ id: "b", title: "B", done: false }] },
390
+ },
391
+ ]),
392
+ context,
393
+ );
394
+
395
+ expect(defs.b1.state).toEqual({
396
+ tasks: [
397
+ { id: "a", title: "A", done: false },
398
+ { id: "b", title: "B", done: false },
399
+ ],
400
+ });
401
+
402
+ await ctx!.tools["update_taskBoard"]!.execute!(
403
+ {
404
+ id: "b1",
405
+ tasks: { add: [{ id: "b", title: "B", done: false }] },
406
+ },
407
+ context,
408
+ );
409
+
410
+ expect(defs.b1.state).toEqual({
411
+ tasks: [
412
+ { id: "a", title: "A", done: false },
413
+ { id: "b", title: "B", done: false },
414
+ ],
415
+ });
416
+ });
417
+ });
418
+ });