@jskit-ai/assistant-core 0.1.215 → 0.1.217
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/package.json +5 -5
- package/src/client/conversation/AssistantConversationStatus.vue +7 -6
- package/src/client/conversation/AssistantTranscript.vue +16 -7
- package/src/client/conversation/messageDelivery.js +2 -1
- package/src/client/conversation/useAssistantSuggestions.js +5 -3
- package/test/messageDelivery.test.js +15 -0
- package/test/suggestions.test.js +34 -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.217",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.237",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.239",
|
|
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.56",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.56"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
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.217",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
48
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
49
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
50
|
-
"@jskit-ai/resource-core": "0.1.
|
|
51
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.181",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.181",
|
|
52
52
|
"@mdi/js": "^7.4.47",
|
|
53
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
54
54
|
"ai": "^7.0.101",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"ws": "^8.20.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.237",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.239",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="message.delivery" role="status">
|
|
3
|
-
<
|
|
4
|
-
<p v-if="message.delivery.error" class="text-body-small">{{ message.delivery.error }}</p>
|
|
2
|
+
<div v-if="message.delivery" class="d-flex flex-wrap align-center ga-1 text-caption text-error" role="status">
|
|
3
|
+
<span>{{ message.delivery.error || message.text }}</span>
|
|
5
4
|
<v-btn
|
|
6
|
-
variant="
|
|
7
|
-
|
|
5
|
+
variant="text"
|
|
6
|
+
color="error"
|
|
7
|
+
size="x-small"
|
|
8
|
+
aria-label="Retry delivery check"
|
|
8
9
|
:disabled="message.delivery.checking"
|
|
9
10
|
@click="$emit('check-delivery', message.delivery.messageId)"
|
|
10
11
|
>
|
|
11
|
-
{{ message.delivery.checking ? 'Checking
|
|
12
|
+
{{ message.delivery.checking ? 'Checking…' : 'Retry' }}
|
|
12
13
|
</v-btn>
|
|
13
14
|
</div>
|
|
14
15
|
<slot v-else>
|
|
@@ -177,22 +177,26 @@
|
|
|
177
177
|
</div>
|
|
178
178
|
<slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
|
|
179
179
|
<div
|
|
180
|
-
v-if="entry.turn.optimistic?.status === 'failed'"
|
|
180
|
+
v-if="entry.delivery || entry.turn.optimistic?.status === 'failed'"
|
|
181
181
|
class="assistant-transcript__optimistic-failure"
|
|
182
182
|
>
|
|
183
|
-
<span role="status">Failed: {
|
|
183
|
+
<span role="status">{{ entry.delivery ? entry.delivery.delivery.error || 'Delivery unconfirmed.' : `Failed: ${entry.turn.optimistic.error || 'Message could not be sent.'}` }}</span>
|
|
184
184
|
<div v-if="!entry.turn.preview" class="assistant-transcript__optimistic-actions">
|
|
185
185
|
<v-btn
|
|
186
186
|
class="assistant-transcript__delivery-action"
|
|
187
|
-
color="
|
|
187
|
+
color="error"
|
|
188
188
|
size="x-small"
|
|
189
189
|
type="button"
|
|
190
|
-
variant="
|
|
191
|
-
|
|
190
|
+
variant="text"
|
|
191
|
+
:disabled="entry.delivery?.delivery.checking"
|
|
192
|
+
:aria-label="entry.delivery ? 'Retry delivery check' : undefined"
|
|
193
|
+
:title="entry.delivery ? entry.delivery.delivery.error || 'Check delivery before resending.' : undefined"
|
|
194
|
+
@click="entry.delivery ? emit('check-delivery', entry.delivery.delivery.messageId) : emit('resend-turn', entry.turn.optimistic.id)"
|
|
192
195
|
>
|
|
193
|
-
Retry
|
|
196
|
+
{{ entry.delivery?.delivery.checking ? 'Checking…' : 'Retry' }}
|
|
194
197
|
</v-btn>
|
|
195
198
|
<v-btn
|
|
199
|
+
v-if="!entry.delivery"
|
|
196
200
|
class="assistant-transcript__delivery-action"
|
|
197
201
|
size="x-small"
|
|
198
202
|
type="button"
|
|
@@ -202,6 +206,7 @@
|
|
|
202
206
|
Cancel
|
|
203
207
|
</v-btn>
|
|
204
208
|
<v-btn
|
|
209
|
+
v-if="!entry.delivery"
|
|
205
210
|
class="assistant-transcript__delivery-action"
|
|
206
211
|
size="x-small"
|
|
207
212
|
type="button"
|
|
@@ -584,8 +589,11 @@ const displayEntries = computed(() => {
|
|
|
584
589
|
const entries = [];
|
|
585
590
|
for (const [index, turn] of props.turns.entries()) {
|
|
586
591
|
const turnId = String(turn.turnId || index + 1);
|
|
592
|
+
// A delivery notice belongs to its exact user message, not the previous reply.
|
|
593
|
+
const userDelivery = turn.user && turn.system?.delivery && turn.system.delivery.messageId === turn.user.messageId
|
|
594
|
+
? turn.system : null;
|
|
587
595
|
const messages = [
|
|
588
|
-
turn.system && { ...turn.system, role: "system" },
|
|
596
|
+
turn.system && !userDelivery && { ...turn.system, role: "system" },
|
|
589
597
|
turn.user && { ...turn.user, role: "user" },
|
|
590
598
|
...conversationAgentMessages(turn)
|
|
591
599
|
].filter(Boolean);
|
|
@@ -604,6 +612,7 @@ const displayEntries = computed(() => {
|
|
|
604
612
|
} else {
|
|
605
613
|
entries.push({
|
|
606
614
|
key, role, turn: { ...turn, turnId }, message: displayed,
|
|
615
|
+
delivery: role === "user" ? userDelivery : null,
|
|
607
616
|
...(role === "thinking" ? { messages: [{ ...displayed, key }] } : {})
|
|
608
617
|
});
|
|
609
618
|
}
|
|
@@ -111,6 +111,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
111
111
|
const ownerPendingSends = pendingSends;
|
|
112
112
|
const current = () => pendingSends === ownerPendingSends && isCurrent();
|
|
113
113
|
const predecessor = deliveryTail;
|
|
114
|
+
const knownUncertain = new Set(state.messages.filter(entry => entry.status === "uncertain").map(entry => entry.id));
|
|
114
115
|
const settled = Promise.withResolvers();
|
|
115
116
|
deliveryTail = settled.promise;
|
|
116
117
|
ownerPendingSends.set(messageId, settled);
|
|
@@ -132,7 +133,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
132
133
|
try {
|
|
133
134
|
if (predecessor) await predecessor;
|
|
134
135
|
if (!current()) return false;
|
|
135
|
-
if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain")) {
|
|
136
|
+
if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain" && !knownUncertain.has(entry.id))) {
|
|
136
137
|
fail(messageId, "Check the previous message's delivery before sending again.");
|
|
137
138
|
return false;
|
|
138
139
|
}
|
|
@@ -3,7 +3,7 @@ import { computed, onScopeDispose, ref, toValue, unref, watch } from "vue";
|
|
|
3
3
|
/** Suggestion requests use an application-supplied agent; credentials stay on its server. */
|
|
4
4
|
export function useAssistantSuggestions({
|
|
5
5
|
active = true, requestKey = "", draft = "", configuration = {},
|
|
6
|
-
generate, onSelect = () => false, debounceMs = 750
|
|
6
|
+
generate, onSelect = () => false, debounceMs = 750, minIntervalMs = 0
|
|
7
7
|
} = {}) {
|
|
8
8
|
const items = ref([]);
|
|
9
9
|
const loading = ref(false);
|
|
@@ -18,6 +18,7 @@ export function useAssistantSuggestions({
|
|
|
18
18
|
const visible = computed(() => eligible.value && (loading.value || items.value.length > 0));
|
|
19
19
|
let timer;
|
|
20
20
|
let request;
|
|
21
|
+
let lastStartedAt = -Infinity;
|
|
21
22
|
|
|
22
23
|
function cancel() {
|
|
23
24
|
clearTimeout(timer);
|
|
@@ -43,6 +44,7 @@ export function useAssistantSuggestions({
|
|
|
43
44
|
const run = unref(generate);
|
|
44
45
|
loading.value = true;
|
|
45
46
|
timer = setTimeout(async () => {
|
|
47
|
+
lastStartedAt = Date.now();
|
|
46
48
|
try {
|
|
47
49
|
const result = await run(input);
|
|
48
50
|
if (request !== controller) return;
|
|
@@ -58,7 +60,8 @@ export function useAssistantSuggestions({
|
|
|
58
60
|
loading.value = false;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
|
-
}, Math.max(0, Number(toValue(debounceMs)) || 0
|
|
63
|
+
}, Math.max(0, Number(toValue(debounceMs)) || 0,
|
|
64
|
+
lastStartedAt + Math.max(0, Number(toValue(minIntervalMs)) || 0) - Date.now()));
|
|
62
65
|
}, { immediate: true, flush: "sync" });
|
|
63
66
|
|
|
64
67
|
function currentSuggestion(value) {
|
|
@@ -80,7 +83,6 @@ export function useAssistantSuggestions({
|
|
|
80
83
|
}
|
|
81
84
|
function focus() {
|
|
82
85
|
if (toValue(active) === false || composerFocused.value) return;
|
|
83
|
-
dismissedKey.value = null;
|
|
84
86
|
composerFocused.value = true;
|
|
85
87
|
}
|
|
86
88
|
function blur() {
|
|
@@ -397,3 +397,18 @@ test("restored application requests retain their origin and attachment-only user
|
|
|
397
397
|
assert.equal(delivery.turns()[0].user.text, "");
|
|
398
398
|
assert.deepEqual(delivery.turns()[0].user.attachments, [{ attachmentId: "image" }]);
|
|
399
399
|
});
|
|
400
|
+
|
|
401
|
+
test("a deliberate new message can follow known uncertainty without resending the old request", async () => {
|
|
402
|
+
const sent = [];
|
|
403
|
+
const delivery = createAssistantMessageDelivery({ deliver: async payload => {
|
|
404
|
+
sent.push(payload);
|
|
405
|
+
if (payload.messageId === "old") throw new Error("Lost receipt");
|
|
406
|
+
return { ok: true };
|
|
407
|
+
} });
|
|
408
|
+
await assert.rejects(delivery.send({ message: "Old request" }, { messageId: "old", uncertainOnError: true }));
|
|
409
|
+
assert.equal(await delivery.send({ message: "Old request" }, { messageId: "old", uncertainOnError: true }), false);
|
|
410
|
+
assert.deepEqual(await delivery.send({ message: "A new request" }, { messageId: "new", uncertainOnError: true }), { ok: true });
|
|
411
|
+
assert.deepEqual(sent.map(message => message.messageId), ["old", "new"]);
|
|
412
|
+
assert.equal(delivery.find("old").status, "uncertain");
|
|
413
|
+
assert.equal(delivery.find("new").status, "accepted");
|
|
414
|
+
});
|
package/test/suggestions.test.js
CHANGED
|
@@ -87,7 +87,7 @@ test("suggestion previews preserve a draft and stale selections cannot submit",
|
|
|
87
87
|
state.focus();
|
|
88
88
|
t.mock.timers.tick(750);
|
|
89
89
|
await flush();
|
|
90
|
-
assert.equal(state.visible.value,
|
|
90
|
+
assert.equal(state.visible.value, false, "Refocusing does not regenerate dismissed suggestions");
|
|
91
91
|
draft.value = "Keep my text";
|
|
92
92
|
assert.equal(state.preview.value, "");
|
|
93
93
|
assert.equal(state.select(suggestion("Next step")), false);
|
|
@@ -112,3 +112,36 @@ test("a failing suggestion agent leaves the composer independent and stops retry
|
|
|
112
112
|
t.mock.timers.tick(60_000);
|
|
113
113
|
assert.equal(requests, 1);
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
test("suggestions wait five seconds and coalesce edited drafts through a thirty-second cooldown", async t => {
|
|
118
|
+
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 1_000 });
|
|
119
|
+
const scope = effectScope();
|
|
120
|
+
t.after(() => scope.stop());
|
|
121
|
+
const draft = ref("First");
|
|
122
|
+
const active = ref(true);
|
|
123
|
+
const calls = [];
|
|
124
|
+
const state = scope.run(() => useAssistantSuggestions({ draft, active, debounceMs: 5_000, minIntervalMs: 30_000,
|
|
125
|
+
generate: async input => { calls.push({ draft: input.draft, at: Date.now() }); return [suggestion(input.draft)]; }
|
|
126
|
+
}));
|
|
127
|
+
t.mock.timers.tick(4_999);
|
|
128
|
+
state.focus(); state.blur(); state.focus();
|
|
129
|
+
assert.equal(calls.length, 0);
|
|
130
|
+
t.mock.timers.tick(1);
|
|
131
|
+
await flush();
|
|
132
|
+
draft.value = "Second";
|
|
133
|
+
t.mock.timers.tick(15_000);
|
|
134
|
+
draft.value = "Newest";
|
|
135
|
+
state.focus();
|
|
136
|
+
t.mock.timers.tick(14_999);
|
|
137
|
+
assert.equal(calls.length, 1);
|
|
138
|
+
t.mock.timers.tick(1);
|
|
139
|
+
await flush();
|
|
140
|
+
assert.deepEqual(calls, [{ draft: "First", at: 6_000 }, { draft: "Newest", at: 36_000 }]);
|
|
141
|
+
t.mock.timers.tick(90_000);
|
|
142
|
+
assert.equal(calls.length, 2, "Cooldown does not poll when the input is unchanged");
|
|
143
|
+
draft.value = "Cancelled before dispatch";
|
|
144
|
+
active.value = false;
|
|
145
|
+
t.mock.timers.tick(30_000);
|
|
146
|
+
assert.equal(calls.length, 2);
|
|
147
|
+
});
|