@assistant-ui/core 0.3.14 → 0.3.15
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.
- package/dist/adapters/voice.js +11 -0
- package/dist/adapters/voice.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/frame/host.d.ts +1 -1
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +17 -3
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts +4 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +50 -3
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/frame/types.d.ts +4 -1
- package/dist/model-context/frame/types.d.ts.map +1 -1
- package/dist/model-context/frame/types.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +1 -1
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +95 -23
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
- package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
- package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
- package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
- package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +192 -50
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +2 -0
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +27 -18
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +25 -6
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +1 -1
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
- package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
- package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
- package/dist/types/message.d.ts +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/silent-runtime-action.d.ts +6 -0
- package/dist/utils/silent-runtime-action.d.ts.map +1 -0
- package/dist/utils/silent-runtime-action.js +7 -0
- package/dist/utils/silent-runtime-action.js.map +1 -0
- package/package.json +4 -4
- package/src/adapters/voice.test.ts +66 -1
- package/src/adapters/voice.ts +8 -0
- package/src/internal.ts +1 -0
- package/src/model-context/frame/host.test.ts +64 -5
- package/src/model-context/frame/host.ts +16 -3
- package/src/model-context/frame/provider.test.ts +347 -20
- package/src/model-context/frame/provider.ts +58 -2
- package/src/model-context/frame/types.ts +5 -0
- package/src/react/client/RemoteThreadList.test.ts +196 -0
- package/src/react/client/RemoteThreadList.ts +93 -24
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
- package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
- package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +268 -63
- package/src/react/runtimes/RemoteThreadResource.ts +2 -0
- package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
- package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
- package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
- package/src/runtime/utils/message-repository.ts +26 -18
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +4 -0
- package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
- package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +319 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +38 -8
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
- package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
- package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
- package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
- package/src/runtimes/remote-thread-list/types.ts +4 -0
- package/src/store/clients/external-thread.ts +1 -1
- package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
- package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
- package/src/tests/MessageRepository.test.ts +48 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
- package/src/tests/empty-thread-core.test.ts +6 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +20 -0
- package/src/tests/external-thread-isEmpty.test.tsx +100 -0
- package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
- package/src/types/message.ts +1 -1
- package/src/utils/silent-runtime-action.ts +6 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Unsubscribe } from "../../types/unsubscribe";
|
|
2
|
+
import type { ThreadMessage } from "../../types/message";
|
|
3
|
+
import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
|
|
4
|
+
import type { ExportedMessageRepository } from "../../runtime/utils/message-repository";
|
|
5
|
+
import { BaseSubscribable } from "../../subscribable/subscribable";
|
|
6
|
+
|
|
7
|
+
export const createInertComposer = <TIsEditing extends boolean>(
|
|
8
|
+
error: Error,
|
|
9
|
+
isEditing: TIsEditing,
|
|
10
|
+
) => ({
|
|
11
|
+
attachments: [] as never[],
|
|
12
|
+
attachmentAccept: "*",
|
|
13
|
+
|
|
14
|
+
async addAttachment() {
|
|
15
|
+
throw error;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async removeAttachment() {
|
|
19
|
+
throw error;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
isEditing,
|
|
23
|
+
canCancel: false,
|
|
24
|
+
canSend: false,
|
|
25
|
+
isEmpty: true,
|
|
26
|
+
text: "",
|
|
27
|
+
|
|
28
|
+
setText() {
|
|
29
|
+
throw error;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
role: "user" as const,
|
|
33
|
+
|
|
34
|
+
setRole() {
|
|
35
|
+
throw error;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
runConfig: {},
|
|
39
|
+
|
|
40
|
+
setRunConfig() {
|
|
41
|
+
throw error;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async reset() {},
|
|
45
|
+
|
|
46
|
+
async clearAttachments() {},
|
|
47
|
+
|
|
48
|
+
send() {
|
|
49
|
+
throw error;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
cancel() {},
|
|
53
|
+
|
|
54
|
+
queue: [] as never[],
|
|
55
|
+
moveQueueItem() {},
|
|
56
|
+
removeQueueItem() {},
|
|
57
|
+
|
|
58
|
+
dictation: undefined,
|
|
59
|
+
|
|
60
|
+
startDictation() {
|
|
61
|
+
throw error;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
stopDictation() {},
|
|
65
|
+
|
|
66
|
+
quote: undefined,
|
|
67
|
+
|
|
68
|
+
setQuote() {
|
|
69
|
+
throw error;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
subscribe() {
|
|
73
|
+
return () => {};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
unstable_on() {
|
|
77
|
+
return () => {};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export abstract class InertThreadRuntimeCore
|
|
82
|
+
extends BaseSubscribable
|
|
83
|
+
implements ThreadRuntimeCore
|
|
84
|
+
{
|
|
85
|
+
protected abstract get error(): Error;
|
|
86
|
+
|
|
87
|
+
abstract readonly messages: readonly ThreadMessage[];
|
|
88
|
+
abstract readonly isLoading: boolean;
|
|
89
|
+
abstract readonly composer: ThreadRuntimeCore["composer"];
|
|
90
|
+
|
|
91
|
+
abstract getMessageById(messageId: string):
|
|
92
|
+
| {
|
|
93
|
+
parentId: string | null;
|
|
94
|
+
message: ThreadMessage;
|
|
95
|
+
index: number;
|
|
96
|
+
}
|
|
97
|
+
| undefined;
|
|
98
|
+
|
|
99
|
+
abstract getBranches(messageId: string): readonly string[];
|
|
100
|
+
|
|
101
|
+
abstract export(): ExportedMessageRepository;
|
|
102
|
+
|
|
103
|
+
switchToBranch(): void {
|
|
104
|
+
throw this.error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
append(): void {
|
|
108
|
+
throw this.error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
deleteMessage(): void {
|
|
112
|
+
throw this.error;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
startRun(): void {
|
|
116
|
+
throw this.error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
resumeRun(): void {
|
|
120
|
+
throw this.error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cancelRun(): void {
|
|
124
|
+
throw this.error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
unstable_notifySessionReset(): void {
|
|
128
|
+
throw this.error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
addToolResult(): void {
|
|
132
|
+
throw this.error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resumeToolCall(): void {
|
|
136
|
+
throw this.error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
respondToToolApproval(): void {
|
|
140
|
+
throw this.error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
speak(): void {
|
|
144
|
+
throw this.error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stopSpeaking(): void {
|
|
148
|
+
throw this.error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
connectVoice(): void {
|
|
152
|
+
throw this.error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
disconnectVoice(): void {
|
|
156
|
+
throw this.error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
muteVoice(): void {
|
|
160
|
+
throw this.error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
unmuteVoice(): void {
|
|
164
|
+
throw this.error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
submitFeedback(): void {
|
|
168
|
+
throw this.error;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
exportExternalState(): void {
|
|
172
|
+
throw this.error;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
importExternalState(): void {
|
|
176
|
+
throw this.error;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
beginEdit(): void {
|
|
180
|
+
throw this.error;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
import(): void {
|
|
184
|
+
throw this.error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
reset(): void {
|
|
188
|
+
throw this.error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getModelContext() {
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getEditComposer() {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getVoiceVolume = () => 0;
|
|
200
|
+
subscribeVoiceVolume = (): Unsubscribe => () => {};
|
|
201
|
+
|
|
202
|
+
speech = undefined;
|
|
203
|
+
voice = undefined;
|
|
204
|
+
|
|
205
|
+
capabilities = {
|
|
206
|
+
switchToBranch: false,
|
|
207
|
+
switchBranchDuringRun: false,
|
|
208
|
+
edit: false,
|
|
209
|
+
delete: false,
|
|
210
|
+
reload: false,
|
|
211
|
+
refetchThread: false,
|
|
212
|
+
cancel: false,
|
|
213
|
+
unstable_copy: false,
|
|
214
|
+
speech: false,
|
|
215
|
+
dictation: false,
|
|
216
|
+
voice: false,
|
|
217
|
+
attachments: false,
|
|
218
|
+
feedback: false,
|
|
219
|
+
queue: false,
|
|
220
|
+
} as const;
|
|
221
|
+
|
|
222
|
+
isDisabled = false;
|
|
223
|
+
isSendDisabled = false;
|
|
224
|
+
|
|
225
|
+
state = null;
|
|
226
|
+
suggestions = [] as never[];
|
|
227
|
+
extras = undefined;
|
|
228
|
+
|
|
229
|
+
unstable_on(): Unsubscribe {
|
|
230
|
+
return () => {};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -673,6 +673,325 @@ describe("LocalThreadRuntimeCore tool approvals", () => {
|
|
|
673
673
|
});
|
|
674
674
|
|
|
675
675
|
describe("LocalThreadRuntimeCore cancellation", () => {
|
|
676
|
+
it("settles a superseded tool-call result without aborting its replacement", async () => {
|
|
677
|
+
let resolveFirst!: (result: ChatModelRunResult) => void;
|
|
678
|
+
let resolveSecond!: (result: ChatModelRunResult) => void;
|
|
679
|
+
const firstResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
680
|
+
resolveFirst = resolve;
|
|
681
|
+
});
|
|
682
|
+
const secondResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
683
|
+
resolveSecond = resolve;
|
|
684
|
+
});
|
|
685
|
+
const runs: ChatModelRunOptions[] = [];
|
|
686
|
+
const thread = createThread({
|
|
687
|
+
run(options) {
|
|
688
|
+
runs.push(options);
|
|
689
|
+
return runs.length === 1 ? firstResult : secondResult;
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const firstAppend = thread.append(userMessage("first"));
|
|
694
|
+
await flush();
|
|
695
|
+
const supersededMessageId = thread.messages.at(-1)!.id;
|
|
696
|
+
const secondAppend = thread.append({
|
|
697
|
+
...userMessage("second"),
|
|
698
|
+
parentId: supersededMessageId,
|
|
699
|
+
});
|
|
700
|
+
await flush();
|
|
701
|
+
|
|
702
|
+
expect(runs).toHaveLength(2);
|
|
703
|
+
expect(runs[0]?.abortSignal.aborted).toBe(true);
|
|
704
|
+
expect(runs[1]?.abortSignal.aborted).toBe(false);
|
|
705
|
+
|
|
706
|
+
resolveFirst(toolCallResult("lookup_weather"));
|
|
707
|
+
await firstAppend;
|
|
708
|
+
|
|
709
|
+
expect(runs).toHaveLength(2);
|
|
710
|
+
expect(runs[1]?.abortSignal.aborted).toBe(false);
|
|
711
|
+
expect(
|
|
712
|
+
thread.messages.find((item) => item.id === supersededMessageId)?.status,
|
|
713
|
+
).toEqual({
|
|
714
|
+
type: "incomplete",
|
|
715
|
+
reason: "cancelled",
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
resolveSecond({ content: [{ type: "text", text: "replacement" }] });
|
|
719
|
+
await secondAppend;
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("cancels a superseded approval pause", async () => {
|
|
723
|
+
let resolveFirst!: (result: ChatModelRunResult) => void;
|
|
724
|
+
let resolveSecond!: (result: ChatModelRunResult) => void;
|
|
725
|
+
const firstResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
726
|
+
resolveFirst = resolve;
|
|
727
|
+
});
|
|
728
|
+
const secondResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
729
|
+
resolveSecond = resolve;
|
|
730
|
+
});
|
|
731
|
+
const runs: ChatModelRunOptions[] = [];
|
|
732
|
+
const thread = createThread({
|
|
733
|
+
run(options) {
|
|
734
|
+
runs.push(options);
|
|
735
|
+
return runs.length === 1 ? firstResult : secondResult;
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const firstAppend = thread.append(userMessage("first"));
|
|
740
|
+
await flush();
|
|
741
|
+
const supersededMessageId = thread.messages.at(-1)!.id;
|
|
742
|
+
const secondAppend = thread.append({
|
|
743
|
+
...userMessage("second"),
|
|
744
|
+
parentId: supersededMessageId,
|
|
745
|
+
});
|
|
746
|
+
await flush();
|
|
747
|
+
|
|
748
|
+
resolveFirst(toolCallResult("deploy", { id: "approval-1" }));
|
|
749
|
+
await firstAppend;
|
|
750
|
+
|
|
751
|
+
expect(
|
|
752
|
+
thread.messages.find((item) => item.id === supersededMessageId)?.status,
|
|
753
|
+
).toEqual({
|
|
754
|
+
type: "incomplete",
|
|
755
|
+
reason: "cancelled",
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
resolveSecond({ content: [{ type: "text", text: "replacement" }] });
|
|
759
|
+
await secondAppend;
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("ignores a stale result after addToolResult replaces the same message", async () => {
|
|
763
|
+
let releaseFirst!: () => void;
|
|
764
|
+
let resolveSecond!: (result: ChatModelRunResult) => void;
|
|
765
|
+
const firstTeardown = new Promise<void>((resolve) => {
|
|
766
|
+
releaseFirst = resolve;
|
|
767
|
+
});
|
|
768
|
+
const secondResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
769
|
+
resolveSecond = resolve;
|
|
770
|
+
});
|
|
771
|
+
const runs: ChatModelRunOptions[] = [];
|
|
772
|
+
const thread = createThread({
|
|
773
|
+
run(options) {
|
|
774
|
+
runs.push(options);
|
|
775
|
+
if (runs.length === 1) {
|
|
776
|
+
return (async function* () {
|
|
777
|
+
yield toolCallResult("send_email");
|
|
778
|
+
await firstTeardown;
|
|
779
|
+
yield toolCallResult("stale_tool");
|
|
780
|
+
})();
|
|
781
|
+
}
|
|
782
|
+
return secondResult;
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const firstAppend = thread.append(userMessage("send an email"));
|
|
787
|
+
await flush();
|
|
788
|
+
const messageId = thread.messages.at(-1)!.id;
|
|
789
|
+
|
|
790
|
+
thread.addToolResult({
|
|
791
|
+
messageId,
|
|
792
|
+
toolCallId: "call-send_email",
|
|
793
|
+
toolName: "send_email",
|
|
794
|
+
result: { approved: true },
|
|
795
|
+
isError: false,
|
|
796
|
+
});
|
|
797
|
+
await flush();
|
|
798
|
+
|
|
799
|
+
expect(runs).toHaveLength(2);
|
|
800
|
+
expect(runs[1]?.abortSignal.aborted).toBe(false);
|
|
801
|
+
|
|
802
|
+
releaseFirst();
|
|
803
|
+
await firstAppend;
|
|
804
|
+
|
|
805
|
+
const toolCall = thread.messages
|
|
806
|
+
.find((item) => item.id === messageId)
|
|
807
|
+
?.content.find(
|
|
808
|
+
(part) =>
|
|
809
|
+
part.type === "tool-call" && part.toolCallId === "call-send_email",
|
|
810
|
+
);
|
|
811
|
+
expect(toolCall?.result).toEqual({ approved: true });
|
|
812
|
+
expect(runs[1]?.abortSignal.aborted).toBe(false);
|
|
813
|
+
|
|
814
|
+
resolveSecond({ content: [{ type: "text", text: "replacement" }] });
|
|
815
|
+
await vi.waitFor(() => {
|
|
816
|
+
expect(
|
|
817
|
+
thread.messages.find((item) => item.id === messageId)?.status.type,
|
|
818
|
+
).toBe("complete");
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("ignores stale chunks after a partial tool result updates the active message", async () => {
|
|
823
|
+
let releaseTeardown!: () => void;
|
|
824
|
+
const teardown = new Promise<void>((resolve) => {
|
|
825
|
+
releaseTeardown = resolve;
|
|
826
|
+
});
|
|
827
|
+
const runs: ChatModelRunOptions[] = [];
|
|
828
|
+
const thread = createThread({
|
|
829
|
+
run(options) {
|
|
830
|
+
runs.push(options);
|
|
831
|
+
return (async function* () {
|
|
832
|
+
yield {
|
|
833
|
+
content: [
|
|
834
|
+
toolCallPart("send_email"),
|
|
835
|
+
toolCallPart("deploy", { id: "approval-1" }),
|
|
836
|
+
],
|
|
837
|
+
status: {
|
|
838
|
+
type: "requires-action",
|
|
839
|
+
reason: "tool-calls",
|
|
840
|
+
},
|
|
841
|
+
} satisfies ChatModelRunResult;
|
|
842
|
+
await teardown;
|
|
843
|
+
yield { content: [{ type: "text", text: "stale" }] };
|
|
844
|
+
})();
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const appendPromise = thread.append(userMessage("send and deploy"));
|
|
849
|
+
await flush();
|
|
850
|
+
const messageId = thread.messages.at(-1)!.id;
|
|
851
|
+
|
|
852
|
+
thread.addToolResult({
|
|
853
|
+
messageId,
|
|
854
|
+
toolCallId: "call-send_email",
|
|
855
|
+
toolName: "send_email",
|
|
856
|
+
result: { approved: true },
|
|
857
|
+
isError: false,
|
|
858
|
+
});
|
|
859
|
+
await flush();
|
|
860
|
+
|
|
861
|
+
expect(runs).toHaveLength(1);
|
|
862
|
+
|
|
863
|
+
releaseTeardown();
|
|
864
|
+
await appendPromise;
|
|
865
|
+
|
|
866
|
+
const message = thread.messages.find((item) => item.id === messageId);
|
|
867
|
+
const sendEmail = message?.content.find(
|
|
868
|
+
(part) =>
|
|
869
|
+
part.type === "tool-call" && part.toolCallId === "call-send_email",
|
|
870
|
+
);
|
|
871
|
+
const deploy = message?.content.find(
|
|
872
|
+
(part) => part.type === "tool-call" && part.toolCallId === "call-deploy",
|
|
873
|
+
);
|
|
874
|
+
expect(sendEmail?.result).toEqual({ approved: true });
|
|
875
|
+
expect(deploy?.approval).toEqual({ id: "approval-1" });
|
|
876
|
+
expect(message?.status.type).toBe("requires-action");
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("ignores a superseded result after its message is removed", async () => {
|
|
880
|
+
let resolveFirst!: (result: ChatModelRunResult) => void;
|
|
881
|
+
const firstResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
882
|
+
resolveFirst = resolve;
|
|
883
|
+
});
|
|
884
|
+
let runCount = 0;
|
|
885
|
+
const thread = createThread({
|
|
886
|
+
run() {
|
|
887
|
+
runCount++;
|
|
888
|
+
if (runCount === 1) return firstResult;
|
|
889
|
+
return Promise.resolve({
|
|
890
|
+
content: [{ type: "text", text: "replacement" }],
|
|
891
|
+
});
|
|
892
|
+
},
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const firstAppend = thread.append(userMessage("first"));
|
|
896
|
+
await flush();
|
|
897
|
+
await thread.append(userMessage("second"));
|
|
898
|
+
thread.reset();
|
|
899
|
+
|
|
900
|
+
resolveFirst(toolCallResult("lookup_weather"));
|
|
901
|
+
|
|
902
|
+
await expect(firstAppend).resolves.toBeUndefined();
|
|
903
|
+
expect(thread.messages).toHaveLength(0);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("does not continue after cancelRun when a delayed tool call resolves", async () => {
|
|
907
|
+
let resolveFirst!: (result: ChatModelRunResult) => void;
|
|
908
|
+
const firstResult = new Promise<ChatModelRunResult>((resolve) => {
|
|
909
|
+
resolveFirst = resolve;
|
|
910
|
+
});
|
|
911
|
+
const runs: ChatModelRunOptions[] = [];
|
|
912
|
+
const thread = createThread({
|
|
913
|
+
run(options) {
|
|
914
|
+
runs.push(options);
|
|
915
|
+
if (runs.length === 1) return firstResult;
|
|
916
|
+
return Promise.resolve({
|
|
917
|
+
content: [{ type: "text", text: "unexpected continuation" }],
|
|
918
|
+
status: { type: "complete", reason: "stop" },
|
|
919
|
+
});
|
|
920
|
+
},
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
const appendPromise = thread.append(userMessage("first"));
|
|
924
|
+
await flush();
|
|
925
|
+
|
|
926
|
+
thread.cancelRun();
|
|
927
|
+
resolveFirst(toolCallResult("lookup_weather"));
|
|
928
|
+
await appendPromise;
|
|
929
|
+
|
|
930
|
+
expect(runs).toHaveLength(1);
|
|
931
|
+
expect(thread.messages.at(-1)?.status).toEqual({
|
|
932
|
+
type: "incomplete",
|
|
933
|
+
reason: "cancelled",
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("preserves a terminal adapter status when cancelled during teardown", async () => {
|
|
938
|
+
let releaseTeardown!: () => void;
|
|
939
|
+
const teardown = new Promise<void>((resolve) => {
|
|
940
|
+
releaseTeardown = resolve;
|
|
941
|
+
});
|
|
942
|
+
const thread = createThread({
|
|
943
|
+
async *run() {
|
|
944
|
+
yield {
|
|
945
|
+
content: [{ type: "text", text: "done" }],
|
|
946
|
+
status: { type: "complete", reason: "stop" },
|
|
947
|
+
};
|
|
948
|
+
await teardown;
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
const appendPromise = thread.append(userMessage("first"));
|
|
953
|
+
await flush();
|
|
954
|
+
|
|
955
|
+
expect(thread.messages.at(-1)?.status).toEqual({
|
|
956
|
+
type: "complete",
|
|
957
|
+
reason: "stop",
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
thread.cancelRun();
|
|
961
|
+
releaseTeardown();
|
|
962
|
+
await appendPromise;
|
|
963
|
+
|
|
964
|
+
expect(thread.messages.at(-1)?.status).toEqual({
|
|
965
|
+
type: "complete",
|
|
966
|
+
reason: "stop",
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it("preserves an approval pause when the adapter yields after cancellation", async () => {
|
|
971
|
+
let releaseTeardown!: () => void;
|
|
972
|
+
const teardown = new Promise<void>((resolve) => {
|
|
973
|
+
releaseTeardown = resolve;
|
|
974
|
+
});
|
|
975
|
+
const thread = createThread({
|
|
976
|
+
async *run() {
|
|
977
|
+
yield toolCallResult("deploy", { id: "approval-1" });
|
|
978
|
+
await teardown;
|
|
979
|
+
yield { content: [{ type: "text", text: "late" }] };
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const appendPromise = thread.append(userMessage("deploy"));
|
|
984
|
+
await flush();
|
|
985
|
+
|
|
986
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
|
|
987
|
+
|
|
988
|
+
thread.cancelRun();
|
|
989
|
+
releaseTeardown();
|
|
990
|
+
await appendPromise;
|
|
991
|
+
|
|
992
|
+
expect(thread.messages.at(-1)?.status?.type).toBe("requires-action");
|
|
993
|
+
});
|
|
994
|
+
|
|
676
995
|
it("keeps a replacement run cancellable after the previous run settles", async () => {
|
|
677
996
|
let releaseFirst!: () => void;
|
|
678
997
|
let releaseSecond!: () => void;
|
|
@@ -501,9 +501,11 @@ export class LocalThreadRuntimeCore
|
|
|
501
501
|
message,
|
|
502
502
|
runConfig,
|
|
503
503
|
alreadyPersisted,
|
|
504
|
+
run,
|
|
504
505
|
runCallback,
|
|
505
506
|
);
|
|
506
507
|
runCallback = undefined;
|
|
508
|
+
if (this._activeRun !== run) break;
|
|
507
509
|
} while (shouldContinue(message, this._options.unstable_humanToolNames));
|
|
508
510
|
} finally {
|
|
509
511
|
this._notifyEventSubscribers("runEnd", {});
|
|
@@ -548,6 +550,7 @@ export class LocalThreadRuntimeCore
|
|
|
548
550
|
message: ThreadAssistantMessage,
|
|
549
551
|
runConfig: RunConfig | undefined,
|
|
550
552
|
alreadyPersisted: boolean,
|
|
553
|
+
run: { cancelled: boolean },
|
|
551
554
|
runCallback?: ChatModelAdapter["run"],
|
|
552
555
|
) {
|
|
553
556
|
const messages = parentId ? this.repository.getMessages(parentId) : [];
|
|
@@ -562,7 +565,23 @@ export class LocalThreadRuntimeCore
|
|
|
562
565
|
const initialData = message.metadata?.unstable_data;
|
|
563
566
|
const initialSteps = message.metadata?.steps;
|
|
564
567
|
const initialCustom = message.metadata?.custom;
|
|
568
|
+
let hasStoredMessage = true;
|
|
569
|
+
try {
|
|
570
|
+
this.repository.getMessage(message.id);
|
|
571
|
+
} catch {
|
|
572
|
+
hasStoredMessage = false;
|
|
573
|
+
}
|
|
574
|
+
// Other writers replace the stored message object, so identity distinguishes this run from a newer owner.
|
|
575
|
+
const ownsMessage = () => {
|
|
576
|
+
if (!hasStoredMessage) return this._activeRun === run;
|
|
577
|
+
try {
|
|
578
|
+
return this.repository.getMessage(message.id).message === message;
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
565
583
|
const updateMessage = (m: Partial<ChatModelRunResult>) => {
|
|
584
|
+
if (!ownsMessage()) return;
|
|
566
585
|
const newSteps = m.metadata?.steps;
|
|
567
586
|
const steps = newSteps
|
|
568
587
|
? [...(initialSteps ?? []), ...newSteps]
|
|
@@ -609,6 +628,7 @@ export class LocalThreadRuntimeCore
|
|
|
609
628
|
: undefined),
|
|
610
629
|
};
|
|
611
630
|
this.repository.addOrUpdateMessage(parentId, message);
|
|
631
|
+
hasStoredMessage = true;
|
|
612
632
|
this._notifySubscribers();
|
|
613
633
|
};
|
|
614
634
|
|
|
@@ -647,6 +667,12 @@ export class LocalThreadRuntimeCore
|
|
|
647
667
|
this.adapters.chatModel.run.bind(this.adapters.chatModel);
|
|
648
668
|
|
|
649
669
|
const abortSignal = abortController.signal;
|
|
670
|
+
const shouldCancelMessage = () =>
|
|
671
|
+
abortSignal.aborted &&
|
|
672
|
+
(message.status.type === "running" ||
|
|
673
|
+
(message.status.type === "requires-action" &&
|
|
674
|
+
(this._activeRun !== run ||
|
|
675
|
+
shouldContinue(message, this._options.unstable_humanToolNames))));
|
|
650
676
|
const threadId = this._getThreadId?.();
|
|
651
677
|
const promiseOrGenerator = runCallback({
|
|
652
678
|
messages,
|
|
@@ -665,9 +691,11 @@ export class LocalThreadRuntimeCore
|
|
|
665
691
|
if (Symbol.asyncIterator in promiseOrGenerator) {
|
|
666
692
|
for await (const r of promiseOrGenerator) {
|
|
667
693
|
if (abortSignal.aborted) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
694
|
+
if (shouldCancelMessage()) {
|
|
695
|
+
updateMessage({
|
|
696
|
+
status: { type: "incomplete", reason: "cancelled" },
|
|
697
|
+
});
|
|
698
|
+
}
|
|
671
699
|
break;
|
|
672
700
|
}
|
|
673
701
|
|
|
@@ -677,11 +705,13 @@ export class LocalThreadRuntimeCore
|
|
|
677
705
|
updateMessage(await promiseOrGenerator);
|
|
678
706
|
}
|
|
679
707
|
|
|
680
|
-
if (
|
|
708
|
+
if (shouldCancelMessage()) {
|
|
709
|
+
updateMessage({
|
|
710
|
+
status: { type: "incomplete", reason: "cancelled" },
|
|
711
|
+
});
|
|
712
|
+
} else if (message.status.type === "running") {
|
|
681
713
|
updateMessage({
|
|
682
|
-
status:
|
|
683
|
-
? { type: "incomplete", reason: "cancelled" }
|
|
684
|
-
: { type: "complete", reason: "unknown" },
|
|
714
|
+
status: { type: "complete", reason: "unknown" },
|
|
685
715
|
});
|
|
686
716
|
}
|
|
687
717
|
} catch (e) {
|
|
@@ -724,7 +754,7 @@ export class LocalThreadRuntimeCore
|
|
|
724
754
|
|
|
725
755
|
// Pauses are written only for adapters that can rewrite the entry later;
|
|
726
756
|
// an append-only adapter would strand a half-finished run in history.
|
|
727
|
-
if (isTerminal || (isPausing && history?.update)) {
|
|
757
|
+
if (ownsMessage() && (isTerminal || (isPausing && history?.update))) {
|
|
728
758
|
const write =
|
|
729
759
|
alreadyPersisted && history?.update
|
|
730
760
|
? history.update.bind(history)
|