@jskit-ai/assistant-runtime 0.2.39 → 0.2.40

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,9 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - 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.
6
+ - **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.
7
+ - A chat turn ends by reading the turn it waited on (`handle.wait({ turnId })`) instead of reading and cloning every turn of the conversation to keep one.
5
8
  - The conversations and messages repositories hand their handle calls only the caller's transaction, with their own context and settings: a caller's other settings (a format, `missing`, `returning`, `fields`) no longer ride into every read and write they make. They take `passedOn` from `@jskit-ai/resource-core/shared/callOptions`, so `@jskit-ai/assistant-runtime/server` still loads on file storage, where `@jskit-ai/resource-runtime` (an optional peer) is not installed.
6
9
  - Its table resources declare the foreign keys their migrations always create (`foreignKeys`), so json-rest-api 3.1.0 can skip the visibility query a system-context read makes for a record such a key guarantees exists. `jskit test --databases` checks each against an app's test database: assistant_conversations.created_by_user_id and assistant_turn_requests.actor_user_id to users, and assistant_messages.conversation_id to assistant_conversations and actor_user_id to users. The workspace_id keys exist only where the application has a workspaces table, so none declares them. `usersForeignKeysMigration.test.js` proves the declarations match the migrations.
7
10
  - A database error in an assistant turn reaches the client only as the kernel's generic answer (`hideDatabaseError`: 409 `kernel.database.concurrentWrite` when the database refused the transaction for a concurrent one, else 500). A failure after the stream started was sent with its own message, which for a database error is the statement and its values, and the same message was saved with the turn and replayed, as an `AppError`, to a client that sent the message again; the chat route's error before the stream and a conversation subscription's acknowledgement carried the driver's code. Each now sends and saves the stand-in and logs the whole error; `createChatService` takes the runtime's `logger`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "jskit test"
@@ -15,21 +15,21 @@
15
15
  "./locales/en.json": "./locales/en.json"
16
16
  },
17
17
  "dependencies": {
18
- "@jskit-ai/assistant-core": "0.2.37",
18
+ "@jskit-ai/assistant-core": "0.2.38",
19
19
  "@jskit-ai/resource-core": "0.1.209",
20
20
  "json-rest-schema": "^1.0.20"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "@jskit-ai/http-runtime": "0.2.18",
24
- "@jskit-ai/http-web": "0.2.27",
24
+ "@jskit-ai/http-web": "0.2.28",
25
25
  "@jskit-ai/kernel": "0.2.18",
26
- "@jskit-ai/realtime": "0.2.25",
27
- "@jskit-ai/shell-web": "0.2.22",
26
+ "@jskit-ai/realtime": "0.2.26",
27
+ "@jskit-ai/shell-web": "0.2.23",
28
28
  "@tanstack/vue-query": "^5.90.5",
29
29
  "vue": "^3.5.13",
30
30
  "vuetify": "^4.0.0",
31
31
  "@jskit-ai/database-runtime": "0.1.267",
32
- "@jskit-ai/resource-runtime": "0.2.20"
32
+ "@jskit-ai/resource-runtime": "0.2.21"
33
33
  },
34
34
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
35
35
  "jskit": {
@@ -139,6 +139,6 @@
139
139
  },
140
140
  "devDependencies": {
141
141
  "@jskit-ai/material-web": "0.1.5",
142
- "@jskit-ai/resource-runtime": "0.2.20"
142
+ "@jskit-ai/resource-runtime": "0.2.21"
143
143
  }
144
144
  }
@@ -29,13 +29,20 @@ async function registerFastifyConversations(app, {
29
29
  !config || typeof config !== "object" || Array.isArray(config)) {
30
30
  throw new TypeError("Fastify conversations require runtime, config and authenticate(request).");
31
31
  }
32
- async function requestContext(request) {
32
+ async function authenticateRequest(request) {
33
33
  const context = await authenticate(request);
34
34
  if (!context || typeof context !== "object" || Array.isArray(context)) {
35
35
  throw new AppError(401, "Authentication required.", { code: "assistant.runtime.authenticationRequired" });
36
36
  }
37
37
  return context;
38
38
  }
39
+ // One authentication per request: the route's check and the action's context share its answer.
40
+ const authenticated = new WeakMap();
41
+ function requestContext(request) {
42
+ if (!request || typeof request !== "object") return authenticateRequest(request);
43
+ if (!authenticated.has(request)) authenticated.set(request, authenticateRequest(request));
44
+ return authenticated.get(request);
45
+ }
39
46
  const AccessProvider = defineProvider({
40
47
  id: "assistant.fastify.access",
41
48
  requires: {
@@ -20,6 +20,16 @@ function normalizeRequiredSurfaceId(value) {
20
20
  return normalizedSurfaceId;
21
21
  }
22
22
 
23
+ // What a list shows of a conversation: never its runtime metadata (the system prompt, every tool's schema and
24
+ // a note per message ever written), which grows with the conversation and only a read of it needs.
25
+ const LIST_FIELDS = Object.freeze(["workspaceId", "userId", "title", "status", "provider", "model", "surfaceId", "messageCount",
26
+ "startedAt", "endedAt", "createdAt", "updatedAt"]);
27
+
28
+ function mapConversationSummary(record = {}) {
29
+ const { metadata: _metadata, ...summary } = mapConversationRecord(record);
30
+ return summary;
31
+ }
32
+
23
33
  function mapConversationRecord(record = {}) {
24
34
  return {
25
35
  id: normalizeRecordId(record.id, { fallback: "" }),
@@ -260,10 +270,11 @@ function createRepository({ conversations, systemContext }) {
260
270
  ...(cursor ? { beforeId: cursor } : {})
261
271
  },
262
272
  sort: ["-id"],
273
+ fields: LIST_FIELDS,
263
274
  page: { size: limit }
264
275
  }, { ...passedOn(options), context: conversationContext.forUser(normalizedActorUserId) });
265
276
 
266
- const items = records.map(mapConversationRecord);
277
+ const items = records.map(mapConversationSummary);
267
278
  const nextCursor = page.hasMore && items.length > 0 ? items[items.length - 1].id : null;
268
279
 
269
280
  return {
@@ -401,7 +401,8 @@ function createChatService({
401
401
  const receipt = await handle.send({ messageId: source.messageId, text: source.input,
402
402
  ...(source.attachmentIds !== undefined ? { attachmentIds: source.attachmentIds } : {}) });
403
403
  turnId = receipt.turnId;
404
- const snapshot = await handle.wait();
404
+ // The finished turn alone: waiting never reads the rest of the conversation.
405
+ const snapshot = await handle.wait({ turnId });
405
406
  const turn = snapshot.conversationLog.find(entry => entry.turnId === turnId);
406
407
  const status = turn?.metadata?.runtime?.status;
407
408
  if (status !== "complete") {
@@ -138,6 +138,8 @@ test("Fastify conversations await trusted authentication across HTTP and shared
138
138
  assert.equal(context.applicationId, trusted.applicationId);
139
139
  }
140
140
  assert.ok(authenticated.some(request => request.method === "POST"));
141
+ assert.equal(authenticated.filter(request => request.method === "POST").length, ids.length,
142
+ "each send authenticates once: the route's check and the action's context share it");
141
143
  assert.ok(authenticated.some(request => request.method === undefined), "socket requests also use the async authenticator");
142
144
 
143
145
  sessions.delete("owner-session");
@@ -219,7 +219,8 @@ test("listForActorScope pages newest first by id, within one actor, surface and
219
219
  assert.deepEqual(Object.keys(all).sort(), ["items", "nextCursor"]);
220
220
  assert.deepEqual(all.items.map((item) => item.id), [archived.id, mine[2].id, mine[1].id, mine[0].id], "newest id first, global scope only");
221
221
  assert.equal(all.nextCursor, null);
222
- assert.deepEqual(all.items[1], mine[2], "items are whole conversation records");
222
+ const { metadata: _metadata, ...summary } = mine[2];
223
+ assert.deepEqual(all.items[1], summary, "items are conversation records without their runtime metadata, which only a read needs");
223
224
 
224
225
  const page = await conversations.listForActorScope({ ...scope, pagination: { limit: 2 } });
225
226
  assert.deepEqual(page.items.map((item) => item.id), [archived.id, mine[2].id]);