@assistant-ui/core 0.2.11 → 0.2.14

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 (212) hide show
  1. package/dist/adapters/thread-history.d.ts +3 -1
  2. package/dist/adapters/thread-history.d.ts.map +1 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/AssistantProvider.js +6 -1
  6. package/dist/react/AssistantProvider.js.map +1 -1
  7. package/dist/react/RuntimeAdapter.d.ts +1 -1
  8. package/dist/react/RuntimeAdapter.d.ts.map +1 -1
  9. package/dist/react/RuntimeAdapter.js +16 -6
  10. package/dist/react/RuntimeAdapter.js.map +1 -1
  11. package/dist/react/client/DataRenderers.d.ts +1 -8
  12. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  13. package/dist/react/client/DataRenderers.js +3 -2
  14. package/dist/react/client/DataRenderers.js.map +1 -1
  15. package/dist/react/client/Interactables.d.ts +1 -1
  16. package/dist/react/client/Interactables.d.ts.map +1 -1
  17. package/dist/react/client/Interactables.js +4 -3
  18. package/dist/react/client/Interactables.js.map +1 -1
  19. package/dist/react/client/Tools.d.ts +2 -13
  20. package/dist/react/client/Tools.d.ts.map +1 -1
  21. package/dist/react/client/Tools.js +4 -3
  22. package/dist/react/client/Tools.js.map +1 -1
  23. package/dist/react/primitives/message/MessageGroupedParts.d.ts +3 -2
  24. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  25. package/dist/react/primitives/message/MessageGroupedParts.js +4 -4
  26. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  27. package/dist/react/primitives/message/MessageParts.d.ts +28 -1
  28. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  29. package/dist/react/primitives/message/MessageParts.js +43 -9
  30. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  31. package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
  32. package/dist/react/providers/TextMessagePartProvider.js +3 -2
  33. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  34. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
  35. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  36. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
  37. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +1 -0
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  40. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  41. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +6 -0
  42. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  43. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  44. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +2 -0
  45. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  46. package/dist/react/utils/groupParts.d.ts +13 -1
  47. package/dist/react/utils/groupParts.d.ts.map +1 -1
  48. package/dist/react/utils/groupParts.js +17 -5
  49. package/dist/react/utils/groupParts.js.map +1 -1
  50. package/dist/runtime/api/bindings.d.ts +1 -0
  51. package/dist/runtime/api/bindings.d.ts.map +1 -1
  52. package/dist/runtime/api/message-runtime.d.ts +2 -0
  53. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  54. package/dist/runtime/api/message-runtime.js +5 -0
  55. package/dist/runtime/api/message-runtime.js.map +1 -1
  56. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  57. package/dist/runtime/api/thread-list-runtime.js +1 -0
  58. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  59. package/dist/runtime/api/thread-runtime.d.ts +3 -0
  60. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  61. package/dist/runtime/api/thread-runtime.js +4 -0
  62. package/dist/runtime/api/thread-runtime.js.map +1 -1
  63. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -0
  64. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  65. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  66. package/dist/runtime/branch/external-thread-branch-adapter.d.ts +30 -0
  67. package/dist/runtime/branch/external-thread-branch-adapter.d.ts.map +1 -0
  68. package/dist/runtime/branch/external-thread-branch-adapter.js +0 -0
  69. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +1 -0
  70. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/interfaces/thread-runtime-core.d.ts +2 -0
  72. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  73. package/dist/runtimes/external-store/external-store-adapter.d.ts +1 -0
  74. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  75. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
  76. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  77. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +13 -0
  78. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  79. package/dist/runtimes/local/local-runtime-options.d.ts +1 -1
  80. package/dist/runtimes/local/local-thread-runtime-core.d.ts +8 -1
  81. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  82. package/dist/runtimes/local/local-thread-runtime-core.js +63 -5
  83. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  84. package/dist/runtimes/local/should-continue.js +4 -2
  85. package/dist/runtimes/local/should-continue.js.map +1 -1
  86. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +2 -0
  87. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  88. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +4 -0
  89. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  90. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  91. package/dist/runtimes/remote-thread-list/empty-thread-core.js +4 -0
  92. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  93. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -0
  94. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  95. package/dist/runtimes/remote-thread-list/remote-thread-state.js +1 -0
  96. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  97. package/dist/runtimes/remote-thread-list/types.d.ts +1 -0
  98. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  99. package/dist/store/clients/chain-of-thought-client.d.ts +2 -7
  100. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  101. package/dist/store/clients/chain-of-thought-client.js +3 -2
  102. package/dist/store/clients/chain-of-thought-client.js.map +1 -1
  103. package/dist/store/clients/model-context-client.d.ts +1 -1
  104. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  105. package/dist/store/clients/model-context-client.js +3 -2
  106. package/dist/store/clients/model-context-client.js.map +1 -1
  107. package/dist/store/clients/no-op-composer-client.d.ts +2 -4
  108. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  109. package/dist/store/clients/no-op-composer-client.js +3 -2
  110. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  111. package/dist/store/clients/runtime-adapter.d.ts +1 -3
  112. package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
  113. package/dist/store/clients/runtime-adapter.js +2 -15
  114. package/dist/store/clients/runtime-adapter.js.map +1 -1
  115. package/dist/store/clients/suggestions.d.ts +1 -4
  116. package/dist/store/clients/suggestions.d.ts.map +1 -1
  117. package/dist/store/clients/suggestions.js +6 -4
  118. package/dist/store/clients/suggestions.js.map +1 -1
  119. package/dist/store/clients/thread-message-client.d.ts +1 -1
  120. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  121. package/dist/store/clients/thread-message-client.js +14 -10
  122. package/dist/store/clients/thread-message-client.js.map +1 -1
  123. package/dist/store/internal.d.ts +2 -2
  124. package/dist/store/internal.js +2 -2
  125. package/dist/store/runtime-clients/attachment-runtime-client.d.ts +2 -4
  126. package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
  127. package/dist/store/runtime-clients/attachment-runtime-client.js +3 -2
  128. package/dist/store/runtime-clients/attachment-runtime-client.js.map +1 -1
  129. package/dist/store/runtime-clients/composer-runtime-client.d.ts +2 -10
  130. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  131. package/dist/store/runtime-clients/composer-runtime-client.js +9 -6
  132. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  133. package/dist/store/runtime-clients/message-part-runtime-client.d.ts +2 -4
  134. package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
  135. package/dist/store/runtime-clients/message-part-runtime-client.js +3 -2
  136. package/dist/store/runtime-clients/message-part-runtime-client.js.map +1 -1
  137. package/dist/store/runtime-clients/message-runtime-client.d.ts +2 -7
  138. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  139. package/dist/store/runtime-clients/message-runtime-client.js +10 -6
  140. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  141. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +2 -4
  142. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  143. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +3 -2
  144. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  145. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts +2 -5
  146. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  147. package/dist/store/runtime-clients/thread-list-runtime-client.js +6 -4
  148. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  149. package/dist/store/runtime-clients/thread-runtime-client.d.ts +2 -4
  150. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  151. package/dist/store/runtime-clients/thread-runtime-client.js +7 -4
  152. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  153. package/dist/store/scopes/message.d.ts +1 -0
  154. package/dist/store/scopes/message.d.ts.map +1 -1
  155. package/dist/store/scopes/thread-list-item.d.ts +1 -0
  156. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  157. package/dist/store/scopes/thread.d.ts +1 -0
  158. package/dist/store/scopes/thread.d.ts.map +1 -1
  159. package/package.json +4 -4
  160. package/src/adapters/thread-history.ts +2 -0
  161. package/src/index.ts +1 -0
  162. package/src/react/AssistantProvider.tsx +3 -1
  163. package/src/react/RuntimeAdapter.ts +25 -8
  164. package/src/react/client/DataRenderers.ts +42 -45
  165. package/src/react/client/Interactables.ts +261 -261
  166. package/src/react/client/Tools.ts +6 -4
  167. package/src/react/primitives/message/MessageGroupedParts.tsx +19 -7
  168. package/src/react/primitives/message/MessageParts.tsx +64 -13
  169. package/src/react/providers/TextMessagePartProvider.tsx +5 -3
  170. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +1 -0
  171. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +11 -0
  172. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +6 -0
  173. package/src/react/utils/groupParts.ts +27 -0
  174. package/src/runtime/api/bindings.ts +1 -0
  175. package/src/runtime/api/message-runtime.ts +7 -0
  176. package/src/runtime/api/thread-list-runtime.ts +1 -0
  177. package/src/runtime/api/thread-runtime.ts +7 -0
  178. package/src/runtime/base/base-thread-runtime-core.ts +1 -0
  179. package/src/runtime/branch/external-thread-branch-adapter.ts +26 -0
  180. package/src/runtime/interfaces/thread-list-runtime-core.ts +1 -0
  181. package/src/runtime/interfaces/thread-runtime-core.ts +2 -0
  182. package/src/runtimes/external-store/external-store-adapter.ts +1 -0
  183. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +24 -0
  184. package/src/runtimes/local/local-runtime-options.ts +1 -1
  185. package/src/runtimes/local/local-thread-runtime-core.test.ts +311 -0
  186. package/src/runtimes/local/local-thread-runtime-core.ts +104 -7
  187. package/src/runtimes/local/should-continue.ts +23 -13
  188. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +5 -0
  189. package/src/runtimes/remote-thread-list/empty-thread-core.ts +5 -0
  190. package/src/runtimes/remote-thread-list/remote-thread-state.ts +2 -0
  191. package/src/runtimes/remote-thread-list/types.ts +1 -0
  192. package/src/store/clients/chain-of-thought-client.ts +5 -3
  193. package/src/store/clients/model-context-client.test.ts +5 -4
  194. package/src/store/clients/model-context-client.ts +21 -21
  195. package/src/store/clients/no-op-composer-client.ts +5 -3
  196. package/src/store/clients/runtime-adapter.ts +0 -24
  197. package/src/store/clients/suggestions.ts +9 -18
  198. package/src/store/clients/thread-message-client.ts +29 -26
  199. package/src/store/internal.ts +1 -4
  200. package/src/store/runtime-clients/attachment-runtime-client.ts +14 -14
  201. package/src/store/runtime-clients/composer-runtime-client.ts +30 -24
  202. package/src/store/runtime-clients/message-part-runtime-client.ts +5 -3
  203. package/src/store/runtime-clients/message-runtime-client.ts +26 -19
  204. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +5 -3
  205. package/src/store/runtime-clients/thread-list-runtime-client.ts +10 -6
  206. package/src/store/runtime-clients/thread-runtime-client.ts +11 -6
  207. package/src/store/scopes/message.ts +1 -0
  208. package/src/store/scopes/thread-list-item.ts +1 -0
  209. package/src/store/scopes/thread.ts +1 -0
  210. package/src/tests/external-store-thread-runtime-core.test.ts +57 -0
  211. package/src/tests/groupMessageParts.test.ts +84 -0
  212. package/src/tests/groupParts.test.ts +55 -0
@@ -0,0 +1,311 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { LocalRuntimeCore } from "./local-runtime-core";
3
+ import type {
4
+ ChatModelAdapter,
5
+ ChatModelRunOptions,
6
+ ChatModelRunResult,
7
+ } from "../../runtime/utils/chat-model-adapter";
8
+ import type { AppendMessage } from "../../types/message";
9
+
10
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 10));
11
+
12
+ const createThread = (adapter: ChatModelAdapter) => {
13
+ const core = new LocalRuntimeCore(
14
+ {
15
+ adapters: { chatModel: adapter },
16
+ unstable_humanToolNames: ["send_email"],
17
+ },
18
+ undefined,
19
+ );
20
+ return core.threads.getMainThreadRuntimeCore();
21
+ };
22
+
23
+ const userMessage = (text: string): AppendMessage => ({
24
+ parentId: null,
25
+ sourceId: null,
26
+ runConfig: {},
27
+ role: "user",
28
+ content: [{ type: "text", text }],
29
+ attachments: [],
30
+ metadata: { custom: {} },
31
+ createdAt: new Date(),
32
+ });
33
+
34
+ const toolCallPart = (toolName: string, approval?: { id: string }) => ({
35
+ type: "tool-call" as const,
36
+ toolCallId: `call-${toolName}`,
37
+ toolName,
38
+ args: {},
39
+ argsText: "{}",
40
+ ...(approval !== undefined ? { approval } : {}),
41
+ });
42
+
43
+ const toolCallResult = (
44
+ toolName: string,
45
+ approval?: { id: string },
46
+ ): ChatModelRunResult => ({
47
+ content: [toolCallPart(toolName, approval)],
48
+ status: { type: "requires-action", reason: "tool-calls" },
49
+ });
50
+
51
+ const createApprovalThread = (firstResult: ChatModelRunResult) => {
52
+ const runs: ChatModelRunOptions[] = [];
53
+ const thread = createThread({
54
+ async run(options) {
55
+ runs.push(options);
56
+ if (runs.length === 1) return firstResult;
57
+ return { content: [{ type: "text", text: "done" }] };
58
+ },
59
+ });
60
+ return { thread, runs };
61
+ };
62
+
63
+ describe("LocalThreadRuntimeCore human-in-the-loop tools", () => {
64
+ it("pauses on requires-action while a listed tool call has no result", async () => {
65
+ const { thread, runs } = createApprovalThread(toolCallResult("send_email"));
66
+
67
+ await thread.append(userMessage("send an email"));
68
+ await flush();
69
+
70
+ expect(runs).toHaveLength(1);
71
+ expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
72
+ });
73
+
74
+ it("does not hold the run for unlisted tool calls", async () => {
75
+ const { thread, runs } = createApprovalThread(
76
+ toolCallResult("lookup_weather"),
77
+ );
78
+
79
+ await thread.append(userMessage("what is the weather"));
80
+ await flush();
81
+
82
+ expect(runs).toHaveLength(2);
83
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
84
+ });
85
+
86
+ it("resumes via addToolResult and exposes the result to the adapter", async () => {
87
+ const { thread, runs } = createApprovalThread(toolCallResult("send_email"));
88
+
89
+ await thread.append(userMessage("send an email"));
90
+ await flush();
91
+
92
+ const assistantMessage = thread.messages.at(-1)!;
93
+ thread.addToolResult({
94
+ messageId: assistantMessage.id,
95
+ toolCallId: "call-send_email",
96
+ toolName: "send_email",
97
+ result: { approved: true },
98
+ isError: false,
99
+ });
100
+ await flush();
101
+
102
+ expect(runs).toHaveLength(2);
103
+ const resumed = runs[1]!;
104
+ expect(resumed.messages.at(-1)?.role).toBe("user");
105
+ const toolCall = resumed
106
+ .unstable_getMessage()
107
+ .content.find((part) => part.type === "tool-call");
108
+ expect(toolCall?.result).toEqual({ approved: true });
109
+
110
+ const finalMessage = thread.messages.at(-1)!;
111
+ expect(finalMessage.status?.type).toBe("complete");
112
+ expect(finalMessage.content.map((part) => part.type)).toEqual([
113
+ "tool-call",
114
+ "text",
115
+ ]);
116
+ });
117
+ });
118
+
119
+ describe("LocalThreadRuntimeCore tool approvals", () => {
120
+ it("pauses the run while an approval is pending, even for unlisted tools", async () => {
121
+ const { thread, runs } = createApprovalThread(
122
+ toolCallResult("deploy", { id: "a1" }),
123
+ );
124
+
125
+ await thread.append(userMessage("deploy the app"));
126
+ await flush();
127
+
128
+ expect(runs).toHaveLength(1);
129
+ expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
130
+ });
131
+
132
+ it("records an approval and resumes, exempting the gated tool from the human tool result requirement", async () => {
133
+ const { thread, runs } = createApprovalThread(
134
+ toolCallResult("send_email", { id: "a1" }),
135
+ );
136
+
137
+ await thread.append(userMessage("send an email"));
138
+ await flush();
139
+
140
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
141
+ await flush();
142
+
143
+ expect(runs).toHaveLength(2);
144
+ const toolCall = runs[1]!
145
+ .unstable_getMessage()
146
+ .content.find((part) => part.type === "tool-call");
147
+ expect(toolCall?.approval).toEqual({ id: "a1", approved: true });
148
+ expect(toolCall?.result).toBeUndefined();
149
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
150
+ });
151
+
152
+ it("continues multi-step turns after an approval resume", async () => {
153
+ const runs: ChatModelRunOptions[] = [];
154
+ const thread = createThread({
155
+ async run(options) {
156
+ runs.push(options);
157
+ if (runs.length === 1) return toolCallResult("deploy", { id: "a1" });
158
+ if (runs.length === 2) return toolCallResult("lookup_weather");
159
+ return { content: [{ type: "text", text: "done" }] };
160
+ },
161
+ });
162
+
163
+ await thread.append(userMessage("deploy the app"));
164
+ await flush();
165
+
166
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
167
+ await flush();
168
+
169
+ expect(runs).toHaveLength(3);
170
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
171
+ });
172
+
173
+ it("resumes when a result is added to an approval-gated tool call", async () => {
174
+ const { thread, runs } = createApprovalThread(
175
+ toolCallResult("deploy", { id: "a1" }),
176
+ );
177
+
178
+ await thread.append(userMessage("deploy the app"));
179
+ await flush();
180
+
181
+ thread.addToolResult({
182
+ messageId: thread.messages.at(-1)!.id,
183
+ toolCallId: "call-deploy",
184
+ toolName: "deploy",
185
+ result: "done manually",
186
+ isError: false,
187
+ });
188
+ await flush();
189
+
190
+ expect(runs).toHaveLength(2);
191
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
192
+ });
193
+
194
+ it("records a denial and synthesizes an error result", async () => {
195
+ const { thread, runs } = createApprovalThread(
196
+ toolCallResult("deploy", { id: "a1" }),
197
+ );
198
+
199
+ await thread.append(userMessage("deploy the app"));
200
+ await flush();
201
+
202
+ thread.respondToToolApproval({
203
+ approvalId: "a1",
204
+ approved: false,
205
+ reason: "not today",
206
+ });
207
+ await flush();
208
+
209
+ expect(runs).toHaveLength(2);
210
+ const toolCall = thread.messages
211
+ .at(-1)!
212
+ .content.find((part) => part.type === "tool-call");
213
+ expect(toolCall?.approval).toEqual({
214
+ id: "a1",
215
+ approved: false,
216
+ reason: "not today",
217
+ });
218
+ expect(toolCall?.result).toEqual({ error: "not today" });
219
+ expect(toolCall?.isError).toBe(true);
220
+ });
221
+
222
+ it("synthesizes a default denial reason", async () => {
223
+ const { thread } = createApprovalThread(
224
+ toolCallResult("deploy", { id: "a1" }),
225
+ );
226
+
227
+ await thread.append(userMessage("deploy the app"));
228
+ await flush();
229
+
230
+ thread.respondToToolApproval({ approvalId: "a1", approved: false });
231
+ await flush();
232
+
233
+ const toolCall = thread.messages
234
+ .at(-1)!
235
+ .content.find((part) => part.type === "tool-call");
236
+ expect(toolCall?.result).toEqual({ error: "Tool approval denied" });
237
+ });
238
+
239
+ it("waits until every pending approval is decided before resuming", async () => {
240
+ const { thread, runs } = createApprovalThread({
241
+ content: [
242
+ toolCallPart("deploy", { id: "a1" }),
243
+ toolCallPart("send_invoice", { id: "a2" }),
244
+ ],
245
+ status: { type: "requires-action", reason: "tool-calls" },
246
+ });
247
+
248
+ await thread.append(userMessage("deploy and bill"));
249
+ await flush();
250
+
251
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
252
+ await flush();
253
+ expect(runs).toHaveLength(1);
254
+
255
+ expect(() =>
256
+ thread.respondToToolApproval({ approvalId: "a1", approved: false }),
257
+ ).toThrowError(/already decided/);
258
+
259
+ thread.respondToToolApproval({ approvalId: "a2", approved: false });
260
+ await flush();
261
+ expect(runs).toHaveLength(2);
262
+ });
263
+
264
+ it("throws while the run is still in flight, even if the message already reads requires-action", async () => {
265
+ let release!: () => void;
266
+ const gate = new Promise<void>((resolve) => (release = resolve));
267
+ const runs: ChatModelRunOptions[] = [];
268
+ const thread = createThread({
269
+ async *run(options) {
270
+ runs.push(options);
271
+ if (runs.length === 1) {
272
+ yield toolCallResult("deploy", { id: "a1" });
273
+ await gate;
274
+ return;
275
+ }
276
+ yield { content: [{ type: "text", text: "done" }] };
277
+ },
278
+ });
279
+
280
+ const appendPromise = thread.append(userMessage("deploy the app"));
281
+ await flush();
282
+
283
+ expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
284
+ expect(() =>
285
+ thread.respondToToolApproval({ approvalId: "a1", approved: true }),
286
+ ).toThrowError(/run is in progress/);
287
+
288
+ release();
289
+ await appendPromise;
290
+
291
+ thread.respondToToolApproval({ approvalId: "a1", approved: true });
292
+ await flush();
293
+
294
+ expect(runs).toHaveLength(2);
295
+ expect(thread.messages.at(-1)?.status?.type).toBe("complete");
296
+ });
297
+
298
+ it("throws for unknown approvals and unsupported tool call resumption", async () => {
299
+ const { thread } = createApprovalThread(toolCallResult("send_email"));
300
+
301
+ await thread.append(userMessage("send an email"));
302
+ await flush();
303
+
304
+ expect(() =>
305
+ thread.respondToToolApproval({ approvalId: "nope", approved: true }),
306
+ ).toThrowError(/non-existing tool approval/);
307
+ expect(() =>
308
+ thread.resumeToolCall({ toolCallId: "call-send_email", payload: {} }),
309
+ ).toThrowError(/unstable_humanToolNames/);
310
+ });
311
+ });
@@ -49,6 +49,7 @@ export class LocalThreadRuntimeCore
49
49
  switchToBranch: true,
50
50
  switchBranchDuringRun: true,
51
51
  edit: true,
52
+ delete: false,
52
53
  reload: true,
53
54
  cancel: true,
54
55
  unstable_copy: true,
@@ -150,6 +151,12 @@ export class LocalThreadRuntimeCore
150
151
  hasUpdates = true;
151
152
  }
152
153
 
154
+ const canDelete = options.adapters?.history?.delete !== undefined;
155
+ if (this.capabilities.delete !== canDelete) {
156
+ this.capabilities.delete = canDelete;
157
+ hasUpdates = true;
158
+ }
159
+
153
160
  const canQueue = options.unstable_enableMessageQueue === true;
154
161
  if (canQueue && !this._queue) {
155
162
  this._queue = createMessageQueue({
@@ -275,6 +282,25 @@ export class LocalThreadRuntimeCore
275
282
  }
276
283
  }
277
284
 
285
+ public async deleteMessage(messageId: string): Promise<void> {
286
+ const adapter = this._options.adapters.history;
287
+ if (!adapter?.delete)
288
+ throw new Error("Runtime does not support deleting messages.");
289
+
290
+ const messages = this.repository.getMessages();
291
+ const messageIndex = messages.findIndex((m) => m.id === messageId);
292
+ if (messageIndex === -1) throw new Error("Message not found.");
293
+
294
+ const message = messages[messageIndex]!;
295
+ const parentId = messages[messageIndex - 1]?.id ?? null;
296
+ const items = [{ parentId, message }];
297
+
298
+ await adapter.delete(items);
299
+
300
+ this.repository.deleteMessage(messageId);
301
+ this._notifySubscribers();
302
+ }
303
+
278
304
  public resumeRun({ stream, ...startConfig }: ResumeRunConfig): Promise<void> {
279
305
  if (!stream)
280
306
  throw new Error("You must pass a stream parameter to resume runs.");
@@ -297,7 +323,7 @@ export class LocalThreadRuntimeCore
297
323
 
298
324
  // add assistant message
299
325
  const id = generateId();
300
- let message: ThreadAssistantMessage = {
326
+ const message: ThreadAssistantMessage = {
301
327
  id,
302
328
  role: "assistant",
303
329
  status: { type: "running" },
@@ -312,6 +338,15 @@ export class LocalThreadRuntimeCore
312
338
  createdAt: new Date(),
313
339
  };
314
340
 
341
+ return this._runLoop(parentId, message, runConfig, runCallback);
342
+ }
343
+
344
+ private async _runLoop(
345
+ parentId: string | null,
346
+ message: ThreadAssistantMessage,
347
+ runConfig: RunConfig | undefined,
348
+ runCallback?: ChatModelAdapter["run"],
349
+ ): Promise<void> {
315
350
  this._notifyEventSubscribers("runStart", {});
316
351
 
317
352
  try {
@@ -592,22 +627,84 @@ export class LocalThreadRuntimeCore
592
627
  content: newContent,
593
628
  };
594
629
  this.repository.addOrUpdateMessage(parentId, message);
630
+ this._notifySubscribers();
595
631
 
632
+ // a result may arrive mid-run or on a non-head message; the resume
633
+ // intentionally aborts any in-flight run, unlike respondToToolApproval
596
634
  if (
597
635
  added &&
598
636
  shouldContinue(message, this._options.unstable_humanToolNames)
599
637
  ) {
600
- this.performRoundtrip(parentId, message, this._lastRunConfig).catch(
601
- () => {},
602
- );
638
+ this._runLoop(parentId, message, this._lastRunConfig).catch(() => {});
603
639
  }
604
640
  }
605
641
 
606
642
  public resumeToolCall(_options: ResumeToolCallOptions) {
607
- throw new Error("Local runtime does not support resuming tool calls.");
643
+ throw new Error(
644
+ "Local runtime does not support resuming tool calls. For human-in-the-loop tools, list the tool in unstable_humanToolNames and complete the call with addToolResult.",
645
+ );
608
646
  }
609
647
 
610
- public respondToToolApproval(_options: RespondToToolApprovalOptions) {
611
- throw new Error("Local runtime does not support tool approvals.");
648
+ public respondToToolApproval({
649
+ approvalId,
650
+ approved,
651
+ reason,
652
+ }: RespondToToolApprovalOptions) {
653
+ let message = this.repository
654
+ .getMessages()
655
+ .findLast(
656
+ (m): m is ThreadAssistantMessage =>
657
+ m.role === "assistant" &&
658
+ m.content.some(
659
+ (c) => c.type === "tool-call" && c.approval?.id === approvalId,
660
+ ),
661
+ );
662
+
663
+ if (!message)
664
+ throw new Error("Tried to respond to a non-existing tool approval");
665
+
666
+ if (this.abortController !== null)
667
+ throw new Error(
668
+ "Tried to respond to a tool approval while a run is in progress",
669
+ );
670
+
671
+ if (message.status?.type !== "requires-action")
672
+ throw new Error(
673
+ "Tried to respond to a tool approval on a message whose status is not requires-action",
674
+ );
675
+
676
+ let recorded = false;
677
+ const newContent = message.content.map((c) => {
678
+ if (c.type !== "tool-call" || c.approval?.id !== approvalId) return c;
679
+ if (c.approval.approved !== undefined) return c;
680
+ recorded = true;
681
+ const approval = {
682
+ ...c.approval,
683
+ approved,
684
+ ...(reason != null && { reason }),
685
+ };
686
+ if (approved) return { ...c, approval };
687
+ return {
688
+ ...c,
689
+ approval,
690
+ result: { error: reason || "Tool approval denied" },
691
+ isError: true,
692
+ };
693
+ });
694
+
695
+ if (!recorded)
696
+ throw new Error("Tried to respond to an already decided tool approval");
697
+
698
+ message = { ...message, content: newContent };
699
+ const { parentId } = this.repository.getMessage(message.id);
700
+ this.repository.addOrUpdateMessage(parentId, message);
701
+ this._notifySubscribers();
702
+
703
+ if (
704
+ this.repository.headId === message.id &&
705
+ shouldContinue(message, this._options.unstable_humanToolNames)
706
+ ) {
707
+ this._runLoop(parentId, message, this._lastRunConfig).catch(() => {});
708
+ }
612
709
  }
613
710
  }
@@ -4,23 +4,33 @@ export const shouldContinue = (
4
4
  result: ThreadAssistantMessage,
5
5
  humanToolNames: string[] | undefined,
6
6
  ) => {
7
+ if (
8
+ result.status?.type !== "requires-action" ||
9
+ result.status.reason !== "tool-calls"
10
+ )
11
+ return false;
12
+
13
+ const hasPendingApproval = result.content.some(
14
+ (c) =>
15
+ c.type === "tool-call" &&
16
+ c.result === undefined &&
17
+ c.approval !== undefined &&
18
+ c.approval.approved === undefined,
19
+ );
20
+ if (hasPendingApproval) return false;
21
+
7
22
  // TODO legacy behavior -- make specifying human tool names required
8
23
  if (humanToolNames === undefined) {
9
- return (
10
- result.status?.type === "requires-action" &&
11
- result.status.reason === "tool-calls" &&
12
- result.content.every((c) => c.type !== "tool-call" || !!c.result)
24
+ return result.content.every(
25
+ (c) => c.type !== "tool-call" || !!c.result || c.approval !== undefined,
13
26
  );
14
27
  }
15
28
 
16
- return (
17
- result.status?.type === "requires-action" &&
18
- result.status.reason === "tool-calls" &&
19
- result.content.every(
20
- (c) =>
21
- c.type !== "tool-call" ||
22
- !!c.result ||
23
- !humanToolNames.includes(c.toolName),
24
- )
29
+ return result.content.every(
30
+ (c) =>
31
+ c.type !== "tool-call" ||
32
+ !!c.result ||
33
+ c.approval !== undefined ||
34
+ !humanToolNames.includes(c.toolName),
25
35
  );
26
36
  };
@@ -47,6 +47,10 @@ export class ReadonlyThreadRuntimeCore
47
47
  throw READONLY_THREAD_ERROR;
48
48
  }
49
49
 
50
+ deleteMessage(): void {
51
+ throw READONLY_THREAD_ERROR;
52
+ }
53
+
50
54
  startRun(): void {
51
55
  throw READONLY_THREAD_ERROR;
52
56
  }
@@ -202,6 +206,7 @@ export class ReadonlyThreadRuntimeCore
202
206
  switchToBranch: false,
203
207
  switchBranchDuringRun: false,
204
208
  edit: false,
209
+ delete: false,
205
210
  reload: false,
206
211
  cancel: false,
207
212
  unstable_copy: false,
@@ -20,6 +20,10 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
20
20
  throw EMPTY_THREAD_ERROR;
21
21
  },
22
22
 
23
+ deleteMessage() {
24
+ throw EMPTY_THREAD_ERROR;
25
+ },
26
+
23
27
  startRun() {
24
28
  throw EMPTY_THREAD_ERROR;
25
29
  },
@@ -183,6 +187,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
183
187
  switchToBranch: false,
184
188
  switchBranchDuringRun: false,
185
189
  edit: false,
190
+ delete: false,
186
191
  reload: false,
187
192
  cancel: false,
188
193
  unstable_copy: false,
@@ -28,6 +28,7 @@ export type RemoteThreadData =
28
28
  readonly externalId: string | undefined;
29
29
  readonly status: "regular" | "archived";
30
30
  readonly title?: string | undefined;
31
+ readonly lastMessageAt?: Date | undefined;
31
32
  readonly custom?: Record<string, unknown> | undefined;
32
33
  };
33
34
 
@@ -75,6 +76,7 @@ export const classifyThreads = (
75
76
  externalId: thread.externalId,
76
77
  status: thread.status,
77
78
  title: thread.title,
79
+ lastMessageAt: thread.lastMessageAt,
78
80
  custom: thread.custom,
79
81
  initializeTask: Promise.resolve({
80
82
  remoteId: thread.remoteId,
@@ -13,6 +13,7 @@ export type RemoteThreadMetadata = {
13
13
  readonly remoteId: string;
14
14
  readonly externalId?: string | undefined;
15
15
  readonly title?: string | undefined;
16
+ readonly lastMessageAt?: Date | undefined;
16
17
  readonly custom?: Record<string, unknown> | undefined;
17
18
  };
18
19
 
@@ -12,13 +12,13 @@ const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
12
12
  type: "complete",
13
13
  });
14
14
 
15
- export const ChainOfThoughtClient = resource(function ChainOfThoughtClient({
15
+ const useChainOfThoughtClient = ({
16
16
  parts,
17
17
  getMessagePart,
18
18
  }: {
19
19
  parts: readonly ChainOfThoughtPart[];
20
20
  getMessagePart: (selector: { index: number }) => PartMethods;
21
- }): ClientOutput<"chainOfThought"> {
21
+ }): ClientOutput<"chainOfThought"> => {
22
22
  const [collapsed, setCollapsed] = useState(true);
23
23
 
24
24
  const status = useMemo(() => {
@@ -36,4 +36,6 @@ export const ChainOfThoughtClient = resource(function ChainOfThoughtClient({
36
36
  setCollapsed,
37
37
  part: getMessagePart,
38
38
  };
39
- });
39
+ };
40
+
41
+ export const ChainOfThoughtClient = resource(useChainOfThoughtClient);
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { createResourceRoot } from "@assistant-ui/tap";
2
+ import { createTapRoot, useResource } from "@assistant-ui/tap";
3
3
  import type { Tool } from "assistant-stream";
4
4
  import { ModelContext } from "./model-context-client";
5
5
  import type {
@@ -18,9 +18,10 @@ const toolFixture = (): Tool<any, any> =>
18
18
  ({ description: "", parameters: {} as any }) as unknown as Tool<any, any>;
19
19
 
20
20
  const render = () => {
21
- const root = createResourceRoot();
22
- const sub = root.render(ModelContext());
23
- return { sub, unmount: () => root.unmount() };
21
+ const root = createTapRoot(function Root() {
22
+ return useResource(ModelContext());
23
+ });
24
+ return { sub: root, unmount: () => root.unmount() };
24
25
  };
25
26
 
26
27
  describe("ModelContext", () => {
@@ -29,25 +29,25 @@ const deriveState = (
29
29
  return { modelName, toolNames };
30
30
  };
31
31
 
32
- export const ModelContext = resource(
33
- function ModelContext(): ClientOutput<"modelContext"> {
34
- const composite = useMemo(() => new CompositeContextProvider(), []);
35
- const [state, setState] = useState<ModelContextState>(() =>
36
- deriveState(composite, INITIAL_STATE),
37
- );
38
-
39
- useEffect(() => {
32
+ const useModelContext = (): ClientOutput<"modelContext"> => {
33
+ const composite = useMemo(() => new CompositeContextProvider(), []);
34
+ const [state, setState] = useState<ModelContextState>(() =>
35
+ deriveState(composite, INITIAL_STATE),
36
+ );
37
+
38
+ useEffect(() => {
39
+ setState((prev) => deriveState(composite, prev));
40
+ return composite.subscribe(() => {
40
41
  setState((prev) => deriveState(composite, prev));
41
- return composite.subscribe(() => {
42
- setState((prev) => deriveState(composite, prev));
43
- });
44
- }, [composite]);
45
-
46
- return {
47
- getState: () => deriveState(composite, state),
48
- getModelContext: () => composite.getModelContext(),
49
- subscribe: (callback) => composite.subscribe(callback),
50
- register: (provider) => composite.registerModelContextProvider(provider),
51
- };
52
- },
53
- );
42
+ });
43
+ }, [composite]);
44
+
45
+ return {
46
+ getState: () => deriveState(composite, state),
47
+ getModelContext: () => composite.getModelContext(),
48
+ subscribe: (callback) => composite.subscribe(callback),
49
+ register: (provider) => composite.registerModelContextProvider(provider),
50
+ };
51
+ };
52
+
53
+ export const ModelContext = resource(useModelContext);