expo-ai-kit 0.3.6 → 0.4.0

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/build/memory.d.ts DELETED
@@ -1,235 +0,0 @@
1
- /**
2
- * Chat Memory Management for On-Device AI Models
3
- *
4
- * WHY CLIENT-MANAGED MEMORY IS REQUIRED:
5
- * --------------------------------------
6
- * On-device AI models (Apple Foundation Models, local LLMs) are stateless.
7
- * Unlike cloud-based APIs that may maintain server-side conversation state,
8
- * on-device models have no session persistence or built-in memory.
9
- *
10
- * Each generation call accepts a single prompt string and returns a response
11
- * with no knowledge of previous interactions. To enable multi-turn conversations,
12
- * the client must:
13
- *
14
- * 1. Store all messages locally (this module handles that)
15
- * 2. Build a complete prompt containing the full conversation history
16
- * 3. Send the entire context on every generation request
17
- * 4. Manage memory limits to avoid exceeding model context windows
18
- *
19
- * This approach is framework-agnostic and works with React, React Native,
20
- * Expo, or any JavaScript/TypeScript environment.
21
- */
22
- import { LLMMessage, ChatMemoryOptions, ChatMemorySnapshot } from './types';
23
- /**
24
- * Build a single prompt string from an array of messages.
25
- *
26
- * Uses a simple, deterministic format optimized for on-device models:
27
- *
28
- * ```
29
- * SYSTEM: You are a helpful assistant.
30
- * USER: Hello!
31
- * ASSISTANT: Hi there!
32
- * USER: How are you?
33
- * ```
34
- *
35
- * This format is:
36
- * - Human-readable for debugging
37
- * - Token-efficient (minimal overhead)
38
- * - Deterministic (same input = same output)
39
- * - Compatible with most instruction-following models
40
- *
41
- * @param messages - Array of messages to convert to a prompt
42
- * @returns A single prompt string ready for generation
43
- *
44
- * @example
45
- * ```ts
46
- * const prompt = buildPrompt([
47
- * { role: 'system', content: 'You are helpful.' },
48
- * { role: 'user', content: 'Hi!' },
49
- * { role: 'assistant', content: 'Hello!' },
50
- * { role: 'user', content: 'What is 2+2?' }
51
- * ]);
52
- * // Returns:
53
- * // "SYSTEM: You are helpful.\nUSER: Hi!\nASSISTANT: Hello!\nUSER: What is 2+2?"
54
- * ```
55
- */
56
- export declare function buildPrompt(messages: LLMMessage[]): string;
57
- /**
58
- * ChatMemoryManager - Manages conversation history for stateless on-device AI models.
59
- *
60
- * IMPORTANT: On-device models have no built-in memory or session state.
61
- * This class stores messages client-side and provides methods to:
62
- * - Add user and assistant messages
63
- * - Build a complete prompt from conversation history
64
- * - Limit history to prevent context overflow
65
- * - Clear or reset the conversation
66
- *
67
- * The manager automatically trims old messages when the turn limit is exceeded,
68
- * keeping the most recent exchanges while preserving the system prompt.
69
- *
70
- * @example
71
- * ```ts
72
- * // Create a memory manager with a system prompt
73
- * const memory = new ChatMemoryManager({
74
- * maxTurns: 5,
75
- * systemPrompt: 'You are a helpful coding assistant.'
76
- * });
77
- *
78
- * // Add a user message
79
- * memory.addUserMessage('How do I reverse a string in JavaScript?');
80
- *
81
- * // Get the full prompt for generation
82
- * const prompt = memory.getPrompt();
83
- * // "SYSTEM: You are a helpful coding assistant.\nUSER: How do I reverse a string in JavaScript?"
84
- *
85
- * // After generation, add the assistant's response
86
- * const response = await generate(prompt);
87
- * memory.addAssistantMessage(response);
88
- *
89
- * // Continue the conversation
90
- * memory.addUserMessage('Can you show me with an arrow function?');
91
- * const nextPrompt = memory.getPrompt();
92
- * // Now includes the full conversation history
93
- * ```
94
- */
95
- export declare class ChatMemoryManager {
96
- private messages;
97
- private systemPrompt;
98
- private maxTurns;
99
- private contextWindow;
100
- /**
101
- * Create a new ChatMemoryManager.
102
- *
103
- * @param options - Configuration options
104
- * @param options.maxTurns - Max turns to keep (default: 10). A turn = 1 user + 1 assistant message.
105
- * @param options.systemPrompt - Optional system prompt to include in every prompt.
106
- * @param options.contextWindow - Max tokens for context (default: Infinity). Trims oldest messages when exceeded.
107
- */
108
- constructor(options?: ChatMemoryOptions);
109
- /**
110
- * Add a user message to the conversation history.
111
- *
112
- * @param content - The user's message content
113
- */
114
- addUserMessage(content: string): void;
115
- /**
116
- * Add an assistant message to the conversation history.
117
- * Call this after receiving a response from the model.
118
- *
119
- * @param content - The assistant's response content
120
- */
121
- addAssistantMessage(content: string): void;
122
- /**
123
- * Add a message with any role to the conversation history.
124
- *
125
- * @param message - The message to add
126
- */
127
- addMessage(message: LLMMessage): void;
128
- /**
129
- * Get the complete prompt string for the current conversation.
130
- *
131
- * This method builds a single prompt containing:
132
- * 1. The system prompt (if set)
133
- * 2. All conversation messages within the turn limit
134
- *
135
- * Send this prompt to the on-device model for generation.
136
- *
137
- * @returns The complete prompt string
138
- */
139
- getPrompt(): string;
140
- /**
141
- * Get all messages including the system prompt as an LLMMessage array.
142
- *
143
- * Useful if you need to pass messages to an API that accepts message arrays
144
- * rather than a single prompt string.
145
- *
146
- * @returns Array of all messages including system prompt
147
- */
148
- getAllMessages(): LLMMessage[];
149
- /**
150
- * Get only the conversation messages (excludes system prompt).
151
- *
152
- * @returns Array of user and assistant messages
153
- */
154
- getMessages(): LLMMessage[];
155
- /**
156
- * Get a snapshot of the current memory state.
157
- *
158
- * Useful for debugging, persistence, or UI display.
159
- *
160
- * @returns Current memory state snapshot
161
- */
162
- getSnapshot(): ChatMemorySnapshot;
163
- /**
164
- * Get the current number of conversation turns.
165
- *
166
- * A turn is counted as a user message (assistant responses don't add turns).
167
- * This reflects how many user interactions have occurred.
168
- *
169
- * @returns Number of turns in the current conversation
170
- */
171
- getTurnCount(): number;
172
- /**
173
- * Update the system prompt.
174
- *
175
- * The system prompt is preserved separately and never trimmed by turn limits.
176
- *
177
- * @param prompt - New system prompt, or undefined to clear
178
- */
179
- setSystemPrompt(prompt: string | undefined): void;
180
- /**
181
- * Get the current system prompt.
182
- *
183
- * @returns The system prompt or undefined if not set
184
- */
185
- getSystemPrompt(): string | undefined;
186
- /**
187
- * Update the maximum number of turns to retain.
188
- *
189
- * If the new limit is lower than current turn count, older messages
190
- * will be trimmed immediately.
191
- *
192
- * @param maxTurns - New maximum turns
193
- */
194
- setMaxTurns(maxTurns: number): void;
195
- /**
196
- * Update the context window (max tokens).
197
- *
198
- * Called internally when setModel() switches to a model with a different
199
- * context window. Can also be called manually.
200
- *
201
- * @param tokens - Maximum token budget. Use Infinity to disable token-based trimming.
202
- */
203
- setContextWindow(tokens: number): void;
204
- /**
205
- * Get the current context window setting.
206
- */
207
- getContextWindow(): number;
208
- /**
209
- * Clear all conversation messages but keep the system prompt.
210
- *
211
- * Use this to start a new conversation with the same assistant persona.
212
- */
213
- clear(): void;
214
- /**
215
- * Reset everything including the system prompt.
216
- *
217
- * Use this for a complete fresh start.
218
- */
219
- reset(): void;
220
- /**
221
- * Trim conversation history to stay within limits.
222
- *
223
- * This preserves the most recent messages while removing older ones.
224
- * The system prompt is never affected by trimming.
225
- *
226
- * Trimming strategy:
227
- * 1. Trim by turn count (maxTurns) -- remove oldest user+assistant pairs
228
- * 2. Trim by token budget (contextWindow) -- remove oldest messages until under budget
229
- * Whichever limit is hit first triggers trimming.
230
- */
231
- private trimHistory;
232
- private trimByTurns;
233
- private trimByTokens;
234
- }
235
- //# sourceMappingURL=memory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAW,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAKrF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,CAe1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,iBAAiB;IAE5B,OAAO,CAAC,QAAQ,CAAoB;IAGpC,OAAO,CAAC,YAAY,CAAqB;IAGzC,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;;;OAOG;gBACS,OAAO,GAAE,iBAAsB;IAM3C;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKrC;;;;;OAKG;IACH,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK1C;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAUrC;;;;;;;;;;OAUG;IACH,SAAS,IAAI,MAAM;IAKnB;;;;;;;OAOG;IACH,cAAc,IAAI,UAAU,EAAE;IAY9B;;;;OAIG;IACH,WAAW,IAAI,UAAU,EAAE;IAI3B;;;;;;OAMG;IACH,WAAW,IAAI,kBAAkB;IASjC;;;;;;;OAOG;IACH,YAAY,IAAI,MAAM;IAItB;;;;;;OAMG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIjD;;;;OAIG;IACH,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC;;;;;;;OAOG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnC;;;;;;;OAOG;IACH,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKtC;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAIb;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAKb;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,YAAY;CAsBrB"}
package/build/memory.js DELETED
@@ -1,353 +0,0 @@
1
- /**
2
- * Chat Memory Management for On-Device AI Models
3
- *
4
- * WHY CLIENT-MANAGED MEMORY IS REQUIRED:
5
- * --------------------------------------
6
- * On-device AI models (Apple Foundation Models, local LLMs) are stateless.
7
- * Unlike cloud-based APIs that may maintain server-side conversation state,
8
- * on-device models have no session persistence or built-in memory.
9
- *
10
- * Each generation call accepts a single prompt string and returns a response
11
- * with no knowledge of previous interactions. To enable multi-turn conversations,
12
- * the client must:
13
- *
14
- * 1. Store all messages locally (this module handles that)
15
- * 2. Build a complete prompt containing the full conversation history
16
- * 3. Send the entire context on every generation request
17
- * 4. Manage memory limits to avoid exceeding model context windows
18
- *
19
- * This approach is framework-agnostic and works with React, React Native,
20
- * Expo, or any JavaScript/TypeScript environment.
21
- */
22
- // Default maximum turns if not specified
23
- const DEFAULT_MAX_TURNS = 10;
24
- /**
25
- * Build a single prompt string from an array of messages.
26
- *
27
- * Uses a simple, deterministic format optimized for on-device models:
28
- *
29
- * ```
30
- * SYSTEM: You are a helpful assistant.
31
- * USER: Hello!
32
- * ASSISTANT: Hi there!
33
- * USER: How are you?
34
- * ```
35
- *
36
- * This format is:
37
- * - Human-readable for debugging
38
- * - Token-efficient (minimal overhead)
39
- * - Deterministic (same input = same output)
40
- * - Compatible with most instruction-following models
41
- *
42
- * @param messages - Array of messages to convert to a prompt
43
- * @returns A single prompt string ready for generation
44
- *
45
- * @example
46
- * ```ts
47
- * const prompt = buildPrompt([
48
- * { role: 'system', content: 'You are helpful.' },
49
- * { role: 'user', content: 'Hi!' },
50
- * { role: 'assistant', content: 'Hello!' },
51
- * { role: 'user', content: 'What is 2+2?' }
52
- * ]);
53
- * // Returns:
54
- * // "SYSTEM: You are helpful.\nUSER: Hi!\nASSISTANT: Hello!\nUSER: What is 2+2?"
55
- * ```
56
- */
57
- export function buildPrompt(messages) {
58
- if (!messages || messages.length === 0) {
59
- return '';
60
- }
61
- // Map role to uppercase label for clarity
62
- const roleLabels = {
63
- system: 'SYSTEM',
64
- user: 'USER',
65
- assistant: 'ASSISTANT',
66
- };
67
- return messages
68
- .map((msg) => `${roleLabels[msg.role]}: ${msg.content}`)
69
- .join('\n');
70
- }
71
- /**
72
- * ChatMemoryManager - Manages conversation history for stateless on-device AI models.
73
- *
74
- * IMPORTANT: On-device models have no built-in memory or session state.
75
- * This class stores messages client-side and provides methods to:
76
- * - Add user and assistant messages
77
- * - Build a complete prompt from conversation history
78
- * - Limit history to prevent context overflow
79
- * - Clear or reset the conversation
80
- *
81
- * The manager automatically trims old messages when the turn limit is exceeded,
82
- * keeping the most recent exchanges while preserving the system prompt.
83
- *
84
- * @example
85
- * ```ts
86
- * // Create a memory manager with a system prompt
87
- * const memory = new ChatMemoryManager({
88
- * maxTurns: 5,
89
- * systemPrompt: 'You are a helpful coding assistant.'
90
- * });
91
- *
92
- * // Add a user message
93
- * memory.addUserMessage('How do I reverse a string in JavaScript?');
94
- *
95
- * // Get the full prompt for generation
96
- * const prompt = memory.getPrompt();
97
- * // "SYSTEM: You are a helpful coding assistant.\nUSER: How do I reverse a string in JavaScript?"
98
- *
99
- * // After generation, add the assistant's response
100
- * const response = await generate(prompt);
101
- * memory.addAssistantMessage(response);
102
- *
103
- * // Continue the conversation
104
- * memory.addUserMessage('Can you show me with an arrow function?');
105
- * const nextPrompt = memory.getPrompt();
106
- * // Now includes the full conversation history
107
- * ```
108
- */
109
- export class ChatMemoryManager {
110
- // Internal storage for conversation messages (excluding system prompt)
111
- messages = [];
112
- // Optional system prompt, stored separately to ensure it's never trimmed
113
- systemPrompt;
114
- // Maximum conversation turns to retain
115
- maxTurns;
116
- // Maximum context window in tokens. Infinity means no token-based trimming.
117
- contextWindow;
118
- /**
119
- * Create a new ChatMemoryManager.
120
- *
121
- * @param options - Configuration options
122
- * @param options.maxTurns - Max turns to keep (default: 10). A turn = 1 user + 1 assistant message.
123
- * @param options.systemPrompt - Optional system prompt to include in every prompt.
124
- * @param options.contextWindow - Max tokens for context (default: Infinity). Trims oldest messages when exceeded.
125
- */
126
- constructor(options = {}) {
127
- this.maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
128
- this.systemPrompt = options.systemPrompt;
129
- this.contextWindow = options.contextWindow ?? Infinity;
130
- }
131
- /**
132
- * Add a user message to the conversation history.
133
- *
134
- * @param content - The user's message content
135
- */
136
- addUserMessage(content) {
137
- this.messages.push({ role: 'user', content });
138
- this.trimHistory();
139
- }
140
- /**
141
- * Add an assistant message to the conversation history.
142
- * Call this after receiving a response from the model.
143
- *
144
- * @param content - The assistant's response content
145
- */
146
- addAssistantMessage(content) {
147
- this.messages.push({ role: 'assistant', content });
148
- this.trimHistory();
149
- }
150
- /**
151
- * Add a message with any role to the conversation history.
152
- *
153
- * @param message - The message to add
154
- */
155
- addMessage(message) {
156
- // System messages update the system prompt instead of adding to history
157
- if (message.role === 'system') {
158
- this.systemPrompt = message.content;
159
- return;
160
- }
161
- this.messages.push(message);
162
- this.trimHistory();
163
- }
164
- /**
165
- * Get the complete prompt string for the current conversation.
166
- *
167
- * This method builds a single prompt containing:
168
- * 1. The system prompt (if set)
169
- * 2. All conversation messages within the turn limit
170
- *
171
- * Send this prompt to the on-device model for generation.
172
- *
173
- * @returns The complete prompt string
174
- */
175
- getPrompt() {
176
- const allMessages = this.getAllMessages();
177
- return buildPrompt(allMessages);
178
- }
179
- /**
180
- * Get all messages including the system prompt as an LLMMessage array.
181
- *
182
- * Useful if you need to pass messages to an API that accepts message arrays
183
- * rather than a single prompt string.
184
- *
185
- * @returns Array of all messages including system prompt
186
- */
187
- getAllMessages() {
188
- const result = [];
189
- // System prompt always comes first (if present)
190
- if (this.systemPrompt) {
191
- result.push({ role: 'system', content: this.systemPrompt });
192
- }
193
- result.push(...this.messages);
194
- return result;
195
- }
196
- /**
197
- * Get only the conversation messages (excludes system prompt).
198
- *
199
- * @returns Array of user and assistant messages
200
- */
201
- getMessages() {
202
- return [...this.messages];
203
- }
204
- /**
205
- * Get a snapshot of the current memory state.
206
- *
207
- * Useful for debugging, persistence, or UI display.
208
- *
209
- * @returns Current memory state snapshot
210
- */
211
- getSnapshot() {
212
- return {
213
- messages: this.getAllMessages(),
214
- systemPrompt: this.systemPrompt,
215
- turnCount: this.getTurnCount(),
216
- maxTurns: this.maxTurns,
217
- };
218
- }
219
- /**
220
- * Get the current number of conversation turns.
221
- *
222
- * A turn is counted as a user message (assistant responses don't add turns).
223
- * This reflects how many user interactions have occurred.
224
- *
225
- * @returns Number of turns in the current conversation
226
- */
227
- getTurnCount() {
228
- return this.messages.filter((m) => m.role === 'user').length;
229
- }
230
- /**
231
- * Update the system prompt.
232
- *
233
- * The system prompt is preserved separately and never trimmed by turn limits.
234
- *
235
- * @param prompt - New system prompt, or undefined to clear
236
- */
237
- setSystemPrompt(prompt) {
238
- this.systemPrompt = prompt;
239
- }
240
- /**
241
- * Get the current system prompt.
242
- *
243
- * @returns The system prompt or undefined if not set
244
- */
245
- getSystemPrompt() {
246
- return this.systemPrompt;
247
- }
248
- /**
249
- * Update the maximum number of turns to retain.
250
- *
251
- * If the new limit is lower than current turn count, older messages
252
- * will be trimmed immediately.
253
- *
254
- * @param maxTurns - New maximum turns
255
- */
256
- setMaxTurns(maxTurns) {
257
- this.maxTurns = maxTurns;
258
- this.trimHistory();
259
- }
260
- /**
261
- * Update the context window (max tokens).
262
- *
263
- * Called internally when setModel() switches to a model with a different
264
- * context window. Can also be called manually.
265
- *
266
- * @param tokens - Maximum token budget. Use Infinity to disable token-based trimming.
267
- */
268
- setContextWindow(tokens) {
269
- this.contextWindow = tokens;
270
- this.trimHistory();
271
- }
272
- /**
273
- * Get the current context window setting.
274
- */
275
- getContextWindow() {
276
- return this.contextWindow;
277
- }
278
- /**
279
- * Clear all conversation messages but keep the system prompt.
280
- *
281
- * Use this to start a new conversation with the same assistant persona.
282
- */
283
- clear() {
284
- this.messages = [];
285
- }
286
- /**
287
- * Reset everything including the system prompt.
288
- *
289
- * Use this for a complete fresh start.
290
- */
291
- reset() {
292
- this.messages = [];
293
- this.systemPrompt = undefined;
294
- }
295
- /**
296
- * Trim conversation history to stay within limits.
297
- *
298
- * This preserves the most recent messages while removing older ones.
299
- * The system prompt is never affected by trimming.
300
- *
301
- * Trimming strategy:
302
- * 1. Trim by turn count (maxTurns) -- remove oldest user+assistant pairs
303
- * 2. Trim by token budget (contextWindow) -- remove oldest messages until under budget
304
- * Whichever limit is hit first triggers trimming.
305
- */
306
- trimHistory() {
307
- // Step 1: Trim by turn count
308
- this.trimByTurns();
309
- // Step 2: Trim by context window (if set)
310
- if (this.contextWindow !== Infinity) {
311
- this.trimByTokens();
312
- }
313
- }
314
- trimByTurns() {
315
- const userMessageIndices = [];
316
- this.messages.forEach((msg, idx) => {
317
- if (msg.role === 'user') {
318
- userMessageIndices.push(idx);
319
- }
320
- });
321
- const turnsToRemove = userMessageIndices.length - this.maxTurns;
322
- if (turnsToRemove <= 0) {
323
- return;
324
- }
325
- const lastTurnToRemoveIdx = userMessageIndices[turnsToRemove - 1];
326
- let cutIndex = lastTurnToRemoveIdx + 1;
327
- while (cutIndex < this.messages.length &&
328
- this.messages[cutIndex].role === 'assistant') {
329
- cutIndex++;
330
- }
331
- this.messages = this.messages.slice(cutIndex);
332
- }
333
- trimByTokens() {
334
- // TODO: chars/4 is a rough heuristic. Different models use different
335
- // tokenizers (SentencePiece for Gemma, BPE for others). If precision
336
- // matters, consider per-model tokenizer config. For now this is
337
- // conservative enough for trimming.
338
- const estimateTokens = (text) => Math.ceil(text.length / 4);
339
- const systemTokens = this.systemPrompt
340
- ? estimateTokens(this.systemPrompt)
341
- : 0;
342
- let totalTokens = systemTokens;
343
- for (const msg of this.messages) {
344
- totalTokens += estimateTokens(msg.content);
345
- }
346
- // Remove oldest messages until we're under budget
347
- while (totalTokens > this.contextWindow && this.messages.length > 0) {
348
- const removed = this.messages.shift();
349
- totalTokens -= estimateTokens(removed.content);
350
- }
351
- }
352
- }
353
- //# sourceMappingURL=memory.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chat Memory Management for On-Device AI Models\n *\n * WHY CLIENT-MANAGED MEMORY IS REQUIRED:\n * --------------------------------------\n * On-device AI models (Apple Foundation Models, local LLMs) are stateless.\n * Unlike cloud-based APIs that may maintain server-side conversation state,\n * on-device models have no session persistence or built-in memory.\n *\n * Each generation call accepts a single prompt string and returns a response\n * with no knowledge of previous interactions. To enable multi-turn conversations,\n * the client must:\n *\n * 1. Store all messages locally (this module handles that)\n * 2. Build a complete prompt containing the full conversation history\n * 3. Send the entire context on every generation request\n * 4. Manage memory limits to avoid exceeding model context windows\n *\n * This approach is framework-agnostic and works with React, React Native,\n * Expo, or any JavaScript/TypeScript environment.\n */\n\nimport { LLMMessage, LLMRole, ChatMemoryOptions, ChatMemorySnapshot } from './types';\n\n// Default maximum turns if not specified\nconst DEFAULT_MAX_TURNS = 10;\n\n/**\n * Build a single prompt string from an array of messages.\n *\n * Uses a simple, deterministic format optimized for on-device models:\n *\n * ```\n * SYSTEM: You are a helpful assistant.\n * USER: Hello!\n * ASSISTANT: Hi there!\n * USER: How are you?\n * ```\n *\n * This format is:\n * - Human-readable for debugging\n * - Token-efficient (minimal overhead)\n * - Deterministic (same input = same output)\n * - Compatible with most instruction-following models\n *\n * @param messages - Array of messages to convert to a prompt\n * @returns A single prompt string ready for generation\n *\n * @example\n * ```ts\n * const prompt = buildPrompt([\n * { role: 'system', content: 'You are helpful.' },\n * { role: 'user', content: 'Hi!' },\n * { role: 'assistant', content: 'Hello!' },\n * { role: 'user', content: 'What is 2+2?' }\n * ]);\n * // Returns:\n * // \"SYSTEM: You are helpful.\\nUSER: Hi!\\nASSISTANT: Hello!\\nUSER: What is 2+2?\"\n * ```\n */\nexport function buildPrompt(messages: LLMMessage[]): string {\n if (!messages || messages.length === 0) {\n return '';\n }\n\n // Map role to uppercase label for clarity\n const roleLabels: Record<LLMRole, string> = {\n system: 'SYSTEM',\n user: 'USER',\n assistant: 'ASSISTANT',\n };\n\n return messages\n .map((msg) => `${roleLabels[msg.role]}: ${msg.content}`)\n .join('\\n');\n}\n\n/**\n * ChatMemoryManager - Manages conversation history for stateless on-device AI models.\n *\n * IMPORTANT: On-device models have no built-in memory or session state.\n * This class stores messages client-side and provides methods to:\n * - Add user and assistant messages\n * - Build a complete prompt from conversation history\n * - Limit history to prevent context overflow\n * - Clear or reset the conversation\n *\n * The manager automatically trims old messages when the turn limit is exceeded,\n * keeping the most recent exchanges while preserving the system prompt.\n *\n * @example\n * ```ts\n * // Create a memory manager with a system prompt\n * const memory = new ChatMemoryManager({\n * maxTurns: 5,\n * systemPrompt: 'You are a helpful coding assistant.'\n * });\n *\n * // Add a user message\n * memory.addUserMessage('How do I reverse a string in JavaScript?');\n *\n * // Get the full prompt for generation\n * const prompt = memory.getPrompt();\n * // \"SYSTEM: You are a helpful coding assistant.\\nUSER: How do I reverse a string in JavaScript?\"\n *\n * // After generation, add the assistant's response\n * const response = await generate(prompt);\n * memory.addAssistantMessage(response);\n *\n * // Continue the conversation\n * memory.addUserMessage('Can you show me with an arrow function?');\n * const nextPrompt = memory.getPrompt();\n * // Now includes the full conversation history\n * ```\n */\nexport class ChatMemoryManager {\n // Internal storage for conversation messages (excluding system prompt)\n private messages: LLMMessage[] = [];\n\n // Optional system prompt, stored separately to ensure it's never trimmed\n private systemPrompt: string | undefined;\n\n // Maximum conversation turns to retain\n private maxTurns: number;\n\n // Maximum context window in tokens. Infinity means no token-based trimming.\n private contextWindow: number;\n\n /**\n * Create a new ChatMemoryManager.\n *\n * @param options - Configuration options\n * @param options.maxTurns - Max turns to keep (default: 10). A turn = 1 user + 1 assistant message.\n * @param options.systemPrompt - Optional system prompt to include in every prompt.\n * @param options.contextWindow - Max tokens for context (default: Infinity). Trims oldest messages when exceeded.\n */\n constructor(options: ChatMemoryOptions = {}) {\n this.maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;\n this.systemPrompt = options.systemPrompt;\n this.contextWindow = options.contextWindow ?? Infinity;\n }\n\n /**\n * Add a user message to the conversation history.\n *\n * @param content - The user's message content\n */\n addUserMessage(content: string): void {\n this.messages.push({ role: 'user', content });\n this.trimHistory();\n }\n\n /**\n * Add an assistant message to the conversation history.\n * Call this after receiving a response from the model.\n *\n * @param content - The assistant's response content\n */\n addAssistantMessage(content: string): void {\n this.messages.push({ role: 'assistant', content });\n this.trimHistory();\n }\n\n /**\n * Add a message with any role to the conversation history.\n *\n * @param message - The message to add\n */\n addMessage(message: LLMMessage): void {\n // System messages update the system prompt instead of adding to history\n if (message.role === 'system') {\n this.systemPrompt = message.content;\n return;\n }\n this.messages.push(message);\n this.trimHistory();\n }\n\n /**\n * Get the complete prompt string for the current conversation.\n *\n * This method builds a single prompt containing:\n * 1. The system prompt (if set)\n * 2. All conversation messages within the turn limit\n *\n * Send this prompt to the on-device model for generation.\n *\n * @returns The complete prompt string\n */\n getPrompt(): string {\n const allMessages = this.getAllMessages();\n return buildPrompt(allMessages);\n }\n\n /**\n * Get all messages including the system prompt as an LLMMessage array.\n *\n * Useful if you need to pass messages to an API that accepts message arrays\n * rather than a single prompt string.\n *\n * @returns Array of all messages including system prompt\n */\n getAllMessages(): LLMMessage[] {\n const result: LLMMessage[] = [];\n\n // System prompt always comes first (if present)\n if (this.systemPrompt) {\n result.push({ role: 'system', content: this.systemPrompt });\n }\n\n result.push(...this.messages);\n return result;\n }\n\n /**\n * Get only the conversation messages (excludes system prompt).\n *\n * @returns Array of user and assistant messages\n */\n getMessages(): LLMMessage[] {\n return [...this.messages];\n }\n\n /**\n * Get a snapshot of the current memory state.\n *\n * Useful for debugging, persistence, or UI display.\n *\n * @returns Current memory state snapshot\n */\n getSnapshot(): ChatMemorySnapshot {\n return {\n messages: this.getAllMessages(),\n systemPrompt: this.systemPrompt,\n turnCount: this.getTurnCount(),\n maxTurns: this.maxTurns,\n };\n }\n\n /**\n * Get the current number of conversation turns.\n *\n * A turn is counted as a user message (assistant responses don't add turns).\n * This reflects how many user interactions have occurred.\n *\n * @returns Number of turns in the current conversation\n */\n getTurnCount(): number {\n return this.messages.filter((m) => m.role === 'user').length;\n }\n\n /**\n * Update the system prompt.\n *\n * The system prompt is preserved separately and never trimmed by turn limits.\n *\n * @param prompt - New system prompt, or undefined to clear\n */\n setSystemPrompt(prompt: string | undefined): void {\n this.systemPrompt = prompt;\n }\n\n /**\n * Get the current system prompt.\n *\n * @returns The system prompt or undefined if not set\n */\n getSystemPrompt(): string | undefined {\n return this.systemPrompt;\n }\n\n /**\n * Update the maximum number of turns to retain.\n *\n * If the new limit is lower than current turn count, older messages\n * will be trimmed immediately.\n *\n * @param maxTurns - New maximum turns\n */\n setMaxTurns(maxTurns: number): void {\n this.maxTurns = maxTurns;\n this.trimHistory();\n }\n\n /**\n * Update the context window (max tokens).\n *\n * Called internally when setModel() switches to a model with a different\n * context window. Can also be called manually.\n *\n * @param tokens - Maximum token budget. Use Infinity to disable token-based trimming.\n */\n setContextWindow(tokens: number): void {\n this.contextWindow = tokens;\n this.trimHistory();\n }\n\n /**\n * Get the current context window setting.\n */\n getContextWindow(): number {\n return this.contextWindow;\n }\n\n /**\n * Clear all conversation messages but keep the system prompt.\n *\n * Use this to start a new conversation with the same assistant persona.\n */\n clear(): void {\n this.messages = [];\n }\n\n /**\n * Reset everything including the system prompt.\n *\n * Use this for a complete fresh start.\n */\n reset(): void {\n this.messages = [];\n this.systemPrompt = undefined;\n }\n\n /**\n * Trim conversation history to stay within limits.\n *\n * This preserves the most recent messages while removing older ones.\n * The system prompt is never affected by trimming.\n *\n * Trimming strategy:\n * 1. Trim by turn count (maxTurns) -- remove oldest user+assistant pairs\n * 2. Trim by token budget (contextWindow) -- remove oldest messages until under budget\n * Whichever limit is hit first triggers trimming.\n */\n private trimHistory(): void {\n // Step 1: Trim by turn count\n this.trimByTurns();\n\n // Step 2: Trim by context window (if set)\n if (this.contextWindow !== Infinity) {\n this.trimByTokens();\n }\n }\n\n private trimByTurns(): void {\n const userMessageIndices: number[] = [];\n this.messages.forEach((msg, idx) => {\n if (msg.role === 'user') {\n userMessageIndices.push(idx);\n }\n });\n\n const turnsToRemove = userMessageIndices.length - this.maxTurns;\n\n if (turnsToRemove <= 0) {\n return;\n }\n\n const lastTurnToRemoveIdx = userMessageIndices[turnsToRemove - 1];\n\n let cutIndex = lastTurnToRemoveIdx + 1;\n while (\n cutIndex < this.messages.length &&\n this.messages[cutIndex].role === 'assistant'\n ) {\n cutIndex++;\n }\n\n this.messages = this.messages.slice(cutIndex);\n }\n\n private trimByTokens(): void {\n // TODO: chars/4 is a rough heuristic. Different models use different\n // tokenizers (SentencePiece for Gemma, BPE for others). If precision\n // matters, consider per-model tokenizer config. For now this is\n // conservative enough for trimming.\n const estimateTokens = (text: string): number => Math.ceil(text.length / 4);\n\n const systemTokens = this.systemPrompt\n ? estimateTokens(this.systemPrompt)\n : 0;\n\n let totalTokens = systemTokens;\n for (const msg of this.messages) {\n totalTokens += estimateTokens(msg.content);\n }\n\n // Remove oldest messages until we're under budget\n while (totalTokens > this.contextWindow && this.messages.length > 0) {\n const removed = this.messages.shift()!;\n totalTokens -= estimateTokens(removed.content);\n }\n }\n}\n"]}