@jskit-ai/assistant-runtime 0.1.143 → 0.1.145

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.143",
3
+ "version": "0.1.145",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -11,15 +11,15 @@
11
11
  "./server/actionIds": "./src/server/actionIds.js"
12
12
  },
13
13
  "dependencies": {
14
- "@jskit-ai/assistant-core": "0.1.148",
15
- "@jskit-ai/database-runtime": "0.1.172",
14
+ "@jskit-ai/assistant-core": "0.1.150",
15
+ "@jskit-ai/database-runtime": "0.1.174",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.170",
20
- "@jskit-ai/http-web": "0.1.17",
21
- "@jskit-ai/kernel": "0.1.172",
22
- "@jskit-ai/shell-web": "0.1.176",
19
+ "@jskit-ai/http-runtime": "0.1.172",
20
+ "@jskit-ai/http-web": "0.1.19",
21
+ "@jskit-ai/kernel": "0.1.174",
22
+ "@jskit-ai/shell-web": "0.1.178",
23
23
  "@tanstack/vue-query": "^5.90.5",
24
24
  "vue": "^3.5.13",
25
25
  "vuetify": "^4.0.0"
@@ -5,14 +5,12 @@ import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/ker
5
5
  import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
6
6
  import {
7
7
  ASSISTANT_STREAM_EVENT_TYPES,
8
- MAX_HISTORY_MESSAGES,
9
8
  MAX_INPUT_CHARS,
10
9
  assistantConversationMessagesQueryKey,
11
10
  assistantConversationsListQueryKey,
12
11
  assistantScopeQueryKey,
13
12
  normalizeAssistantStreamEventType,
14
13
  normalizeConversationStatus as normalizeAssistantConversationStatus,
15
- parseJsonObject,
16
14
  toPositiveInteger
17
15
  } from "@jskit-ai/assistant-core/shared";
18
16
  import {
@@ -23,14 +21,23 @@ import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
23
21
  import { usePagedCollection } from "@jskit-ai/http-web/client/composables/usePagedCollection";
24
22
  import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
25
23
  import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
24
+ import {
25
+ buildHistory,
26
+ buildId,
27
+ interruptPendingToolEvents,
28
+ mapTranscriptEntriesToAssistantState,
29
+ normalizeToolName
30
+ } from "../support/assistantRuntimeState.js";
26
31
  import { insertTextAtSelection } from "../support/composerInputSupport.js";
32
+ import {
33
+ loadConversationTranscript,
34
+ resolveConversationRestorePolicy
35
+ } from "../support/conversationRestoreSupport.js";
27
36
  import { useWorkspaceWebScopeSupport } from "../support/workspaceScopeSupport.js";
28
37
 
29
38
  const DEFAULT_STREAM_TIMEOUT_MS = 120_000;
30
39
  const DEFAULT_HISTORY_PAGE_SIZE = 20;
31
- const DEFAULT_MESSAGES_PAGE_SIZE = 200;
32
40
  const DEFAULT_HISTORY_STALE_TIME_MS = 60_000;
33
- const RESTORE_MESSAGES_PAGE = 1;
34
41
 
35
42
  function toNonNegativeInteger(value, fallback = 0) {
36
43
  const parsed = Number(value);
@@ -78,18 +85,6 @@ function writeStoredActiveConversationId(scope = {}, conversationId) {
78
85
  }
79
86
  }
80
87
 
81
- function buildId(prefix = "id") {
82
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
83
- return `${prefix}_${crypto.randomUUID()}`;
84
- }
85
-
86
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
87
- }
88
-
89
- function normalizeToolName(value) {
90
- return normalizeText(value) || "tool";
91
- }
92
-
93
88
  function normalizeConversationStatus(value) {
94
89
  return normalizeAssistantConversationStatus(value, {
95
90
  fallback: "unknown"
@@ -110,111 +105,19 @@ function formatConversationStartedAt(value) {
110
105
  return date.toLocaleString();
111
106
  }
112
107
 
113
- function parseToolResultPayload(value) {
114
- return parseJsonObject(value);
115
- }
116
-
117
- function buildHistory(messages) {
118
- const normalizedHistory = (Array.isArray(messages) ? messages : [])
119
- .filter((message) => {
120
- if (!message || typeof message !== "object") {
121
- return false;
122
- }
123
- if (message.kind !== "chat") {
124
- return false;
125
- }
126
- if (message.role !== "user" && message.role !== "assistant") {
127
- return false;
128
- }
129
- if (normalizeText(message.status).toLowerCase() !== "done") {
130
- return false;
131
- }
132
- return Boolean(normalizeText(message.text));
133
- })
134
- .map((message) => ({
135
- role: message.role,
136
- content: String(message.text || "").slice(0, MAX_INPUT_CHARS)
137
- }));
138
-
139
- return normalizedHistory.slice(-MAX_HISTORY_MESSAGES);
140
- }
141
-
142
- function mapTranscriptEntriesToAssistantState(entries) {
143
- const sourceEntries = Array.isArray(entries) ? entries : [];
144
- const messages = [];
145
- const toolEventsById = new Map();
146
-
147
- function ensureToolEvent(toolCallId, toolName) {
148
- const key = normalizeText(toolCallId) || buildId("tool_call");
149
- if (toolEventsById.has(key)) {
150
- return toolEventsById.get(key);
151
- }
152
-
153
- const next = {
154
- id: key,
155
- name: normalizeToolName(toolName),
156
- arguments: "",
157
- status: "pending",
158
- result: null,
159
- error: null
160
- };
161
- toolEventsById.set(key, next);
162
- return next;
163
- }
164
-
165
- for (const entry of sourceEntries) {
166
- const role = normalizeText(entry?.role).toLowerCase();
167
- const kind = normalizeText(entry?.kind).toLowerCase();
168
- const metadata = normalizeObject(entry?.metadata);
169
- const transcriptId = normalizeRecordId(entry?.id, { fallback: null });
170
- const messageId = transcriptId ? `transcript_${transcriptId}` : buildId("transcript");
171
-
172
- if (kind === "chat" && (role === "user" || role === "assistant")) {
173
- messages.push({
174
- id: messageId,
175
- role,
176
- kind: "chat",
177
- text: entry?.contentText == null ? "" : String(entry.contentText),
178
- status: "done"
179
- });
180
- continue;
181
- }
182
-
183
- if (kind === "tool_call") {
184
- const toolCallId = normalizeText(metadata.toolCallId) || `tool_call_${messageId}`;
185
- const toolName = normalizeToolName(metadata.tool);
186
- const toolEvent = ensureToolEvent(toolCallId, toolName);
187
- toolEvent.arguments = String(entry?.contentText || "");
188
- toolEvent.status = "pending";
189
- continue;
190
- }
191
-
192
- if (kind === "tool_result") {
193
- const parsedResult = parseToolResultPayload(entry?.contentText);
194
- const toolCallId = normalizeText(metadata.toolCallId || parsedResult.toolCallId) || `tool_result_${messageId}`;
195
- const toolName = normalizeToolName(metadata.tool || parsedResult.tool);
196
- const toolEvent = ensureToolEvent(toolCallId, toolName);
197
- const failed = parsedResult.ok === false || metadata.ok === false;
198
- toolEvent.status = failed ? "failed" : "done";
199
- toolEvent.result = failed ? null : parsedResult.result;
200
- toolEvent.error = failed ? parsedResult.error || metadata.error || null : null;
201
- }
202
- }
203
-
204
- return {
205
- messages,
206
- pendingToolEvents: [...toolEventsById.values()]
207
- };
208
- }
209
-
210
108
  function resolveRuntimePolicy() {
211
109
  const appConfig = getClientAppConfig();
212
110
  const assistantConfig = normalizeObject(appConfig?.assistant);
111
+ const conversationRestorePolicy = resolveConversationRestorePolicy({
112
+ pageSize: assistantConfig.restoreMessagesPageSize,
113
+ maxEntries: assistantConfig.restoreMessagesMaxEntries
114
+ });
213
115
 
214
116
  return Object.freeze({
215
117
  timeoutMs: toPositiveInteger(assistantConfig.timeoutMs, DEFAULT_STREAM_TIMEOUT_MS),
216
118
  historyPageSize: toPositiveInteger(assistantConfig.historyPageSize, DEFAULT_HISTORY_PAGE_SIZE),
217
- restoreMessagesPageSize: toPositiveInteger(assistantConfig.restoreMessagesPageSize, DEFAULT_MESSAGES_PAGE_SIZE),
119
+ restoreMessagesPageSize: conversationRestorePolicy.pageSize,
120
+ restoreMessagesMaxEntries: conversationRestorePolicy.maxEntries,
218
121
  historyStaleTimeMs: toNonNegativeInteger(assistantConfig.historyStaleTimeMs, DEFAULT_HISTORY_STALE_TIME_MS)
219
122
  });
220
123
  }
@@ -474,19 +377,23 @@ function useAssistantRuntime({ api = null, surfaceId = "" } = {}) {
474
377
  setRuntimeError("");
475
378
 
476
379
  try {
477
- const response = await queryClient.fetchQuery({
478
- queryKey: assistantConversationMessagesQueryKey(runtimeScope.value, parsedConversationId, {
479
- page: RESTORE_MESSAGES_PAGE,
480
- pageSize: runtimePolicy.restoreMessagesPageSize
481
- }),
482
- queryFn: () =>
483
- runtimeApi.getConversationMessages(parsedConversationId, {
484
- page: RESTORE_MESSAGES_PAGE,
485
- pageSize: runtimePolicy.restoreMessagesPageSize
486
- })
380
+ const transcript = await loadConversationTranscript({
381
+ pageSize: runtimePolicy.restoreMessagesPageSize,
382
+ maxEntries: runtimePolicy.restoreMessagesMaxEntries,
383
+ fetchPage: (page, pageSize) => queryClient.fetchQuery({
384
+ queryKey: assistantConversationMessagesQueryKey(runtimeScope.value, parsedConversationId, {
385
+ page,
386
+ pageSize
387
+ }),
388
+ queryFn: () => runtimeApi.getConversationMessages(parsedConversationId, {
389
+ page,
390
+ pageSize
391
+ }),
392
+ staleTime: runtimePolicy.historyStaleTimeMs
393
+ })
487
394
  });
488
395
 
489
- const restored = mapTranscriptEntriesToAssistantState(response?.entries);
396
+ const restored = mapTranscriptEntriesToAssistantState(transcript.entries);
490
397
  messages.value = restored.messages;
491
398
  pendingToolEvents.value = restored.pendingToolEvents;
492
399
  input.value = "";
@@ -724,6 +631,7 @@ function useAssistantRuntime({ api = null, surfaceId = "" } = {}) {
724
631
  }
725
632
  } finally {
726
633
  clearTimeout(streamTimeout);
634
+ pendingToolEvents.value = interruptPendingToolEvents(pendingToolEvents.value);
727
635
  abortController.value = null;
728
636
  isStreaming.value = false;
729
637
  await invalidateConversationScope();
@@ -0,0 +1,140 @@
1
+ import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import {
3
+ MAX_HISTORY_MESSAGES,
4
+ MAX_INPUT_CHARS,
5
+ parseJsonObject
6
+ } from "@jskit-ai/assistant-core/shared";
7
+ import { isAssistantProgressOnlyText } from "../../shared/assistantResponseText.js";
8
+
9
+ function buildId(prefix = "id") {
10
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
11
+ return `${prefix}_${crypto.randomUUID()}`;
12
+ }
13
+
14
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+
17
+ function normalizeToolName(value) {
18
+ return normalizeText(value) || "tool";
19
+ }
20
+
21
+ function buildHistory(messages) {
22
+ const normalizedHistory = (Array.isArray(messages) ? messages : [])
23
+ .filter((message) => {
24
+ if (!message || typeof message !== "object") {
25
+ return false;
26
+ }
27
+ if (message.kind !== "chat") {
28
+ return false;
29
+ }
30
+ if (message.role !== "user" && message.role !== "assistant") {
31
+ return false;
32
+ }
33
+ if (normalizeText(message.status).toLowerCase() !== "done") {
34
+ return false;
35
+ }
36
+ const text = normalizeText(message.text);
37
+ if (!text) {
38
+ return false;
39
+ }
40
+ return message.role !== "assistant" || !isAssistantProgressOnlyText(text);
41
+ })
42
+ .map((message) => ({
43
+ role: message.role,
44
+ content: String(message.text || "").slice(0, MAX_INPUT_CHARS)
45
+ }));
46
+
47
+ return normalizedHistory.slice(-MAX_HISTORY_MESSAGES);
48
+ }
49
+
50
+ function interruptPendingToolEvents(toolEvents) {
51
+ return (Array.isArray(toolEvents) ? toolEvents : []).map((toolEvent) => {
52
+ if (normalizeText(toolEvent?.status).toLowerCase() !== "pending") {
53
+ return toolEvent;
54
+ }
55
+
56
+ return {
57
+ ...toolEvent,
58
+ status: "interrupted"
59
+ };
60
+ });
61
+ }
62
+
63
+ function mapTranscriptEntriesToAssistantState(entries) {
64
+ const sourceEntries = Array.isArray(entries) ? entries : [];
65
+ const messages = [];
66
+ const toolEventsById = new Map();
67
+
68
+ function ensureToolEvent(toolCallId, toolName) {
69
+ const key = normalizeText(toolCallId) || buildId("tool_call");
70
+ if (toolEventsById.has(key)) {
71
+ return toolEventsById.get(key);
72
+ }
73
+
74
+ const next = {
75
+ id: key,
76
+ name: normalizeToolName(toolName),
77
+ arguments: "",
78
+ status: "pending",
79
+ result: null,
80
+ error: null
81
+ };
82
+ toolEventsById.set(key, next);
83
+ return next;
84
+ }
85
+
86
+ for (const entry of sourceEntries) {
87
+ const role = normalizeText(entry?.role).toLowerCase();
88
+ const kind = normalizeText(entry?.kind).toLowerCase();
89
+ const metadata = normalizeObject(entry?.metadata);
90
+ const transcriptId = normalizeRecordId(entry?.id, { fallback: null });
91
+ const messageId = transcriptId ? `transcript_${transcriptId}` : buildId("transcript");
92
+
93
+ if (kind === "chat" && (role === "user" || role === "assistant")) {
94
+ const text = entry?.contentText == null ? "" : String(entry.contentText);
95
+ if (role === "assistant" && isAssistantProgressOnlyText(text)) {
96
+ continue;
97
+ }
98
+
99
+ messages.push({
100
+ id: messageId,
101
+ role,
102
+ kind: "chat",
103
+ text,
104
+ status: "done"
105
+ });
106
+ continue;
107
+ }
108
+
109
+ if (kind === "tool_call") {
110
+ const toolCallId = normalizeText(metadata.toolCallId) || `tool_call_${messageId}`;
111
+ const toolEvent = ensureToolEvent(toolCallId, metadata.tool);
112
+ toolEvent.arguments = String(entry?.contentText || "");
113
+ toolEvent.status = "pending";
114
+ continue;
115
+ }
116
+
117
+ if (kind === "tool_result") {
118
+ const parsedResult = parseJsonObject(entry?.contentText);
119
+ const toolCallId = normalizeText(metadata.toolCallId || parsedResult.toolCallId) || `tool_result_${messageId}`;
120
+ const toolEvent = ensureToolEvent(toolCallId, metadata.tool || parsedResult.tool);
121
+ const failed = parsedResult.ok === false || metadata.ok === false;
122
+ toolEvent.status = failed ? "failed" : "done";
123
+ toolEvent.result = failed ? null : parsedResult.result;
124
+ toolEvent.error = failed ? parsedResult.error || metadata.error || null : null;
125
+ }
126
+ }
127
+
128
+ return {
129
+ messages,
130
+ pendingToolEvents: interruptPendingToolEvents([...toolEventsById.values()])
131
+ };
132
+ }
133
+
134
+ export {
135
+ buildHistory,
136
+ buildId,
137
+ interruptPendingToolEvents,
138
+ mapTranscriptEntriesToAssistantState,
139
+ normalizeToolName
140
+ };
@@ -0,0 +1,75 @@
1
+ import { MAX_MESSAGE_PAGE_SIZE } from "@jskit-ai/assistant-core/shared";
2
+
3
+ const DEFAULT_RESTORE_MESSAGES_PAGE_SIZE = 200;
4
+ const DEFAULT_RESTORE_MESSAGES_MAX_ENTRIES = 2000;
5
+ const MAX_RESTORE_MESSAGES_MAX_ENTRIES = 5000;
6
+
7
+ function normalizeBoundedPositiveInteger(value, fallback, maximum) {
8
+ const parsed = Number(value);
9
+ if (!Number.isInteger(parsed) || parsed < 1) {
10
+ return fallback;
11
+ }
12
+
13
+ return Math.min(parsed, maximum);
14
+ }
15
+
16
+ function normalizeTotalPages(value) {
17
+ const parsed = Number(value);
18
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
19
+ }
20
+
21
+ function resolveConversationRestorePolicy({ pageSize, maxEntries } = {}) {
22
+ return Object.freeze({
23
+ pageSize: normalizeBoundedPositiveInteger(
24
+ pageSize,
25
+ DEFAULT_RESTORE_MESSAGES_PAGE_SIZE,
26
+ MAX_MESSAGE_PAGE_SIZE
27
+ ),
28
+ maxEntries: normalizeBoundedPositiveInteger(
29
+ maxEntries,
30
+ DEFAULT_RESTORE_MESSAGES_MAX_ENTRIES,
31
+ MAX_RESTORE_MESSAGES_MAX_ENTRIES
32
+ )
33
+ });
34
+ }
35
+
36
+ async function loadConversationTranscript({
37
+ fetchPage,
38
+ pageSize = DEFAULT_RESTORE_MESSAGES_PAGE_SIZE,
39
+ maxEntries = DEFAULT_RESTORE_MESSAGES_MAX_ENTRIES
40
+ } = {}) {
41
+ if (typeof fetchPage !== "function") {
42
+ throw new TypeError("loadConversationTranscript requires fetchPage().");
43
+ }
44
+
45
+ const restorePolicy = resolveConversationRestorePolicy({ pageSize, maxEntries });
46
+ const normalizedPageSize = restorePolicy.pageSize;
47
+ const normalizedMaxEntries = restorePolicy.maxEntries;
48
+ const firstResponse = await fetchPage(1, normalizedPageSize);
49
+ const totalPages = normalizeTotalPages(firstResponse?.totalPages);
50
+ const restoredPageCount = Math.max(1, Math.ceil(normalizedMaxEntries / normalizedPageSize));
51
+ const firstRestoredPage = Math.max(1, totalPages - restoredPageCount + 1);
52
+ const entries = firstRestoredPage === 1 && Array.isArray(firstResponse?.entries)
53
+ ? [...firstResponse.entries]
54
+ : [];
55
+
56
+ for (let page = Math.max(2, firstRestoredPage); page <= totalPages; page += 1) {
57
+ const response = await fetchPage(page, normalizedPageSize);
58
+ if (Array.isArray(response?.entries)) {
59
+ entries.push(...response.entries);
60
+ }
61
+ }
62
+
63
+ return Object.freeze({
64
+ entries: Object.freeze(entries.slice(-normalizedMaxEntries)),
65
+ firstRestoredPage,
66
+ pageSize: normalizedPageSize,
67
+ totalPages,
68
+ truncated: firstRestoredPage > 1
69
+ });
70
+ }
71
+
72
+ export {
73
+ loadConversationTranscript,
74
+ resolveConversationRestorePolicy
75
+ };