@kognitivedev/adapter-chat-local 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.
@@ -0,0 +1,2 @@
1
+
2
+ $ tsc
@@ -0,0 +1,11 @@
1
+ $ vitest run
2
+
3
+ RUN v3.2.4 /Users/vserifsaglam/work/memory-experiment/packages/adapter-chat-local
4
+
5
+ ✓ src/__tests__/local-backend.test.ts (3 tests) 6ms
6
+
7
+ Test Files 1 passed (1)
8
+ Tests 3 passed (3)
9
+ Start at 17:30:04
10
+ Duration 529ms (transform 58ms, setup 0ms, collect 64ms, tests 6ms, environment 0ms, prepare 152ms)
11
+
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @kognitivedev/adapter-chat-local
2
+
3
+ ## 0.2.29
4
+
5
+ ### Patch Changes
6
+
7
+ - release
8
+
9
+ - Updated dependencies []:
10
+ - @kognitivedev/shared@0.2.29
11
+ - @kognitivedev/ui@0.2.29
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @kognitivedev/adapter-chat-local
2
+
3
+ In-memory local chat backend adapter for `@kognitivedev/ui`.
4
+
5
+ Useful for demos, tests, and local-first prototypes.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../index");
5
+ (0, vitest_1.describe)("createLocalChatBackend", () => {
6
+ (0, vitest_1.it)("manages thread state in memory", async () => {
7
+ const backend = (0, index_1.createLocalChatBackend)();
8
+ const threadClient = backend.createThreadClient({ agentName: "assistant" });
9
+ const created = await threadClient.create({ title: "Prototype" });
10
+ const listed = await threadClient.list();
11
+ const detail = await threadClient.get(created.sessionId);
12
+ (0, vitest_1.expect)(created.title).toBe("Prototype");
13
+ (0, vitest_1.expect)(listed.total).toBe(1);
14
+ (0, vitest_1.expect)(detail.session.sessionId).toBe(created.sessionId);
15
+ });
16
+ (0, vitest_1.it)("persists assistant messages from the default responder", async () => {
17
+ const backend = (0, index_1.createLocalChatBackend)();
18
+ const executionClient = backend.createExecutionClient({ agentName: "assistant" });
19
+ const threadClient = backend.createThreadClient({ agentName: "assistant" });
20
+ await executionClient.stream({
21
+ sessionId: "s1",
22
+ messages: [{ id: "u1", role: "user", parts: [{ type: "text", text: "Hello" }] }],
23
+ onEvent: async () => { },
24
+ });
25
+ const detail = await threadClient.get("s1");
26
+ (0, vitest_1.expect)(detail.messages).toHaveLength(2);
27
+ (0, vitest_1.expect)(detail.messages[1]).toMatchObject({
28
+ role: "assistant",
29
+ content: [{ type: "text", text: "Local adapter reply: Hello" }],
30
+ });
31
+ });
32
+ (0, vitest_1.it)("supports custom streaming responders", async () => {
33
+ const onEvent = vitest_1.vi.fn(async () => { });
34
+ const backend = (0, index_1.createLocalChatBackend)({
35
+ stream: async ({ emit }) => {
36
+ await emit("debug", { step: "started" });
37
+ await emit("custom", {
38
+ role: "assistant",
39
+ content: [{ type: "text", text: "Custom reply" }],
40
+ });
41
+ },
42
+ });
43
+ await backend.createExecutionClient({ agentName: "assistant" }).stream({
44
+ sessionId: "s1",
45
+ messages: [{ id: "u1", role: "user", parts: [{ type: "text", text: "Hello" }] }],
46
+ onEvent,
47
+ });
48
+ (0, vitest_1.expect)(onEvent).toHaveBeenCalledWith("debug", { step: "started" });
49
+ (0, vitest_1.expect)(onEvent).toHaveBeenCalledWith("custom", {
50
+ role: "assistant",
51
+ content: [{ type: "text", text: "Custom reply" }],
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,20 @@
1
+ import type { KognitiveMessage, KognitiveUIMessage } from "@kognitivedev/shared";
2
+ import type { ChatBackendAdapter, ChatBackendContext, ChatBackendEventSink, ThreadSummary } from "@kognitivedev/ui";
3
+ export interface LocalChatBackendOptions {
4
+ initialThreads?: ThreadSummary[];
5
+ initialMessagesBySessionId?: Record<string, KognitiveMessage[]>;
6
+ idGenerator?: () => string;
7
+ now?: () => Date;
8
+ respond?: (input: {
9
+ messages: KognitiveUIMessage[];
10
+ sessionId?: string;
11
+ context: ChatBackendContext;
12
+ }) => Promise<KognitiveMessage> | KognitiveMessage;
13
+ stream?: (input: {
14
+ messages: KognitiveUIMessage[];
15
+ sessionId?: string;
16
+ context: ChatBackendContext;
17
+ emit: ChatBackendEventSink;
18
+ }) => Promise<KognitiveMessage | void> | KognitiveMessage | void;
19
+ }
20
+ export declare function createLocalChatBackend(options?: LocalChatBackendOptions): ChatBackendAdapter;
package/dist/index.js ADDED
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLocalChatBackend = createLocalChatBackend;
4
+ function defaultId() {
5
+ return crypto.randomUUID();
6
+ }
7
+ function defaultNow() {
8
+ return new Date();
9
+ }
10
+ function getMessageText(message) {
11
+ return message.parts
12
+ .filter((part) => part.type === "text")
13
+ .map((part) => part.text)
14
+ .join(" ")
15
+ .trim();
16
+ }
17
+ function toRuntimeMessage(message) {
18
+ return {
19
+ role: message.role,
20
+ content: message.parts,
21
+ metadata: message.metadata,
22
+ };
23
+ }
24
+ function truncate(value, limit = 72) {
25
+ const normalized = value.trim();
26
+ if (normalized.length <= limit)
27
+ return normalized;
28
+ return `${normalized.slice(0, limit - 1)}…`;
29
+ }
30
+ function textParts(parts) {
31
+ return parts
32
+ .filter((part) => part.type === "text" && typeof part.text === "string")
33
+ .map((part) => part.text)
34
+ .join(" ");
35
+ }
36
+ function defaultAssistantReply(messages) {
37
+ const lastUserMessage = [...messages].reverse().find((message) => message.role === "user");
38
+ const text = lastUserMessage ? getMessageText(lastUserMessage) : "";
39
+ return {
40
+ role: "assistant",
41
+ content: [{
42
+ type: "text",
43
+ text: text
44
+ ? `Local adapter reply: ${text}`
45
+ : "Local adapter reply ready.",
46
+ }],
47
+ };
48
+ }
49
+ function getDurableMessageId(message) {
50
+ const value = message.metadata?.kognitiveMessageId;
51
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
52
+ }
53
+ function withDurableMessageMetadata(message, fallbackId, feedback) {
54
+ const messageId = getDurableMessageId(message) ?? fallbackId;
55
+ const metadata = (message.metadata ?? {});
56
+ const { feedback: _ignoredFeedback, ...restMetadata } = metadata;
57
+ return {
58
+ ...message,
59
+ metadata: {
60
+ ...restMetadata,
61
+ kognitiveMessageId: messageId,
62
+ ...(feedback ? { feedback } : {}),
63
+ },
64
+ };
65
+ }
66
+ function createSummary(now, id, title = "", metadata = null) {
67
+ return {
68
+ sessionDbId: `local-${id}`,
69
+ sessionId: id,
70
+ title,
71
+ status: "idle",
72
+ updatedAt: now.toISOString(),
73
+ messageCount: 0,
74
+ lastUserPreview: "",
75
+ lastAssistantPreview: "",
76
+ lastError: null,
77
+ lastTraceDbId: null,
78
+ metadata,
79
+ };
80
+ }
81
+ function createDetail(record) {
82
+ return {
83
+ session: {
84
+ id: record.summary.sessionDbId,
85
+ sessionId: record.summary.sessionId,
86
+ messageCount: record.summary.messageCount,
87
+ metadata: record.summary.metadata ?? null,
88
+ },
89
+ messages: record.messages.map((message) => withDurableMessageMetadata(message, crypto.randomUUID(), (() => {
90
+ const messageId = getDurableMessageId(message);
91
+ return messageId ? record.feedbackByMessageId.get(messageId) : undefined;
92
+ })())),
93
+ events: [],
94
+ traces: [],
95
+ runs: [],
96
+ };
97
+ }
98
+ function createLocalChatBackend(options = {}) {
99
+ const makeId = options.idGenerator ?? defaultId;
100
+ const now = options.now ?? defaultNow;
101
+ const sessions = new Map();
102
+ for (const thread of options.initialThreads ?? []) {
103
+ sessions.set(thread.sessionId, {
104
+ summary: thread,
105
+ messages: (options.initialMessagesBySessionId?.[thread.sessionId] ?? []).map((message) => withDurableMessageMetadata(message, makeId())),
106
+ feedbackByMessageId: new Map(),
107
+ });
108
+ }
109
+ const ensureSession = (sessionId, input) => {
110
+ const id = sessionId ?? input?.sessionId ?? makeId();
111
+ const existing = sessions.get(id);
112
+ if (existing)
113
+ return existing;
114
+ const record = {
115
+ summary: createSummary(now(), id, input?.title ?? "", input?.metadata ?? null),
116
+ messages: [],
117
+ feedbackByMessageId: new Map(),
118
+ };
119
+ sessions.set(id, record);
120
+ return record;
121
+ };
122
+ const updateSummaryFromMessages = (record) => {
123
+ const userMessages = record.messages.filter((message) => message.role === "user");
124
+ const assistantMessages = record.messages.filter((message) => message.role === "assistant");
125
+ const latestUser = userMessages.at(-1);
126
+ const latestAssistant = assistantMessages.at(-1);
127
+ const latestUserText = typeof latestUser?.content === "string"
128
+ ? latestUser.content
129
+ : Array.isArray(latestUser?.content)
130
+ ? textParts(latestUser.content)
131
+ : "";
132
+ const latestAssistantText = typeof latestAssistant?.content === "string"
133
+ ? latestAssistant.content
134
+ : Array.isArray(latestAssistant?.content)
135
+ ? textParts(latestAssistant.content)
136
+ : "";
137
+ record.summary = {
138
+ ...record.summary,
139
+ title: record.summary.title || truncate(latestUserText, 48) || "New chat",
140
+ updatedAt: now().toISOString(),
141
+ messageCount: record.messages.length,
142
+ lastUserPreview: truncate(latestUserText),
143
+ lastAssistantPreview: truncate(latestAssistantText),
144
+ };
145
+ };
146
+ const threadManager = {
147
+ async list(query) {
148
+ const all = [...sessions.values()]
149
+ .map((record) => record.summary)
150
+ .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
151
+ const offset = query?.offset ?? 0;
152
+ const limit = query?.limit ?? all.length;
153
+ return {
154
+ threads: all.slice(offset, offset + limit),
155
+ total: all.length,
156
+ };
157
+ },
158
+ async get(sessionId) {
159
+ const record = sessions.get(sessionId);
160
+ if (!record) {
161
+ throw new Error(`Thread "${sessionId}" not found.`);
162
+ }
163
+ return createDetail(record);
164
+ },
165
+ async create(input) {
166
+ const record = ensureSession(undefined, input);
167
+ updateSummaryFromMessages(record);
168
+ return record.summary;
169
+ },
170
+ async delete(sessionId) {
171
+ sessions.delete(sessionId);
172
+ },
173
+ async rename(sessionId, title) {
174
+ const record = ensureSession(sessionId);
175
+ record.summary = {
176
+ ...record.summary,
177
+ title,
178
+ updatedAt: now().toISOString(),
179
+ };
180
+ return record.summary;
181
+ },
182
+ async archive(sessionId) {
183
+ const record = ensureSession(sessionId);
184
+ record.summary = {
185
+ ...record.summary,
186
+ status: "archived",
187
+ updatedAt: now().toISOString(),
188
+ };
189
+ return record.summary;
190
+ },
191
+ async unarchive(sessionId) {
192
+ const record = ensureSession(sessionId);
193
+ record.summary = {
194
+ ...record.summary,
195
+ status: "idle",
196
+ updatedAt: now().toISOString(),
197
+ };
198
+ return record.summary;
199
+ },
200
+ async setMessageFeedback(sessionId, messageId, value) {
201
+ const record = ensureSession(sessionId);
202
+ if (value) {
203
+ record.feedbackByMessageId.set(messageId, value);
204
+ }
205
+ else {
206
+ record.feedbackByMessageId.delete(messageId);
207
+ }
208
+ record.messages = record.messages.map((message) => {
209
+ if (getDurableMessageId(message) !== messageId)
210
+ return message;
211
+ return withDurableMessageMetadata(message, messageId, value ?? undefined);
212
+ });
213
+ },
214
+ };
215
+ return {
216
+ capabilities: {
217
+ threads: true,
218
+ threadDelete: true,
219
+ threadRename: true,
220
+ threadArchive: true,
221
+ messageFeedback: true,
222
+ },
223
+ createExecutionClient(context) {
224
+ return {
225
+ async stream(request) {
226
+ const record = ensureSession(request.sessionId);
227
+ record.messages = request.messages
228
+ .map(toRuntimeMessage)
229
+ .map((message) => withDurableMessageMetadata(message, makeId(), (() => {
230
+ const messageId = getDurableMessageId(message);
231
+ return messageId ? record.feedbackByMessageId.get(messageId) : undefined;
232
+ })()));
233
+ let assistantMessage;
234
+ if (options.stream) {
235
+ const streamedMessages = [];
236
+ const result = await options.stream({
237
+ messages: request.messages,
238
+ sessionId: request.sessionId,
239
+ context,
240
+ emit: async (eventName, data) => {
241
+ if (eventName === "custom" && data && typeof data === "object" && "role" in data) {
242
+ streamedMessages.push(withDurableMessageMetadata(data, makeId()));
243
+ }
244
+ await request.onEvent(eventName, data);
245
+ },
246
+ });
247
+ assistantMessage = result ? withDurableMessageMetadata(result, makeId()) : streamedMessages.at(-1);
248
+ }
249
+ else {
250
+ assistantMessage = withDurableMessageMetadata(options.respond
251
+ ? await options.respond({ messages: request.messages, sessionId: request.sessionId, context })
252
+ : defaultAssistantReply(request.messages), makeId());
253
+ await request.onEvent("custom", assistantMessage);
254
+ }
255
+ if (assistantMessage) {
256
+ record.messages = [...record.messages, assistantMessage];
257
+ updateSummaryFromMessages(record);
258
+ }
259
+ },
260
+ };
261
+ },
262
+ createThreadClient() {
263
+ return threadManager;
264
+ },
265
+ };
266
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@kognitivedev/adapter-chat-local",
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": "In-memory local chat backend adapter for @kognitivedev/ui",
20
+ "keywords": [
21
+ "kognitive",
22
+ "chat",
23
+ "local",
24
+ "adapter"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/kognitivedev/kognitive",
30
+ "directory": "packages/adapter-chat-local"
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,61 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createLocalChatBackend } from "../index";
3
+
4
+ describe("createLocalChatBackend", () => {
5
+ it("manages thread state in memory", async () => {
6
+ const backend = createLocalChatBackend();
7
+ const threadClient = backend.createThreadClient!({ agentName: "assistant" })!;
8
+
9
+ const created = await threadClient.create({ title: "Prototype" });
10
+ const listed = await threadClient.list();
11
+ const detail = await threadClient.get(created.sessionId);
12
+
13
+ expect(created.title).toBe("Prototype");
14
+ expect(listed.total).toBe(1);
15
+ expect(detail.session.sessionId).toBe(created.sessionId);
16
+ });
17
+
18
+ it("persists assistant messages from the default responder", async () => {
19
+ const backend = createLocalChatBackend();
20
+ const executionClient = backend.createExecutionClient({ agentName: "assistant" });
21
+ const threadClient = backend.createThreadClient!({ agentName: "assistant" })!;
22
+
23
+ await executionClient.stream({
24
+ sessionId: "s1",
25
+ messages: [{ id: "u1", role: "user", parts: [{ type: "text", text: "Hello" }] }],
26
+ onEvent: async () => {},
27
+ });
28
+
29
+ const detail = await threadClient.get("s1");
30
+ expect(detail.messages).toHaveLength(2);
31
+ expect(detail.messages[1]).toMatchObject({
32
+ role: "assistant",
33
+ content: [{ type: "text", text: "Local adapter reply: Hello" }],
34
+ });
35
+ });
36
+
37
+ it("supports custom streaming responders", async () => {
38
+ const onEvent = vi.fn(async () => {});
39
+ const backend = createLocalChatBackend({
40
+ stream: async ({ emit }) => {
41
+ await emit("debug", { step: "started" });
42
+ await emit("custom", {
43
+ role: "assistant",
44
+ content: [{ type: "text", text: "Custom reply" }],
45
+ });
46
+ },
47
+ });
48
+
49
+ await backend.createExecutionClient({ agentName: "assistant" }).stream({
50
+ sessionId: "s1",
51
+ messages: [{ id: "u1", role: "user", parts: [{ type: "text", text: "Hello" }] }],
52
+ onEvent,
53
+ });
54
+
55
+ expect(onEvent).toHaveBeenCalledWith("debug", { step: "started" });
56
+ expect(onEvent).toHaveBeenCalledWith("custom", {
57
+ role: "assistant",
58
+ content: [{ type: "text", text: "Custom reply" }],
59
+ });
60
+ });
61
+ });
package/src/index.ts ADDED
@@ -0,0 +1,332 @@
1
+ import type { KognitiveContentPart, KognitiveMessage, KognitiveUIMessage } from "@kognitivedev/shared";
2
+ import type {
3
+ ChatBackendAdapter,
4
+ ChatBackendContext,
5
+ ChatBackendEventSink,
6
+ ChatThreadClient,
7
+ ThreadCreateInput,
8
+ ThreadDetail,
9
+ ThreadMetadata,
10
+ ThreadSummary,
11
+ } from "@kognitivedev/ui";
12
+
13
+ export interface LocalChatBackendOptions {
14
+ initialThreads?: ThreadSummary[];
15
+ initialMessagesBySessionId?: Record<string, KognitiveMessage[]>;
16
+ idGenerator?: () => string;
17
+ now?: () => Date;
18
+ respond?: (input: {
19
+ messages: KognitiveUIMessage[];
20
+ sessionId?: string;
21
+ context: ChatBackendContext;
22
+ }) => Promise<KognitiveMessage> | KognitiveMessage;
23
+ stream?: (input: {
24
+ messages: KognitiveUIMessage[];
25
+ sessionId?: string;
26
+ context: ChatBackendContext;
27
+ emit: ChatBackendEventSink;
28
+ }) => Promise<KognitiveMessage | void> | KognitiveMessage | void;
29
+ }
30
+
31
+ interface LocalSessionRecord {
32
+ summary: ThreadSummary;
33
+ messages: KognitiveMessage[];
34
+ feedbackByMessageId: Map<string, "positive" | "negative">;
35
+ }
36
+
37
+ function defaultId() {
38
+ return crypto.randomUUID();
39
+ }
40
+
41
+ function defaultNow() {
42
+ return new Date();
43
+ }
44
+
45
+ function getMessageText(message: KognitiveUIMessage): string {
46
+ return message.parts
47
+ .filter((part): part is Extract<KognitiveUIMessage["parts"][number], { type: "text" }> => part.type === "text")
48
+ .map((part) => part.text)
49
+ .join(" ")
50
+ .trim();
51
+ }
52
+
53
+ function toRuntimeMessage(message: KognitiveUIMessage): KognitiveMessage {
54
+ return {
55
+ role: message.role,
56
+ content: message.parts,
57
+ metadata: message.metadata,
58
+ };
59
+ }
60
+
61
+ function truncate(value: string, limit = 72) {
62
+ const normalized = value.trim();
63
+ if (normalized.length <= limit) return normalized;
64
+ return `${normalized.slice(0, limit - 1)}…`;
65
+ }
66
+
67
+ function textParts(parts: readonly KognitiveContentPart[]) {
68
+ return parts
69
+ .filter((part): part is Extract<KognitiveContentPart, { type: "text" }> => part.type === "text" && typeof part.text === "string")
70
+ .map((part) => part.text)
71
+ .join(" ");
72
+ }
73
+
74
+ function defaultAssistantReply(messages: KognitiveUIMessage[]): KognitiveMessage {
75
+ const lastUserMessage = [...messages].reverse().find((message) => message.role === "user");
76
+ const text = lastUserMessage ? getMessageText(lastUserMessage) : "";
77
+ return {
78
+ role: "assistant",
79
+ content: [{
80
+ type: "text",
81
+ text: text
82
+ ? `Local adapter reply: ${text}`
83
+ : "Local adapter reply ready.",
84
+ }],
85
+ };
86
+ }
87
+
88
+ function getDurableMessageId(message: Pick<KognitiveMessage, "metadata">): string | null {
89
+ const value = message.metadata?.kognitiveMessageId;
90
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
91
+ }
92
+
93
+ function withDurableMessageMetadata(
94
+ message: KognitiveMessage,
95
+ fallbackId: string,
96
+ feedback?: "positive" | "negative",
97
+ ): KognitiveMessage {
98
+ const messageId = getDurableMessageId(message) ?? fallbackId;
99
+ const metadata = (message.metadata ?? {}) as Record<string, unknown>;
100
+ const { feedback: _ignoredFeedback, ...restMetadata } = metadata;
101
+ return {
102
+ ...message,
103
+ metadata: {
104
+ ...restMetadata,
105
+ kognitiveMessageId: messageId,
106
+ ...(feedback ? { feedback } : {}),
107
+ },
108
+ };
109
+ }
110
+
111
+ function createSummary(now: Date, id: string, title = "", metadata: ThreadMetadata | null = null): ThreadSummary {
112
+ return {
113
+ sessionDbId: `local-${id}`,
114
+ sessionId: id,
115
+ title,
116
+ status: "idle",
117
+ updatedAt: now.toISOString(),
118
+ messageCount: 0,
119
+ lastUserPreview: "",
120
+ lastAssistantPreview: "",
121
+ lastError: null,
122
+ lastTraceDbId: null,
123
+ metadata,
124
+ };
125
+ }
126
+
127
+ function createDetail(record: LocalSessionRecord): ThreadDetail {
128
+ return {
129
+ session: {
130
+ id: record.summary.sessionDbId,
131
+ sessionId: record.summary.sessionId,
132
+ messageCount: record.summary.messageCount,
133
+ metadata: record.summary.metadata ?? null,
134
+ },
135
+ messages: record.messages.map((message) =>
136
+ withDurableMessageMetadata(
137
+ message,
138
+ crypto.randomUUID(),
139
+ (() => {
140
+ const messageId = getDurableMessageId(message);
141
+ return messageId ? record.feedbackByMessageId.get(messageId) : undefined;
142
+ })(),
143
+ ),
144
+ ),
145
+ events: [],
146
+ traces: [],
147
+ runs: [],
148
+ };
149
+ }
150
+
151
+ export function createLocalChatBackend(options: LocalChatBackendOptions = {}): ChatBackendAdapter {
152
+ const makeId = options.idGenerator ?? defaultId;
153
+ const now = options.now ?? defaultNow;
154
+ const sessions = new Map<string, LocalSessionRecord>();
155
+
156
+ for (const thread of options.initialThreads ?? []) {
157
+ sessions.set(thread.sessionId, {
158
+ summary: thread,
159
+ messages: (options.initialMessagesBySessionId?.[thread.sessionId] ?? []).map((message) =>
160
+ withDurableMessageMetadata(message, makeId()),
161
+ ),
162
+ feedbackByMessageId: new Map(),
163
+ });
164
+ }
165
+
166
+ const ensureSession = (sessionId?: string, input?: ThreadCreateInput) => {
167
+ const id = sessionId ?? input?.sessionId ?? makeId();
168
+ const existing = sessions.get(id);
169
+ if (existing) return existing;
170
+ const record: LocalSessionRecord = {
171
+ summary: createSummary(now(), id, input?.title ?? "", input?.metadata ?? null),
172
+ messages: [],
173
+ feedbackByMessageId: new Map(),
174
+ };
175
+ sessions.set(id, record);
176
+ return record;
177
+ };
178
+
179
+ const updateSummaryFromMessages = (record: LocalSessionRecord) => {
180
+ const userMessages = record.messages.filter((message) => message.role === "user");
181
+ const assistantMessages = record.messages.filter((message) => message.role === "assistant");
182
+ const latestUser = userMessages.at(-1);
183
+ const latestAssistant = assistantMessages.at(-1);
184
+ const latestUserText = typeof latestUser?.content === "string"
185
+ ? latestUser.content
186
+ : Array.isArray(latestUser?.content)
187
+ ? textParts(latestUser.content)
188
+ : "";
189
+ const latestAssistantText = typeof latestAssistant?.content === "string"
190
+ ? latestAssistant.content
191
+ : Array.isArray(latestAssistant?.content)
192
+ ? textParts(latestAssistant.content)
193
+ : "";
194
+
195
+ record.summary = {
196
+ ...record.summary,
197
+ title: record.summary.title || truncate(latestUserText, 48) || "New chat",
198
+ updatedAt: now().toISOString(),
199
+ messageCount: record.messages.length,
200
+ lastUserPreview: truncate(latestUserText),
201
+ lastAssistantPreview: truncate(latestAssistantText),
202
+ };
203
+ };
204
+
205
+ const threadManager: ChatThreadClient = {
206
+ async list(query) {
207
+ const all = [...sessions.values()]
208
+ .map((record) => record.summary)
209
+ .sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
210
+ const offset = query?.offset ?? 0;
211
+ const limit = query?.limit ?? all.length;
212
+ return {
213
+ threads: all.slice(offset, offset + limit),
214
+ total: all.length,
215
+ };
216
+ },
217
+ async get(sessionId) {
218
+ const record = sessions.get(sessionId);
219
+ if (!record) {
220
+ throw new Error(`Thread "${sessionId}" not found.`);
221
+ }
222
+ return createDetail(record);
223
+ },
224
+ async create(input) {
225
+ const record = ensureSession(undefined, input);
226
+ updateSummaryFromMessages(record);
227
+ return record.summary;
228
+ },
229
+ async delete(sessionId) {
230
+ sessions.delete(sessionId);
231
+ },
232
+ async rename(sessionId, title) {
233
+ const record = ensureSession(sessionId);
234
+ record.summary = {
235
+ ...record.summary,
236
+ title,
237
+ updatedAt: now().toISOString(),
238
+ };
239
+ return record.summary;
240
+ },
241
+ async archive(sessionId) {
242
+ const record = ensureSession(sessionId);
243
+ record.summary = {
244
+ ...record.summary,
245
+ status: "archived",
246
+ updatedAt: now().toISOString(),
247
+ };
248
+ return record.summary;
249
+ },
250
+ async unarchive(sessionId) {
251
+ const record = ensureSession(sessionId);
252
+ record.summary = {
253
+ ...record.summary,
254
+ status: "idle",
255
+ updatedAt: now().toISOString(),
256
+ };
257
+ return record.summary;
258
+ },
259
+ async setMessageFeedback(sessionId, messageId, value) {
260
+ const record = ensureSession(sessionId);
261
+ if (value) {
262
+ record.feedbackByMessageId.set(messageId, value);
263
+ } else {
264
+ record.feedbackByMessageId.delete(messageId);
265
+ }
266
+ record.messages = record.messages.map((message) => {
267
+ if (getDurableMessageId(message) !== messageId) return message;
268
+ return withDurableMessageMetadata(message, messageId, value ?? undefined);
269
+ });
270
+ },
271
+ };
272
+
273
+ return {
274
+ capabilities: {
275
+ threads: true,
276
+ threadDelete: true,
277
+ threadRename: true,
278
+ threadArchive: true,
279
+ messageFeedback: true,
280
+ },
281
+ createExecutionClient(context) {
282
+ return {
283
+ async stream(request) {
284
+ const record = ensureSession(request.sessionId);
285
+ record.messages = request.messages
286
+ .map(toRuntimeMessage)
287
+ .map((message) => withDurableMessageMetadata(
288
+ message,
289
+ makeId(),
290
+ (() => {
291
+ const messageId = getDurableMessageId(message);
292
+ return messageId ? record.feedbackByMessageId.get(messageId) : undefined;
293
+ })(),
294
+ ));
295
+
296
+ let assistantMessage: KognitiveMessage | undefined;
297
+ if (options.stream) {
298
+ const streamedMessages: KognitiveMessage[] = [];
299
+ const result = await options.stream({
300
+ messages: request.messages,
301
+ sessionId: request.sessionId,
302
+ context,
303
+ emit: async (eventName, data) => {
304
+ if (eventName === "custom" && data && typeof data === "object" && "role" in (data as Record<string, unknown>)) {
305
+ streamedMessages.push(withDurableMessageMetadata(data as KognitiveMessage, makeId()));
306
+ }
307
+ await request.onEvent(eventName, data);
308
+ },
309
+ });
310
+ assistantMessage = result ? withDurableMessageMetadata(result, makeId()) : streamedMessages.at(-1);
311
+ } else {
312
+ assistantMessage = withDurableMessageMetadata(
313
+ options.respond
314
+ ? await options.respond({ messages: request.messages, sessionId: request.sessionId, context })
315
+ : defaultAssistantReply(request.messages),
316
+ makeId(),
317
+ );
318
+ await request.onEvent("custom", assistantMessage);
319
+ }
320
+
321
+ if (assistantMessage) {
322
+ record.messages = [...record.messages, assistantMessage];
323
+ updateSummaryFromMessages(record);
324
+ }
325
+ },
326
+ };
327
+ },
328
+ createThreadClient() {
329
+ return threadManager;
330
+ },
331
+ };
332
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "jsx": "react-jsx",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": ["src"]
14
+ }