@jskit-ai/assistant-runtime 0.1.232 → 0.1.234

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.232",
3
+ "version": "0.1.234",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -12,19 +12,19 @@
12
12
  "./server": "./src/server/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/assistant-core": "0.1.238",
15
+ "@jskit-ai/assistant-core": "0.1.240",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.242",
20
- "@jskit-ai/http-web": "0.1.90",
21
- "@jskit-ai/kernel": "0.1.244",
22
- "@jskit-ai/realtime": "0.1.242",
23
- "@jskit-ai/shell-web": "0.1.249",
19
+ "@jskit-ai/http-runtime": "0.1.243",
20
+ "@jskit-ai/http-web": "0.1.92",
21
+ "@jskit-ai/kernel": "0.1.245",
22
+ "@jskit-ai/realtime": "0.1.244",
23
+ "@jskit-ai/shell-web": "0.1.251",
24
24
  "@tanstack/vue-query": "^5.90.5",
25
25
  "vue": "^3.5.13",
26
26
  "vuetify": "^4.0.0",
27
- "@jskit-ai/database-runtime": "0.1.244"
27
+ "@jskit-ai/database-runtime": "0.1.245"
28
28
  },
29
29
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
30
30
  "jskit": {
@@ -419,20 +419,23 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
419
419
  }
420
420
  return result;
421
421
  }
422
- function waitUntilReady({ controller }) {
422
+ // A deferred message waits only while the working turn cannot take it. It
423
+ // steers as soon as that turn accepts steering, or sends once it is ready.
424
+ function waitForAdmission({ controller }) {
423
425
  return new Promise(resolve => {
424
426
  let stop;
425
- const finish = ready => {
427
+ const finish = admission => {
426
428
  stop?.();
427
429
  controller.signal.removeEventListener("abort", check);
428
- resolve(ready);
430
+ resolve(admission);
429
431
  };
430
432
  const check = () => {
431
433
  if (controller.signal.aborted || !available.value) finish(false);
432
- else if (snapshot.value.status === "ready") finish(true);
434
+ else if (snapshot.value.status === "ready") finish("send");
435
+ else if (steerable.value) finish("steer");
433
436
  else if (snapshot.value.status !== "working") finish(false);
434
437
  };
435
- stop = watch([available, snapshot], check, { flush: "sync" });
438
+ stop = watch([available, snapshot, steerable], check, { flush: "sync" });
436
439
  controller.signal.addEventListener("abort", check, { once: true });
437
440
  check();
438
441
  });
@@ -451,19 +454,31 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
451
454
  receiptTurns: turns, uncertainOnError: true,
452
455
  async deliver(submission) {
453
456
  pending.started = true;
457
+ let admission = null;
454
458
  try {
455
459
  if (controller.signal.aborted || appAdmission && (!available.value || stopping.value || !workingPolicy.value.admitWhileWorking)) return false;
456
460
  if (deferred) {
457
461
  // Refresh through the same subscription after a queued predecessor;
458
462
  // its receipt may have arrived before the working snapshot.
459
463
  await subscription?.reload();
460
- if (error.value || !await waitUntilReady(pending)) return false;
461
- if (controller.signal.aborted || !available.value || snapshot.value.status !== "ready") return false;
464
+ if (error.value) return false;
465
+ if (snapshot.value.status === "working" && !steerable.value) {
466
+ delivery.wait(messageId, "Waiting for the assistant to finish this step");
467
+ }
468
+ admission = await waitForAdmission(pending);
469
+ delivery.wait(messageId, "");
470
+ if (!admission || controller.signal.aborted || !available.value ||
471
+ snapshot.value.status !== (admission === "steer" ? "working" : "ready")) return false;
462
472
  if (submission.displayAttachments?.length && snapshot.value.capabilities?.attachments !== true) {
463
473
  return { ok: false, error: "This conversation no longer accepts attachments." };
464
474
  }
465
475
  }
466
476
  let request = prepare ? await prepare(submission, { signal: controller.signal }) : submission.request;
477
+ if (admission === "steer") {
478
+ request = { ...request, steer: true };
479
+ const retained = delivery.find(messageId);
480
+ if (retained) retained.payload.request = request;
481
+ }
467
482
  if (prepare) {
468
483
  if (!current.value || accessDenied.value || controller.signal.aborted || request === false) return false;
469
484
  if (!request || typeof request !== "object" || Array.isArray(request)) {
@@ -952,6 +952,32 @@ test("opted-in nonsteerable follow-ups use the original serial queue and dispatc
952
952
  assert.equal((await f.sent()).at(-1).input.steer, true);
953
953
  });
954
954
 
955
+ test("a deferred follow-up explains its wait and steers once the working turn accepts steering", options, async t => {
956
+ const f = await fixture(t);
957
+ await f.page.evaluate(() => {
958
+ window.deferredBinding = window.conversationFixture.acquireConversation({
959
+ conversationId: "chat:2", deferWhileWorking: true, queueWhileSending: true
960
+ });
961
+ window.conversationFixture.target("chat:2");
962
+ });
963
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current()?.identity.conversationId)).toBe("chat:2");
964
+ await expect(f.input).toBeEnabled();
965
+ await f.input.fill("Original nonsteerable request");
966
+ await f.input.press("Enter");
967
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value.status)).toBe("working");
968
+ await f.input.fill("Follow-up while the turn starts");
969
+ await f.input.press("Enter");
970
+ await expect(f.primary.getByText("Pending: Waiting for the assistant to finish this step")).toHaveCount(1);
971
+ assert.equal((await f.sent()).length, 1, "the follow-up waits while the turn cannot take steering");
972
+ await f.command("steering", { id: "chat:2", enabled: true });
973
+ await f.notify("chat:2");
974
+ await expect.poll(async () => (await f.sent()).length).toBe(2);
975
+ const [, followUp] = await f.sent();
976
+ assert.equal(followUp.input.text, "Follow-up while the turn starts");
977
+ assert.equal(followUp.input.steer, true, "the waiting follow-up reaches the running turn without waiting for it to finish");
978
+ await expect(f.primary.getByText(/^Pending/)).toHaveCount(0);
979
+ });
980
+
955
981
  for (const steer of [false, true]) {
956
982
  test(`Stop cancels locally queued native ${steer ? "steering" : "startup"} follow-ups`, options, async t => {
957
983
  const f = await fixture(t);