@jskit-ai/assistant-core 0.1.187 → 0.1.189
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/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/DeliveryFixture.vue +4 -3
- package/package.json +5 -5
- package/src/client/conversation/AssistantComposerActions.vue +2 -2
- package/src/client/conversation/AssistantConversationElement.vue +7 -3
- package/src/client/conversation/AssistantTranscript.vue +36 -4
- package/src/client/conversation/messageDelivery.js +32 -10
- package/test/messageDelivery.browser.test.js +30 -2
- package/test/messageDelivery.test.js +77 -1
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.189",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.209",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.211",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.28",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.28"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -8,8 +8,9 @@ const turns = ref([]);
|
|
|
8
8
|
const requests = ref([]);
|
|
9
9
|
const configuration = ref({ tone: "concise" });
|
|
10
10
|
const clearedFiles = ref([]);
|
|
11
|
+
const queueWhileSending = new URLSearchParams(location.search).has("queue");
|
|
11
12
|
const attachments = reactive({
|
|
12
|
-
attachments: [{ attachmentId: "original-file", fileName: "original.txt" }],
|
|
13
|
+
attachments: queueWhileSending ? [] : [{ attachmentId: "original-file", fileName: "original.txt" }],
|
|
13
14
|
queueItems: [], canSubmit: true, canAddFiles: false,
|
|
14
15
|
clearAttachments({ attachmentIds }) {
|
|
15
16
|
clearedFiles.value.push(...attachmentIds);
|
|
@@ -24,14 +25,14 @@ const delivery = createAssistantMessageDelivery({ deliver(payload) {
|
|
|
24
25
|
} });
|
|
25
26
|
function accept() {
|
|
26
27
|
const payload = requests.value.at(-1);
|
|
27
|
-
turns.value = [{ turnId:
|
|
28
|
+
turns.value = [...turns.value, { turnId: payload.messageId, user: { messageId: payload.messageId, text: payload.message },
|
|
28
29
|
assistant: { text: "Accepted once." } }];
|
|
29
30
|
pendingRequest.resolve({ ok: true });
|
|
30
31
|
}
|
|
31
32
|
const adapter = reactive({
|
|
32
33
|
delivery, attachments,
|
|
33
34
|
conversation: { turns, visible: true, scrollKey: "delivery", welcomeMessage: "Start a conversation." },
|
|
34
|
-
composer: { draft, canSend: true, submitOnEnter: true },
|
|
35
|
+
composer: { draft, canSend: true, submitOnEnter: true, queueWhileSending },
|
|
35
36
|
actions: {
|
|
36
37
|
setDraft(value) { draft.value = value; }
|
|
37
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.189",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
35
35
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
36
36
|
"@ai-sdk/xai": "^4.0.58",
|
|
37
|
-
"@jskit-ai/resource-core": "0.1.
|
|
38
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
37
|
+
"@jskit-ai/resource-core": "0.1.153",
|
|
38
|
+
"@jskit-ai/resource-crud-core": "0.1.153",
|
|
39
39
|
"@mdi/js": "^7.4.47",
|
|
40
40
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
41
41
|
"ai": "^7.0.101",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"ws": "^8.20.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
48
|
-
"@jskit-ai/kernel": "0.1.
|
|
47
|
+
"@jskit-ai/http-runtime": "0.1.209",
|
|
48
|
+
"@jskit-ai/kernel": "0.1.211",
|
|
49
49
|
"vue": "^3.5.13",
|
|
50
50
|
"vuetify": "^4.0.0"
|
|
51
51
|
},
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
</v-btn>
|
|
12
12
|
<v-btn
|
|
13
13
|
ref="sendButton" class="assistant-composer-actions__send" color="primary" :disabled="!state.canSend"
|
|
14
|
-
:aria-busy="state.pending ? 'true' : undefined" :aria-label="state.submitAriaLabel || state.submitLabel || 'Send message'"
|
|
14
|
+
:aria-busy="state.pending && !state.canSend ? 'true' : undefined" :aria-label="state.submitAriaLabel || state.submitLabel || 'Send message'"
|
|
15
15
|
:title="state.submitTitle" :prepend-icon="state.submitIcon || mdiSend" size="small" variant="flat"
|
|
16
16
|
@click="emit('submit')"
|
|
17
17
|
>
|
|
18
|
-
{{ state.submitLabel || (state.pending ? 'Sending…' : 'Send') }}
|
|
18
|
+
{{ state.submitLabel || (state.pending && !state.canSend ? 'Sending…' : 'Send') }}
|
|
19
19
|
</v-btn>
|
|
20
20
|
</div>
|
|
21
21
|
<slot name="feedback" />
|
|
@@ -125,6 +125,7 @@ watch(() => [props.adapter.delivery, props.adapter.conversation.turns], ([contro
|
|
|
125
125
|
controller?.reconcile(turns || []);
|
|
126
126
|
}, { immediate: true });
|
|
127
127
|
const pending = computed(() => Boolean(props.adapter.delivery?.state.sending || props.adapter.composer?.pending));
|
|
128
|
+
const queueWhileSending = computed(() => props.adapter.composer?.queueWhileSending === true);
|
|
128
129
|
const working = computed(() => conversation.value.working ?? (!pending.value && Boolean(
|
|
129
130
|
props.adapter.composer?.canStop || conversation.value.turns?.some(turn => turn.pending)
|
|
130
131
|
)));
|
|
@@ -139,14 +140,17 @@ const attachmentState = computed(() => ({
|
|
|
139
140
|
count: attachmentsEnabled.value ? props.adapter.attachments.queueItems?.length || 0 : 0,
|
|
140
141
|
canSubmit: !attachmentsEnabled.value || props.adapter.attachments.canSubmit !== false
|
|
141
142
|
}));
|
|
142
|
-
const canSend = computed(() =>
|
|
143
|
+
const canSend = computed(() => (
|
|
144
|
+
(!pending.value || (queueWhileSending.value && attachmentState.value.count === 0)) &&
|
|
145
|
+
props.adapter.composer?.canSend && attachmentState.value.canSubmit
|
|
146
|
+
));
|
|
143
147
|
async function resend(id) {
|
|
144
148
|
if (props.adapter.actions?.resend) return props.adapter.actions.resend(id);
|
|
145
149
|
const controller = props.adapter.delivery;
|
|
146
150
|
const payload = controller?.find(id)?.payload;
|
|
147
151
|
const attachments = props.adapter.attachments;
|
|
148
152
|
try {
|
|
149
|
-
const response = await controller?.resend(id);
|
|
153
|
+
const response = await controller?.resend(id, { queue: queueWhileSending.value });
|
|
150
154
|
clearAcceptedAttachments(response, payload, attachments);
|
|
151
155
|
return response;
|
|
152
156
|
}
|
|
@@ -202,7 +206,7 @@ async function submit() {
|
|
|
202
206
|
};
|
|
203
207
|
props.adapter.actions.setDraft("");
|
|
204
208
|
try {
|
|
205
|
-
const response = await controller.send(payload);
|
|
209
|
+
const response = await controller.send(payload, { queue: queueWhileSending.value });
|
|
206
210
|
clearAcceptedAttachments(response, payload, attachments);
|
|
207
211
|
return response;
|
|
208
212
|
} catch { return false; /* Delivery retains the failed message and its actions. */ }
|
|
@@ -124,7 +124,13 @@
|
|
|
124
124
|
v-else-if="entry.role === 'user'"
|
|
125
125
|
class="assistant-transcript__message-row assistant-transcript__message-row--user"
|
|
126
126
|
>
|
|
127
|
-
<div
|
|
127
|
+
<div
|
|
128
|
+
class="assistant-transcript__message assistant-transcript__message--user"
|
|
129
|
+
:class="{
|
|
130
|
+
'assistant-transcript__message--pending': entry.turn.optimistic?.status === 'pending',
|
|
131
|
+
'assistant-transcript__message--failed': entry.turn.optimistic?.status === 'failed'
|
|
132
|
+
}"
|
|
133
|
+
>
|
|
128
134
|
<div
|
|
129
135
|
class="assistant-transcript__user-content"
|
|
130
136
|
>
|
|
@@ -146,27 +152,30 @@
|
|
|
146
152
|
</button>
|
|
147
153
|
<slot name="attachments" :items="entry.message.attachments" :message="entry.message"><AssistantMessageAttachments :attachments="entry.message.attachments || []" /></slot>
|
|
148
154
|
<div
|
|
149
|
-
v-if="entry.message.displayAt"
|
|
155
|
+
v-if="entry.message.displayAt || entry.turn.optimistic?.status === 'pending'"
|
|
150
156
|
class="assistant-transcript__message-footer assistant-transcript__message-footer--user"
|
|
151
157
|
>
|
|
152
158
|
<time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
|
|
159
|
+
<span v-if="entry.turn.optimistic?.status === 'pending'" role="status">Pending</span>
|
|
153
160
|
</div>
|
|
154
161
|
<div
|
|
155
162
|
v-if="entry.turn.optimistic?.status === 'failed'"
|
|
156
163
|
class="assistant-transcript__optimistic-failure"
|
|
157
164
|
>
|
|
158
|
-
<span>{{ entry.turn.optimistic.error || "Message could not be sent." }}</span>
|
|
165
|
+
<span role="status">Failed: {{ entry.turn.optimistic.error || "Message could not be sent." }}</span>
|
|
159
166
|
<div class="assistant-transcript__optimistic-actions">
|
|
160
167
|
<v-btn
|
|
168
|
+
class="assistant-transcript__delivery-action"
|
|
161
169
|
color="primary"
|
|
162
170
|
size="x-small"
|
|
163
171
|
type="button"
|
|
164
172
|
variant="tonal"
|
|
165
173
|
@click="emit('resend-turn', entry.turn.optimistic.id)"
|
|
166
174
|
>
|
|
167
|
-
|
|
175
|
+
Retry
|
|
168
176
|
</v-btn>
|
|
169
177
|
<v-btn
|
|
178
|
+
class="assistant-transcript__delivery-action"
|
|
170
179
|
size="x-small"
|
|
171
180
|
type="button"
|
|
172
181
|
variant="text"
|
|
@@ -175,6 +184,7 @@
|
|
|
175
184
|
Cancel
|
|
176
185
|
</v-btn>
|
|
177
186
|
<v-btn
|
|
187
|
+
class="assistant-transcript__delivery-action"
|
|
178
188
|
size="x-small"
|
|
179
189
|
type="button"
|
|
180
190
|
variant="text"
|
|
@@ -1101,6 +1111,21 @@ watch(timelineScrollTrigger, () => {
|
|
|
1101
1111
|
width: fit-content;
|
|
1102
1112
|
}
|
|
1103
1113
|
|
|
1114
|
+
.assistant-transcript__message--user.assistant-transcript__message--pending {
|
|
1115
|
+
background: rgba(var(--v-theme-on-surface), 0.04);
|
|
1116
|
+
outline: 1px dashed rgba(var(--v-theme-on-surface), 0.4);
|
|
1117
|
+
outline-offset: -1px;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.assistant-transcript__message--pending .assistant-transcript__message-footer {
|
|
1121
|
+
color: rgba(var(--v-theme-on-surface), 0.7);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.assistant-transcript__message--user.assistant-transcript__message--failed {
|
|
1125
|
+
outline: 1px solid rgb(var(--v-theme-error));
|
|
1126
|
+
outline-offset: -1px;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1104
1129
|
.assistant-transcript__message--assistant {
|
|
1105
1130
|
background: rgb(var(--v-theme-surface));
|
|
1106
1131
|
color: rgb(var(--v-theme-on-surface));
|
|
@@ -1232,6 +1257,13 @@ watch(timelineScrollTrigger, () => {
|
|
|
1232
1257
|
gap: 0.25rem;
|
|
1233
1258
|
}
|
|
1234
1259
|
|
|
1260
|
+
@media (max-width: 600px), (pointer: coarse) {
|
|
1261
|
+
.assistant-transcript__delivery-action {
|
|
1262
|
+
min-height: 3rem;
|
|
1263
|
+
min-width: 3rem;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1235
1267
|
.assistant-transcript__message--assistant :deep(.studio-long-text-review__blocks),
|
|
1236
1268
|
.assistant-transcript__message--user :deep(.studio-long-text-review__blocks) {
|
|
1237
1269
|
color: inherit;
|
|
@@ -34,7 +34,8 @@ function unmatchedOptimisticMessages(turns = [], optimisticMessages = []) {
|
|
|
34
34
|
// the shared assistant element renders these pending and failed turns.
|
|
35
35
|
function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
36
36
|
const state = reactive({ messages: [], sending: false });
|
|
37
|
-
let
|
|
37
|
+
let deliveryTail = null;
|
|
38
|
+
let pendingSends = new Map();
|
|
38
39
|
|
|
39
40
|
function find(messageId) {
|
|
40
41
|
return state.messages.find((message) => message.id === messageId) || null;
|
|
@@ -65,12 +66,21 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
65
66
|
];
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
async function send(payload, {
|
|
69
|
-
|
|
69
|
+
async function send(payload, {
|
|
70
|
+
messageId = crypto.randomUUID(),
|
|
71
|
+
deliver = defaultDeliver,
|
|
72
|
+
isCurrent = () => true,
|
|
73
|
+
queue = false
|
|
74
|
+
} = {}) {
|
|
75
|
+
if ((state.sending && !queue) || pendingSends.has(messageId) || !messageText(payload?.message)) return false;
|
|
70
76
|
if (typeof deliver !== "function") throw new TypeError("Message delivery requires a deliver function.");
|
|
71
77
|
const snapshot = JSON.parse(JSON.stringify(payload));
|
|
72
|
-
const
|
|
73
|
-
const current = () =>
|
|
78
|
+
const ownerPendingSends = pendingSends;
|
|
79
|
+
const current = () => pendingSends === ownerPendingSends && isCurrent();
|
|
80
|
+
const predecessor = deliveryTail;
|
|
81
|
+
const settled = Promise.withResolvers();
|
|
82
|
+
deliveryTail = settled.promise;
|
|
83
|
+
ownerPendingSends.set(messageId, settled);
|
|
74
84
|
const now = new Date();
|
|
75
85
|
const message = {
|
|
76
86
|
attachments: snapshot.displayAttachments || [],
|
|
@@ -85,10 +95,15 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
85
95
|
state.messages = [...state.messages.filter((entry) => entry.id !== messageId), message];
|
|
86
96
|
state.sending = true;
|
|
87
97
|
try {
|
|
98
|
+
if (predecessor) await predecessor;
|
|
99
|
+
if (!current()) return false;
|
|
88
100
|
const response = await deliver({ ...snapshot, messageId });
|
|
89
101
|
if (!current()) return false;
|
|
90
102
|
if (response === false || response?.ok === false) {
|
|
91
103
|
fail(messageId, response?.error || "Message could not be sent.");
|
|
104
|
+
} else {
|
|
105
|
+
const acceptedMessage = find(messageId);
|
|
106
|
+
if (acceptedMessage) acceptedMessage.status = "accepted";
|
|
92
107
|
}
|
|
93
108
|
return response;
|
|
94
109
|
} catch (error) {
|
|
@@ -96,7 +111,12 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
96
111
|
fail(messageId, error);
|
|
97
112
|
throw error;
|
|
98
113
|
} finally {
|
|
99
|
-
|
|
114
|
+
ownerPendingSends.delete(messageId);
|
|
115
|
+
if (pendingSends === ownerPendingSends) {
|
|
116
|
+
state.sending = ownerPendingSends.size > 0;
|
|
117
|
+
if (!state.sending) deliveryTail = null;
|
|
118
|
+
}
|
|
119
|
+
settled.resolve();
|
|
100
120
|
}
|
|
101
121
|
}
|
|
102
122
|
|
|
@@ -109,7 +129,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
function cancel(messageId) {
|
|
112
|
-
if (
|
|
132
|
+
if (find(messageId)?.status !== "failed") return false;
|
|
113
133
|
remove(messageId);
|
|
114
134
|
return true;
|
|
115
135
|
}
|
|
@@ -120,14 +140,16 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
120
140
|
return !draft || draft.startsWith(message.text) ? draft || message.text : `${message.text}\n\n${draft}`;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
function resend(messageId) {
|
|
143
|
+
function resend(messageId, { queue = false } = {}) {
|
|
124
144
|
const message = find(messageId);
|
|
125
145
|
if (message?.status !== "failed") return false;
|
|
126
|
-
return send(message.payload, { messageId });
|
|
146
|
+
return send(message.payload, { messageId, queue });
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
function reset() {
|
|
130
|
-
|
|
150
|
+
deliveryTail = null;
|
|
151
|
+
for (const pending of pendingSends.values()) pending.resolve();
|
|
152
|
+
pendingSends = new Map();
|
|
131
153
|
state.messages = [];
|
|
132
154
|
state.sending = false;
|
|
133
155
|
}
|
|
@@ -26,10 +26,10 @@ test("the shared element renders delayed sends and supplies retry, edit and canc
|
|
|
26
26
|
await expect(page.getByText("Start a conversation.", { exact: true })).toHaveCount(0);
|
|
27
27
|
await expect(page.locator(".assistant-composer-support__assistant-status")).toHaveText("Sending to assistant…");
|
|
28
28
|
await fail();
|
|
29
|
-
await expect(page.getByText("Delivery unavailable.", { exact: true })).toHaveCount(1);
|
|
29
|
+
await expect(page.getByText("Failed: Delivery unavailable.", { exact: true })).toHaveCount(1);
|
|
30
30
|
await input.fill("Keep my newer draft.");
|
|
31
31
|
await page.getByRole("button", { name: "Prepare newer input" }).click();
|
|
32
|
-
await page.getByRole("button", { name: "
|
|
32
|
+
await page.getByRole("button", { name: "Retry", exact: true }).click();
|
|
33
33
|
await expect(input).toHaveValue("Keep my newer draft.");
|
|
34
34
|
await page.getByRole("button", { name: "Accept request" }).click();
|
|
35
35
|
await expect(page.getByText("Accepted once.", { exact: true })).toBeVisible();
|
|
@@ -53,6 +53,34 @@ test("the shared element renders delayed sends and supplies retry, edit and canc
|
|
|
53
53
|
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
|
54
54
|
await expect(input).toHaveValue("Retain this.");
|
|
55
55
|
await expect(page.getByText("Cancel this question.", { exact: true })).toHaveCount(0);
|
|
56
|
+
await page.goto(`${vite.baseURL}/?delivery=1&queue=1`);
|
|
57
|
+
await send("First steer.");
|
|
58
|
+
await send("Second steer.");
|
|
59
|
+
await send("Third steer.");
|
|
60
|
+
await expect(input).toHaveValue("");
|
|
61
|
+
await expect(page.getByText("Pending", { exact: true })).toHaveCount(3);
|
|
62
|
+
await expect(page.locator(".assistant-transcript__message--pending")).toHaveCount(3);
|
|
63
|
+
assert.equal(JSON.parse(await page.locator("[data-requests]").textContent()).length, 1);
|
|
64
|
+
await input.fill("Keep this newer draft.");
|
|
65
|
+
await fail();
|
|
66
|
+
await expect(page.locator(".assistant-transcript__message--failed")).toHaveCount(1);
|
|
67
|
+
await expect(page.getByRole("button", { name: "Retry", exact: true })).toBeVisible();
|
|
68
|
+
if (width === 390) {
|
|
69
|
+
const retryBox = await page.getByRole("button", { name: "Retry", exact: true }).boundingBox();
|
|
70
|
+
assert.ok(retryBox.width >= 48 && retryBox.height >= 48);
|
|
71
|
+
}
|
|
72
|
+
await page.getByRole("button", { name: "Retry", exact: true }).click();
|
|
73
|
+
await expect(page.getByText("Pending", { exact: true })).toHaveCount(3);
|
|
74
|
+
for (let count = 2; count <= 4; count += 1) {
|
|
75
|
+
await expect.poll(async () => JSON.parse(await page.locator("[data-requests]").textContent()).length).toBe(count);
|
|
76
|
+
await page.getByRole("button", { name: "Accept request" }).click();
|
|
77
|
+
}
|
|
78
|
+
await expect(page.getByText("Pending", { exact: true })).toHaveCount(0);
|
|
79
|
+
await expect(page.locator(".assistant-transcript__message--pending")).toHaveCount(0);
|
|
80
|
+
await expect(input).toHaveValue("Keep this newer draft.");
|
|
81
|
+
const queuedRequests = JSON.parse(await page.locator("[data-requests]").textContent());
|
|
82
|
+
assert.deepEqual(queuedRequests.map(item => item.message), ["First steer.", "Second steer.", "Third steer.", "First steer."]);
|
|
83
|
+
assert.deepEqual(queuedRequests[0], queuedRequests[3]);
|
|
56
84
|
assert.deepEqual(errors, []);
|
|
57
85
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
58
86
|
await page.close();
|
|
@@ -41,7 +41,7 @@ test("failed delivery retains the original payload and identity for an explicit
|
|
|
41
41
|
assert.deepEqual(sent[0], sent[1]);
|
|
42
42
|
assert.equal(sent[1].configuration.model, "one");
|
|
43
43
|
assert.equal(delivery.state.messages.length, 1);
|
|
44
|
-
assert.equal(delivery.state.messages[0].status, "
|
|
44
|
+
assert.equal(delivery.state.messages[0].status, "accepted");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("cancel and edit affect failed messages and preserve a newer draft", async () => {
|
|
@@ -70,3 +70,79 @@ test("retiring a conversation isolates late responses from the next conversation
|
|
|
70
70
|
await newSend;
|
|
71
71
|
assert.equal(delivery.state.sending, false);
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
test("queued messages appear immediately and deliver in order with captured payloads", async () => {
|
|
75
|
+
const requests = [Promise.withResolvers(), Promise.withResolvers(), Promise.withResolvers()];
|
|
76
|
+
const sent = [];
|
|
77
|
+
const delivery = createAssistantMessageDelivery({ deliver: payload => {
|
|
78
|
+
sent.push(payload);
|
|
79
|
+
return requests[sent.length - 1].promise;
|
|
80
|
+
} });
|
|
81
|
+
const first = delivery.send({ message: "First" }, { messageId: "one" });
|
|
82
|
+
const payload = { message: "Second", configuration: { model: "original" } };
|
|
83
|
+
const second = delivery.send(payload, { messageId: "two", queue: true });
|
|
84
|
+
const third = delivery.send({ message: "Third" }, { messageId: "three", queue: true });
|
|
85
|
+
payload.configuration.model = "changed";
|
|
86
|
+
assert.deepEqual(delivery.turns().map(turn => [turn.user.text, turn.optimistic.status]),
|
|
87
|
+
[["First", "pending"], ["Second", "pending"], ["Third", "pending"]]);
|
|
88
|
+
assert.equal(await delivery.send({ message: "Duplicate" }, { messageId: "two", queue: true }), false);
|
|
89
|
+
assert.deepEqual(sent.map(message => message.messageId), ["one"]);
|
|
90
|
+
requests[0].resolve({ ok: true });
|
|
91
|
+
await first;
|
|
92
|
+
assert.equal(delivery.state.sending, true);
|
|
93
|
+
assert.equal(delivery.find("one").status, "accepted");
|
|
94
|
+
assert.deepEqual(sent.map(message => message.messageId), ["one", "two"]);
|
|
95
|
+
assert.equal(sent[1].configuration.model, "original");
|
|
96
|
+
requests[1].resolve({ ok: true });
|
|
97
|
+
await second;
|
|
98
|
+
assert.deepEqual(sent.map(message => message.messageId), ["one", "two", "three"]);
|
|
99
|
+
requests[2].resolve({ ok: true });
|
|
100
|
+
await third;
|
|
101
|
+
assert.equal(delivery.state.sending, false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a failed message retains its retry identity while later guidance is pending", async () => {
|
|
105
|
+
const firstRequest = Promise.withResolvers();
|
|
106
|
+
const secondRequest = Promise.withResolvers();
|
|
107
|
+
const sent = [];
|
|
108
|
+
const delivery = createAssistantMessageDelivery({ deliver: payload => {
|
|
109
|
+
sent.push(payload);
|
|
110
|
+
return [firstRequest.promise, secondRequest.promise, { ok: true }][sent.length - 1];
|
|
111
|
+
} });
|
|
112
|
+
const first = delivery.send({ message: "First", configuration: { model: "one" } }, { messageId: "one" });
|
|
113
|
+
const second = delivery.send({ message: "Second" }, { messageId: "two", queue: true });
|
|
114
|
+
const rejected = assert.rejects(first, /Offline/);
|
|
115
|
+
firstRequest.reject(new Error("Offline"));
|
|
116
|
+
await rejected;
|
|
117
|
+
assert.equal(delivery.find("one").status, "failed");
|
|
118
|
+
assert.equal(delivery.find("one").error, "Offline");
|
|
119
|
+
const retry = delivery.resend("one", { queue: true });
|
|
120
|
+
assert.equal(delivery.find("one").status, "pending");
|
|
121
|
+
assert.equal(sent.length, 2);
|
|
122
|
+
secondRequest.resolve({ ok: true });
|
|
123
|
+
await second;
|
|
124
|
+
await retry;
|
|
125
|
+
assert.deepEqual(sent[2], sent[0]);
|
|
126
|
+
assert.equal(delivery.state.sending, false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("reset discards queued delivery without waiting for the old transport", async () => {
|
|
130
|
+
const request = Promise.withResolvers();
|
|
131
|
+
const sent = [];
|
|
132
|
+
const delivery = createAssistantMessageDelivery({ deliver: payload => {
|
|
133
|
+
sent.push(payload.message);
|
|
134
|
+
return request.promise;
|
|
135
|
+
} });
|
|
136
|
+
const first = delivery.send({ message: "Old" });
|
|
137
|
+
const queued = delivery.send({ message: "Queued" }, { queue: true });
|
|
138
|
+
delivery.reset();
|
|
139
|
+
assert.equal(await queued, false);
|
|
140
|
+
assert.deepEqual(sent, ["Old"]);
|
|
141
|
+
const next = delivery.send({ message: "New" });
|
|
142
|
+
assert.deepEqual(sent, ["Old", "New"]);
|
|
143
|
+
assert.equal(delivery.state.sending, true);
|
|
144
|
+
request.resolve({ ok: true });
|
|
145
|
+
assert.equal(await first, false);
|
|
146
|
+
assert.deepEqual(await next, { ok: true });
|
|
147
|
+
assert.equal(delivery.state.sending, false);
|
|
148
|
+
});
|