@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 +3 -0
- package/package.json +7 -7
- package/src/server/registerFastifyConversations.js +8 -1
- package/src/server/repositories/conversationsRepository.js +12 -1
- package/src/server/services/chatService.js +2 -1
- package/test/registerFastifyConversations.test.js +2 -0
- package/test/repositories.mysql.test.js +2 -1
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.
|
|
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.
|
|
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.
|
|
24
|
+
"@jskit-ai/http-web": "0.2.28",
|
|
25
25
|
"@jskit-ai/kernel": "0.2.18",
|
|
26
|
-
"@jskit-ai/realtime": "0.2.
|
|
27
|
-
"@jskit-ai/shell-web": "0.2.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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]);
|