@jskit-ai/assistant-core 0.1.215 → 0.1.216
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/AssistantTranscript.vue +33 -2
- 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.216",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.236",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.238",
|
|
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.55",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.55"
|
|
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.216",
|
|
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.180",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.180",
|
|
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.236",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.238",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -139,7 +139,8 @@
|
|
|
139
139
|
class="assistant-transcript__message assistant-transcript__message--user"
|
|
140
140
|
:class="{
|
|
141
141
|
'assistant-transcript__message--pending': entry.turn.optimistic?.status === 'pending',
|
|
142
|
-
'assistant-transcript__message--failed': entry.turn.optimistic?.status === 'failed'
|
|
142
|
+
'assistant-transcript__message--failed': entry.turn.optimistic?.status === 'failed',
|
|
143
|
+
'assistant-transcript__message--unconfirmed': Boolean(entry.delivery)
|
|
143
144
|
}"
|
|
144
145
|
>
|
|
145
146
|
<div
|
|
@@ -175,6 +176,12 @@
|
|
|
175
176
|
<time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
|
|
176
177
|
<span v-if="entry.turn.optimistic?.status === 'pending'" role="status">Pending</span>
|
|
177
178
|
</div>
|
|
179
|
+
<div v-if="entry.delivery" class="assistant-transcript__unconfirmed-delivery">
|
|
180
|
+
<AssistantConversationStatus
|
|
181
|
+
:message="{ ...entry.delivery, text: 'Delivery unconfirmed' }"
|
|
182
|
+
@check-delivery="emit('check-delivery', $event)"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
178
185
|
<slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
|
|
179
186
|
<div
|
|
180
187
|
v-if="entry.turn.optimistic?.status === 'failed'"
|
|
@@ -584,8 +591,11 @@ const displayEntries = computed(() => {
|
|
|
584
591
|
const entries = [];
|
|
585
592
|
for (const [index, turn] of props.turns.entries()) {
|
|
586
593
|
const turnId = String(turn.turnId || index + 1);
|
|
594
|
+
// A delivery notice belongs to its exact user message, not the previous reply.
|
|
595
|
+
const userDelivery = turn.user && turn.system?.delivery && turn.system.delivery.messageId === turn.user.messageId
|
|
596
|
+
? turn.system : null;
|
|
587
597
|
const messages = [
|
|
588
|
-
turn.system && { ...turn.system, role: "system" },
|
|
598
|
+
turn.system && !userDelivery && { ...turn.system, role: "system" },
|
|
589
599
|
turn.user && { ...turn.user, role: "user" },
|
|
590
600
|
...conversationAgentMessages(turn)
|
|
591
601
|
].filter(Boolean);
|
|
@@ -604,6 +614,7 @@ const displayEntries = computed(() => {
|
|
|
604
614
|
} else {
|
|
605
615
|
entries.push({
|
|
606
616
|
key, role, turn: { ...turn, turnId }, message: displayed,
|
|
617
|
+
delivery: role === "user" ? userDelivery : null,
|
|
607
618
|
...(role === "thinking" ? { messages: [{ ...displayed, key }] } : {})
|
|
608
619
|
});
|
|
609
620
|
}
|
|
@@ -1431,6 +1442,26 @@ watch(() => props.visible, (visible) => {
|
|
|
1431
1442
|
margin-top: -0.15rem;
|
|
1432
1443
|
}
|
|
1433
1444
|
|
|
1445
|
+
.assistant-transcript__message--user.assistant-transcript__message--unconfirmed {
|
|
1446
|
+
background: rgba(var(--v-theme-warning), 0.08);
|
|
1447
|
+
border: 1px solid rgba(var(--v-theme-warning), 0.6);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
.assistant-transcript__unconfirmed-delivery {
|
|
1451
|
+
border-top: 1px solid rgba(var(--v-theme-warning), 0.4);
|
|
1452
|
+
font-size: 0.875rem;
|
|
1453
|
+
padding-top: 0.65rem;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
.assistant-transcript__unconfirmed-delivery :deep(p:first-child) {
|
|
1457
|
+
font-weight: 700;
|
|
1458
|
+
margin-bottom: 0.4rem;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.assistant-transcript__unconfirmed-delivery :deep(p) {
|
|
1462
|
+
overflow-wrap: anywhere;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1434
1465
|
.assistant-transcript__optimistic-failure {
|
|
1435
1466
|
align-items: center;
|
|
1436
1467
|
color: rgba(var(--v-theme-error), 0.92);
|
|
@@ -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
|
+
});
|