@jskit-ai/assistant-core 0.1.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.
Files changed (37) hide show
  1. package/package.descriptor.mjs +67 -0
  2. package/package.json +24 -0
  3. package/src/client/components/AssistantClientElement.vue +1316 -0
  4. package/src/client/components/AssistantSettingsFormCard.vue +76 -0
  5. package/src/client/index.js +5 -0
  6. package/src/client/lib/assistantApi.js +140 -0
  7. package/src/client/lib/assistantHttpClient.js +10 -0
  8. package/src/client/lib/markdownRenderer.js +31 -0
  9. package/src/server/index.js +21 -0
  10. package/src/server/lib/aiClient.js +43 -0
  11. package/src/server/lib/ndjson.js +47 -0
  12. package/src/server/lib/providers/anthropicClient.js +375 -0
  13. package/src/server/lib/providers/common.js +150 -0
  14. package/src/server/lib/providers/deepSeekClient.js +22 -0
  15. package/src/server/lib/providers/openAiClient.js +13 -0
  16. package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
  17. package/src/server/lib/resolveWorkspaceSlug.js +24 -0
  18. package/src/server/lib/serviceToolCatalog.js +459 -0
  19. package/src/server/repositories/repositoryPersistenceUtils.js +48 -0
  20. package/src/shared/assistantPaths.js +77 -0
  21. package/src/shared/assistantResource.js +309 -0
  22. package/src/shared/assistantSettingsResource.js +90 -0
  23. package/src/shared/index.js +47 -0
  24. package/src/shared/queryKeys.js +84 -0
  25. package/src/shared/settingsEvents.js +5 -0
  26. package/src/shared/streamEvents.js +29 -0
  27. package/src/shared/support/conversationStatus.js +18 -0
  28. package/src/shared/support/jsonObject.js +18 -0
  29. package/src/shared/support/positiveInteger.js +9 -0
  30. package/test/aiConfigValidation.test.js +15 -0
  31. package/test/assistantApiSurfaceHeader.test.js +66 -0
  32. package/test/assistantPaths.test.js +51 -0
  33. package/test/assistantResource.test.js +49 -0
  34. package/test/assistantSettingsResource.test.js +32 -0
  35. package/test/queryKeys.test.js +44 -0
  36. package/test/resolveWorkspaceSlug.test.js +83 -0
  37. package/test/serviceToolCatalog.test.js +1235 -0
@@ -0,0 +1,309 @@
1
+ import { Type } from "typebox";
2
+ import {
3
+ normalizeObjectInput,
4
+ createCursorListValidator
5
+ } from "@jskit-ai/kernel/shared/validators";
6
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
7
+ import { normalizeConversationStatus } from "./support/conversationStatus.js";
8
+ import { toPositiveInteger } from "./support/positiveInteger.js";
9
+
10
+ const MAX_INPUT_CHARS = 8000;
11
+ const MAX_HISTORY_MESSAGES = 20;
12
+ const MAX_PAGE_SIZE = 200;
13
+ const MAX_MESSAGE_PAGE_SIZE = 500;
14
+
15
+ function normalizePaginationValue(value, fallback, max) {
16
+ const parsed = toPositiveInteger(value, fallback);
17
+ return Math.max(1, Math.min(max, parsed));
18
+ }
19
+
20
+ function normalizeChatStreamBody(payload = {}) {
21
+ const source = normalizeObjectInput(payload);
22
+ const normalized = {
23
+ messageId: normalizeText(source.messageId),
24
+ input: normalizeText(source.input)
25
+ };
26
+
27
+ const conversationId = toPositiveInteger(source.conversationId, 0);
28
+ if (conversationId > 0) {
29
+ normalized.conversationId = conversationId;
30
+ }
31
+
32
+ const history = Array.isArray(source.history) ? source.history : [];
33
+ normalized.history = history
34
+ .slice(0, MAX_HISTORY_MESSAGES)
35
+ .map((entry) => {
36
+ const item = normalizeObjectInput(entry);
37
+ const role = normalizeText(item.role).toLowerCase();
38
+ if (role !== "user" && role !== "assistant") {
39
+ return null;
40
+ }
41
+
42
+ const content = normalizeText(item.content);
43
+ if (!content) {
44
+ return null;
45
+ }
46
+
47
+ return {
48
+ role,
49
+ content: content.slice(0, MAX_INPUT_CHARS)
50
+ };
51
+ })
52
+ .filter(Boolean);
53
+
54
+ const clientContext = normalizeObjectInput(source.clientContext);
55
+ if (Object.keys(clientContext).length > 0) {
56
+ normalized.clientContext = {
57
+ locale: normalizeText(clientContext.locale),
58
+ timezone: normalizeText(clientContext.timezone)
59
+ };
60
+ }
61
+
62
+ return normalized;
63
+ }
64
+
65
+ function normalizeConversationsListQuery(payload = {}) {
66
+ const source = normalizeObjectInput(payload);
67
+ const status = normalizeConversationStatus(source.status, {
68
+ fallback: ""
69
+ });
70
+ const normalized = {};
71
+
72
+ if (Object.hasOwn(source, "cursor")) {
73
+ normalized.cursor = toPositiveInteger(source.cursor, 0);
74
+ }
75
+ if (Object.hasOwn(source, "limit")) {
76
+ normalized.limit = toPositiveInteger(source.limit, 0);
77
+ }
78
+ if (status) {
79
+ normalized.status = status;
80
+ }
81
+
82
+ return normalized;
83
+ }
84
+
85
+ function normalizeConversationMessagesQuery(payload = {}) {
86
+ const source = normalizeObjectInput(payload);
87
+
88
+ return {
89
+ page: normalizePaginationValue(source.page, 1, MAX_MESSAGE_PAGE_SIZE),
90
+ pageSize: normalizePaginationValue(source.pageSize, 200, MAX_MESSAGE_PAGE_SIZE)
91
+ };
92
+ }
93
+
94
+ function normalizeConversationMessagesParams(payload = {}) {
95
+ const source = normalizeObjectInput(payload);
96
+ return {
97
+ conversationId: toPositiveInteger(source.conversationId, 0)
98
+ };
99
+ }
100
+
101
+ function createOptionalPositiveIntegerQuerySchema(max = null) {
102
+ const numericSchema = max == null
103
+ ? Type.Integer({ minimum: 1 })
104
+ : Type.Integer({ minimum: 1, maximum: max });
105
+
106
+ return Type.Optional(
107
+ Type.Union([
108
+ numericSchema,
109
+ Type.String({ pattern: "^[1-9][0-9]*$" })
110
+ ])
111
+ );
112
+ }
113
+
114
+ function normalizeConversationRecord(payload = {}) {
115
+ const source = normalizeObjectInput(payload);
116
+
117
+ return {
118
+ id: toPositiveInteger(source.id, 0),
119
+ workspaceId: toPositiveInteger(source.workspaceId, 0) || null,
120
+ title: normalizeText(source.title),
121
+ createdByUserId: toPositiveInteger(source.createdByUserId, 0) || null,
122
+ status: normalizeText(source.status),
123
+ provider: normalizeText(source.provider),
124
+ model: normalizeText(source.model),
125
+ surfaceId: normalizeText(source.surfaceId || source.surfaceSid).toLowerCase(),
126
+ startedAt: normalizeText(source.startedAt),
127
+ endedAt: normalizeText(source.endedAt) || null,
128
+ messageCount: Math.max(0, Number(source.messageCount || 0)),
129
+ metadata: normalizeObjectInput(source.metadata),
130
+ createdAt: normalizeText(source.createdAt),
131
+ updatedAt: normalizeText(source.updatedAt)
132
+ };
133
+ }
134
+
135
+ function normalizeConversationMessageRecord(payload = {}) {
136
+ const source = normalizeObjectInput(payload);
137
+
138
+ return {
139
+ id: toPositiveInteger(source.id, 0),
140
+ conversationId: toPositiveInteger(source.conversationId, 0),
141
+ workspaceId: toPositiveInteger(source.workspaceId, 0) || null,
142
+ seq: toPositiveInteger(source.seq, 0),
143
+ role: normalizeText(source.role),
144
+ kind: normalizeText(source.kind),
145
+ clientMessageSid: normalizeText(source.clientMessageSid),
146
+ actorUserId: toPositiveInteger(source.actorUserId, 0) || null,
147
+ contentText: source.contentText == null ? null : String(source.contentText),
148
+ metadata: normalizeObjectInput(source.metadata),
149
+ createdAt: normalizeText(source.createdAt)
150
+ };
151
+ }
152
+
153
+ const historyMessageSchema = Type.Object(
154
+ {
155
+ role: Type.Union([Type.Literal("user"), Type.Literal("assistant")]),
156
+ content: Type.String({ minLength: 1, maxLength: MAX_INPUT_CHARS })
157
+ },
158
+ { additionalProperties: false }
159
+ );
160
+
161
+ const chatStreamBodySchema = Type.Object(
162
+ {
163
+ messageId: Type.String({ minLength: 1, maxLength: 128 }),
164
+ conversationId: Type.Optional(Type.Integer({ minimum: 1 })),
165
+ input: Type.String({ minLength: 1, maxLength: MAX_INPUT_CHARS }),
166
+ history: Type.Optional(Type.Array(historyMessageSchema, { maxItems: MAX_HISTORY_MESSAGES })),
167
+ clientContext: Type.Optional(
168
+ Type.Object(
169
+ {
170
+ locale: Type.Optional(Type.String({ maxLength: 64 })),
171
+ timezone: Type.Optional(Type.String({ maxLength: 64 }))
172
+ },
173
+ { additionalProperties: false }
174
+ )
175
+ )
176
+ },
177
+ {
178
+ additionalProperties: false
179
+ }
180
+ );
181
+
182
+ const conversationRecordSchema = Type.Object(
183
+ {
184
+ id: Type.Integer({ minimum: 1 }),
185
+ workspaceId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
186
+ title: Type.String(),
187
+ createdByUserId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
188
+ status: Type.String(),
189
+ provider: Type.String(),
190
+ model: Type.String(),
191
+ surfaceId: Type.String(),
192
+ startedAt: Type.String({ minLength: 1 }),
193
+ endedAt: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
194
+ messageCount: Type.Integer({ minimum: 0 }),
195
+ metadata: Type.Record(Type.String(), Type.Unknown()),
196
+ createdAt: Type.String({ minLength: 1 }),
197
+ updatedAt: Type.String({ minLength: 1 })
198
+ },
199
+ { additionalProperties: false }
200
+ );
201
+
202
+ const conversationRecordValidator = Object.freeze({
203
+ schema: conversationRecordSchema,
204
+ normalize: normalizeConversationRecord
205
+ });
206
+
207
+ const messageRecordSchema = Type.Object(
208
+ {
209
+ id: Type.Integer({ minimum: 1 }),
210
+ conversationId: Type.Integer({ minimum: 1 }),
211
+ workspaceId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
212
+ seq: Type.Integer({ minimum: 1 }),
213
+ role: Type.String({ minLength: 1 }),
214
+ kind: Type.String({ minLength: 1 }),
215
+ clientMessageSid: Type.String(),
216
+ actorUserId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
217
+ contentText: Type.Union([Type.String(), Type.Null()]),
218
+ metadata: Type.Record(Type.String(), Type.Unknown()),
219
+ createdAt: Type.String({ minLength: 1 })
220
+ },
221
+ { additionalProperties: false }
222
+ );
223
+
224
+ const paginationProperties = Object.freeze({
225
+ page: Type.Integer({ minimum: 1 }),
226
+ pageSize: Type.Integer({ minimum: 1 }),
227
+ total: Type.Integer({ minimum: 0 }),
228
+ totalPages: Type.Integer({ minimum: 1 })
229
+ });
230
+
231
+ const assistantResource = Object.freeze({
232
+ resource: "assistant",
233
+ operations: {
234
+ chatStream: {
235
+ method: "POST",
236
+ bodyValidator: Object.freeze({
237
+ schema: chatStreamBodySchema,
238
+ normalize: normalizeChatStreamBody
239
+ })
240
+ },
241
+ conversationsList: {
242
+ method: "GET",
243
+ queryValidator: Object.freeze({
244
+ schema: Type.Object(
245
+ {
246
+ cursor: createOptionalPositiveIntegerQuerySchema(),
247
+ limit: createOptionalPositiveIntegerQuerySchema(MAX_PAGE_SIZE),
248
+ status: Type.Optional(Type.String({ minLength: 1, maxLength: 32 }))
249
+ },
250
+ { additionalProperties: false }
251
+ ),
252
+ normalize: normalizeConversationsListQuery
253
+ }),
254
+ outputValidator: createCursorListValidator(conversationRecordValidator)
255
+ },
256
+ conversationMessagesList: {
257
+ method: "GET",
258
+ paramsValidator: Object.freeze({
259
+ schema: Type.Object(
260
+ {
261
+ conversationId: Type.Union([
262
+ Type.Integer({ minimum: 1 }),
263
+ Type.String({ pattern: "^[1-9][0-9]*$" })
264
+ ])
265
+ },
266
+ { additionalProperties: false }
267
+ ),
268
+ normalize: normalizeConversationMessagesParams
269
+ }),
270
+ queryValidator: Object.freeze({
271
+ schema: Type.Object(
272
+ {
273
+ page: createOptionalPositiveIntegerQuerySchema(),
274
+ pageSize: createOptionalPositiveIntegerQuerySchema(MAX_MESSAGE_PAGE_SIZE)
275
+ },
276
+ { additionalProperties: false }
277
+ ),
278
+ normalize: normalizeConversationMessagesQuery
279
+ }),
280
+ outputValidator: Object.freeze({
281
+ schema: Type.Object(
282
+ {
283
+ ...paginationProperties,
284
+ conversation: conversationRecordSchema,
285
+ entries: Type.Array(messageRecordSchema)
286
+ },
287
+ { additionalProperties: false }
288
+ ),
289
+ normalize(payload = {}) {
290
+ const source = normalizeObjectInput(payload);
291
+ return {
292
+ conversation: normalizeConversationRecord(source.conversation),
293
+ entries: (Array.isArray(source.entries) ? source.entries : []).map(normalizeConversationMessageRecord),
294
+ page: normalizePaginationValue(source.page, 1, MAX_MESSAGE_PAGE_SIZE),
295
+ pageSize: normalizePaginationValue(source.pageSize, 200, MAX_MESSAGE_PAGE_SIZE),
296
+ total: Math.max(0, Number(source.total || 0)),
297
+ totalPages: Math.max(1, Number(source.totalPages || 1))
298
+ };
299
+ }
300
+ })
301
+ }
302
+ }
303
+ });
304
+
305
+ export {
306
+ MAX_INPUT_CHARS,
307
+ MAX_HISTORY_MESSAGES,
308
+ assistantResource
309
+ };
@@ -0,0 +1,90 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators";
4
+ import { toPositiveInteger } from "./support/positiveInteger.js";
5
+
6
+ const MAX_SYSTEM_PROMPT_CHARS = 12_000;
7
+
8
+ const assistantConfigRecordSchema = Type.Object(
9
+ {
10
+ targetSurfaceId: Type.String({ minLength: 1, maxLength: 64 }),
11
+ scopeKey: Type.String({ minLength: 1, maxLength: 160 }),
12
+ workspaceId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
13
+ settings: Type.Object(
14
+ {
15
+ systemPrompt: Type.String({ maxLength: MAX_SYSTEM_PROMPT_CHARS })
16
+ },
17
+ { additionalProperties: false }
18
+ )
19
+ },
20
+ { additionalProperties: false }
21
+ );
22
+
23
+ const assistantConfigPatchSchema = Type.Object(
24
+ {
25
+ systemPrompt: Type.Optional(
26
+ Type.String({
27
+ maxLength: MAX_SYSTEM_PROMPT_CHARS,
28
+ messages: {
29
+ maxLength: `Assistant system prompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters.`,
30
+ default: "Assistant system prompt must be valid text."
31
+ }
32
+ })
33
+ )
34
+ },
35
+ { additionalProperties: false }
36
+ );
37
+
38
+ function normalizeConfigPatch(payload = {}) {
39
+ const source = normalizeObjectInput(payload);
40
+ const normalized = {};
41
+
42
+ if (Object.hasOwn(source, "systemPrompt")) {
43
+ normalized.systemPrompt = String(source.systemPrompt || "");
44
+ }
45
+
46
+ return normalized;
47
+ }
48
+
49
+ function normalizeConfigRecord(payload = {}) {
50
+ const source = normalizeObjectInput(payload);
51
+ const settings = normalizeObjectInput(source.settings);
52
+
53
+ return {
54
+ targetSurfaceId: normalizeText(source.targetSurfaceId).toLowerCase(),
55
+ scopeKey: normalizeText(source.scopeKey),
56
+ workspaceId: toPositiveInteger(source.workspaceId, 0) || null,
57
+ settings: {
58
+ systemPrompt: String(settings.systemPrompt || "")
59
+ }
60
+ };
61
+ }
62
+
63
+ const assistantConfigResource = Object.freeze({
64
+ resource: "assistantConfig",
65
+ operations: Object.freeze({
66
+ view: Object.freeze({
67
+ method: "GET",
68
+ outputValidator: Object.freeze({
69
+ schema: assistantConfigRecordSchema,
70
+ normalize: normalizeConfigRecord
71
+ })
72
+ }),
73
+ patch: Object.freeze({
74
+ method: "PATCH",
75
+ bodyValidator: Object.freeze({
76
+ schema: assistantConfigPatchSchema,
77
+ normalize: normalizeConfigPatch
78
+ }),
79
+ outputValidator: Object.freeze({
80
+ schema: assistantConfigRecordSchema,
81
+ normalize: normalizeConfigRecord
82
+ })
83
+ })
84
+ })
85
+ });
86
+
87
+ export {
88
+ MAX_SYSTEM_PROMPT_CHARS,
89
+ assistantConfigResource
90
+ };
@@ -0,0 +1,47 @@
1
+ export {
2
+ ASSISTANT_API_RELATIVE_PATH,
3
+ ASSISTANT_SETTINGS_API_RELATIVE_PATH,
4
+ ASSISTANT_WORKSPACE_API_BASE_PATH_TEMPLATE,
5
+ ASSISTANT_PUBLIC_API_BASE_PATH,
6
+ ASSISTANT_WORKSPACE_SETTINGS_API_PATH_TEMPLATE,
7
+ ASSISTANT_PUBLIC_SETTINGS_API_PATH,
8
+ resolveAssistantApiBasePath,
9
+ resolveAssistantSettingsApiPath,
10
+ buildAssistantApiPath,
11
+ buildAssistantSettingsApiPath
12
+ } from "./assistantPaths.js";
13
+
14
+ export {
15
+ ASSISTANT_QUERY_KEY_PREFIX,
16
+ assistantRootQueryKey,
17
+ assistantScopeQueryKey,
18
+ assistantSettingsQueryKey,
19
+ assistantConversationsListQueryKey,
20
+ assistantConversationMessagesQueryKey
21
+ } from "./queryKeys.js";
22
+
23
+ export {
24
+ ASSISTANT_STREAM_EVENT_TYPES,
25
+ normalizeAssistantStreamEventType
26
+ } from "./streamEvents.js";
27
+
28
+ export {
29
+ MAX_INPUT_CHARS,
30
+ MAX_HISTORY_MESSAGES,
31
+ assistantResource
32
+ } from "./assistantResource.js";
33
+
34
+ export {
35
+ MAX_SYSTEM_PROMPT_CHARS,
36
+ assistantConfigResource
37
+ } from "./assistantSettingsResource.js";
38
+
39
+ export { assistantSettingsEvents } from "./settingsEvents.js";
40
+
41
+ export {
42
+ ASSISTANT_CONVERSATION_STATUSES,
43
+ normalizeConversationStatus
44
+ } from "./support/conversationStatus.js";
45
+
46
+ export { parseJsonObject } from "./support/jsonObject.js";
47
+ export { toPositiveInteger } from "./support/positiveInteger.js";
@@ -0,0 +1,84 @@
1
+ const ASSISTANT_QUERY_KEY_PREFIX = Object.freeze(["assistant"]);
2
+
3
+ function normalizeSurfaceId(value) {
4
+ return String(value || "").trim().toLowerCase() || "none";
5
+ }
6
+
7
+ function normalizeWorkspaceSlug(value) {
8
+ return String(value || "").trim() || "none";
9
+ }
10
+
11
+ function normalizePositiveInteger(value, fallback) {
12
+ const parsed = Number(value);
13
+ if (!Number.isInteger(parsed) || parsed < 1) {
14
+ return fallback;
15
+ }
16
+
17
+ return parsed;
18
+ }
19
+
20
+ function normalizeScopeKey({ targetSurfaceId = "", workspaceSlug = "", workspaceId = 0 } = {}) {
21
+ const normalizedWorkspaceId = normalizePositiveInteger(workspaceId, 0);
22
+ const normalizedSurfaceId = normalizeSurfaceId(targetSurfaceId);
23
+ if (normalizedWorkspaceId > 0) {
24
+ return `${normalizedSurfaceId}:workspace:${normalizedWorkspaceId}`;
25
+ }
26
+
27
+ const normalizedWorkspaceSlug = normalizeWorkspaceSlug(workspaceSlug);
28
+ if (normalizedWorkspaceSlug !== "none") {
29
+ return `${normalizedSurfaceId}:slug:${normalizedWorkspaceSlug}`;
30
+ }
31
+
32
+ return `${normalizedSurfaceId}:global`;
33
+ }
34
+
35
+ function normalizeStatus(value) {
36
+ const normalized = String(value || "").trim().toLowerCase();
37
+ return normalized || "all";
38
+ }
39
+
40
+ function normalizeConversationId(value) {
41
+ return String(normalizePositiveInteger(value, 0) || "none");
42
+ }
43
+
44
+ function assistantRootQueryKey() {
45
+ return [...ASSISTANT_QUERY_KEY_PREFIX];
46
+ }
47
+
48
+ function assistantScopeQueryKey(scope = {}) {
49
+ return [...assistantRootQueryKey(), normalizeScopeKey(scope)];
50
+ }
51
+
52
+ function assistantSettingsQueryKey(scope = {}) {
53
+ return [...assistantScopeQueryKey(scope), "settings"];
54
+ }
55
+
56
+ function assistantConversationsListQueryKey(scope = {}, { limit = 20, status = "" } = {}) {
57
+ return [
58
+ ...assistantScopeQueryKey(scope),
59
+ "conversations",
60
+ "list",
61
+ normalizePositiveInteger(limit, 20),
62
+ normalizeStatus(status)
63
+ ];
64
+ }
65
+
66
+ function assistantConversationMessagesQueryKey(scope = {}, conversationId, { page = 1, pageSize = 200 } = {}) {
67
+ return [
68
+ ...assistantScopeQueryKey(scope),
69
+ "conversations",
70
+ normalizeConversationId(conversationId),
71
+ "messages",
72
+ normalizePositiveInteger(page, 1),
73
+ normalizePositiveInteger(pageSize, 200)
74
+ ];
75
+ }
76
+
77
+ export {
78
+ ASSISTANT_QUERY_KEY_PREFIX,
79
+ assistantRootQueryKey,
80
+ assistantScopeQueryKey,
81
+ assistantSettingsQueryKey,
82
+ assistantConversationsListQueryKey,
83
+ assistantConversationMessagesQueryKey
84
+ };
@@ -0,0 +1,5 @@
1
+ const assistantSettingsEvents = Object.freeze({
2
+ settingsChanged: "assistant.settings.changed"
3
+ });
4
+
5
+ export { assistantSettingsEvents };
@@ -0,0 +1,29 @@
1
+ const ASSISTANT_STREAM_EVENT_TYPES = Object.freeze({
2
+ META: "meta",
3
+ ASSISTANT_DELTA: "assistant_delta",
4
+ ASSISTANT_MESSAGE: "assistant_message",
5
+ TOOL_CALL: "tool_call",
6
+ TOOL_RESULT: "tool_result",
7
+ ERROR: "error",
8
+ DONE: "done"
9
+ });
10
+
11
+ const STREAM_EVENT_TYPE_SET = new Set(Object.values(ASSISTANT_STREAM_EVENT_TYPES));
12
+
13
+ function normalizeAssistantStreamEventType(value, fallback = "") {
14
+ const normalized = String(value || "").trim().toLowerCase();
15
+ if (!normalized) {
16
+ return fallback;
17
+ }
18
+
19
+ if (!STREAM_EVENT_TYPE_SET.has(normalized)) {
20
+ return fallback;
21
+ }
22
+
23
+ return normalized;
24
+ }
25
+
26
+ export {
27
+ ASSISTANT_STREAM_EVENT_TYPES,
28
+ normalizeAssistantStreamEventType
29
+ };
@@ -0,0 +1,18 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ const ASSISTANT_CONVERSATION_STATUSES = Object.freeze(["active", "completed", "failed", "aborted"]);
4
+ const ASSISTANT_CONVERSATION_STATUS_SET = new Set(ASSISTANT_CONVERSATION_STATUSES);
5
+
6
+ function normalizeConversationStatus(value, { fallback = "" } = {}) {
7
+ const normalized = normalizeText(value).toLowerCase();
8
+ if (ASSISTANT_CONVERSATION_STATUS_SET.has(normalized)) {
9
+ return normalized;
10
+ }
11
+
12
+ return normalizeText(fallback).toLowerCase();
13
+ }
14
+
15
+ export {
16
+ ASSISTANT_CONVERSATION_STATUSES,
17
+ normalizeConversationStatus
18
+ };
@@ -0,0 +1,18 @@
1
+ function parseJsonObject(value) {
2
+ if (value == null) {
3
+ return {};
4
+ }
5
+
6
+ try {
7
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
8
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9
+ return {};
10
+ }
11
+
12
+ return parsed;
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ export { parseJsonObject };
@@ -0,0 +1,9 @@
1
+ import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function toPositiveInteger(value, fallback = 0) {
4
+ return normalizePositiveInteger(value, {
5
+ fallback
6
+ });
7
+ }
8
+
9
+ export { toPositiveInteger };
@@ -0,0 +1,15 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { normalizeOptionalHttpUrl } from "../src/server/lib/providers/common.js";
4
+
5
+ test("normalizeOptionalHttpUrl accepts empty values", () => {
6
+ assert.equal(normalizeOptionalHttpUrl(""), "");
7
+ assert.equal(normalizeOptionalHttpUrl(" "), "");
8
+ });
9
+
10
+ test("normalizeOptionalHttpUrl rejects non-http absolute values", () => {
11
+ assert.throws(
12
+ () => normalizeOptionalHttpUrl("cd ../89", { context: "assistant AI_BASE_URL" }),
13
+ /assistant AI_BASE_URL must be an absolute http\(s\) URL\./
14
+ );
15
+ });
@@ -0,0 +1,66 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createAssistantApi } from "../src/client/lib/assistantApi.js";
4
+
5
+ test("assistant api forwards normalized surface header on requests", async () => {
6
+ const observed = {
7
+ stream: null,
8
+ list: null,
9
+ messages: null
10
+ };
11
+
12
+ const api = createAssistantApi({
13
+ resolveBasePath: () => "/api/assistant",
14
+ resolveSurfaceId: () => "AdMiN",
15
+ async request(url, options = {}) {
16
+ if (url.includes("/messages")) {
17
+ observed.messages = { url, options };
18
+ } else {
19
+ observed.list = { url, options };
20
+ }
21
+ return {};
22
+ },
23
+ async requestStream(url, options = {}) {
24
+ observed.stream = { url, options };
25
+ return null;
26
+ }
27
+ });
28
+
29
+ await api.streamChat({
30
+ messageId: "msg_1",
31
+ input: "Hello"
32
+ });
33
+ await api.listConversations({
34
+ limit: 5
35
+ });
36
+ await api.getConversationMessages(99, {
37
+ page: 1,
38
+ pageSize: 5
39
+ });
40
+
41
+ assert.equal(observed.stream?.options?.headers?.["x-jskit-surface"], "admin");
42
+ assert.equal(observed.list?.options?.headers?.["x-jskit-surface"], "admin");
43
+ assert.equal(observed.messages?.options?.headers?.["x-jskit-surface"], "admin");
44
+ });
45
+
46
+ test("assistant api omits surface header when surface id is empty", async () => {
47
+ const observed = [];
48
+ const api = createAssistantApi({
49
+ resolveBasePath: () => "/api/assistant",
50
+ resolveSurfaceId: () => "",
51
+ async request(url, options = {}) {
52
+ observed.push({
53
+ url,
54
+ options
55
+ });
56
+ return {};
57
+ },
58
+ async requestStream(_url, _options = {}) {
59
+ return null;
60
+ }
61
+ });
62
+
63
+ await api.listConversations();
64
+ assert.equal(observed.length, 1);
65
+ assert.equal(Object.hasOwn(observed[0].options || {}, "headers"), false);
66
+ });