@theihtisham/ai-agent-starter-kit 1.0.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/.env.example +33 -0
- package/Dockerfile +35 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +28 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +17 -0
- package/package.json +85 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +157 -0
- package/prisma/seed.ts +46 -0
- package/src/app/(auth)/forgot-password/page.tsx +56 -0
- package/src/app/(auth)/layout.tsx +7 -0
- package/src/app/(auth)/login/page.tsx +83 -0
- package/src/app/(auth)/signup/page.tsx +108 -0
- package/src/app/(dashboard)/agents/[id]/edit/page.tsx +68 -0
- package/src/app/(dashboard)/agents/[id]/page.tsx +114 -0
- package/src/app/(dashboard)/agents/new/page.tsx +43 -0
- package/src/app/(dashboard)/agents/page.tsx +63 -0
- package/src/app/(dashboard)/api-keys/page.tsx +139 -0
- package/src/app/(dashboard)/dashboard/page.tsx +79 -0
- package/src/app/(dashboard)/layout.tsx +16 -0
- package/src/app/(dashboard)/settings/billing/page.tsx +59 -0
- package/src/app/(dashboard)/settings/page.tsx +45 -0
- package/src/app/(dashboard)/usage/page.tsx +46 -0
- package/src/app/api/agents/[id]/chat/route.ts +100 -0
- package/src/app/api/agents/[id]/chats/route.ts +36 -0
- package/src/app/api/agents/[id]/route.ts +97 -0
- package/src/app/api/agents/route.ts +84 -0
- package/src/app/api/api-keys/[id]/route.ts +25 -0
- package/src/app/api/api-keys/route.ts +72 -0
- package/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/src/app/api/auth/register/route.ts +53 -0
- package/src/app/api/health/route.ts +26 -0
- package/src/app/api/stripe/checkout/route.ts +37 -0
- package/src/app/api/stripe/plans/route.ts +16 -0
- package/src/app/api/stripe/portal/route.ts +29 -0
- package/src/app/api/stripe/webhook/route.ts +45 -0
- package/src/app/api/usage/route.ts +43 -0
- package/src/app/globals.css +59 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/page.tsx +32 -0
- package/src/app/pricing/page.tsx +25 -0
- package/src/components/agents/agent-form.tsx +137 -0
- package/src/components/agents/model-selector.tsx +35 -0
- package/src/components/agents/tool-selector.tsx +48 -0
- package/src/components/auth-provider.tsx +17 -0
- package/src/components/billing/plan-badge.tsx +23 -0
- package/src/components/billing/pricing-table.tsx +95 -0
- package/src/components/billing/usage-meter.tsx +39 -0
- package/src/components/chat/chat-input.tsx +68 -0
- package/src/components/chat/chat-interface.tsx +152 -0
- package/src/components/chat/chat-message.tsx +50 -0
- package/src/components/chat/chat-sidebar.tsx +49 -0
- package/src/components/chat/code-block.tsx +38 -0
- package/src/components/chat/markdown-renderer.tsx +56 -0
- package/src/components/chat/streaming-text.tsx +46 -0
- package/src/components/dashboard/agent-card.tsx +52 -0
- package/src/components/dashboard/header.tsx +75 -0
- package/src/components/dashboard/sidebar.tsx +52 -0
- package/src/components/dashboard/stat-card.tsx +42 -0
- package/src/components/dashboard/usage-chart.tsx +42 -0
- package/src/components/landing/cta.tsx +30 -0
- package/src/components/landing/features.tsx +75 -0
- package/src/components/landing/hero.tsx +42 -0
- package/src/components/landing/pricing.tsx +28 -0
- package/src/components/ui/avatar.tsx +24 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +39 -0
- package/src/components/ui/card.tsx +50 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/dropdown.tsx +77 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +48 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +20 -0
- package/src/hooks/use-agent.ts +44 -0
- package/src/hooks/use-streaming.ts +82 -0
- package/src/hooks/use-subscription.ts +40 -0
- package/src/hooks/use-usage.ts +43 -0
- package/src/hooks/use-user.ts +13 -0
- package/src/lib/agents/index.ts +60 -0
- package/src/lib/agents/memory/long-term.ts +241 -0
- package/src/lib/agents/memory/manager.ts +154 -0
- package/src/lib/agents/memory/short-term.ts +155 -0
- package/src/lib/agents/memory/types.ts +68 -0
- package/src/lib/agents/orchestration/debate.ts +170 -0
- package/src/lib/agents/orchestration/index.ts +103 -0
- package/src/lib/agents/orchestration/parallel.ts +143 -0
- package/src/lib/agents/orchestration/router.ts +199 -0
- package/src/lib/agents/orchestration/sequential.ts +127 -0
- package/src/lib/agents/orchestration/types.ts +68 -0
- package/src/lib/agents/tools/calculator.ts +131 -0
- package/src/lib/agents/tools/code-executor.ts +191 -0
- package/src/lib/agents/tools/file-reader.ts +129 -0
- package/src/lib/agents/tools/index.ts +48 -0
- package/src/lib/agents/tools/registry.ts +182 -0
- package/src/lib/agents/tools/web-search.ts +83 -0
- package/src/lib/ai/agent.ts +275 -0
- package/src/lib/ai/context.ts +68 -0
- package/src/lib/ai/memory.ts +98 -0
- package/src/lib/ai/models.ts +80 -0
- package/src/lib/ai/streaming.ts +80 -0
- package/src/lib/ai/tools.ts +149 -0
- package/src/lib/auth/middleware.ts +41 -0
- package/src/lib/auth/nextauth.ts +69 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/rate-limit/limiter.ts +93 -0
- package/src/lib/rate-limit/rules.ts +38 -0
- package/src/lib/stripe/client.ts +25 -0
- package/src/lib/stripe/plans.ts +75 -0
- package/src/lib/stripe/usage.ts +123 -0
- package/src/lib/stripe/webhooks.ts +96 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/errors.ts +73 -0
- package/src/lib/utils/helpers.ts +50 -0
- package/src/lib/utils/id.ts +21 -0
- package/src/lib/utils/logger.ts +38 -0
- package/src/lib/utils/validation.ts +44 -0
- package/src/middleware.ts +13 -0
- package/src/types/agent.ts +31 -0
- package/src/types/api.ts +38 -0
- package/src/types/billing.ts +35 -0
- package/src/types/chat.ts +30 -0
- package/src/types/next-auth.d.ts +19 -0
- package/tailwind.config.ts +72 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-Term Memory — Persistent key-value store with cosine similarity search.
|
|
3
|
+
*
|
|
4
|
+
* Stores memories with optional embeddings. When no embeddings are provided,
|
|
5
|
+
* falls back to keyword-based matching. Supports eviction of the least-recently-used
|
|
6
|
+
* entries when capacity is reached.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MemoryEntry, MemoryConfig, RecallOptions } from './types';
|
|
10
|
+
import { DEFAULT_MEMORY_CONFIG } from './types';
|
|
11
|
+
|
|
12
|
+
export class LongTermMemory {
|
|
13
|
+
private store: Map<string, MemoryEntry> = new Map();
|
|
14
|
+
private readonly config: MemoryConfig;
|
|
15
|
+
private idCounter = 0;
|
|
16
|
+
|
|
17
|
+
constructor(config: Partial<MemoryConfig> = {}) {
|
|
18
|
+
this.config = { ...DEFAULT_MEMORY_CONFIG, ...config };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Store a new memory entry */
|
|
22
|
+
store_entry(
|
|
23
|
+
key: string,
|
|
24
|
+
value: string,
|
|
25
|
+
embedding?: number[],
|
|
26
|
+
metadata?: Record<string, unknown>,
|
|
27
|
+
): MemoryEntry {
|
|
28
|
+
const entry: MemoryEntry = {
|
|
29
|
+
id: `ltm_${++this.idCounter}_${Date.now()}`,
|
|
30
|
+
content: value,
|
|
31
|
+
role: 'observation',
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
embedding,
|
|
34
|
+
metadata: { ...metadata, key },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.store.set(key, entry);
|
|
38
|
+
|
|
39
|
+
// Evict least-recently-accessed entries if over capacity
|
|
40
|
+
this.evictIfNeeded();
|
|
41
|
+
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Retrieve a specific entry by key */
|
|
46
|
+
retrieve(key: string): MemoryEntry | undefined {
|
|
47
|
+
const entry = this.store.get(key);
|
|
48
|
+
if (entry) {
|
|
49
|
+
// Update timestamp to mark as recently accessed
|
|
50
|
+
entry.timestamp = Date.now();
|
|
51
|
+
}
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Search for memories matching a query using cosine similarity or keyword fallback */
|
|
56
|
+
recall(query: string, options?: RecallOptions): MemoryEntry[] {
|
|
57
|
+
const allEntries = Array.from(this.store.values());
|
|
58
|
+
|
|
59
|
+
let results: Array<{ entry: MemoryEntry; score: number }>;
|
|
60
|
+
|
|
61
|
+
// Check if query has an embedding (passed via metadata) or use keyword matching
|
|
62
|
+
const queryEmbedding = options?.minSimilarity !== undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
if (queryEmbedding) {
|
|
67
|
+
results = this.searchByEmbedding(queryEmbedding, allEntries, options);
|
|
68
|
+
} else {
|
|
69
|
+
results = this.searchByKeywords(query, allEntries);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Apply filters
|
|
73
|
+
if (options?.role) {
|
|
74
|
+
results = results.filter((r) => r.entry.role === options.role);
|
|
75
|
+
}
|
|
76
|
+
if (options?.since) {
|
|
77
|
+
results = results.filter((r) => r.entry.timestamp >= options.since!);
|
|
78
|
+
}
|
|
79
|
+
if (options?.until) {
|
|
80
|
+
results = results.filter((r) => r.entry.timestamp <= options.until!);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sort by score descending
|
|
84
|
+
results.sort((a, b) => b.score - a.score);
|
|
85
|
+
|
|
86
|
+
const topK = options?.topK ?? 5;
|
|
87
|
+
return results.slice(0, topK).map((r) => r.entry);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Remove a specific memory by key */
|
|
91
|
+
forget(key: string): boolean {
|
|
92
|
+
return this.store.delete(key);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Clear all long-term memories */
|
|
96
|
+
clear(): void {
|
|
97
|
+
this.store.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Get the number of stored entries */
|
|
101
|
+
get count(): number {
|
|
102
|
+
return this.store.size;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Get all stored entries */
|
|
106
|
+
getAll(): MemoryEntry[] {
|
|
107
|
+
return Array.from(this.store.values());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get all keys */
|
|
111
|
+
keys(): string[] {
|
|
112
|
+
return Array.from(this.store.keys());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Search entries by embedding similarity */
|
|
116
|
+
searchByEmbedding(
|
|
117
|
+
queryEmbedding: number[],
|
|
118
|
+
entries: MemoryEntry[],
|
|
119
|
+
options?: RecallOptions,
|
|
120
|
+
): Array<{ entry: MemoryEntry; score: number }> {
|
|
121
|
+
const threshold = options?.minSimilarity ?? this.config.similarityThreshold;
|
|
122
|
+
|
|
123
|
+
return entries
|
|
124
|
+
.filter((e) => e.embedding !== undefined)
|
|
125
|
+
.map((entry) => ({
|
|
126
|
+
entry,
|
|
127
|
+
score: cosineSimilarity(queryEmbedding, entry.embedding!),
|
|
128
|
+
}))
|
|
129
|
+
.filter((r) => r.score >= threshold);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Search entries by keyword matching */
|
|
133
|
+
private searchByKeywords(
|
|
134
|
+
query: string,
|
|
135
|
+
entries: MemoryEntry[],
|
|
136
|
+
): Array<{ entry: MemoryEntry; score: number }> {
|
|
137
|
+
const queryLower = query.toLowerCase();
|
|
138
|
+
const terms = queryLower.split(/\s+/).filter(Boolean);
|
|
139
|
+
|
|
140
|
+
return entries
|
|
141
|
+
.map((entry) => {
|
|
142
|
+
const contentLower = entry.content.toLowerCase();
|
|
143
|
+
const keyLower = (entry.metadata?.key as string | undefined)?.toLowerCase() ?? '';
|
|
144
|
+
|
|
145
|
+
// Score: count of matching terms + recency bonus
|
|
146
|
+
let score = 0;
|
|
147
|
+
for (const term of terms) {
|
|
148
|
+
if (contentLower.includes(term)) score += 1;
|
|
149
|
+
if (keyLower.includes(term)) score += 0.5;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Normalize by query length
|
|
153
|
+
if (terms.length > 0) {
|
|
154
|
+
score = score / terms.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Small recency bonus (0-0.2 range)
|
|
158
|
+
const ageMs = Date.now() - entry.timestamp;
|
|
159
|
+
const ageHours = ageMs / (1000 * 60 * 60);
|
|
160
|
+
const recencyBonus = Math.max(0, 0.2 - ageHours * 0.001);
|
|
161
|
+
score += recencyBonus;
|
|
162
|
+
|
|
163
|
+
return { entry, score };
|
|
164
|
+
})
|
|
165
|
+
.filter((r) => r.score > 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Evict least-recently-accessed entries when over capacity */
|
|
169
|
+
private evictIfNeeded(): void {
|
|
170
|
+
while (this.store.size > this.config.longTermMaxEntries) {
|
|
171
|
+
let oldestKey: string | null = null;
|
|
172
|
+
let oldestTime = Infinity;
|
|
173
|
+
|
|
174
|
+
this.store.forEach((entry, key) => {
|
|
175
|
+
if (entry.timestamp < oldestTime) {
|
|
176
|
+
oldestTime = entry.timestamp;
|
|
177
|
+
oldestKey = key;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (oldestKey) {
|
|
182
|
+
this.store.delete(oldestKey);
|
|
183
|
+
} else {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Compute cosine similarity between two vectors.
|
|
192
|
+
* Returns a value between -1 and 1, where 1 means identical direction.
|
|
193
|
+
*/
|
|
194
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
195
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
196
|
+
|
|
197
|
+
let dotProduct = 0;
|
|
198
|
+
let normA = 0;
|
|
199
|
+
let normB = 0;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < a.length; i++) {
|
|
202
|
+
dotProduct += a[i]! * b[i]!;
|
|
203
|
+
normA += a[i]! * a[i]!;
|
|
204
|
+
normB += b[i]! * b[i]!;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
208
|
+
if (denominator === 0) return 0;
|
|
209
|
+
|
|
210
|
+
return dotProduct / denominator;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Generate a simple embedding from text using a bag-of-words approach.
|
|
215
|
+
* This is a placeholder — in production, use OpenAI embeddings or similar.
|
|
216
|
+
*/
|
|
217
|
+
export function simpleEmbedding(text: string, dimensions: number = 128): number[] {
|
|
218
|
+
const vector = new Array(dimensions).fill(0);
|
|
219
|
+
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < words.length; i++) {
|
|
222
|
+
const word = words[i]!;
|
|
223
|
+
// Simple hash to spread across dimensions
|
|
224
|
+
let hash = 0;
|
|
225
|
+
for (let j = 0; j < word.length; j++) {
|
|
226
|
+
hash = ((hash << 5) - hash + word.charCodeAt(j)) | 0;
|
|
227
|
+
}
|
|
228
|
+
const dim = Math.abs(hash) % dimensions;
|
|
229
|
+
vector[dim]! += 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Normalize
|
|
233
|
+
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
|
234
|
+
if (norm > 0) {
|
|
235
|
+
for (let i = 0; i < vector.length; i++) {
|
|
236
|
+
vector[i] = vector[i]! / norm;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return vector;
|
|
241
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Manager — Orchestrates short-term and long-term memory.
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface for the agent memory system:
|
|
5
|
+
* - Automatic promotion of short-term entries to long-term
|
|
6
|
+
* - Summarization of short-term memory when it fills up
|
|
7
|
+
* - Combined recall across both memory layers
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ShortTermMemory } from './short-term';
|
|
11
|
+
import { LongTermMemory, simpleEmbedding } from './long-term';
|
|
12
|
+
import type {
|
|
13
|
+
MemoryEntry,
|
|
14
|
+
MemoryConfig,
|
|
15
|
+
RecallOptions,
|
|
16
|
+
MemoryStats,
|
|
17
|
+
MemorySummary,
|
|
18
|
+
} from './types';
|
|
19
|
+
import { DEFAULT_MEMORY_CONFIG } from './types';
|
|
20
|
+
|
|
21
|
+
export class MemoryManager {
|
|
22
|
+
public readonly shortTerm: ShortTermMemory;
|
|
23
|
+
public readonly longTerm: LongTermMemory;
|
|
24
|
+
private readonly config: MemoryConfig;
|
|
25
|
+
|
|
26
|
+
constructor(config: Partial<MemoryConfig> = {}) {
|
|
27
|
+
this.config = { ...DEFAULT_MEMORY_CONFIG, ...config };
|
|
28
|
+
this.shortTerm = new ShortTermMemory(config);
|
|
29
|
+
this.longTerm = new LongTermMemory(config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remember a piece of content.
|
|
34
|
+
* Stores in short-term memory. When short-term is near capacity,
|
|
35
|
+
* older entries are promoted to long-term memory.
|
|
36
|
+
*/
|
|
37
|
+
remember(
|
|
38
|
+
content: string,
|
|
39
|
+
role: MemoryEntry['role'] = 'observation',
|
|
40
|
+
metadata?: Record<string, unknown>,
|
|
41
|
+
): MemoryEntry {
|
|
42
|
+
const entry = this.shortTerm.add({ content, role, metadata });
|
|
43
|
+
|
|
44
|
+
// Check if short-term is getting full — promote older entries
|
|
45
|
+
if (this.shortTerm.count >= this.config.shortTermMaxEntries * 0.8) {
|
|
46
|
+
this.promoteToLongTerm();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recall relevant memories from both short-term and long-term stores.
|
|
54
|
+
* Returns results merged and ranked by relevance.
|
|
55
|
+
*/
|
|
56
|
+
recall(query: string, options?: RecallOptions): MemoryEntry[] {
|
|
57
|
+
const topK = options?.topK ?? 5;
|
|
58
|
+
|
|
59
|
+
// Search both stores
|
|
60
|
+
const shortTermResults = this.shortTerm.recall(query, { ...options, topK });
|
|
61
|
+
const longTermResults = this.longTerm.recall(query, { ...options, topK });
|
|
62
|
+
|
|
63
|
+
// Merge and deduplicate by ID
|
|
64
|
+
const seen = new Set<string>();
|
|
65
|
+
const merged: MemoryEntry[] = [];
|
|
66
|
+
|
|
67
|
+
// Prefer short-term results (more recent context)
|
|
68
|
+
for (const entry of shortTermResults) {
|
|
69
|
+
if (!seen.has(entry.id)) {
|
|
70
|
+
seen.add(entry.id);
|
|
71
|
+
merged.push(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add long-term results
|
|
76
|
+
for (const entry of longTermResults) {
|
|
77
|
+
if (!seen.has(entry.id)) {
|
|
78
|
+
seen.add(entry.id);
|
|
79
|
+
merged.push(entry);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return merged.slice(0, topK);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate a summary of the current short-term memory.
|
|
88
|
+
* Also stores the summary in long-term memory for future reference.
|
|
89
|
+
*/
|
|
90
|
+
summarize(): MemorySummary {
|
|
91
|
+
const summary = this.shortTerm.summarize();
|
|
92
|
+
|
|
93
|
+
// Store the summary in long-term memory
|
|
94
|
+
this.longTerm.store_entry(
|
|
95
|
+
`summary_${summary.id}`,
|
|
96
|
+
summary.summary,
|
|
97
|
+
simpleEmbedding(summary.summary),
|
|
98
|
+
{
|
|
99
|
+
type: 'summary',
|
|
100
|
+
entryCount: summary.entryCount,
|
|
101
|
+
fromTimestamp: summary.fromTimestamp,
|
|
102
|
+
toTimestamp: summary.toTimestamp,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return summary;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Clear all memory (both short-term and long-term) */
|
|
110
|
+
clear(): void {
|
|
111
|
+
this.shortTerm.clear();
|
|
112
|
+
this.longTerm.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Get statistics about the memory system */
|
|
116
|
+
getStats(): MemoryStats {
|
|
117
|
+
const stEntries = this.shortTerm.getAll();
|
|
118
|
+
const ltEntries = this.longTerm.getAll();
|
|
119
|
+
|
|
120
|
+
const allTimestamps = [...stEntries, ...ltEntries].map((e) => e.timestamp);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
shortTermCount: stEntries.length,
|
|
124
|
+
longTermCount: ltEntries.length,
|
|
125
|
+
summaryCount: this.shortTerm.getSummaries().length,
|
|
126
|
+
oldestEntry: allTimestamps.length > 0 ? Math.min(...allTimestamps) : null,
|
|
127
|
+
newestEntry: allTimestamps.length > 0 ? Math.max(...allTimestamps) : null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Promote the oldest short-term entries to long-term memory.
|
|
133
|
+
* Called automatically when short-term memory approaches capacity.
|
|
134
|
+
*/
|
|
135
|
+
private promoteToLongTerm(): void {
|
|
136
|
+
const promoteCount = Math.floor(this.config.shortTermMaxEntries * 0.3);
|
|
137
|
+
const evicted = this.shortTerm.evictOldest(promoteCount);
|
|
138
|
+
|
|
139
|
+
for (const entry of evicted) {
|
|
140
|
+
const embedding = entry.embedding ?? simpleEmbedding(entry.content);
|
|
141
|
+
this.longTerm.store_entry(
|
|
142
|
+
`promoted_${entry.id}`,
|
|
143
|
+
entry.content,
|
|
144
|
+
embedding,
|
|
145
|
+
{
|
|
146
|
+
...entry.metadata,
|
|
147
|
+
originalId: entry.id,
|
|
148
|
+
role: entry.role,
|
|
149
|
+
promotedAt: Date.now(),
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Short-Term Memory — Sliding window of recent conversation context.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the most recent N messages in memory. When the window is full,
|
|
5
|
+
* the oldest entries are evicted (and optionally summarized first).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MemoryEntry, MemorySummary, RecallOptions, MemoryConfig } from './types';
|
|
9
|
+
import { DEFAULT_MEMORY_CONFIG } from './types';
|
|
10
|
+
|
|
11
|
+
export class ShortTermMemory {
|
|
12
|
+
private entries: MemoryEntry[] = [];
|
|
13
|
+
private summaries: MemorySummary[] = [];
|
|
14
|
+
private readonly config: MemoryConfig;
|
|
15
|
+
private idCounter = 0;
|
|
16
|
+
|
|
17
|
+
constructor(config: Partial<MemoryConfig> = {}) {
|
|
18
|
+
this.config = { ...DEFAULT_MEMORY_CONFIG, ...config };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Add a new entry to short-term memory */
|
|
22
|
+
add(entry: Omit<MemoryEntry, 'id' | 'timestamp'>): MemoryEntry {
|
|
23
|
+
const fullEntry: MemoryEntry = {
|
|
24
|
+
...entry,
|
|
25
|
+
id: `stm_${++this.idCounter}_${Date.now()}`,
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.entries.push(fullEntry);
|
|
30
|
+
|
|
31
|
+
// Evict oldest entries if over capacity
|
|
32
|
+
while (this.entries.length > this.config.shortTermMaxEntries) {
|
|
33
|
+
const evicted = this.entries.shift();
|
|
34
|
+
if (evicted && this.config.autoSummarize) {
|
|
35
|
+
// The MemoryManager handles transferring evicted entries to long-term
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fullEntry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Recall entries matching the given options */
|
|
43
|
+
recall(query: string, options?: RecallOptions): MemoryEntry[] {
|
|
44
|
+
let results = [...this.entries];
|
|
45
|
+
|
|
46
|
+
// Filter by role
|
|
47
|
+
if (options?.role) {
|
|
48
|
+
results = results.filter((e) => e.role === options.role);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Filter by time range
|
|
52
|
+
if (options?.since) {
|
|
53
|
+
results = results.filter((e) => e.timestamp >= options.since!);
|
|
54
|
+
}
|
|
55
|
+
if (options?.until) {
|
|
56
|
+
results = results.filter((e) => e.timestamp <= options.until!);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Simple keyword matching for non-vector search
|
|
60
|
+
if (query.trim()) {
|
|
61
|
+
const queryLower = query.toLowerCase();
|
|
62
|
+
const terms = queryLower.split(/\s+/).filter(Boolean);
|
|
63
|
+
|
|
64
|
+
results = results.filter((entry) => {
|
|
65
|
+
const contentLower = entry.content.toLowerCase();
|
|
66
|
+
return terms.some((term) => contentLower.includes(term));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Score by number of matching terms and recency
|
|
70
|
+
results.sort((a, b) => {
|
|
71
|
+
const scoreA = terms.filter((t) => a.content.toLowerCase().includes(t)).length;
|
|
72
|
+
const scoreB = terms.filter((t) => b.content.toLowerCase().includes(t)).length;
|
|
73
|
+
if (scoreA !== scoreB) return scoreB - scoreA;
|
|
74
|
+
return b.timestamp - a.timestamp;
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
// No query — return most recent first
|
|
78
|
+
results.sort((a, b) => b.timestamp - a.timestamp);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Apply topK limit
|
|
82
|
+
const topK = options?.topK ?? 5;
|
|
83
|
+
return results.slice(0, topK);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Get all entries in chronological order */
|
|
87
|
+
getAll(): MemoryEntry[] {
|
|
88
|
+
return [...this.entries];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Generate a summary of all current entries */
|
|
92
|
+
summarize(): MemorySummary {
|
|
93
|
+
const summary: MemorySummary = {
|
|
94
|
+
id: `sum_${++this.idCounter}_${Date.now()}`,
|
|
95
|
+
summary: this.generateSummaryText(),
|
|
96
|
+
entryCount: this.entries.length,
|
|
97
|
+
fromTimestamp: this.entries[0]?.timestamp ?? Date.now(),
|
|
98
|
+
toTimestamp: this.entries[this.entries.length - 1]?.timestamp ?? Date.now(),
|
|
99
|
+
createdAt: Date.now(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.summaries.push(summary);
|
|
103
|
+
return summary;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Clear all short-term memory */
|
|
107
|
+
clear(): void {
|
|
108
|
+
this.entries = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Get the number of entries currently stored */
|
|
112
|
+
get count(): number {
|
|
113
|
+
return this.entries.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Get all summaries */
|
|
117
|
+
getSummaries(): MemorySummary[] {
|
|
118
|
+
return [...this.summaries];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Evict and return the oldest N entries */
|
|
122
|
+
evictOldest(count: number): MemoryEntry[] {
|
|
123
|
+
const evicted: MemoryEntry[] = [];
|
|
124
|
+
for (let i = 0; i < count && this.entries.length > 0; i++) {
|
|
125
|
+
const entry = this.entries.shift();
|
|
126
|
+
if (entry) evicted.push(entry);
|
|
127
|
+
}
|
|
128
|
+
return evicted;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private generateSummaryText(): string {
|
|
132
|
+
if (this.entries.length === 0) return 'No entries to summarize.';
|
|
133
|
+
|
|
134
|
+
const userMessages = this.entries
|
|
135
|
+
.filter((e) => e.role === 'user')
|
|
136
|
+
.map((e) => e.content);
|
|
137
|
+
|
|
138
|
+
const assistantMessages = this.entries
|
|
139
|
+
.filter((e) => e.role === 'assistant')
|
|
140
|
+
.map((e) => e.content);
|
|
141
|
+
|
|
142
|
+
const parts: string[] = [];
|
|
143
|
+
|
|
144
|
+
if (userMessages.length > 0) {
|
|
145
|
+
parts.push(`User discussed: ${userMessages.map((m) => `"${m.slice(0, 80)}"`).join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
if (assistantMessages.length > 0) {
|
|
148
|
+
parts.push(
|
|
149
|
+
`Assistant covered: ${assistantMessages.map((m) => `"${m.slice(0, 80)}"`).join(', ')}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return parts.join('. ') + `. (${this.entries.length} messages total)`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Memory System Types
|
|
3
|
+
*
|
|
4
|
+
* Dual-layer memory: short-term sliding window + long-term persistent store
|
|
5
|
+
* with optional embedding-based similarity search.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A single memory entry stored in the system */
|
|
9
|
+
export interface MemoryEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
content: string;
|
|
12
|
+
role: 'user' | 'assistant' | 'system' | 'observation';
|
|
13
|
+
timestamp: number;
|
|
14
|
+
embedding?: number[];
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Configuration for the memory store */
|
|
19
|
+
export interface MemoryConfig {
|
|
20
|
+
/** Maximum entries in short-term memory (default: 50) */
|
|
21
|
+
shortTermMaxEntries: number;
|
|
22
|
+
/** Maximum entries to keep in long-term memory before eviction (default: 1000) */
|
|
23
|
+
longTermMaxEntries: number;
|
|
24
|
+
/** Whether to auto-summarize when short-term memory is full (default: true) */
|
|
25
|
+
autoSummarize: boolean;
|
|
26
|
+
/** Similarity threshold for embedding-based recall (0-1, default: 0.7) */
|
|
27
|
+
similarityThreshold: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
|
|
31
|
+
shortTermMaxEntries: 50,
|
|
32
|
+
longTermMaxEntries: 1000,
|
|
33
|
+
autoSummarize: true,
|
|
34
|
+
similarityThreshold: 0.7,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Options for recalling memories */
|
|
38
|
+
export interface RecallOptions {
|
|
39
|
+
/** Maximum number of results to return (default: 5) */
|
|
40
|
+
topK?: number;
|
|
41
|
+
/** Only recall entries after this timestamp */
|
|
42
|
+
since?: number;
|
|
43
|
+
/** Only recall entries before this timestamp */
|
|
44
|
+
until?: number;
|
|
45
|
+
/** Filter by role */
|
|
46
|
+
role?: MemoryEntry['role'];
|
|
47
|
+
/** Minimum similarity score for embedding-based search (0-1) */
|
|
48
|
+
minSimilarity?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A summarized version of recent conversation */
|
|
52
|
+
export interface MemorySummary {
|
|
53
|
+
id: string;
|
|
54
|
+
summary: string;
|
|
55
|
+
entryCount: number;
|
|
56
|
+
fromTimestamp: number;
|
|
57
|
+
toTimestamp: number;
|
|
58
|
+
createdAt: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Stats about the memory system */
|
|
62
|
+
export interface MemoryStats {
|
|
63
|
+
shortTermCount: number;
|
|
64
|
+
longTermCount: number;
|
|
65
|
+
summaryCount: number;
|
|
66
|
+
oldestEntry: number | null;
|
|
67
|
+
newestEntry: number | null;
|
|
68
|
+
}
|