@kognitivedev/adapter-chat-kognitive 0.2.29

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 ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@kognitivedev/adapter-chat-kognitive",
3
+ "version": "0.2.29",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc -w --noCheck",
12
+ "prepublishOnly": "npm run build",
13
+ "test": "vitest run"
14
+ },
15
+ "dependencies": {
16
+ "@kognitivedev/shared": "^0.2.29",
17
+ "@kognitivedev/ui": "^0.2.29"
18
+ },
19
+ "description": "Kognitive chat backend adapter for @kognitivedev/ui",
20
+ "keywords": [
21
+ "kognitive",
22
+ "chat",
23
+ "adapter",
24
+ "ui"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/kognitivedev/kognitive",
30
+ "directory": "packages/adapter-chat-kognitive"
31
+ },
32
+ "homepage": "https://kognitive.dev",
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "typescript": "^5.0.0",
36
+ "vitest": "^3.0.0"
37
+ }
38
+ }
@@ -0,0 +1,331 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
4
+ import { ModerationError, type KognitiveUIMessage } from "@kognitivedev/shared";
5
+ import { createKognitiveChatBackend } from "../index";
6
+
7
+ describe("createKognitiveChatBackend", () => {
8
+ const originalFetch = globalThis.fetch;
9
+ const fetchMock = vi.fn();
10
+
11
+ const userMessage: KognitiveUIMessage = {
12
+ id: "u1",
13
+ role: "user",
14
+ parts: [{ type: "text", text: "Hello" }],
15
+ };
16
+
17
+ function loadFixture(name: string) {
18
+ const fixturePath = path.resolve(
19
+ __dirname,
20
+ "../../../shared/src/__tests__/fixtures/runtime",
21
+ `${name}.json`,
22
+ );
23
+ return JSON.parse(readFileSync(fixturePath, "utf8")) as {
24
+ request: {
25
+ sessionId: string;
26
+ messages: KognitiveUIMessage[];
27
+ expectedRequestBody: Record<string, unknown>;
28
+ };
29
+ };
30
+ }
31
+
32
+ beforeEach(() => {
33
+ fetchMock.mockReset();
34
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
35
+ });
36
+
37
+ afterEach(() => {
38
+ globalThis.fetch = originalFetch;
39
+ });
40
+
41
+ it("uses product-local kognitive routes by default", async () => {
42
+ fetchMock.mockResolvedValueOnce(new Response("", {
43
+ status: 200,
44
+ headers: { "Content-Type": "text/event-stream" },
45
+ }));
46
+
47
+ const backend = createKognitiveChatBackend();
48
+ const client = backend.createExecutionClient({ agentName: "assistant" });
49
+
50
+ await client.stream({
51
+ messages: [userMessage],
52
+ onEvent: async () => {},
53
+ });
54
+
55
+ expect(fetchMock).toHaveBeenCalledWith(
56
+ "/api/kognitive/agents/assistant/stream",
57
+ expect.objectContaining({ method: "POST" }),
58
+ );
59
+ });
60
+
61
+ it("preserves the canonical request body shape", async () => {
62
+ fetchMock.mockResolvedValueOnce(new Response("", {
63
+ status: 200,
64
+ headers: { "Content-Type": "text/event-stream" },
65
+ }));
66
+
67
+ const backend = createKognitiveChatBackend();
68
+ const client = backend.createExecutionClient({
69
+ agentName: "assistant",
70
+ runScope: "session",
71
+ resourceId: { userId: "user-1" },
72
+ });
73
+
74
+ await client.stream({
75
+ messages: [userMessage],
76
+ sessionId: "session-1",
77
+ body: { customField: true },
78
+ onEvent: async () => {},
79
+ });
80
+
81
+ expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
82
+ customField: true,
83
+ sessionId: "session-1",
84
+ runScope: "session",
85
+ resourceId: { userId: "user-1" },
86
+ messages: [{ role: "user", content: "Hello" }],
87
+ });
88
+ });
89
+
90
+ it.each([
91
+ "assistant-tool-stream",
92
+ "assistant-reasoning-stream",
93
+ "assistant-error-stream",
94
+ "assistant-cancelled-stream",
95
+ ])("matches the shared canonical request fixture for %s", async (fixtureName) => {
96
+ fetchMock.mockResolvedValueOnce(new Response("", {
97
+ status: 200,
98
+ headers: { "Content-Type": "text/event-stream" },
99
+ }));
100
+ const fixture = loadFixture(fixtureName);
101
+ const backend = createKognitiveChatBackend({
102
+ serverUrl: "http://localhost:3001",
103
+ streamPath: "/api/runtime/agents/:agentName/stream",
104
+ });
105
+
106
+ await backend.createExecutionClient({
107
+ agentName: "assistant",
108
+ }).stream({
109
+ messages: fixture.request.messages,
110
+ sessionId: fixture.request.sessionId,
111
+ onEvent: async () => {},
112
+ });
113
+
114
+ expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual(
115
+ fixture.request.expectedRequestBody,
116
+ );
117
+ });
118
+
119
+ it("supports custom stream and thread routes", async () => {
120
+ fetchMock
121
+ .mockResolvedValueOnce(new Response("", {
122
+ status: 200,
123
+ headers: { "Content-Type": "text/event-stream" },
124
+ }))
125
+ .mockResolvedValueOnce(new Response(JSON.stringify({
126
+ threads: [],
127
+ total: 0,
128
+ }), {
129
+ status: 200,
130
+ headers: { "Content-Type": "application/json" },
131
+ }));
132
+
133
+ const backend = createKognitiveChatBackend({
134
+ serverUrl: "https://example.com",
135
+ streamPath: "/api/runtime/agents/:agentName/stream",
136
+ threadBasePath: "/api/cognitive/threads/agents/:agentName",
137
+ });
138
+ const executionClient = backend.createExecutionClient({ agentName: "assistant" });
139
+ const threadClient = backend.createThreadClient!({ agentName: "assistant" })!;
140
+
141
+ await executionClient.stream({
142
+ messages: [userMessage],
143
+ onEvent: async () => {},
144
+ });
145
+ await threadClient.list({ limit: 10, offset: 5 });
146
+
147
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://example.com/api/runtime/agents/assistant/stream");
148
+ expect(fetchMock.mock.calls[1]?.[0]).toBe("https://example.com/api/cognitive/threads/agents/assistant?limit=10&offset=5");
149
+ });
150
+
151
+ it("preserves canonical tool-result and file history when sending the next turn", async () => {
152
+ fetchMock.mockResolvedValueOnce(new Response("", {
153
+ status: 200,
154
+ headers: { "Content-Type": "text/event-stream" },
155
+ }));
156
+
157
+ const backend = createKognitiveChatBackend();
158
+ const client = backend.createExecutionClient({ agentName: "assistant" });
159
+
160
+ await client.stream({
161
+ messages: [
162
+ {
163
+ id: "assistant-1",
164
+ role: "assistant",
165
+ parts: [
166
+ { type: "tool-call", toolCallId: "table-1", toolName: "table", input: { topic: "revenue" } },
167
+ { type: "tool-result", toolCallId: "table-1", toolName: "table", result: { rows: [{ region: "EMEA" }] } },
168
+ { type: "text", text: "Here is the breakdown." },
169
+ ],
170
+ },
171
+ {
172
+ id: "user-2",
173
+ role: "user",
174
+ parts: [
175
+ {
176
+ type: "file",
177
+ data: "JVBERi0xLjc=",
178
+ filename: "q3.pdf",
179
+ mediaType: "application/pdf",
180
+ },
181
+ ],
182
+ },
183
+ ],
184
+ onEvent: async () => {},
185
+ });
186
+
187
+ expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
188
+ messages: [
189
+ {
190
+ role: "assistant",
191
+ content: [
192
+ { type: "tool-call", toolCallId: "table-1", toolName: "table", input: { topic: "revenue" } },
193
+ { type: "tool-result", toolCallId: "table-1", toolName: "table", result: { rows: [{ region: "EMEA" }] } },
194
+ { type: "text", text: "Here is the breakdown." },
195
+ ],
196
+ },
197
+ {
198
+ role: "user",
199
+ content: [
200
+ {
201
+ type: "file",
202
+ data: "JVBERi0xLjc=",
203
+ filename: "q3.pdf",
204
+ mediaType: "application/pdf",
205
+ },
206
+ ],
207
+ },
208
+ ],
209
+ });
210
+ });
211
+
212
+ it("maps stream events into the normalized event sink", async () => {
213
+ fetchMock.mockResolvedValueOnce(new Response([
214
+ "event: custom",
215
+ 'data: {"role":"assistant","content":"Hi"}',
216
+ "",
217
+ "",
218
+ ].join("\n"), {
219
+ status: 200,
220
+ headers: { "Content-Type": "text/event-stream" },
221
+ }));
222
+
223
+ const onEvent = vi.fn(async () => {});
224
+ const backend = createKognitiveChatBackend();
225
+ const client = backend.createExecutionClient({ agentName: "assistant" });
226
+
227
+ await client.stream({
228
+ messages: [userMessage],
229
+ onEvent,
230
+ });
231
+
232
+ expect(onEvent).toHaveBeenCalledWith("custom", {
233
+ role: "assistant",
234
+ content: "Hi",
235
+ });
236
+ });
237
+
238
+ it("throws typed moderation errors", async () => {
239
+ fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
240
+ error: "USER_RESTRICTED",
241
+ code: "user_banned",
242
+ kind: "ban",
243
+ surface: "runtime_ui",
244
+ reasonCode: "abuse",
245
+ message: "This user is banned.",
246
+ retryable: false,
247
+ expiresAt: null,
248
+ }), {
249
+ status: 403,
250
+ headers: { "Content-Type": "application/json" },
251
+ }));
252
+
253
+ const backend = createKognitiveChatBackend();
254
+ const client = backend.createExecutionClient({ agentName: "assistant" });
255
+
256
+ await expect(client.stream({
257
+ messages: [userMessage],
258
+ onEvent: async () => {},
259
+ })).rejects.toBeInstanceOf(ModerationError);
260
+ });
261
+
262
+ it("supports thread create and update routes", async () => {
263
+ fetchMock
264
+ .mockResolvedValueOnce(new Response(JSON.stringify({
265
+ thread: { sessionId: "s1", title: "Prototype", metadata: { pinned: true } },
266
+ }), {
267
+ status: 200,
268
+ headers: { "Content-Type": "application/json" },
269
+ }))
270
+ .mockResolvedValueOnce(new Response(JSON.stringify({
271
+ thread: { sessionId: "s1", title: "Renamed" },
272
+ }), {
273
+ status: 200,
274
+ headers: { "Content-Type": "application/json" },
275
+ }))
276
+ .mockResolvedValueOnce(new Response(JSON.stringify({
277
+ thread: {
278
+ sessionId: "s1",
279
+ title: "Renamed",
280
+ metadata: { platforms: { selectedConnectionIds: ["conn-1"] } },
281
+ },
282
+ }), {
283
+ status: 200,
284
+ headers: { "Content-Type": "application/json" },
285
+ }));
286
+
287
+ const backend = createKognitiveChatBackend();
288
+ const threadClient = backend.createThreadClient!({ agentName: "assistant" })!;
289
+
290
+ await threadClient.create({ title: "Prototype", metadata: { pinned: true } });
291
+ await threadClient.rename?.("s1", "Renamed");
292
+ const updated = await threadClient.updateMetadata?.("s1", {
293
+ platforms: { selectedConnectionIds: ["conn-1"] },
294
+ });
295
+
296
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("/api/kognitive/threads/agents/assistant");
297
+ expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
298
+ title: "Prototype",
299
+ metadata: { pinned: true },
300
+ });
301
+ expect(fetchMock.mock.calls[1]?.[0]).toBe("/api/kognitive/threads/agents/assistant/s1");
302
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
303
+ title: "Renamed",
304
+ });
305
+ expect(fetchMock.mock.calls[2]?.[0]).toBe("/api/kognitive/threads/agents/assistant/s1");
306
+ expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
307
+ metadataPatch: { platforms: { selectedConnectionIds: ["conn-1"] } },
308
+ });
309
+ expect(updated?.metadata).toEqual({ platforms: { selectedConnectionIds: ["conn-1"] } });
310
+ });
311
+
312
+ it("surfaces thread metadata update errors", async () => {
313
+ fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
314
+ error: "metadata failed",
315
+ }), {
316
+ status: 500,
317
+ headers: { "Content-Type": "application/json" },
318
+ }));
319
+
320
+ const backend = createKognitiveChatBackend();
321
+ const threadClient = backend.createThreadClient!({
322
+ agentName: "assistant",
323
+ resourceId: { userId: "user-1" },
324
+ })!;
325
+
326
+ await expect(threadClient.updateMetadata?.("s1", {
327
+ platforms: { selectedConnectionIds: [] },
328
+ })).rejects.toThrow("metadata failed");
329
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("/api/kognitive/threads/agents/assistant/s1?userId=user-1");
330
+ });
331
+ });