@jskit-ai/assistant-runtime 0.2.27 → 0.2.29
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- `@jskit-ai/assistant-runtime/client/providers/AssistantClientProvider` exports the client provider by itself, so the client bootstrap loads it without the assistant's elements and what they import.
|
|
6
|
+
- `subscribeAssistantConversation()` takes `noticeMs`, how long an unanswered subscription waits before the loading notice. It stays ten seconds; tests pass a shorter one, so the real-socket proof of a slow subscription no longer waits ten seconds.
|
|
5
7
|
- The assistant settings are a settings pane: the app bar shows their title and Save (a primary bar action that submits the form, with the card's loading and disabled states), and the card no longer has a Save row at its bottom. The button reads **Save** (`jskit.assistant.runtime.settings.save`, was "Save assistant settings"), since the bar's title names the settings. Visual only: what saving does is unchanged.
|
|
6
8
|
- A browser test (`test/assistantSettings.browser.test.js`, fixture `fixtures/settings/`, run in CI) renders the settings pane in the shell's `ShellLayout` at 390 and 1280 px: the bar's title is the page's one `h1`, its **Save** is a submit button for `assistant-settings-form` that sends the edited prompt and shows its loading state until the save answers, a refused save shows the prompt's field message and reports the failure, and workspace settings show their note and no Save until the address names a workspace.
|
|
7
9
|
- The assistant surface pattern's icon is `$smartToy`, and the fixtures render with `@jskit-ai/material-web`.
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "jskit test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
|
+
"./client/providers/AssistantClientProvider": "./src/client/providers/AssistantClientProvider.js",
|
|
10
11
|
"./shared": "./src/shared/index.js",
|
|
11
12
|
"./server/actionIds": "./src/server/actionIds.js",
|
|
12
13
|
"./server": "./src/server/index.js",
|
|
@@ -14,19 +15,19 @@
|
|
|
14
15
|
"./locales/en.json": "./locales/en.json"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
|
-
"@jskit-ai/assistant-core": "0.2.
|
|
18
|
+
"@jskit-ai/assistant-core": "0.2.27",
|
|
18
19
|
"json-rest-schema": "^1.0.17"
|
|
19
20
|
},
|
|
20
21
|
"peerDependencies": {
|
|
21
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
22
|
-
"@jskit-ai/http-web": "0.2.
|
|
23
|
-
"@jskit-ai/kernel": "0.2.
|
|
24
|
-
"@jskit-ai/realtime": "0.2.
|
|
25
|
-
"@jskit-ai/shell-web": "0.2.
|
|
22
|
+
"@jskit-ai/http-runtime": "0.2.13",
|
|
23
|
+
"@jskit-ai/http-web": "0.2.21",
|
|
24
|
+
"@jskit-ai/kernel": "0.2.13",
|
|
25
|
+
"@jskit-ai/realtime": "0.2.19",
|
|
26
|
+
"@jskit-ai/shell-web": "0.2.16",
|
|
26
27
|
"@tanstack/vue-query": "^5.90.5",
|
|
27
28
|
"vue": "^3.5.13",
|
|
28
29
|
"vuetify": "^4.0.0",
|
|
29
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
30
|
+
"@jskit-ai/database-runtime": "0.1.262"
|
|
30
31
|
},
|
|
31
32
|
"description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
|
|
32
33
|
"jskit": {
|
|
@@ -88,6 +89,10 @@
|
|
|
88
89
|
"subpath": "./client",
|
|
89
90
|
"summary": "Exports assistant runtime client elements and composables."
|
|
90
91
|
},
|
|
92
|
+
{
|
|
93
|
+
"subpath": "./client/providers/AssistantClientProvider",
|
|
94
|
+
"summary": "Exports the assistant client provider, which the client bootstrap loads without the elements."
|
|
95
|
+
},
|
|
91
96
|
{
|
|
92
97
|
"subpath": "./shared",
|
|
93
98
|
"summary": "Exports assistant runtime shared config helpers."
|
|
@@ -45,12 +45,14 @@ function ignoreSubscriptionError() {}
|
|
|
45
45
|
* read: (read: { current: () => boolean }) => Promise<any>,
|
|
46
46
|
* onState: (state: any, options: { initial: boolean, canonical: boolean }) => void,
|
|
47
47
|
* onEvent?: (event: any) => void,
|
|
48
|
-
* onError?: (failure: Error) => void
|
|
49
|
-
*
|
|
48
|
+
* onError?: (failure: Error) => void,
|
|
49
|
+
* noticeMs?: number
|
|
50
|
+
* }} subscription `noticeMs` is how long an unanswered subscription waits before the loading notice: ten seconds,
|
|
51
|
+
* shorter only in tests.
|
|
50
52
|
*/
|
|
51
53
|
function subscribeAssistantConversation({
|
|
52
54
|
socket, conversationId, targetSurfaceId, hostSurfaceId, workspaceSlug,
|
|
53
|
-
read, onState, onEvent = ignoreConversationEvent, onError = ignoreSubscriptionError
|
|
55
|
+
read, onState, onEvent = ignoreConversationEvent, onError = ignoreSubscriptionError, noticeMs = SUBSCRIPTION_NOTICE_MS
|
|
54
56
|
}) {
|
|
55
57
|
// readable-check: allow method-probe - input validation: an application may hand the conversation binding its own socket
|
|
56
58
|
if (typeof socket?.on !== "function" || typeof socket?.off !== "function" || typeof socket?.timeout !== "function") {
|
|
@@ -239,7 +241,7 @@ function subscribeAssistantConversation({
|
|
|
239
241
|
reloadQueued = false;
|
|
240
242
|
waitingTimer = setTimeout(() => onError(Object.assign(
|
|
241
243
|
new Error("Chat is still loading. Waiting for the server; Reload chat to try now."),
|
|
242
|
-
{ code: "assistant_subscription_waiting" })),
|
|
244
|
+
{ code: "assistant_subscription_waiting" })), noticeMs);
|
|
243
245
|
socket.timeout(SUBSCRIPTION_LOST_MS).emit(ASSISTANT_CONVERSATION_SUBSCRIBE, input, (error, response) => {
|
|
244
246
|
if (disposed || attempt !== generation) return;
|
|
245
247
|
clearTimers();
|
|
@@ -191,9 +191,9 @@ async function fixture(t, { diagnostics = false, onSetup } = {}) {
|
|
|
191
191
|
});
|
|
192
192
|
return socket;
|
|
193
193
|
}
|
|
194
|
-
function view(socket, id) {
|
|
194
|
+
function view(socket, id, options = {}) {
|
|
195
195
|
const states = [], received = [], errors = [];
|
|
196
|
-
const dispose = subscribeAssistantConversation({ socket, conversationId: id, targetSurfaceId: "home", hostSurfaceId: "home",
|
|
196
|
+
const dispose = subscribeAssistantConversation({ ...options, socket, conversationId: id, targetSurfaceId: "home", hostSurfaceId: "home",
|
|
197
197
|
read() {
|
|
198
198
|
return actions.execute({ actionId: actionIds.conversationRead,
|
|
199
199
|
input: { targetSurfaceId: "home", conversationId: id },
|
|
@@ -332,7 +332,10 @@ test("slow successful subscriptions warn once with the blocked stage and no conv
|
|
|
332
332
|
assert.equal(f.fetches.length, 0);
|
|
333
333
|
});
|
|
334
334
|
|
|
335
|
-
|
|
335
|
+
// The client's ten-second notice is proven on a mocked clock below ("a connected subscription keeps waiting for a slow
|
|
336
|
+
// server ..."); here a real socket and server run the same path with a shorter notice, and the server's clock is moved
|
|
337
|
+
// past ten seconds, as in the test above.
|
|
338
|
+
test("a real subscription slower than its notice keeps its observer, says it is still loading and delivers the late snapshot", async t => {
|
|
336
339
|
const f = await fixture(t);
|
|
337
340
|
const socket = await f.connect();
|
|
338
341
|
const socketId = socket.id;
|
|
@@ -340,22 +343,23 @@ test("a real subscription slower than ten seconds keeps its observer, says it is
|
|
|
340
343
|
await f.conversations.get(id).send({ messageId: "survives-wait", text: "Keep working through a slow snapshot" });
|
|
341
344
|
await until(f.changes, () => f.fetches.length === 1);
|
|
342
345
|
const barrier = f.holdNextRead(id);
|
|
343
|
-
const
|
|
346
|
+
const noticeMs = 300;
|
|
347
|
+
const view = f.view(socket, id, { noticeMs });
|
|
344
348
|
await until(f.changes, () => barrier.started);
|
|
345
|
-
const readStartedAt = performance.now();
|
|
346
349
|
const subscriptionEpoch = f.subscriptionTimings.at(-1).subscriptionEpoch;
|
|
347
|
-
while (!view.errors.length) await once(f.changes, "change", { signal: AbortSignal.timeout(
|
|
350
|
+
while (!view.errors.length) await once(f.changes, "change", { signal: AbortSignal.timeout(5_000) });
|
|
348
351
|
assert.equal(view.errors[0].message, "Chat is still loading. Waiting for the server; Reload chat to try now.");
|
|
349
352
|
assert.equal(view.errors[0].code, "assistant_subscription_waiting");
|
|
350
353
|
assert.equal(f.active(), 1, "The slow observer stays attached while its snapshot is read");
|
|
351
354
|
assert.equal(socket.id, socketId);
|
|
352
355
|
assert.equal(socket.connected, true);
|
|
353
356
|
assert.equal(view.states.length, 0);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
357
|
+
const now = performance.now.bind(performance);
|
|
358
|
+
const clock = t.mock.method(performance, "now", () => now() + 10_001);
|
|
359
|
+
try {
|
|
360
|
+
barrier.resolve();
|
|
361
|
+
await until(f.changes, () => view.states.length === 1);
|
|
362
|
+
} finally { clock.mock.restore(); }
|
|
359
363
|
assert.equal(view.states[0].conversationLog[0].user.messageId, "survives-wait");
|
|
360
364
|
const records = f.subscriptionTimings.filter(record => record.subscriptionEpoch === subscriptionEpoch);
|
|
361
365
|
assert.equal(records.some(record => record.stage === "abandoned"), false, "Slow work is never abandoned");
|
|
@@ -434,8 +438,19 @@ test("Reload recovers a failed initial subscription on the same connected socket
|
|
|
434
438
|
const barrier = f.holdNextRead(f.ids[0]);
|
|
435
439
|
const view = f.view(socket, f.ids[0]);
|
|
436
440
|
await until(f.changes, () => barrier.started);
|
|
441
|
+
// A failed read is a server failure, so the client also retries by itself a second later. Here the person reloads
|
|
442
|
+
// within that second; on a loaded host the steps below can take longer, and the retry would then subscribe before
|
|
443
|
+
// the conversation is revoked. The test holds that retry so only Reload recovers.
|
|
444
|
+
const setTimer = globalThis.setTimeout;
|
|
445
|
+
let retry = null;
|
|
446
|
+
t.mock.method(globalThis, "setTimeout", (callback, delay, ...args) => {
|
|
447
|
+
if (retry || delay !== 1_000) return setTimer(callback, delay, ...args);
|
|
448
|
+
retry = callback;
|
|
449
|
+
return setTimer(() => {}, 0);
|
|
450
|
+
});
|
|
437
451
|
barrier.reject(new Error("The initial history read failed."));
|
|
438
452
|
await until(f.changes, () => view.errors.length === 1 && f.active() === 0);
|
|
453
|
+
assert.equal(typeof retry, "function", "The failed subscription scheduled its automatic retry");
|
|
439
454
|
assert.equal(socket.connected, true);
|
|
440
455
|
assert.equal(view.states.length, 0);
|
|
441
456
|
|
|
@@ -833,10 +848,14 @@ test("subscription identity and surface checks reject forged bodies and honor co
|
|
|
833
848
|
await until(f.changes, () => view.received.some(event => event.type === "message"));
|
|
834
849
|
assert.equal(otherEvents.length, 0);
|
|
835
850
|
f.revoked.add(f.ids[0]);
|
|
851
|
+
// A streamed delta may reuse the subscriber's authorization while it is under a second
|
|
852
|
+
// old; once a second has passed, the next delta authorizes again and is refused.
|
|
853
|
+
t.mock.timers.enable({ apis: ["Date"], now: Date.now() + 1000 });
|
|
836
854
|
const before = view.received.length;
|
|
837
855
|
f.fetches[0].text(" Secret output after access was revoked.");
|
|
838
856
|
f.fetches[0].finish();
|
|
839
857
|
await f.conversations.get(f.ids[0]).wait();
|
|
858
|
+
t.mock.timers.reset();
|
|
840
859
|
assert.equal(view.received.length, before);
|
|
841
860
|
assert.equal(f.published.some(event => event.realtime?.payload?.event?.text?.includes("Secret output")), false);
|
|
842
861
|
const second = f.view(owner, f.ids[1]);
|
|
@@ -178,7 +178,7 @@ test("registerRoutes forwards the resolved host surface through every direct act
|
|
|
178
178
|
params: {
|
|
179
179
|
conversationId: "conversation_1",
|
|
180
180
|
surfaceId: "admin",
|
|
181
|
-
workspaceSlug: "
|
|
181
|
+
workspaceSlug: "acme"
|
|
182
182
|
},
|
|
183
183
|
query: {
|
|
184
184
|
limit: 20
|
|
@@ -233,7 +233,7 @@ test("registerRoutes builds workspace action input from explicit config and scop
|
|
|
233
233
|
},
|
|
234
234
|
input: {
|
|
235
235
|
params: {
|
|
236
|
-
workspaceSlug: "
|
|
236
|
+
workspaceSlug: "acme",
|
|
237
237
|
surfaceId: "admin"
|
|
238
238
|
},
|
|
239
239
|
query: {
|
|
@@ -259,7 +259,7 @@ test("registerRoutes builds workspace action input from explicit config and scop
|
|
|
259
259
|
assert.equal(reply.statusCode, 200);
|
|
260
260
|
assert.equal(capturedAction?.surface, "admin");
|
|
261
261
|
assert.equal(capturedAction?.input?.targetSurfaceId, "admin");
|
|
262
|
-
assert.equal(capturedAction?.input?.workspaceSlug, "
|
|
262
|
+
assert.equal(capturedAction?.input?.workspaceSlug, "acme");
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
test("registerRoutes returns clear AppError payload for pre-stream assistant failures", async () => {
|
|
@@ -306,7 +306,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
|
|
|
306
306
|
},
|
|
307
307
|
input: {
|
|
308
308
|
params: {
|
|
309
|
-
workspaceSlug: "
|
|
309
|
+
workspaceSlug: "acme",
|
|
310
310
|
surfaceId: "admin"
|
|
311
311
|
},
|
|
312
312
|
body: {
|
|
@@ -330,7 +330,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
|
|
|
330
330
|
});
|
|
331
331
|
assert.deepEqual(capturedInput, {
|
|
332
332
|
targetSurfaceId: "admin",
|
|
333
|
-
workspaceSlug: "
|
|
333
|
+
workspaceSlug: "acme",
|
|
334
334
|
messageId: "msg_1",
|
|
335
335
|
input: "hello"
|
|
336
336
|
});
|
|
@@ -349,7 +349,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
|
|
|
349
349
|
await route.handler({
|
|
350
350
|
raw: { on() {}, off() {} },
|
|
351
351
|
headers: { "x-jskit-surface": "admin" },
|
|
352
|
-
input: { params: { workspaceSlug: "
|
|
352
|
+
input: { params: { workspaceSlug: "acme", surfaceId: "admin" }, body: { messageId: "msg_2", input: "hello" } },
|
|
353
353
|
executeAction: async () => { throw failure; }
|
|
354
354
|
}, reply);
|
|
355
355
|
assert.equal(reply.statusCode, status);
|
|
@@ -389,7 +389,7 @@ test("chat service uses explicit app config when conversations are listed", asyn
|
|
|
389
389
|
{
|
|
390
390
|
input: {
|
|
391
391
|
targetSurfaceId: "admin",
|
|
392
|
-
workspaceSlug: "
|
|
392
|
+
workspaceSlug: "acme"
|
|
393
393
|
},
|
|
394
394
|
context: {
|
|
395
395
|
actor: {
|
|
@@ -398,14 +398,14 @@ test("chat service uses explicit app config when conversations are listed", asyn
|
|
|
398
398
|
},
|
|
399
399
|
workspace: {
|
|
400
400
|
id: 7,
|
|
401
|
-
slug: "
|
|
401
|
+
slug: "acme"
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
);
|
|
406
406
|
|
|
407
407
|
assert.equal(response.assistantSurface.targetSurfaceId, "admin");
|
|
408
|
-
assert.equal(response.workspace.slug, "
|
|
408
|
+
assert.equal(response.workspace.slug, "acme");
|
|
409
409
|
});
|
|
410
410
|
|
|
411
411
|
test("chat service rejects workspace-scoped assistant surfaces when workspace support is unavailable", async () => {
|
|
@@ -435,7 +435,7 @@ test("chat service rejects workspace-scoped assistant surfaces when workspace su
|
|
|
435
435
|
{
|
|
436
436
|
input: {
|
|
437
437
|
targetSurfaceId: "admin",
|
|
438
|
-
workspaceSlug: "
|
|
438
|
+
workspaceSlug: "acme"
|
|
439
439
|
},
|
|
440
440
|
context: {
|
|
441
441
|
actor: {
|