@jskit-ai/assistant-runtime 0.1.238 → 0.2.0
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 +138 -0
- package/locales/en.json +136 -0
- package/package.json +10 -9
- package/src/client/components/AssistantConversationClientElement.vue +11 -6
- package/src/client/components/AssistantSettingsClientElement.vue +19 -14
- package/src/client/components/AssistantSurfaceClientElement.vue +52 -29
- package/src/client/composables/useAssistantConversation.js +48 -29
- package/src/client/composables/useAssistantRuntime.js +18 -18
- package/src/client/i18n.js +10 -0
- package/src/client/support/assistantRuntimeState.js +15 -2
- package/src/client/support/subscribeAssistantConversation.js +3 -1
- package/src/server/actions.js +1 -1
- package/src/server/createAssistantRuntime.js +4 -2
- package/src/server/registerConversationSubscriptions.js +8 -4
- package/src/server/registerFastifyConversations.js +1 -1
- package/src/server/registerRoutes.js +8 -3
- package/src/server/services/assistantConfigService.js +1 -1
- package/src/server/services/chatService.js +38 -25
- package/src/server/services/transcriptService.js +8 -12
- package/src/server/sqlConversationStorage.js +1 -1
- package/src/server/support/assistantErrors.js +16 -1
- package/src/server/support/assistantRouteContext.js +5 -3
- package/src/server/support/assistantSurface.js +1 -1
- package/test/assistantRuntimeState.test.js +16 -0
- package/test/i18nCatalog.test.js +16 -0
- package/test/lazyAppConfig.test.js +24 -2
- package/test/turnRequests.test.js +12 -2
|
@@ -15,8 +15,17 @@ import { resolveAssistantRuntimeRoute, resolveAssistantSurfaceConfig } from "../
|
|
|
15
15
|
import { useWorkspaceWebScopeSupport } from "../support/workspaceScopeSupport.js";
|
|
16
16
|
import { isAccessDenied } from "../support/accessDenied.js";
|
|
17
17
|
import { subscribeAssistantConversation, refreshedHistoryPages, patchLoadedHistoryPages } from "../support/subscribeAssistantConversation.js";
|
|
18
|
+
import { useJskitText } from "../i18n.js";
|
|
18
19
|
|
|
19
20
|
const text = value => String(toValue(value) ?? "").trim();
|
|
21
|
+
const { serverMessage, t } = useJskitText();
|
|
22
|
+
// A failure in the active language when its code or messageCode is JSKIT's English;
|
|
23
|
+
// otherwise its message as sent. A result's error text is read the same way.
|
|
24
|
+
const failureMessage = (failure, fallback = "") => failure?.message ? serverMessage(failure) : fallback;
|
|
25
|
+
const resultError = result => failureMessage({ ...result, message: result?.error });
|
|
26
|
+
// A conversation snapshot's error names its runtime sentence with errorMessageCode.
|
|
27
|
+
const stateError = state => failureMessage({ message: state?.error,
|
|
28
|
+
messageCode: state?.errorMessageCode, messageParams: state?.errorMessageParams });
|
|
20
29
|
// Keep the injection key stable across optimized package and raw Vue imports.
|
|
21
30
|
const conversationDefaultsKey = Symbol.for("jskit.assistant-runtime.conversation.defaults");
|
|
22
31
|
const compareTurnIds = (left, right) => String(left).localeCompare(String(right), undefined, { numeric: true });
|
|
@@ -150,7 +159,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
150
159
|
const restoredMessages = messages.filter(message => !delivery.find(message.id)).map(message => {
|
|
151
160
|
const status = ["failed", "accepted"].includes(message.status) ? message.status : "uncertain";
|
|
152
161
|
return { ...message, status, checking: false, error: message.error || (status === "accepted" ? "" :
|
|
153
|
-
"
|
|
162
|
+
t("jskit.assistant.runtime.delivery.unconfirmedBeforeClose")) };
|
|
154
163
|
});
|
|
155
164
|
delivery.state.messages = unmatchedOptimisticMessages(turns.value, [...delivery.state.messages, ...restoredMessages], { receiptsOnly: true });
|
|
156
165
|
} catch {
|
|
@@ -243,7 +252,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
243
252
|
function receiveError(failure) {
|
|
244
253
|
if (!current.value) return;
|
|
245
254
|
loading.value = false;
|
|
246
|
-
error.value = failure
|
|
255
|
+
error.value = failureMessage(failure);
|
|
247
256
|
accessDenied.value = isAccessDenied(failure);
|
|
248
257
|
if (accessDenied.value) {
|
|
249
258
|
deniedHistoryRevision += 1;
|
|
@@ -286,7 +295,9 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
286
295
|
async function readLoadedHistory({ current: subscriptionCurrent }) {
|
|
287
296
|
const denial = deniedHistoryRevision;
|
|
288
297
|
const valid = () => {
|
|
289
|
-
if (denial !== deniedHistoryRevision)
|
|
298
|
+
if (denial !== deniedHistoryRevision) {
|
|
299
|
+
throw Object.assign(new Error("Access denied."), { status: 403, messageCode: "assistant.runtime.accessDenied" });
|
|
300
|
+
}
|
|
290
301
|
return current.value && active.value && subscriptionCurrent();
|
|
291
302
|
};
|
|
292
303
|
// A concurrent Load older operation extends the desired span. Retry one
|
|
@@ -306,7 +317,11 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
306
317
|
const reachedFullOldestTurn = page.pagination.oldestTurnId === oldest && !page.conversationLog[0]?.paginationPartial;
|
|
307
318
|
const passedOldestTurn = compareTurnIds(page.pagination.oldestTurnId, oldest) < 0;
|
|
308
319
|
if (cursor && (cursor === oldestCursor || passedOldestTurn || reachedFullOldestTurn)) break;
|
|
309
|
-
if (!cursor || cursors.has(cursor))
|
|
320
|
+
if (!cursor || cursors.has(cursor)) {
|
|
321
|
+
throw Object.assign(new Error("Older conversation history could not be refreshed. Reload chat to try again."), {
|
|
322
|
+
messageCode: "assistant.runtime.history.refreshFailed"
|
|
323
|
+
});
|
|
324
|
+
}
|
|
310
325
|
cursors.add(cursor);
|
|
311
326
|
page = normalizeConversationLogPage(await api.readConversation(identity.conversationId, {
|
|
312
327
|
beforeTurnId: cursor, limit: latest.pagination.limit
|
|
@@ -469,14 +484,14 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
469
484
|
await subscription?.reload();
|
|
470
485
|
if (error.value) return false;
|
|
471
486
|
if (snapshot.value.status === "working" && !steerable.value) {
|
|
472
|
-
delivery.wait(messageId, "
|
|
487
|
+
delivery.wait(messageId, t("jskit.assistant.runtime.delivery.waitingForStep"));
|
|
473
488
|
}
|
|
474
489
|
admission = await waitForAdmission(pending);
|
|
475
490
|
delivery.wait(messageId, "");
|
|
476
491
|
if (!admission || controller.signal.aborted || !available.value ||
|
|
477
492
|
snapshot.value.status !== (admission === "steer" ? "working" : "ready")) return false;
|
|
478
493
|
if (submission.displayAttachments?.length && snapshot.value.capabilities?.attachments !== true) {
|
|
479
|
-
return { ok: false, error: "
|
|
494
|
+
return { ok: false, error: t("jskit.assistant.runtime.attachments.noLongerAccepted") };
|
|
480
495
|
}
|
|
481
496
|
}
|
|
482
497
|
let request = prepare ? await prepare(submission, { signal: controller.signal }) : submission.request;
|
|
@@ -520,14 +535,14 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
520
535
|
}
|
|
521
536
|
catch (failure) {
|
|
522
537
|
if (controller.signal.aborted) return false;
|
|
523
|
-
if (prepare && !pending.dispatched) return { ok: false, error: failure
|
|
538
|
+
if (prepare && !pending.dispatched) return { ok: false, error: failureMessage(failure),
|
|
524
539
|
...(failure.code ? { code: failure.code } : {}) };
|
|
525
540
|
const rejection = failure.details?.delivery;
|
|
526
541
|
const notSent = Number.isInteger(failure.status) && failure.status >= 400 && failure.status < 500 &&
|
|
527
542
|
rejection && !Array.isArray(rejection) && rejection.status === "not-sent" && rejection.messageId === messageId;
|
|
528
543
|
// A definite rejection keeps its code so the application can offer
|
|
529
544
|
// the matching recovery, such as freeing assistant context.
|
|
530
|
-
if (notSent || SEND_REJECTIONS.has(failure.code) || failure.code === "conversation_steer_turn_finished" && failure.status === 409 && !failure.details?.delivery) return { ok: false, error: failure
|
|
545
|
+
if (notSent || SEND_REJECTIONS.has(failure.code) || failure.code === "conversation_steer_turn_finished" && failure.status === 409 && !failure.details?.delivery) return { ok: false, error: failureMessage(failure),
|
|
531
546
|
...(failure.code ? { code: failure.code } : {}) };
|
|
532
547
|
throw failure;
|
|
533
548
|
} finally {
|
|
@@ -538,7 +553,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
538
553
|
return result;
|
|
539
554
|
} catch (failure) {
|
|
540
555
|
// Delivery keeps its exact authored payload until an explicit receipt check.
|
|
541
|
-
if (prepare) return { ok: false, status: "uncertain", error: failure
|
|
556
|
+
if (prepare) return { ok: false, status: "uncertain", error: failureMessage(failure),
|
|
542
557
|
...(failure.code ? { code: failure.code } : {}) };
|
|
543
558
|
return false;
|
|
544
559
|
} finally {
|
|
@@ -588,7 +603,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
588
603
|
return true;
|
|
589
604
|
} catch (failure) {
|
|
590
605
|
if (current.value) {
|
|
591
|
-
loadMoreError.value = String(failure
|
|
606
|
+
loadMoreError.value = String(failureMessage(failure) || failure || t("jskit.assistant.runtime.history.loadFailed"));
|
|
592
607
|
if (isAccessDenied(failure)) receiveError(failure);
|
|
593
608
|
}
|
|
594
609
|
finish({ changed: false, loaded: false });
|
|
@@ -619,7 +634,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
619
634
|
return true;
|
|
620
635
|
}).catch(failure => {
|
|
621
636
|
if (current.value && !controller.signal.aborted && snapshot.value?.segmentId === segment) {
|
|
622
|
-
goalLoadError.value = failure
|
|
637
|
+
goalLoadError.value = failureMessage(failure, t("jskit.assistant.runtime.goal.unavailable"));
|
|
623
638
|
}
|
|
624
639
|
return false;
|
|
625
640
|
}).finally(() => {
|
|
@@ -642,7 +657,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
642
657
|
!["message", "control"].includes(command?.delivery) ||
|
|
643
658
|
delivery.state.messages.some(message => message.status === "uncertain")) return false;
|
|
644
659
|
if (input.tokenBudget != null && !target.capabilities.goalBudgets) {
|
|
645
|
-
goalError.value = "
|
|
660
|
+
goalError.value = t("jskit.assistant.runtime.goal.budgetsUnsupported");
|
|
646
661
|
return false;
|
|
647
662
|
}
|
|
648
663
|
const retry = delivery.find(messageId);
|
|
@@ -656,21 +671,24 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
656
671
|
try {
|
|
657
672
|
if (retry || command.delivery === "message") {
|
|
658
673
|
const result = await submitRequest(retry?.payload || {
|
|
659
|
-
message: action === "set" ? request.objective
|
|
674
|
+
message: action === "set" ? request.objective
|
|
675
|
+
: t(action === "resume" ? "jskit.assistant.runtime.goal.resumeMessage" : "jskit.assistant.runtime.goal.cancelMessage"),
|
|
660
676
|
goalRequest: request
|
|
661
677
|
}, { messageId });
|
|
662
678
|
if (result === false || result?.ok === false) {
|
|
663
|
-
if (current.value)
|
|
679
|
+
if (current.value) {
|
|
680
|
+
goalError.value = result?.error || delivery.find(messageId)?.error || t("jskit.assistant.runtime.goal.changeFailed");
|
|
681
|
+
}
|
|
664
682
|
return false;
|
|
665
683
|
}
|
|
666
684
|
return result;
|
|
667
685
|
}
|
|
668
686
|
const result = await api.updateConversationGoal(identity.conversationId, request);
|
|
669
|
-
if (result?.ok === false) throw new Error(result
|
|
687
|
+
if (result?.ok === false) throw new Error(resultError(result) || t("jskit.assistant.runtime.goal.changeFailed"));
|
|
670
688
|
return result;
|
|
671
689
|
} catch (failure) {
|
|
672
690
|
if (current.value) {
|
|
673
|
-
goalError.value = failure
|
|
691
|
+
goalError.value = failureMessage(failure);
|
|
674
692
|
if (isAccessDenied(failure)) receiveError(failure);
|
|
675
693
|
}
|
|
676
694
|
return false;
|
|
@@ -730,14 +748,14 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
730
748
|
delivery.accept(messageId);
|
|
731
749
|
if (receipt.messageId === messageId && message?.payload.goalRequest) goalError.value = "";
|
|
732
750
|
}
|
|
733
|
-
else if (receipt.status === "not-sent" && receipt.messageId === messageId) delivery.rejectUnsent(messageId, receipt
|
|
751
|
+
else if (receipt.status === "not-sent" && receipt.messageId === messageId) delivery.rejectUnsent(messageId, resultError(receipt));
|
|
734
752
|
else if (receipt.status === "unknown" && message?.status === "uncertain") {
|
|
735
|
-
message.error = "
|
|
753
|
+
message.error = t("jskit.assistant.runtime.delivery.stillUnknown");
|
|
736
754
|
}
|
|
737
755
|
subscription?.reload();
|
|
738
756
|
return receipt;
|
|
739
757
|
} catch (failure) {
|
|
740
|
-
if (current.value && message?.status === "uncertain") message.error = failure
|
|
758
|
+
if (current.value && message?.status === "uncertain") message.error = failureMessage(failure, t("jskit.assistant.runtime.delivery.checkFailed"));
|
|
741
759
|
receiveError(failure);
|
|
742
760
|
return false;
|
|
743
761
|
}
|
|
@@ -790,7 +808,7 @@ function useBoundedTask({ command, endpoint, scope, input, result, onResult }, {
|
|
|
790
808
|
return {
|
|
791
809
|
conversation: {
|
|
792
810
|
turns: conversationTurnsFromMessages(messages.value.map(message => ({ ...message, text: message.content }))),
|
|
793
|
-
assistantLabel: display.assistantLabel || "
|
|
811
|
+
assistantLabel: display.assistantLabel || t("jskit.assistant.runtime.conversation.assistantLabel"), welcomeMessage: display.welcomeMessage,
|
|
794
812
|
visible: display.visible, retainWhenHidden: display.retainWhenHidden, variant: display.variant, scrollKey: toValue(scope)
|
|
795
813
|
},
|
|
796
814
|
composer: {
|
|
@@ -979,7 +997,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
979
997
|
current.draftAttachments.value = current.draftAttachments.value.filter(file => !attachmentIds.includes(file.attachmentId));
|
|
980
998
|
if (runtime.value === current) {
|
|
981
999
|
try { attachments?.clearAttachments({ accepted: true, attachmentIds }); }
|
|
982
|
-
catch (failure) { current.error.value = failure
|
|
1000
|
+
catch (failure) { current.error.value = failureMessage(failure, t("jskit.assistant.runtime.attachments.clearFailed")); }
|
|
983
1001
|
}
|
|
984
1002
|
}
|
|
985
1003
|
onAccepted?.();
|
|
@@ -996,7 +1014,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
996
1014
|
if (retry?.status === "uncertain") return current.inspectDelivery(messageId).then(receipt => {
|
|
997
1015
|
if (!current.current.value || !isCurrent()) return false;
|
|
998
1016
|
if (receipt?.status !== "accepted") {
|
|
999
|
-
const error = current.error.value || "
|
|
1017
|
+
const error = current.error.value || t("jskit.assistant.runtime.delivery.notConfirmed");
|
|
1000
1018
|
const pending = current.delivery.find(messageId);
|
|
1001
1019
|
if (pending) pending.error = error;
|
|
1002
1020
|
return { ok: false, status: "uncertain", error };
|
|
@@ -1027,12 +1045,13 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
1027
1045
|
models: current?.available.value ? modelOwner.value : null, questions: prompts,
|
|
1028
1046
|
goal: (toValue(goal) === true ? null : toValue(goal)) || current?.goalState.value || null,
|
|
1029
1047
|
conversation: { turns: current?.turns.value || [], interimReply: state?.interimReply || null,
|
|
1030
|
-
error: current?.error.value || state
|
|
1048
|
+
error: current?.error.value || stateError(state), errorReloadable: Boolean(current) && !current.accessDenied.value,
|
|
1031
1049
|
scrollKey: JSON.stringify(identity.value),
|
|
1032
1050
|
hasMoreBefore: current?.hasMoreBefore.value || false,
|
|
1033
1051
|
loadingMore: current?.loadingMore.value || false, loadMoreError: current?.loadMoreError.value || "",
|
|
1034
1052
|
loading: current?.loading.value || false, working: !current?.error.value && state?.status === "working" && !current.delivery.state.sending,
|
|
1035
|
-
assistantLabel: display.assistantLabel || "
|
|
1053
|
+
assistantLabel: display.assistantLabel || t("jskit.assistant.runtime.conversation.assistantLabel"),
|
|
1054
|
+
welcomeMessage: display.welcomeMessage ?? t("jskit.assistant.runtime.conversation.welcomeMessage"),
|
|
1036
1055
|
systemLabel: display.systemLabel, variant: display.variant, visible: display.visible, retainWhenHidden: display.retainWhenHidden,
|
|
1037
1056
|
userMessageFormat: display.userMessageFormat, progressPreviewLimit: display.progressPreviewLimit,
|
|
1038
1057
|
previewMessage: current?.available.value ? display.previewMessage : null },
|
|
@@ -1042,10 +1061,10 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
1042
1061
|
canStop: state?.status === "working" || state?.status === "unavailable", stopPending: current?.stopping.value || false,
|
|
1043
1062
|
queueWhileSending: current?.queueWhileSending.value || false,
|
|
1044
1063
|
ariaLabel: display.ariaLabel, submitAriaLabel: display.submitAriaLabel, submitLabel: display.submitLabel,
|
|
1045
|
-
placeholder: formatsQuestions && prompts.questions?.length ? "
|
|
1046
|
-
: formatsQuestions && prompts.choices?.length ? "
|
|
1047
|
-
: display.placeholder || "
|
|
1048
|
-
hint: formatsQuestions && prompts.active ? "
|
|
1064
|
+
placeholder: formatsQuestions && prompts.questions?.length ? t("jskit.assistant.runtime.composer.questionsPlaceholder")
|
|
1065
|
+
: formatsQuestions && prompts.choices?.length ? t("jskit.assistant.runtime.composer.choicesPlaceholder")
|
|
1066
|
+
: display.placeholder || t("jskit.assistant.runtime.conversation.placeholder"),
|
|
1067
|
+
hint: formatsQuestions && prompts.active ? t("jskit.assistant.runtime.composer.questionsHint") : "",
|
|
1049
1068
|
rows: display.rows ?? 2, density: display.layout === "compact" ? "compact" : "default", submitOnEnter: true, submitOnModifierEnter: true },
|
|
1050
1069
|
actions: { setDraft(value) {
|
|
1051
1070
|
if (current) current.draft.value = value;
|
|
@@ -1055,7 +1074,7 @@ function createConversationBinding({ conversationId, endpoint = "", surfaceId =
|
|
|
1055
1074
|
checkDelivery: messageId => current?.inspectDelivery(messageId), loadMore: input => current?.loadMore(input), reload: () => current?.reload() }
|
|
1056
1075
|
};
|
|
1057
1076
|
});
|
|
1058
|
-
return { runtime, adapter, submitPrepared, error: computed(() => runtime.value?.error.value || runtime.value?.snapshot.value
|
|
1077
|
+
return { runtime, adapter, submitPrepared, error: computed(() => runtime.value?.error.value || stateError(runtime.value?.snapshot.value)) };
|
|
1059
1078
|
}
|
|
1060
1079
|
|
|
1061
1080
|
/** The standard element and custom text/voice presentation share the supplied binding. */
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
resolveConversationRestorePolicy
|
|
34
34
|
} from "../support/conversationRestoreSupport.js";
|
|
35
35
|
import { useWorkspaceWebScopeSupport } from "../support/workspaceScopeSupport.js";
|
|
36
|
+
import { useJskitText } from "../i18n.js";
|
|
36
37
|
|
|
37
38
|
const DEFAULT_STREAM_TIMEOUT_MS = 120_000;
|
|
38
39
|
const DEFAULT_HISTORY_PAGE_SIZE = 20;
|
|
@@ -92,17 +93,15 @@ function normalizeConversationStatus(value) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
function formatConversationStartedAt(value) {
|
|
96
|
+
const { formatDateTime, t } = useJskitText();
|
|
95
97
|
const source = normalizeText(value);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const date = new Date(source);
|
|
101
|
-
if (Number.isNaN(date.getTime())) {
|
|
102
|
-
return "unknown";
|
|
103
|
-
}
|
|
98
|
+
return (source && formatDateTime(source)) || t("jskit.assistant.runtime.conversation.startedAtUnknown");
|
|
99
|
+
}
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
// A failure or stream error event in the active language when its code is JSKIT's;
|
|
102
|
+
// otherwise its message as sent.
|
|
103
|
+
function failureText(failure) {
|
|
104
|
+
return failure?.message ? normalizeText(useJskitText().serverMessage(failure)) : "";
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
function resolveRuntimePolicy() {
|
|
@@ -143,6 +142,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
143
142
|
const workspaceScopeSupport = useWorkspaceWebScopeSupport();
|
|
144
143
|
const { placementContext, currentSurfaceId } = routeContext;
|
|
145
144
|
const appConfig = getClientAppConfig();
|
|
145
|
+
const { t } = useJskitText();
|
|
146
146
|
|
|
147
147
|
const messages = ref([]);
|
|
148
148
|
const input = ref("");
|
|
@@ -226,7 +226,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
226
226
|
refetchOnMount: false,
|
|
227
227
|
refetchOnWindowFocus: false
|
|
228
228
|
},
|
|
229
|
-
fallbackLoadError: "
|
|
229
|
+
fallbackLoadError: t("jskit.assistant.runtime.history.listFailed")
|
|
230
230
|
});
|
|
231
231
|
|
|
232
232
|
const conversationHistory = conversationHistoryCollection.items;
|
|
@@ -376,7 +376,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
376
376
|
writeStoredActiveConversationId(scope, "");
|
|
377
377
|
}
|
|
378
378
|
if (!input.value) input.value = previousDraft;
|
|
379
|
-
setRuntimeError(
|
|
379
|
+
setRuntimeError(failureText(loadError) || t("jskit.assistant.runtime.history.conversationLoadFailed"));
|
|
380
380
|
} finally {
|
|
381
381
|
if (version === restoreVersion) isRestoringConversation.value = false;
|
|
382
382
|
}
|
|
@@ -441,7 +441,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
441
441
|
deliver(submission) {
|
|
442
442
|
const admission = Promise.withResolvers();
|
|
443
443
|
runStream(submission, { onAccepted, admission }).then(
|
|
444
|
-
() => admission.reject(new Error("
|
|
444
|
+
() => admission.reject(new Error(t("jskit.assistant.runtime.request.notConfirmed"))),
|
|
445
445
|
admission.reject
|
|
446
446
|
);
|
|
447
447
|
return admission.promise;
|
|
@@ -554,11 +554,11 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
554
554
|
|
|
555
555
|
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ERROR) {
|
|
556
556
|
if (!accepted) {
|
|
557
|
-
admissionError =
|
|
557
|
+
admissionError = failureText(event) || t("jskit.assistant.runtime.request.failed");
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
setRuntimeError(
|
|
561
|
-
|
|
561
|
+
failureText(event) || t("jskit.assistant.runtime.request.failed"),
|
|
562
562
|
"assistant.runtime:stream-event-error"
|
|
563
563
|
);
|
|
564
564
|
updateMessage(assistantMessageId, {
|
|
@@ -575,13 +575,13 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
575
575
|
);
|
|
576
576
|
|
|
577
577
|
if (!ownsStream()) return;
|
|
578
|
-
if (!accepted) throw new Error(admissionError || "
|
|
578
|
+
if (!accepted) throw new Error(admissionError || t("jskit.assistant.runtime.request.notConfirmed"));
|
|
579
579
|
if (streamAbortController.signal.aborted) streamDoneStatus = "aborted";
|
|
580
580
|
const assistantMessage = findMessage(assistantMessageId);
|
|
581
581
|
const assistantMessageText = normalizeText(assistantMessage?.text);
|
|
582
582
|
if (!assistantMessageText && streamDoneStatus !== "aborted") {
|
|
583
583
|
if (!error.value) {
|
|
584
|
-
setRuntimeError("
|
|
584
|
+
setRuntimeError(t("jskit.assistant.runtime.request.noOutput"), "assistant.runtime:empty-output");
|
|
585
585
|
}
|
|
586
586
|
updateMessage(assistantMessageId, {
|
|
587
587
|
status: "error"
|
|
@@ -603,7 +603,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
603
603
|
status: "canceled"
|
|
604
604
|
});
|
|
605
605
|
} else {
|
|
606
|
-
setRuntimeError(
|
|
606
|
+
setRuntimeError(failureText(streamError) || t("jskit.assistant.runtime.request.failed"));
|
|
607
607
|
updateMessage(assistantMessageId, {
|
|
608
608
|
status: "error"
|
|
609
609
|
});
|
|
@@ -631,7 +631,7 @@ function useAssistantRuntime({ api = null, surfaceId = "", integrationId = "" }
|
|
|
631
631
|
const user = normalizeObject(placementSnapshot.value.user);
|
|
632
632
|
|
|
633
633
|
return {
|
|
634
|
-
displayName: normalizeText(user.displayName || user.name) || "
|
|
634
|
+
displayName: normalizeText(user.displayName || user.name) || t("jskit.assistant.runtime.viewer.you"),
|
|
635
635
|
avatarUrl: normalizeText(user.avatarUrl)
|
|
636
636
|
};
|
|
637
637
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { registerJskitMessages, useJskitText } from "@jskit-ai/kernel/client";
|
|
2
|
+
// The runtime client also shows assistant-core's conversation errors, so it
|
|
3
|
+
// registers that catalog too.
|
|
4
|
+
import coreEnglish from "@jskit-ai/assistant-core/locales/en.json" with { type: "json" };
|
|
5
|
+
import en from "../../locales/en.json" with { type: "json" };
|
|
6
|
+
|
|
7
|
+
registerJskitMessages(coreEnglish);
|
|
8
|
+
registerJskitMessages(en);
|
|
9
|
+
|
|
10
|
+
export { useJskitText };
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseJsonObject
|
|
7
7
|
} from "@jskit-ai/assistant-core/shared";
|
|
8
8
|
import { conversationTurnsFromMessages } from "@jskit-ai/assistant-core/shared/conversation";
|
|
9
|
+
import { useJskitText } from "../i18n.js";
|
|
9
10
|
|
|
10
11
|
const CONVERSATION_MESSAGE_STATUS = Object.freeze({
|
|
11
12
|
streaming: "inProgress",
|
|
@@ -14,14 +15,25 @@ const CONVERSATION_MESSAGE_STATUS = Object.freeze({
|
|
|
14
15
|
canceled: "interrupted"
|
|
15
16
|
});
|
|
16
17
|
|
|
18
|
+
const TOOL_STATUS_KEY = "jskit.assistant.runtime.tool.status.";
|
|
19
|
+
|
|
20
|
+
/** A tool event's status in the active language; an unknown status is shown as given. */
|
|
21
|
+
function toolStatusText(status) {
|
|
22
|
+
const { hasMessage, t } = useJskitText();
|
|
23
|
+
const key = `${TOOL_STATUS_KEY}${status}`;
|
|
24
|
+
return hasMessage(key) ? t(key) : String(status ?? "");
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
function mapAssistantConversationTurns(messages, toolEvents = [], { showToolActivity = true } = {}) {
|
|
28
|
+
const { t } = useJskitText();
|
|
18
29
|
return conversationTurnsFromMessages(messages.map((message) => ({
|
|
19
30
|
...message,
|
|
20
31
|
messageId: String(message.id),
|
|
21
32
|
status: CONVERSATION_MESSAGE_STATUS[message.status],
|
|
22
33
|
progressUpdates: showToolActivity ? toolEvents
|
|
23
34
|
.filter((event) => event.messageId === message.id)
|
|
24
|
-
.map((event) => ({ id: event.id,
|
|
35
|
+
.map((event) => ({ id: event.id,
|
|
36
|
+
text: t("jskit.assistant.runtime.tool.progress", { name: event.name, status: toolStatusText(event.status) }) })) : []
|
|
25
37
|
})));
|
|
26
38
|
}
|
|
27
39
|
|
|
@@ -172,5 +184,6 @@ export {
|
|
|
172
184
|
interruptPendingToolEvents,
|
|
173
185
|
mapTranscriptEntriesToAssistantState,
|
|
174
186
|
mapAssistantConversationTurns,
|
|
175
|
-
normalizeToolName
|
|
187
|
+
normalizeToolName,
|
|
188
|
+
toolStatusText
|
|
176
189
|
};
|
|
@@ -218,7 +218,9 @@ function subscribeAssistantConversation({
|
|
|
218
218
|
onError(error ? Object.assign(new Error("Chat updates could not reconnect. Retrying automatically; Reload chat to try now.", { cause: error }), {
|
|
219
219
|
code: "assistant_subscription_transport_failed"
|
|
220
220
|
}) : Object.assign(new Error(response?.error || "Conversation subscription failed."), {
|
|
221
|
-
code: response?.code, statusCode: response?.status
|
|
221
|
+
code: response?.code, statusCode: response?.status, params: response?.params,
|
|
222
|
+
...(response?.error ? { messageCode: response.messageCode, messageParams: response.messageParams }
|
|
223
|
+
: { messageCode: "assistant.runtime.subscription.failed" })
|
|
222
224
|
}));
|
|
223
225
|
return;
|
|
224
226
|
}
|
package/src/server/actions.js
CHANGED
|
@@ -117,7 +117,7 @@ function createConversationActions({ conversationRuntime, conversationDataSchema
|
|
|
117
117
|
throw new Error("Assistant conversation actions require workspace server scope support.");
|
|
118
118
|
}
|
|
119
119
|
const workspace = workspaceScopeSupport.resolveWorkspace(context, { workspaceSlug: input.workspaceSlug });
|
|
120
|
-
if (!workspace?.id) throw new AppError(409, "Workspace selection required.");
|
|
120
|
+
if (!workspace?.id) throw new AppError(409, "Workspace selection required.", { code: "assistant.runtime.workspace.selectionRequired" });
|
|
121
121
|
conversationContext = { ...context, workspace };
|
|
122
122
|
}
|
|
123
123
|
return conversationRuntime.open({ id: input.conversationId, context: conversationContext });
|
|
@@ -26,13 +26,15 @@ function createAssistantAiClientFactory(config = {}) {
|
|
|
26
26
|
if (surfaceConfig.aiIntegrationId) {
|
|
27
27
|
const selected = integrationId || surfaceConfig.aiIntegrationId;
|
|
28
28
|
if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
|
|
29
|
-
throw new AppError(403, "This AI connection is not available in this assistant.");
|
|
29
|
+
throw new AppError(403, "This AI connection is not available in this assistant.", { code: "assistant.runtime.connection.unavailable" });
|
|
30
30
|
}
|
|
31
31
|
if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
|
|
32
32
|
const connection = await config.aiConnections.resolve({ context, integrationId: selected });
|
|
33
33
|
return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
|
|
34
34
|
}
|
|
35
|
-
if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection."
|
|
35
|
+
if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.", {
|
|
36
|
+
code: "assistant.runtime.connection.selectionNotAllowed"
|
|
37
|
+
});
|
|
36
38
|
|
|
37
39
|
if (cache.has(normalizedTargetSurfaceId)) {
|
|
38
40
|
return cache.get(normalizedTargetSurfaceId);
|
|
@@ -3,7 +3,7 @@ import { attachDirectRequestActionExecutor } from "@jskit-ai/kernel/server/http"
|
|
|
3
3
|
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
4
4
|
import { actionIds } from "./actionIds.js";
|
|
5
5
|
import { resolveRouteRequestState } from "./support/assistantRouteContext.js";
|
|
6
|
-
import { errorStatus } from "./support/assistantErrors.js";
|
|
6
|
+
import { errorMessageFields, errorStatus } from "./support/assistantErrors.js";
|
|
7
7
|
import { requireAssistantSurface } from "./support/assistantSurface.js";
|
|
8
8
|
import {
|
|
9
9
|
ASSISTANT_CONVERSATION_EVENT,
|
|
@@ -13,17 +13,21 @@ import {
|
|
|
13
13
|
|
|
14
14
|
function requireSubscriptionId(input) {
|
|
15
15
|
if (typeof input?.subscriptionId !== "string" || !input.subscriptionId.trim() || input.subscriptionId.length > 128) {
|
|
16
|
-
throw new AppError(400, "A conversation subscription id is required.");
|
|
16
|
+
throw new AppError(400, "A conversation subscription id is required.", { code: "assistant.runtime.subscription.idRequired" });
|
|
17
17
|
}
|
|
18
18
|
return input.subscriptionId;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function subscriptionError(error) {
|
|
22
22
|
const status = errorStatus(error);
|
|
23
|
+
const exposed = status < 500 && Boolean(error.message);
|
|
24
|
+
const code = String(error?.code || "assistant_subscription_failed");
|
|
23
25
|
return {
|
|
24
26
|
ok: false,
|
|
25
|
-
error:
|
|
26
|
-
code
|
|
27
|
+
error: exposed ? String(error.message) : "Conversation subscription failed.",
|
|
28
|
+
code,
|
|
29
|
+
...(exposed ? errorMessageFields(error, { expose: code !== "ACTION_PERMISSION_DENIED" })
|
|
30
|
+
: { params: {}, messageCode: "assistant.runtime.subscription.failed", messageParams: {} }),
|
|
27
31
|
status
|
|
28
32
|
};
|
|
29
33
|
}
|
|
@@ -24,7 +24,7 @@ async function registerFastifyConversations(app, {
|
|
|
24
24
|
async function requestContext(request) {
|
|
25
25
|
const context = await authenticate(request);
|
|
26
26
|
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
27
|
-
throw new AppError(401, "Authentication required.");
|
|
27
|
+
throw new AppError(401, "Authentication required.", { code: "assistant.runtime.authenticationRequired" });
|
|
28
28
|
}
|
|
29
29
|
return context;
|
|
30
30
|
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
assistantSurfaceRouteParamsValidator
|
|
28
28
|
} from "./inputSchemas.js";
|
|
29
29
|
import { registerSettingsRoutes } from "./registerSettingsRoutes.js";
|
|
30
|
-
import { errorStatus, exposedAppErrorDetails } from "./support/assistantErrors.js";
|
|
30
|
+
import { errorMessageFields, errorStatus, exposedAppErrorDetails } from "./support/assistantErrors.js";
|
|
31
31
|
import { ASSISTANT_STREAM_WRITER_METHODS } from "./support/assistantStreamWriter.js";
|
|
32
32
|
import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
|
|
33
33
|
|
|
@@ -35,11 +35,13 @@ function sendPreStreamErrorResponse(reply, error) {
|
|
|
35
35
|
const statusCode = errorStatus(error);
|
|
36
36
|
const safeStatusCode = Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
|
|
37
37
|
|
|
38
|
+
// The kernel API error payload, so clients read one shape before and after streaming.
|
|
38
39
|
if (error instanceof AppError) {
|
|
39
40
|
const appErrorCode = String(error?.code || "app_error").trim() || "app_error";
|
|
40
41
|
const payload = {
|
|
41
42
|
error: error.message,
|
|
42
|
-
code: appErrorCode
|
|
43
|
+
code: appErrorCode,
|
|
44
|
+
...errorMessageFields(error, { expose: appErrorCode !== "ACTION_PERMISSION_DENIED" })
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
const details = exposedAppErrorDetails(error);
|
|
@@ -60,8 +62,11 @@ function sendPreStreamErrorResponse(reply, error) {
|
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const exposed = safeStatusCode < 500;
|
|
63
66
|
reply.code(safeStatusCode).send({
|
|
64
|
-
error:
|
|
67
|
+
error: exposed ? String(error?.message || "Request failed.") : "Internal server error.",
|
|
68
|
+
code: String(error?.code || "").trim() || (exposed ? "request_failed" : "internal_server_error"),
|
|
69
|
+
...errorMessageFields(exposed ? error : null)
|
|
65
70
|
});
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -62,7 +62,7 @@ function createService({
|
|
|
62
62
|
const resolvedWorkspace = workspace || workspaceScopeSupport.resolveWorkspace(context, input);
|
|
63
63
|
const workspaceId = normalizeRecordId(resolvedWorkspace?.id, { fallback: null });
|
|
64
64
|
if (!workspaceId) {
|
|
65
|
-
throw new AppError(409, "Workspace selection required.");
|
|
65
|
+
throw new AppError(409, "Workspace selection required.", { code: "assistant.runtime.workspace.selectionRequired" });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return workspaceId;
|