@jskit-ai/assistant-runtime 0.2.45 → 0.2.46

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,7 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - A Set or Resume goal that the server refuses after its message already appeared in the conversation shows the server's reason (for example that Claude's goal attachment is not available yet) instead of only `jskit.assistant.runtime.goal.changeFailed` ("Could not change the goal."): the transcript turn settled the optimistic message first, so the failure's own text was dropped with it.
5
6
  - The saved-conversations list (`GET /api/assistant/:surfaceId/conversations`, and its workspace form) answers 200 again instead of 500 ("metadata is required!") once a person has a conversation: the route still serialized each item with the conversation's read schema, which requires `metadata`, after list items stopped carrying it. It serializes them with assistant-core's `assistantConversationListItemOutputValidator`, the item of `assistantContract`'s `conversationsList`. `conversationsListRoute.test.js` lists a saved conversation over HTTP through the real route, actions and SQL repositories.
6
7
  - Each conversation request authenticates once: the route's access check and the action's request context share one sign-in lookup instead of each asking again.
7
8
  - **Breaking.** Conversation list items no longer carry `metadata`: the conversations repository's `listForActorScope` reads only the fields a list shows, as `assistantContract`'s `conversationsList` now answers them. Upgrading: code that read `metadata` from a list item reads the conversation instead, with `findByIdForActorScope` or the conversation's messages read, which still answer it.
@@ -156,6 +156,10 @@ export default defineConfig({
156
156
  state.queuedGoal = input;
157
157
  return json(receipt);
158
158
  }
159
+ // The engine received the command, but the server could not confirm its goal.
160
+ if (receipt && mode === "goal-refused") {
161
+ return json({ error: "Claude did not confirm the goal in time.", code: "fixture_goal_unconfirmed" }, 503);
162
+ }
159
163
  changeGoal(state, input);
160
164
  const result = receipt || state.goal || null;
161
165
  return json(result, result === null ? 204 : 200);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "jskit test"
@@ -15,16 +15,16 @@
15
15
  "./locales/en.json": "./locales/en.json"
16
16
  },
17
17
  "dependencies": {
18
- "@jskit-ai/assistant-core": "0.2.43",
18
+ "@jskit-ai/assistant-core": "0.2.44",
19
19
  "@jskit-ai/resource-core": "0.1.211",
20
20
  "json-rest-schema": "^1.0.20"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "@jskit-ai/http-runtime": "0.2.20",
24
- "@jskit-ai/http-web": "0.2.31",
24
+ "@jskit-ai/http-web": "0.2.32",
25
25
  "@jskit-ai/kernel": "0.2.20",
26
- "@jskit-ai/realtime": "0.2.29",
27
- "@jskit-ai/shell-web": "0.2.26",
26
+ "@jskit-ai/realtime": "0.2.30",
27
+ "@jskit-ai/shell-web": "0.2.27",
28
28
  "@tanstack/vue-query": "^5.90.5",
29
29
  "vue": "^3.5.13",
30
30
  "vuetify": "^4.0.0",
@@ -504,6 +504,10 @@ function createConversationRuntime({ identity, api, socket, actorKey, placement,
504
504
  // Delivery keeps its exact authored payload until an explicit receipt check.
505
505
  if (prepare) return { ok: false, status: "uncertain", error: failureMessage(failure),
506
506
  ...(failure.code ? { code: failure.code } : {}) };
507
+ // A goal command's transcript turn can settle its message before the
508
+ // server answers; the server's refusal still says why the goal did not change.
509
+ if (payload.goalRequest) return { ok: false, error: failureMessage(failure),
510
+ ...(failure.code ? { code: failure.code } : {}) };
507
511
  return false;
508
512
  } finally {
509
513
  if (!pending.started && pendingMessages.get(messageId) === pending) pendingMessages.delete(messageId);
@@ -519,6 +519,46 @@ test("Cancel stays available while a Resume waits behind the running turn and en
519
519
  assert.deepEqual(commands.map(request => request.input.action), ["set", "pause", "resume", "cancel"]);
520
520
  });
521
521
 
522
+ test("a Resume the server refuses after its message reached the chat shows the server's reason", options, async t => {
523
+ const f = await fixture(t);
524
+ await f.command("goals", { id: "chat:1", budgets: false, commands: {
525
+ set: { delivery: "message", interruptsTurn: false }, resume: { delivery: "message", interruptsTurn: false },
526
+ pause: { delivery: "control", interruptsTurn: false }
527
+ } });
528
+ await f.notify("chat:1");
529
+ const control = name => f.primary.getByRole("button", { name, exact: true });
530
+ const menu = name => f.page.getByRole("button", { name, exact: true });
531
+ await control("Set goal").click();
532
+ await f.page.getByRole("textbox", { name: "Goal objective" }).fill("Refused after its message arrived");
533
+ await f.page.locator(".assistant-goal__form button[type=submit]").click();
534
+ await expect(control("Goal running")).toHaveAttribute("aria-expanded", "false");
535
+ await control("Goal running").click();
536
+ await menu("Pause goal").click();
537
+ await expect(control("Goal paused")).toBeVisible();
538
+ await f.command("finish", { id: "chat:1" });
539
+ await f.notify("chat:1");
540
+ // The server records the Resume's message, and its refusal arrives only after
541
+ // the chat has shown that message.
542
+ await f.command("mode", { mode: "goal-refused" });
543
+ const answer = Promise.withResolvers();
544
+ let resumes = 0;
545
+ await f.page.route("**/conversations/*/goal", async route => {
546
+ if (route.request().method() !== "POST") return route.continue();
547
+ resumes += 1;
548
+ const response = await route.fetch();
549
+ await answer.promise;
550
+ await route.fulfill({ response });
551
+ });
552
+ await menu("Resume goal").click();
553
+ await expect.poll(() => resumes).toBe(1);
554
+ await f.notify("chat:1");
555
+ await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().delivery.state.messages.length)).toBe(0);
556
+ answer.resolve();
557
+ await expect(f.page.getByRole("alert").filter({ hasText: "Claude did not confirm the goal in time." })).toBeVisible();
558
+ await expect(menu("Resume goal")).toBeEnabled();
559
+ assert.equal(resumes, 1);
560
+ });
561
+
522
562
  test("a supplied goal view keeps its pinned target and refreshes through the retained binding without polling", options, async t => {
523
563
  const f = await fixture(t);
524
564
  const goal = { id: "retained-goal", objective: "The plain product objective", status: "paused",