@jskit-ai/assistant-runtime 0.1.207 → 0.1.209

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.
@@ -155,6 +155,7 @@ export default defineConfig({
155
155
  return json({ ok: true, delivered: true, messageId: input.messageId }, 202);
156
156
  }
157
157
  if (mode === "rejected") return json({ error: "The message was rejected before admission.", code: "ACTION_VALIDATION_FAILED" }, 400);
158
+ if (mode === "unadmitted") return json({ error: "Request failed with status 503." }, 503);
158
159
  if (mode === "uncertain") {
159
160
  state.status = "unconfirmed";
160
161
  state.pendingRequest = { messageId: input.messageId, text: input.text, origin: "user",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.207",
3
+ "version": "0.1.209",
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.214",
15
+ "@jskit-ai/assistant-core": "0.1.216",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.234",
20
- "@jskit-ai/http-web": "0.1.81",
21
- "@jskit-ai/kernel": "0.1.236",
22
- "@jskit-ai/realtime": "0.1.233",
23
- "@jskit-ai/shell-web": "0.1.240",
19
+ "@jskit-ai/http-runtime": "0.1.236",
20
+ "@jskit-ai/http-web": "0.1.83",
21
+ "@jskit-ai/kernel": "0.1.238",
22
+ "@jskit-ai/realtime": "0.1.235",
23
+ "@jskit-ai/shell-web": "0.1.242",
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.236"
27
+ "@jskit-ai/database-runtime": "0.1.238"
28
28
  },
29
29
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
30
30
  "jskit": {
@@ -117,7 +117,8 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
117
117
  const queueing = computed(() => queueWhileSending !== false && (steerable.value || workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking));
118
118
  const canSubmit = computed(() => available.value && (snapshot.value.status === "ready" || steerable.value || (workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking) && snapshot.value.status === "working") &&
119
119
  (queueWhileSending !== false || !delivery.state.sending) &&
120
- !delivery.state.messages.some(message => message.status === "uncertain"));
120
+ !delivery.state.messages.some(message => message.status === "uncertain" &&
121
+ (snapshot.value.status !== "ready" || !message.payload || text(message.payload.draft ?? message.payload.message) === text(draft))));
121
122
  const savedDraft = toValue(draftStorage);
122
123
  if (savedDraft && (typeof savedDraft.key !== "string" || !savedDraft.key ||
123
124
  !["getItem", "setItem", "removeItem"].every(name => typeof savedDraft.storage?.[name] === "function"))) {
@@ -340,8 +341,6 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
340
341
  for (const pending of pendingMessages.values()) pending.controller.abort();
341
342
  pendingMessages.clear();
342
343
  goalReadController?.abort();
343
- globalThis.removeEventListener?.("focus", refreshVisibleGoal);
344
- globalThis.document?.removeEventListener("visibilitychange", refreshVisibleGoal);
345
344
  delivery.reset();
346
345
  });
347
346
 
@@ -350,6 +349,8 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
350
349
  if (retry?.status === "failed" && retry.payload.goalRequest) return changeGoal(retry.payload.goalRequest.action, {}, { messageId });
351
350
  if (!canSubmit.value) return false;
352
351
  if (retry && retry.status !== "failed") return false;
352
+ if (delivery.state.messages.some(message => message.status === "uncertain" &&
353
+ text(message.payload?.message) === text((retry?.payload || payload).message))) return false;
353
354
  const request = retry?.payload?.request || payload.request;
354
355
  const requestedSteering = request?.steer === true;
355
356
  if (requestedSteering && snapshot.value.capabilities?.steering !== true) return false;
@@ -572,11 +573,6 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
572
573
  goalRead = job;
573
574
  return job;
574
575
  }
575
- function refreshVisibleGoal() {
576
- if (!globalThis.document?.hidden) void refreshGoal();
577
- }
578
- globalThis.addEventListener?.("focus", refreshVisibleGoal);
579
- globalThis.document?.addEventListener("visibilitychange", refreshVisibleGoal);
580
576
  async function changeGoal(action, input = {}, { messageId = crypto.randomUUID() } = {}) {
581
577
  const view = goalView.value;
582
578
  const target = view?.target;
@@ -665,9 +661,16 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
665
661
  if (!current.value) return false;
666
662
  if (receipt.status === "accepted") delivery.accept(messageId);
667
663
  else if (receipt.status === "not-sent" && receipt.messageId === messageId) delivery.rejectUnsent(messageId, receipt.error);
664
+ else if (receipt.status === "unknown" && message?.status === "uncertain") {
665
+ message.error = "Delivery is still unknown. You can send a different message when the conversation is ready. Check delivery before resending this one.";
666
+ }
668
667
  subscription?.reload();
669
668
  return receipt;
670
- } catch (failure) { receiveError(failure); return false; }
669
+ } catch (failure) {
670
+ if (current.value && message?.status === "uncertain") message.error = failure.message || "Delivery could not be checked.";
671
+ receiveError(failure);
672
+ return false;
673
+ }
671
674
  finally { if (message) message.checking = false; }
672
675
  }
673
676
 
@@ -62,7 +62,7 @@ test("inspecting a restored no-admission request releases Send while preserving
62
62
  await attach();
63
63
  await expect(f.input).toHaveValue("A separate newer draft");
64
64
  const send = f.primary.getByRole("button", { name: "Send message", exact: true });
65
- await expect(send).toBeDisabled();
65
+ await expect(send).toBeEnabled();
66
66
  await f.page.evaluate(() => window.conversationFixture.attachments(true));
67
67
  await f.primary.locator("input[type=file]").setInputFiles({ name: "newer.txt", mimeType: "text/plain", buffer: Buffer.from("Keep this upload") });
68
68
  const uploads = () => f.page.evaluate(() => window.conversationFixture.attachmentState());
@@ -288,7 +288,9 @@ test("a supplied goal view keeps its pinned target and refreshes through the ret
288
288
  await expect.poll(() => reads).toBeGreaterThan(beforeReconnect);
289
289
  const beforeFocus = reads;
290
290
  await f.page.evaluate(() => window.dispatchEvent(new Event("focus")));
291
- await expect.poll(() => reads).toBeGreaterThan(beforeFocus);
291
+ await f.page.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
292
+ await f.page.waitForTimeout(100);
293
+ assert.equal(reads, beforeFocus, "Focus and visibility do not request goal state");
292
294
  await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalState.value.pending)).toBe(false);
293
295
  failedRead = true;
294
296
  await f.page.evaluate(() => window.conversationFixture.current().refreshGoal());
@@ -2892,3 +2894,45 @@ test("changing the observed native segment blocks cached goal controls until the
2892
2894
  releaseRead?.();
2893
2895
  }
2894
2896
  });
2897
+
2898
+
2899
+ test("unknown delivery reports its current result and allows different idle input without replaying the old message", options, async t => {
2900
+ const f = await fixture(t, 390);
2901
+ await f.command("mode", { mode: "unadmitted" });
2902
+ await f.input.fill("Original uncertain request");
2903
+ await f.input.press("Enter");
2904
+ const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
2905
+ const send = f.primary.getByRole("button", { name: "Send message", exact: true });
2906
+ await expect(check).toBeVisible();
2907
+ const bubble = f.primary.locator(".assistant-transcript__message--user").filter({ hasText: "Original uncertain request" });
2908
+ await expect(bubble.getByText("Delivery unconfirmed", { exact: true })).toBeVisible();
2909
+ await expect(bubble.getByRole("button", { name: "Check delivery", exact: true })).toHaveCount(1);
2910
+ await expect(bubble).toHaveClass(/assistant-transcript__message--unconfirmed/u);
2911
+ await expect(f.primary.locator(".assistant-transcript__system")).toHaveCount(0);
2912
+ await expect(bubble.getByText("Request failed with status 503.", { exact: true })).toBeVisible();
2913
+ await f.input.fill("Original uncertain request");
2914
+ await expect(send).toBeDisabled();
2915
+ await f.input.fill("A different message");
2916
+ await expect(send).toBeEnabled();
2917
+ await check.click();
2918
+ await expect(f.primary.getByText("Delivery is still unknown.", { exact: false })).toBeVisible();
2919
+ await expect(f.input).toHaveValue("A different message");
2920
+ await expect(bubble.getByText("Request failed with status 503.", { exact: true })).toHaveCount(0);
2921
+ const screenshots = process.env.JSKIT_ASSISTANT_SCREENSHOTS;
2922
+ for (const width of [390, 1280]) {
2923
+ await f.page.setViewportSize({ width, height: 900 });
2924
+ await expect(bubble.getByRole("button", { name: "Check delivery", exact: true })).toBeVisible();
2925
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
2926
+ if (screenshots) await f.page.screenshot({ path: `${screenshots}/delivery-attached-${width}.png` });
2927
+ }
2928
+ assert.equal((await f.sent()).length, 1);
2929
+ await f.command("mode", { mode: "product-accepted" });
2930
+ await send.click();
2931
+ await expect.poll(async () => (await f.sent()).length).toBe(2);
2932
+ const [old, next] = await f.sent();
2933
+ assert.notEqual(old.input.messageId, next.input.messageId);
2934
+ assert.equal(next.input.text, "A different message");
2935
+ await expect(check).toBeVisible();
2936
+ assert.equal(await f.page.evaluate(id => window.conversationFixture.current().delivery.find(id).status,
2937
+ old.input.messageId), "uncertain");
2938
+ });