@jskit-ai/assistant-runtime 0.1.234 → 0.1.236
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/README.md +36 -0
- package/fixtures/conversation/vite.config.mjs +2 -2
- package/package.json +9 -10
- package/src/client/components/AssistantSettingsClientElement.vue +0 -3
- package/src/client/components/AssistantSurfaceClientElement.vue +2 -5
- package/src/client/composables/useAssistantConversation.js +57 -55
- package/src/client/composables/useAssistantRuntime.js +14 -29
- package/src/client/support/accessDenied.js +6 -0
- package/src/client/support/subscribeAssistantConversation.js +4 -5
- package/src/server/actions.js +3 -36
- package/src/server/createAssistantSettingsActions.js +0 -8
- package/src/server/inputSchemas.js +3 -8
- package/src/server/registerConversationSubscriptions.js +13 -15
- package/src/server/registerRoutes.js +15 -39
- package/src/server/repositories/conversationsRepository.js +9 -47
- package/src/server/repositories/messagesRepository.js +5 -22
- package/src/server/repositories/repositoryPersistenceUtils.js +34 -1
- package/src/server/services/assistantConfigService.js +4 -17
- package/src/server/services/chatService.js +14 -37
- package/src/server/services/transcriptService.js +1 -54
- package/src/server/support/assistantErrors.js +13 -0
- package/src/server/support/assistantRouteContext.js +1 -10
- package/src/server/support/assistantStreamWriter.js +12 -0
- package/src/server/support/assistantSurface.js +13 -0
- package/src/server/support/createSurfaceAwareToolCatalog.js +5 -6
- package/src/shared/assistantRuntimeConfig.js +3 -1
- package/src/shared/assistantSurfaces.js +18 -0
- package/test/actionDefinitions.test.js +0 -1
- package/test/assistantSurfaces.test.js +29 -0
- package/test/chatServiceLifecycle.test.js +12 -3
- package/test/turnRequests.test.js +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
# @jskit-ai/assistant-runtime
|
|
2
|
+
|
|
3
|
+
Hosts JSKIT's common conversation runtime (from `@jskit-ai/assistant-core`)
|
|
4
|
+
through authorized HTTP operations, shared realtime subscriptions and Vue/Vuetify
|
|
5
|
+
conversation elements. Package discovery registers `AssistantFeature` once on a
|
|
6
|
+
JSKIT host; plain Fastify hosts use `registerFastifyConversations`.
|
|
7
|
+
|
|
8
|
+
Two hosting modes share that runtime:
|
|
9
|
+
|
|
10
|
+
- **Application-supplied runtime.** The host supplies its own
|
|
11
|
+
`createConversationRuntime` (file or application storage) as the
|
|
12
|
+
`assistant.conversations` capability. No database is required.
|
|
13
|
+
- **Built-in database-backed surface.** The optional SQL integration adds
|
|
14
|
+
database conversations, per-surface settings and the request-streaming chat
|
|
15
|
+
route. It needs `npm run db:migrate` and is composed by the
|
|
16
|
+
`assistant/assistant-surface` pattern.
|
|
17
|
+
|
|
18
|
+
Main exports by subpath:
|
|
19
|
+
|
|
20
|
+
| Subpath | Provides |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `@jskit-ai/assistant-runtime/server` | `AssistantFeature`, `createAssistantActions`, `registerAssistantRoutes`, `registerConversationSubscriptions`, `registerFastifyConversations` |
|
|
23
|
+
| `@jskit-ai/assistant-runtime/server/actionIds` | Assistant action identifiers |
|
|
24
|
+
| `@jskit-ai/assistant-runtime/client` | `AssistantConversationClientElement`, `AssistantSurfaceClientElement`, `AssistantSettingsClientElement`, `useAssistantConversation`, `configureAssistantConversations` |
|
|
25
|
+
| `@jskit-ai/assistant-runtime/shared` | Assistant surface configuration helpers |
|
|
26
|
+
|
|
27
|
+
The full guides are in the JSKIT agent docs:
|
|
28
|
+
`packages/agent-docs/site/guide/app-extras/assistant.md` (setup and hosting
|
|
29
|
+
modes) and `packages/agent-docs/site/guide/app-extras/assistant-conversation.md`
|
|
30
|
+
(client contract and advanced host facilities).
|
|
31
|
+
|
|
32
|
+
## Subscription diagnostics
|
|
1
33
|
|
|
2
34
|
With the existing realtime diagnostic option (`JSKIT_REALTIME_DEBUG=1`, or
|
|
3
35
|
`config.realtime.debug=true`), conversation subscriptions emit one `pending`
|
|
@@ -12,6 +44,8 @@ Timers are removed on acknowledgement, failure, replacement, unsubscribe
|
|
|
12
44
|
and connection close. Diagnostics contain no request body or conversation text,
|
|
13
45
|
and do not change authorization, subscription deadlines or model work.
|
|
14
46
|
|
|
47
|
+
## Subscription release
|
|
48
|
+
|
|
15
49
|
Released subscriptions stop setup at the next completed phase. The existing
|
|
16
50
|
socket owner supplies its current subscription identity to the subscribe action;
|
|
17
51
|
an obsolete open cannot attach a new observer, and a late observer is detached
|
|
@@ -19,6 +53,8 @@ without starting a snapshot. Reads already in progress may finish, but cannot
|
|
|
19
53
|
acknowledge into a replacement view. Reload performs fresh authorization, and
|
|
20
54
|
releasing a view never stops its model turn.
|
|
21
55
|
|
|
56
|
+
## Queued prepared submissions
|
|
57
|
+
|
|
22
58
|
Prepared application submissions may explicitly enable `queueWhileSending` to
|
|
23
59
|
reuse the binding's serial delivery queue while application preparation is still
|
|
24
60
|
running. Each queued request retains its captured payload and receipt identity.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createMemoryTurnRequests } from "../../test/support/memoryTurnRequests.js";
|
|
2
2
|
import { createChatService } from "../../src/server/services/chatService.js";
|
|
3
|
+
import { ASSISTANT_STREAM_WRITER_METHODS } from "../../src/server/support/assistantStreamWriter.js";
|
|
3
4
|
import { createAssistantConversationRuntime } from "../../src/server/createAssistantRuntime.js";
|
|
4
5
|
import { createMemoryAssistantRows } from "../../test/support/memoryAssistantRows.js";
|
|
5
6
|
import { defineConfig } from "vite";
|
|
@@ -81,7 +82,6 @@ export default defineConfig({
|
|
|
81
82
|
} },
|
|
82
83
|
transcriptService: {
|
|
83
84
|
async createConversationForTurn() { return { conversation: { id: "100" } }; },
|
|
84
|
-
async appendMessage(_surface, _id, message) { transcript.push(message); },
|
|
85
85
|
async completeConversation() {}
|
|
86
86
|
},
|
|
87
87
|
serviceToolCatalog: { limits: { maxToolArgumentBytes: 64 * 1024 }, resolveToolSet: () => ({ tools: [] }),
|
|
@@ -96,7 +96,7 @@ export default defineConfig({
|
|
|
96
96
|
attachments: dependencies.attachments
|
|
97
97
|
});
|
|
98
98
|
const service = createChatService({ ...dependencies, conversationRuntime });
|
|
99
|
-
const streamWriter = Object.fromEntries(
|
|
99
|
+
const streamWriter = Object.fromEntries(ASSISTANT_STREAM_WRITER_METHODS.map(method => [method, event => send(stream, event)]));
|
|
100
100
|
try { await service.streamChat({ ...payload, targetSurfaceId: "admin" }, { streamWriter, abortSignal: controller.signal, context: { actor: { id: "1" } } }); }
|
|
101
101
|
catch (error) { if (!res.destroyed) send(stream, { type: "error", message: error.message }); }
|
|
102
102
|
finally { res.end(); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.236",
|
|
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.
|
|
15
|
+
"@jskit-ai/assistant-core": "0.1.242",
|
|
16
16
|
"json-rest-schema": "^1.0.17"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
20
|
-
"@jskit-ai/http-web": "0.1.
|
|
21
|
-
"@jskit-ai/kernel": "0.1.
|
|
22
|
-
"@jskit-ai/realtime": "0.1.
|
|
23
|
-
"@jskit-ai/shell-web": "0.1.
|
|
19
|
+
"@jskit-ai/http-runtime": "0.1.244",
|
|
20
|
+
"@jskit-ai/http-web": "0.1.94",
|
|
21
|
+
"@jskit-ai/kernel": "0.1.246",
|
|
22
|
+
"@jskit-ai/realtime": "0.1.246",
|
|
23
|
+
"@jskit-ai/shell-web": "0.1.253",
|
|
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.
|
|
27
|
+
"@jskit-ai/database-runtime": "0.1.246"
|
|
28
28
|
},
|
|
29
29
|
"description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
|
|
30
30
|
"jskit": {
|
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
"runtime.config",
|
|
39
39
|
"runtime.env",
|
|
40
40
|
"runtime.http",
|
|
41
|
-
"runtime.http-web"
|
|
42
|
-
"runtime.http-client"
|
|
41
|
+
"runtime.http-web"
|
|
43
42
|
]
|
|
44
43
|
},
|
|
45
44
|
"runtime": {
|
|
@@ -180,9 +180,6 @@ async function submit() {
|
|
|
180
180
|
});
|
|
181
181
|
if (!validation.ok) {
|
|
182
182
|
fieldErrors.systemPrompt = String(validation.fieldErrors.systemPrompt || "");
|
|
183
|
-
if (validation.globalErrors.length > 0) {
|
|
184
|
-
reportAssistantSettingsError(validation.globalErrors[0], "assistant.settings:validation");
|
|
185
|
-
}
|
|
186
183
|
return;
|
|
187
184
|
}
|
|
188
185
|
|
|
@@ -98,11 +98,8 @@ function submitMessage({ attachments = [], retryMessageId = "" }) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function conversationSubtitle(entry) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
details.push(entry.createdByUserDisplayName || entry.createdByUserEmail || (entry.createdByUserId ? `User #${entry.createdByUserId}` : "Unknown user"));
|
|
104
|
-
}
|
|
105
|
-
return details.join(" · ");
|
|
101
|
+
return [runtime.meta.normalizeConversationStatus(entry.status), runtime.meta.formatConversationStartedAt(entry.startedAt),
|
|
102
|
+
`${Number(entry.messageCount || 0)} messages`].join(" · ");
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
async function selectConversation(entry) {
|
|
@@ -4,15 +4,16 @@ import { createAssistantMessageDelivery, retainAssistantConversation, unmatchedO
|
|
|
4
4
|
import { createAssistantTextSubmission } from "@jskit-ai/assistant-core/client/conversation";
|
|
5
5
|
import { useAssistantQuestions } from "@jskit-ai/assistant-core/client/conversation-questions";
|
|
6
6
|
import { assistantHttpClient, createAssistantApi } from "@jskit-ai/assistant-core/client";
|
|
7
|
-
import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
|
|
8
7
|
import {
|
|
9
|
-
conversationTurnsFromMessages, latestAssistantMessageAwaitingUserReply, mergeConversationLogPages, mergeConversationStream,
|
|
8
|
+
conversationTurnMessages, conversationTurnsFromMessages, latestAssistantMessageAwaitingUserReply, mergeConversationLogPages, mergeConversationStream,
|
|
10
9
|
normalizeConversationLogPage, normalizeConversationLogPagination
|
|
11
10
|
} from "@jskit-ai/assistant-core/shared/conversation";
|
|
12
11
|
import { useRealtimeSocket } from "@jskit-ai/realtime/client/composables/useRealtimeEvent";
|
|
12
|
+
import { SHELL_WEB_PLACEMENT_RUNTIME_KEY } from "@jskit-ai/shell-web/client/placement";
|
|
13
13
|
import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
|
|
14
|
-
import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
14
|
+
import { resolveAssistantRuntimeRoute, resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
15
15
|
import { useWorkspaceWebScopeSupport } from "../support/workspaceScopeSupport.js";
|
|
16
|
+
import { isAccessDenied } from "../support/accessDenied.js";
|
|
16
17
|
import { subscribeAssistantConversation, refreshedHistoryPages, patchLoadedHistoryPages } from "../support/subscribeAssistantConversation.js";
|
|
17
18
|
|
|
18
19
|
const text = value => String(toValue(value) ?? "").trim();
|
|
@@ -40,6 +41,20 @@ function isQuestionConfiguration(value) {
|
|
|
40
41
|
!["questions", "choices", "capture"].some(key => key in value));
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function questionsAnswered(prompts) {
|
|
45
|
+
return prompts.questions.every(question => String(prompts.answers[question.name] || "").trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The authored conversation request for a composer payload without one. */
|
|
49
|
+
function authoredRequest(payload, { attachmentIds, steer }) {
|
|
50
|
+
return {
|
|
51
|
+
text: String(payload.message || ""),
|
|
52
|
+
...(payload.data !== undefined ? { data: payload.data } : {}),
|
|
53
|
+
...(attachmentIds?.length ? { attachmentIds } : {}),
|
|
54
|
+
...(steer ? { steer: true } : {})
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
function draftAfterAcceptedSubmission(currentDraft = "", submittedDraft = "") {
|
|
44
59
|
const current = String(currentDraft || "");
|
|
45
60
|
const submitted = String(submittedDraft || "");
|
|
@@ -103,9 +118,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
103
118
|
]).conversationLog;
|
|
104
119
|
// Use the original overlay against the full loaded saved span. A synthetic
|
|
105
120
|
// live older turn must not replace its authored request or saved progress.
|
|
106
|
-
const messages = (snapshot.value?.turns || []).flatMap(
|
|
107
|
-
turn.system, turn.user, ...(turn.thinking || []), ...(turn.commentary || []), turn.assistant
|
|
108
|
-
].filter(Boolean));
|
|
121
|
+
const messages = (snapshot.value?.turns || []).flatMap(conversationTurnMessages);
|
|
109
122
|
return mergeConversationStream(saved, { messages });
|
|
110
123
|
});
|
|
111
124
|
const oldestLoadedPage = computed(() => olderPages.value[0] || snapshot.value);
|
|
@@ -231,24 +244,29 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
231
244
|
if (!current.value) return;
|
|
232
245
|
loading.value = false;
|
|
233
246
|
error.value = failure.message;
|
|
234
|
-
accessDenied.value =
|
|
247
|
+
accessDenied.value = isAccessDenied(failure);
|
|
235
248
|
if (accessDenied.value) {
|
|
236
249
|
deniedHistoryRevision += 1;
|
|
237
|
-
|
|
238
|
-
historyRevision += 1;
|
|
239
|
-
olderPages.value = [];
|
|
240
|
-
draft.value = "";
|
|
241
|
-
draftMessageId.value = "";
|
|
242
|
-
draftAttachments.value = [];
|
|
243
|
-
draftRetry.value = null;
|
|
244
|
-
delivery.reset();
|
|
245
|
-
goalError.value = "";
|
|
246
|
-
goalView.value = null;
|
|
250
|
+
clearConversationView();
|
|
247
251
|
goalTargetCurrent.value = false;
|
|
248
|
-
goalLoadError.value = "";
|
|
249
|
-
goalReadController?.abort();
|
|
250
252
|
}
|
|
251
253
|
}
|
|
254
|
+
// Shared by access denial and actor change. Access denial keeps the open goal
|
|
255
|
+
// command, queued goal read and question form; an actor change clears them.
|
|
256
|
+
function clearConversationView() {
|
|
257
|
+
delivery.reset();
|
|
258
|
+
snapshot.value = null;
|
|
259
|
+
historyRevision += 1;
|
|
260
|
+
olderPages.value = [];
|
|
261
|
+
draft.value = "";
|
|
262
|
+
draftMessageId.value = "";
|
|
263
|
+
draftAttachments.value = [];
|
|
264
|
+
draftRetry.value = null;
|
|
265
|
+
goalError.value = "";
|
|
266
|
+
goalView.value = null;
|
|
267
|
+
goalLoadError.value = "";
|
|
268
|
+
goalReadController?.abort();
|
|
269
|
+
}
|
|
252
270
|
function receiveEvent(event) {
|
|
253
271
|
if (event.type === "transcript" && olderPages.value.some(page =>
|
|
254
272
|
page.conversationLog.some(turn => turn.turnId === event.patch?.turn?.turnId))) {
|
|
@@ -330,20 +348,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
330
348
|
watch(current, value => {
|
|
331
349
|
if (value) return;
|
|
332
350
|
for (const pending of pendingMessages.values()) if (!pending.dispatched) pending.controller.abort();
|
|
333
|
-
|
|
334
|
-
snapshot.value = null;
|
|
335
|
-
historyRevision += 1;
|
|
336
|
-
olderPages.value = [];
|
|
337
|
-
draft.value = "";
|
|
338
|
-
draftMessageId.value = "";
|
|
339
|
-
draftAttachments.value = [];
|
|
340
|
-
draftRetry.value = null;
|
|
341
|
-
goalError.value = "";
|
|
351
|
+
clearConversationView();
|
|
342
352
|
goalAction.value = "";
|
|
343
|
-
goalView.value = null;
|
|
344
|
-
goalLoadError.value = "";
|
|
345
353
|
goalReadQueued = false;
|
|
346
|
-
goalReadController?.abort();
|
|
347
354
|
questions.reset();
|
|
348
355
|
}, { flush: "sync" });
|
|
349
356
|
onScopeDispose(() => {
|
|
@@ -374,12 +381,11 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
374
381
|
const appAdmission = policy.admitWhileWorking && !steering;
|
|
375
382
|
if ((snapshot.value.status === "working" || delivery.state.sending) && !steering && !deferred && !appAdmission) return false;
|
|
376
383
|
if ((retry?.payload || payload).displayAttachments?.length && snapshot.value.capabilities?.attachments !== true) return false;
|
|
377
|
-
const captured = retry?.payload?.request ? retry.payload : { ...(retry?.payload || payload), request: payload.request ||
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
} };
|
|
384
|
+
const captured = retry?.payload?.request ? retry.payload : { ...(retry?.payload || payload), request: payload.request ||
|
|
385
|
+
authoredRequest(payload, {
|
|
386
|
+
attachmentIds: payload.displayAttachments?.map(file => file.attachmentId),
|
|
387
|
+
steer: steerable.value
|
|
388
|
+
}) };
|
|
383
389
|
return submitRequest(captured, { messageId, onAccepted, queue: queueWhileSending !== false && (steering || deferred || appAdmission), deferred, appAdmission });
|
|
384
390
|
}
|
|
385
391
|
function submitPrepared(payload, { messageId = crypto.randomUUID(), onAccepted, prepare } = {}) {
|
|
@@ -583,7 +589,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
583
589
|
} catch (failure) {
|
|
584
590
|
if (current.value) {
|
|
585
591
|
loadMoreError.value = String(failure?.message || failure || "Older conversation history could not be loaded.");
|
|
586
|
-
if (
|
|
592
|
+
if (isAccessDenied(failure)) receiveError(failure);
|
|
587
593
|
}
|
|
588
594
|
finish({ changed: false, loaded: false });
|
|
589
595
|
return false;
|
|
@@ -665,7 +671,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
665
671
|
} catch (failure) {
|
|
666
672
|
if (current.value) {
|
|
667
673
|
goalError.value = failure.message;
|
|
668
|
-
if (
|
|
674
|
+
if (isAccessDenied(failure)) receiveError(failure);
|
|
669
675
|
}
|
|
670
676
|
return false;
|
|
671
677
|
} finally {
|
|
@@ -804,7 +810,7 @@ function conversationBindingSetup({ socket, boundedTask = null } = {}) {
|
|
|
804
810
|
if (!app) throw new Error("Conversation binding must be acquired from the application setup.");
|
|
805
811
|
if (boundedTask !== null) return { app };
|
|
806
812
|
const routeContext = useSurfaceRouteContext();
|
|
807
|
-
const placement = inject(
|
|
813
|
+
const placement = inject(SHELL_WEB_PLACEMENT_RUNTIME_KEY, null);
|
|
808
814
|
if (typeof placement?.getContext !== "function" || typeof placement?.subscribe !== "function") {
|
|
809
815
|
throw new Error("Conversation binding requires the application's placement runtime.");
|
|
810
816
|
}
|
|
@@ -856,15 +862,13 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
856
862
|
const host = (text(hostSurfaceId) || text(routeContext.currentSurfaceId)).toLowerCase();
|
|
857
863
|
const target = (text(surfaceId) || host).toLowerCase();
|
|
858
864
|
const surface = resolveAssistantSurfaceConfig(config, target);
|
|
859
|
-
const
|
|
860
|
-
?
|
|
861
|
-
: text(workspaceSlug);
|
|
865
|
+
const route = resolveAssistantRuntimeRoute(surface, toValue(workspaceSlug) === undefined
|
|
866
|
+
? workspaceScope.readRouteScope(routeContext).workspaceSlug : text(workspaceSlug));
|
|
862
867
|
const actor = toValue(actorKey);
|
|
863
868
|
return Object.freeze({ conversationId: text(conversationId), targetSurfaceId: target, hostSurfaceId: host || target,
|
|
864
|
-
workspaceSlug:
|
|
869
|
+
workspaceSlug: route.workspaceSlug, configured: route.configured,
|
|
865
870
|
actorKey: actor === undefined ? String(routeContext.placementContext.value?.user?.id || "") : text(actor),
|
|
866
|
-
endpoint: text(endpoint) ||
|
|
867
|
-
suffix: `/${target}` }) });
|
|
871
|
+
endpoint: text(endpoint) || route.endpoint });
|
|
868
872
|
});
|
|
869
873
|
// The original composer owns input before a conversation ID is available.
|
|
870
874
|
// It is scoped to the real actor/surface/workspace, never a placeholder ID.
|
|
@@ -922,7 +926,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
922
926
|
const prompts = questionOwner.value;
|
|
923
927
|
const formatsQuestions = typeof prompts?.capture === "function";
|
|
924
928
|
if (!retry && formatsQuestions && prompts.questions?.length &&
|
|
925
|
-
!
|
|
929
|
+
!questionsAnswered(prompts)) return false;
|
|
926
930
|
const answer = !retry && formatsQuestions ? prompts.capture(current.draft.value) : null;
|
|
927
931
|
const captured = retry?.payload || { message: answer?.message ?? current.draft.value, displayAttachments: files,
|
|
928
932
|
...(waitForAcceptance ? { draft: current.draft.value } : {}),
|
|
@@ -948,13 +952,11 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
948
952
|
return false;
|
|
949
953
|
}
|
|
950
954
|
const captured = previous?.payload?.request ? previous.payload : payload;
|
|
951
|
-
const authored = captured.request ? captured : { ...captured, request: {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
...((previous ? captured.submissionKind === "steer" : working) ? { steer: true } : {})
|
|
957
|
-
} };
|
|
955
|
+
const authored = captured.request ? captured : { ...captured, request: authoredRequest(captured, {
|
|
956
|
+
attachmentIds: captured.attachmentIds?.length ? captured.attachmentIds
|
|
957
|
+
: captured.displayAttachments?.map(file => file.attachmentId),
|
|
958
|
+
steer: previous ? captured.submissionKind === "steer" : working
|
|
959
|
+
}) };
|
|
958
960
|
return submitCaptured(current, authored, { messageId: id, retry: previous, clearDraft,
|
|
959
961
|
attachments, isCurrent, onSubmit, onAccepted, prepare });
|
|
960
962
|
}
|
|
@@ -1016,7 +1018,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
1016
1018
|
const editable = canDraftWhileLoading.value ? !current || current.editable.value : current?.available.value;
|
|
1017
1019
|
const message = formatsQuestions ? prompts.capture(draft).message : draft;
|
|
1018
1020
|
const answered = !formatsQuestions || !prompts.questions?.length ||
|
|
1019
|
-
|
|
1021
|
+
questionsAnswered(prompts);
|
|
1020
1022
|
const canSend = current?.canSubmit.value || false;
|
|
1021
1023
|
return {
|
|
1022
1024
|
delivery: current?.delivery,
|
|
@@ -3,7 +3,6 @@ import { useQueryClient } from "@tanstack/vue-query";
|
|
|
3
3
|
import { getClientAppConfig } from "@jskit-ai/kernel/client";
|
|
4
4
|
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
5
|
import { createAssistantMessageDelivery } from "@jskit-ai/assistant-core/client/conversation-delivery";
|
|
6
|
-
import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
|
|
7
6
|
import {
|
|
8
7
|
ASSISTANT_STREAM_EVENT_TYPES,
|
|
9
8
|
MAX_INPUT_CHARS,
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
22
21
|
import { usePagedCollection } from "@jskit-ai/http-web/client/composables/usePagedCollection";
|
|
23
22
|
import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
|
|
24
|
-
import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
23
|
+
import { resolveAssistantRuntimeRoute, resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
25
24
|
import {
|
|
26
25
|
buildHistory,
|
|
27
26
|
buildId,
|
|
@@ -162,35 +161,22 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
162
161
|
resolveAssistantSurfaceConfig(appConfig, toValue(surfaceId))
|
|
163
162
|
);
|
|
164
163
|
const routeScope = computed(() => workspaceScopeSupport.readRouteScope(routeContext));
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
workspaceSlug
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
const hasRuntimeScope = computed(() =>
|
|
177
|
-
Boolean(assistantSurface.value && runtimeScope.value.userId) &&
|
|
178
|
-
(assistantSurface.value?.runtimeSurfaceRequiresWorkspace ? Boolean(runtimeScope.value.workspaceSlug) : true)
|
|
179
|
-
);
|
|
164
|
+
const runtimeRoute = computed(() => resolveAssistantRuntimeRoute(assistantSurface.value,
|
|
165
|
+
normalizeText(routeScope.value.workspaceSlug).toLowerCase()));
|
|
166
|
+
const runtimeScope = computed(() => ({
|
|
167
|
+
targetSurfaceId: normalizeText(assistantSurface.value?.targetSurfaceId).toLowerCase(),
|
|
168
|
+
userId: normalizeRecordId(placementSnapshot.value?.user?.id, { fallback: null }),
|
|
169
|
+
workspaceSlug: runtimeRoute.value.workspaceSlug
|
|
170
|
+
}));
|
|
171
|
+
const hasRuntimeScope = computed(() => runtimeRoute.value.configured && Boolean(runtimeScope.value.userId));
|
|
180
172
|
|
|
181
173
|
const runtimeApi = createRuntimeApi({
|
|
182
174
|
overrideApi: api,
|
|
183
|
-
resolveBasePath: () =>
|
|
184
|
-
buildAssistantApiPath({
|
|
185
|
-
requiresWorkspace: assistantSurface.value?.runtimeSurfaceRequiresWorkspace === true,
|
|
186
|
-
workspaceSlug: runtimeScope.value.workspaceSlug,
|
|
187
|
-
suffix: `/${runtimeScope.value.targetSurfaceId}`
|
|
188
|
-
}),
|
|
175
|
+
resolveBasePath: () => runtimeRoute.value.endpoint,
|
|
189
176
|
resolveSurfaceId: () => normalizeText(currentSurfaceId.value).toLowerCase()
|
|
190
177
|
});
|
|
191
178
|
|
|
192
179
|
const activeConversationId = computed(() => normalizeRecordId(conversationId.value, { fallback: "" }));
|
|
193
|
-
const isAdminSurface = computed(() => normalizeText(currentSurfaceId.value).toLowerCase() === "admin");
|
|
194
180
|
const canSend = computed(() => {
|
|
195
181
|
return Boolean(
|
|
196
182
|
hasRuntimeScope.value &&
|
|
@@ -324,13 +310,14 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
324
310
|
return messages.value.find((entry) => entry.id === messageId) || null;
|
|
325
311
|
}
|
|
326
312
|
|
|
327
|
-
|
|
313
|
+
// Invalidation refetches the active conversation list; a turn never changes the scope's settings.
|
|
314
|
+
async function invalidateConversations() {
|
|
328
315
|
if (!hasRuntimeScope.value) {
|
|
329
316
|
return;
|
|
330
317
|
}
|
|
331
318
|
|
|
332
319
|
await queryClient.invalidateQueries({
|
|
333
|
-
queryKey: assistantScopeQueryKey(runtimeScope.value)
|
|
320
|
+
queryKey: [...assistantScopeQueryKey(runtimeScope.value), "conversations"]
|
|
334
321
|
});
|
|
335
322
|
}
|
|
336
323
|
|
|
@@ -634,8 +621,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
634
621
|
isStreaming.value = false;
|
|
635
622
|
isCanceling.value = false;
|
|
636
623
|
if (accepted) {
|
|
637
|
-
await
|
|
638
|
-
await refreshConversationHistory();
|
|
624
|
+
await invalidateConversations();
|
|
639
625
|
}
|
|
640
626
|
}
|
|
641
627
|
}
|
|
@@ -673,7 +659,6 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
673
659
|
conversationHistoryLoadingMore,
|
|
674
660
|
conversationHistoryHasMore,
|
|
675
661
|
conversationHistoryError,
|
|
676
|
-
isAdminSurface,
|
|
677
662
|
canSend,
|
|
678
663
|
canStartNewConversation
|
|
679
664
|
},
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { applyConversationLogPatch, mergeConversationStream } from "@jskit-ai/assistant-core/shared/conversation";
|
|
1
|
+
import { applyConversationLogPatch, conversationTurnMessages, mergeConversationStream } from "@jskit-ai/assistant-core/shared/conversation";
|
|
2
2
|
import {
|
|
3
3
|
ASSISTANT_CONVERSATION_EVENT,
|
|
4
4
|
ASSISTANT_CONVERSATION_SUBSCRIBE,
|
|
5
5
|
ASSISTANT_CONVERSATION_UNSUBSCRIBE
|
|
6
6
|
} from "../../shared/conversationRealtime.js";
|
|
7
|
+
import { isAccessDenied } from "./accessDenied.js";
|
|
7
8
|
|
|
8
9
|
// Private to the original client history/subscription coordination. Symbols
|
|
9
10
|
// survive local snapshot spreads but never enter the conversation wire format.
|
|
@@ -44,9 +45,7 @@ function subscribeAssistantConversation({
|
|
|
44
45
|
const pendingReads = new Set();
|
|
45
46
|
|
|
46
47
|
function savedMessageIds() {
|
|
47
|
-
return new Set((state?.conversationLog || []).flatMap(
|
|
48
|
-
turn.system, turn.user, ...(turn.thinking || []), ...(turn.commentary || []), turn.assistant
|
|
49
|
-
].filter(Boolean)).map(message => message.messageId));
|
|
48
|
+
return new Set((state?.conversationLog || []).flatMap(conversationTurnMessages).map(message => message.messageId));
|
|
50
49
|
}
|
|
51
50
|
function receiveStreaming(snapshot) {
|
|
52
51
|
if (!Number.isSafeInteger(snapshot?.revision) || (streaming && snapshot.revision <= streaming.revision)) return false;
|
|
@@ -97,7 +96,7 @@ function subscribeAssistantConversation({
|
|
|
97
96
|
receiveState(snapshot);
|
|
98
97
|
}).catch(error => {
|
|
99
98
|
if (current()) {
|
|
100
|
-
if (
|
|
99
|
+
if (isAccessDenied(error)) {
|
|
101
100
|
if (epoch === null) {
|
|
102
101
|
disconnected();
|
|
103
102
|
if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
|
package/src/server/actions.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@jskit-ai/assistant-core/shared";
|
|
13
13
|
import { actionIds } from "./actionIds.js";
|
|
14
14
|
import { createAssistantSettingsActions } from "./createAssistantSettingsActions.js";
|
|
15
|
+
import { requireAssistantSurface } from "./support/assistantSurface.js";
|
|
15
16
|
import {
|
|
16
17
|
assistantConversationIdInputValidator,
|
|
17
18
|
assistantConversationGoalBodyValidator,
|
|
@@ -109,8 +110,7 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
109
110
|
{ mode: "patch", context: "assistant-runtime conversation send action input" }
|
|
110
111
|
);
|
|
111
112
|
async function openConversation(input, context) {
|
|
112
|
-
const assistantSurface =
|
|
113
|
-
if (!assistantSurface) throw new AppError(404, "Assistant not found.");
|
|
113
|
+
const assistantSurface = requireAssistantSurface(config, input.targetSurfaceId);
|
|
114
114
|
let conversationContext = context;
|
|
115
115
|
if (assistantSurface.runtimeSurfaceRequiresWorkspace) {
|
|
116
116
|
if (typeof workspaceScopeSupport?.resolveWorkspace !== "function") {
|
|
@@ -134,8 +134,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
134
134
|
input: conversationReadInputValidator,
|
|
135
135
|
output: null,
|
|
136
136
|
idempotency: "none",
|
|
137
|
-
audit: { actionName: actionIds.conversationRead },
|
|
138
|
-
observability: {},
|
|
139
137
|
async execute(input, context) {
|
|
140
138
|
const conversation = await openConversation(input, context);
|
|
141
139
|
const options = {};
|
|
@@ -155,8 +153,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
155
153
|
input: conversationSendInputValidator,
|
|
156
154
|
output: null,
|
|
157
155
|
idempotency: "none",
|
|
158
|
-
audit: { actionName: actionIds.conversationSend },
|
|
159
|
-
observability: {},
|
|
160
156
|
async execute(input, context) {
|
|
161
157
|
const conversation = await openConversation(input, context);
|
|
162
158
|
return conversation.send({
|
|
@@ -178,8 +174,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
178
174
|
input: conversationInputValidator,
|
|
179
175
|
output: null,
|
|
180
176
|
idempotency: "none",
|
|
181
|
-
audit: { actionName: actionIds.conversationCancel },
|
|
182
|
-
observability: {},
|
|
183
177
|
async execute(input, context) {
|
|
184
178
|
const conversation = await openConversation(input, context);
|
|
185
179
|
return conversation.cancel();
|
|
@@ -195,8 +189,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
195
189
|
input: conversationInputValidator,
|
|
196
190
|
output: null,
|
|
197
191
|
idempotency: "none",
|
|
198
|
-
audit: { actionName: actionIds.conversationSubscribe },
|
|
199
|
-
observability: {},
|
|
200
192
|
async execute(input, context, deps) {
|
|
201
193
|
if (typeof deps?.onEvent !== "function" || typeof deps?.onRelease !== "function") {
|
|
202
194
|
throw new TypeError("Conversation subscriptions require their server-owned event and cleanup callbacks.");
|
|
@@ -228,8 +220,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
228
220
|
input: conversationInspectInputValidator,
|
|
229
221
|
output: null,
|
|
230
222
|
idempotency: "none",
|
|
231
|
-
audit: { actionName: actionIds.conversationInspectDelivery },
|
|
232
|
-
observability: {},
|
|
233
223
|
async execute(input, context) {
|
|
234
224
|
const conversation = await openConversation(input, context);
|
|
235
225
|
return conversation.inspectDelivery({ messageId: input.messageId });
|
|
@@ -245,8 +235,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
245
235
|
input: conversationInputValidator,
|
|
246
236
|
output: null,
|
|
247
237
|
idempotency: "none",
|
|
248
|
-
audit: { actionName: actionIds.conversationGoalRead },
|
|
249
|
-
observability: {},
|
|
250
238
|
async execute(input, context) {
|
|
251
239
|
return (await openConversation(input, context)).readGoal();
|
|
252
240
|
}
|
|
@@ -263,8 +251,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
263
251
|
}),
|
|
264
252
|
output: null,
|
|
265
253
|
idempotency: "none",
|
|
266
|
-
audit: { actionName: actionIds.conversationGoalUpdate },
|
|
267
|
-
observability: {},
|
|
268
254
|
async execute(input, context) {
|
|
269
255
|
const conversation = await openConversation(input, context);
|
|
270
256
|
const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId };
|
|
@@ -286,8 +272,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
286
272
|
}),
|
|
287
273
|
output: null,
|
|
288
274
|
idempotency: "none",
|
|
289
|
-
audit: { actionName: actionIds.conversationConfigure },
|
|
290
|
-
observability: {},
|
|
291
275
|
async execute(input, context) {
|
|
292
276
|
return (await openConversation(input, context)).configure(input.configuration);
|
|
293
277
|
}
|
|
@@ -304,8 +288,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
304
288
|
}),
|
|
305
289
|
output: null,
|
|
306
290
|
idempotency: "none",
|
|
307
|
-
audit: { actionName: actionIds.conversationSelect },
|
|
308
|
-
observability: {},
|
|
309
291
|
async execute(input, context) {
|
|
310
292
|
return (await openConversation(input, context)).select(input.selection);
|
|
311
293
|
}
|
|
@@ -322,8 +304,6 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
322
304
|
}),
|
|
323
305
|
output: null,
|
|
324
306
|
idempotency: "none",
|
|
325
|
-
audit: { actionName: actionIds.conversationReplace },
|
|
326
|
-
observability: {},
|
|
327
307
|
async execute(input, context) {
|
|
328
308
|
return (await openConversation(input, context)).replace(input.replacement);
|
|
329
309
|
}
|
|
@@ -364,8 +344,7 @@ function createAssistantActions({
|
|
|
364
344
|
if (!conversationAccess) return definition;
|
|
365
345
|
const selected = definition.id === actionIds.conversationSubscribe ? {
|
|
366
346
|
...definition,
|
|
367
|
-
id: conversationAccess.subscribeActionId
|
|
368
|
-
audit: { ...definition.audit, actionName: conversationAccess.subscribeActionId }
|
|
347
|
+
id: conversationAccess.subscribeActionId
|
|
369
348
|
} : definition;
|
|
370
349
|
return conversationAccess.wrapAction(selected);
|
|
371
350
|
}),
|
|
@@ -385,10 +364,6 @@ function createAssistantActions({
|
|
|
385
364
|
},
|
|
386
365
|
input: runtimeChatStreamInputValidator,
|
|
387
366
|
idempotency: "optional",
|
|
388
|
-
audit: {
|
|
389
|
-
actionName: actionIds.chatStream
|
|
390
|
-
},
|
|
391
|
-
observability: {},
|
|
392
367
|
async execute(input, context, deps) {
|
|
393
368
|
return chatService.streamChat(input, {
|
|
394
369
|
context,
|
|
@@ -409,10 +384,6 @@ function createAssistantActions({
|
|
|
409
384
|
input: runtimeConversationsListInputValidator,
|
|
410
385
|
output: null,
|
|
411
386
|
idempotency: "none",
|
|
412
|
-
audit: {
|
|
413
|
-
actionName: actionIds.conversationsList
|
|
414
|
-
},
|
|
415
|
-
observability: {},
|
|
416
387
|
async execute(input, context) {
|
|
417
388
|
return returnJsonApiData(await chatService.listConversations(input.query, {
|
|
418
389
|
context,
|
|
@@ -432,10 +403,6 @@ function createAssistantActions({
|
|
|
432
403
|
input: runtimeConversationMessagesListInputValidator,
|
|
433
404
|
output: null,
|
|
434
405
|
idempotency: "none",
|
|
435
|
-
audit: {
|
|
436
|
-
actionName: actionIds.conversationMessagesList
|
|
437
|
-
},
|
|
438
|
-
observability: {},
|
|
439
406
|
async execute(input, context) {
|
|
440
407
|
return returnJsonApiData(await chatService.getConversationMessages(input.conversationId, input.query, {
|
|
441
408
|
context,
|