@jskit-ai/assistant-runtime 0.1.238 → 0.2.1

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.
@@ -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
- "Delivery was not confirmed before this page closed. Check the conversation before retrying.") };
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.message;
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) throw Object.assign(new Error("Access denied."), { status: 403 });
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)) throw new Error("Older conversation history could not be refreshed. Reload chat to try again.");
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, "Waiting for the assistant to finish this step");
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: "This conversation no longer accepts attachments." };
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.message,
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.message,
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.message,
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?.message || failure || "Older conversation history could not be loaded.");
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.message || "Goal status is unavailable.";
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 = "This conversation does not support goal token budgets.";
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 : action === "resume" ? "Resume the goal." : "Cancel the goal.",
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) goalError.value = result?.error || delivery.find(messageId)?.error || "Could not change the goal.";
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.error || "Could not change the goal.");
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.message;
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.error);
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 = "Delivery is still unknown. Retry to check again.";
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.message || "Delivery could not be checked.";
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 || "Assistant", welcomeMessage: display.welcomeMessage,
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?.message || "Accepted attachments could not be cleared from the composer."; }
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 || "Message delivery is not confirmed.";
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?.error || "", errorReloadable: Boolean(current) && !current.accessDenied.value,
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 || "Assistant", welcomeMessage: display.welcomeMessage ?? "What would you like to do?",
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 ? "Optional additional context…"
1046
- : formatsQuestions && prompts.choices?.length ? "Choose an answer above, or type another answer…"
1047
- : display.placeholder || "Message the assistant…",
1048
- hint: formatsQuestions && prompts.active ? "Answer the assistant, then send one combined reply." : "",
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?.error || "") };
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
- if (!source) {
97
- return "unknown";
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
- return date.toLocaleString();
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: "Unable to load conversation history."
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(normalizeText(loadError?.message) || "Unable to load conversation.");
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("Assistant did not confirm this message.")),
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 = normalizeText(event?.message) || "Assistant request failed.";
557
+ admissionError = failureText(event) || t("jskit.assistant.runtime.request.failed");
558
558
  return;
559
559
  }
560
560
  setRuntimeError(
561
- normalizeText(event?.message) || "Assistant request failed.",
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 || "Assistant did not confirm this message.");
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("Assistant returned no output.", "assistant.runtime:empty-output");
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(normalizeText(streamError?.message) || "Assistant request failed.");
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) || "You",
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, text: `${event.name} — ${event.status}` })) : []
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
  }
@@ -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: status < 500 ? String(error.message || "Conversation subscription failed.") : "Conversation subscription failed.",
26
- code: String(error?.code || "assistant_subscription_failed"),
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: safeStatusCode >= 500 ? "Internal server error." : String(error?.message || "Request failed.")
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;