clawlet 0.2.1 → 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/src/memory.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { createStorage, type Storage } from "unstorage";
2
2
  import fsDriver from "unstorage/drivers/fs";
3
- import { type ModelMessage } from "ai";
3
+ import { generateText, type LanguageModel, type ModelMessage } from "ai";
4
4
  import path from "path";
5
- import { LibSqlKeyValueStorage, LibSqlListStorage, SkillHistoryStorage } from "./storage.js";
5
+ import { LibSqlKeyValueStorage, LibSqlListStorage } from "./storage.js";
6
+ import { logger } from './logger.js';
7
+ import memoryDriver from 'unstorage/drivers/memory';
8
+
9
+
10
+ // --- COMPACTION CONFIG ---
11
+ const COMPACT_THRESHOLD = 25; // Trigger compaction when history reaches this many items
12
+ const COMPACT_RANGE = 10; // Number of messages to summarize (items 1..10, skipping system prompt at 0)
6
13
 
7
14
  export class AgentMemory {
8
15
  // 1. Secrets (libSQL - file:secrets.db)
@@ -11,37 +18,98 @@ export class AgentMemory {
11
18
  // 2. History (libSQL - file:history.db)
12
19
  public history: LibSqlListStorage<ModelMessage>;
13
20
 
14
- // 3. Skill History (libSQL - file:history.db, table: skill_history)
15
- public skillHistory: SkillHistoryStorage<ModelMessage>;
16
-
17
- // 4. Workspace (Unstorage - ./workspace)
21
+ // 3. Workspace (Unstorage - ./workspace)
18
22
  public workspace: Storage;
19
23
 
20
- constructor() {
21
- // A. Init Secrets DB
22
- // In Production: process.env.SECRETS_DB_URL (libsql://...)
23
- this.secrets = new LibSqlKeyValueStorage(
24
- process.env.SECRETS_DB_URL || "file:secrets.db",
25
- process.env.SECRETS_AUTH_TOKEN
26
- );
24
+ private constructor(secrets: LibSqlKeyValueStorage, history: LibSqlListStorage<ModelMessage>, workspace: Storage) {
25
+ this.secrets = secrets;
26
+ this.history = history;
27
+ this.workspace = workspace
28
+ }
27
29
 
28
- // B. Init History DB
29
- // In Production: process.env.HISTORY_DB_URL (libsql://...)
30
- this.history = new LibSqlListStorage<ModelMessage>(
31
- process.env.HISTORY_DB_URL || "file:history.db",
32
- process.env.HISTORY_AUTH_TOKEN
30
+ static async createInMemory() {
31
+ return new AgentMemory(
32
+ await LibSqlKeyValueStorage.create(':memory:'),
33
+ await LibSqlListStorage.create<ModelMessage>(':memory:'),
34
+ createStorage({ driver: memoryDriver() })
33
35
  );
36
+ }
34
37
 
35
- // C. Init Skill History (same DB as history, different table)
36
- this.skillHistory = new SkillHistoryStorage<ModelMessage>(
37
- process.env.HISTORY_DB_URL || "file:history.db",
38
- process.env.HISTORY_AUTH_TOKEN
38
+ static async create() {
39
+ return new AgentMemory(
40
+ await LibSqlKeyValueStorage.create(
41
+ process.env.SECRETS_DB_URL || "file:secrets.db",
42
+ process.env.SECRETS_AUTH_TOKEN
43
+ ),
44
+ await LibSqlListStorage.create<ModelMessage>(
45
+ process.env.HISTORY_DB_URL || "file:history.db",
46
+ process.env.HISTORY_AUTH_TOKEN
47
+ ),
48
+ createStorage({
49
+ driver: fsDriver({ base: path.join(process.cwd(), "workspace") })
50
+ })
39
51
  );
52
+ }
53
+
54
+
55
+ /**
56
+ * Compacts history when it reaches COMPACT_THRESHOLD items.
57
+ * Summarizes items 1..COMPACT_RANGE (the system prompt is not included) into a single message
58
+ * using the LLM, then replaces in-memory + persisted history.
59
+ * Result: summary message + remaining messages.
60
+ */
61
+ async compactHistory(name:string, model: LanguageModel): Promise<ModelMessage[]> {
62
+ const messages = await this.history.getAll(name);
63
+ if (messages.length < COMPACT_THRESHOLD) return messages;
64
+
65
+ logger.info({count: messages.length}, `messages to be compacted.`);
66
+
67
+ const toSummarize = messages.slice(0, COMPACT_RANGE);
68
+ const remaining = messages.slice(COMPACT_RANGE);
69
+
70
+ // Build a transcript for the LLM to summarize
71
+ const transcript = toSummarize.map(m => {
72
+ const role = m.role ?? 'unknown';
73
+ const content = typeof m.content === 'string'
74
+ ? m.content
75
+ : JSON.stringify(m.content);
76
+ return `[${role}]: ${content}`;
77
+ }).join('\n\n');
78
+
79
+ try {
80
+ const { text: summary } = await generateText({
81
+ model,
82
+ messages: [
83
+ {
84
+ role: 'system',
85
+ content: 'You are a conversation summarizer. Summarize the following conversation transcript concisely, preserving key facts, decisions, tool results, and context that would be needed to continue the conversation. Be factual and dense. Do not add commentary.',
86
+ },
87
+ {
88
+ role: 'user',
89
+ content: `Summarize this conversation transcript:\n\n${transcript}`,
90
+ },
91
+ ],
92
+ temperature: 0.3,
93
+ });
94
+
95
+ const summaryMessage: ModelMessage = {
96
+ role: 'assistant',
97
+ content: `[Conversation Summary — compacted ${COMPACT_RANGE} messages]\n\n${summary}`,
98
+ };
99
+
100
+ // Rebuild in-memory messages: summary + remaining
101
+ const compactedMessages = [summaryMessage, ...remaining];
102
+
103
+ // Persist: clear DB and re-write all messages
104
+ await this.history.replaceAll(name, compactedMessages);
40
105
 
41
- // D. Init Workspace (Filesystem)
42
- // Unstorage abstrahiert hier nur das "Wie", aber es bleibt lokal im Ordner.
43
- this.workspace = createStorage({
44
- driver: fsDriver({ base: path.join(process.cwd(), "workspace") })
45
- });
106
+ logger.info({count: compactedMessages.length}, ` Compacted messages.`);
107
+ return compactedMessages;
108
+ } catch (e) {
109
+ logger.error({ err: e }, 'Compaction failed, keeping original history');
110
+ await this.history.replaceAll(name, messages);
111
+ return messages;
112
+ }
46
113
  }
114
+
47
115
  }
package/src/storage.ts CHANGED
@@ -5,10 +5,16 @@ export class LibSqlKeyValueStorage {
5
5
  private client: Client;
6
6
  private tableName: string;
7
7
 
8
- constructor(url: string, authToken?: string, tableName = 'kv_store') {
8
+
9
+ private constructor(url: string, authToken?: string, tableName = 'kv_store') {
10
+ this.client = authToken ? createClient({ url, authToken }) : createClient({ url });
9
11
  this.tableName = tableName;
10
- this.client = createClient({ url, authToken });
11
- this.init();
12
+ }
13
+
14
+ static async create<T>(url: string, authToken?: string, tableName = 'kv_store') {
15
+ const s = new LibSqlKeyValueStorage(url, authToken, tableName);
16
+ await s.init();
17
+ return s;
12
18
  }
13
19
 
14
20
  private async init() {
@@ -61,130 +67,176 @@ export class LibSqlKeyValueStorage {
61
67
  // --- B. List Storage (für History/Logs) ---
62
68
  export class LibSqlListStorage<T = any> {
63
69
  private client: Client;
64
- private tableName: string;
65
-
66
- constructor(url: string, authToken?: string, tableName = 'list_store') {
67
- this.tableName = tableName;
68
- this.client = createClient({ url, authToken });
69
- this.init();
70
- }
71
-
72
- private async init() {
73
- await this.client.execute(`
74
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
75
- id INTEGER PRIMARY KEY AUTOINCREMENT,
76
- item TEXT,
77
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
78
- )
79
- `);
80
- }
81
-
82
- async push(item: T) {
83
- await this.client.execute({
84
- sql: `INSERT INTO ${this.tableName} (item) VALUES (?)`,
85
- args: [JSON.stringify(item)]
86
- });
87
- }
88
-
89
- // Bulk Insert für Performance
90
- async pushMany(items: T[]) {
91
- const promises = items.map(item => this.client.execute({
92
- sql: `INSERT INTO ${this.tableName} (item) VALUES (?)`,
93
- args: [JSON.stringify(item)]
94
- }));
95
- await Promise.all(promises);
96
- }
97
-
98
- async getAll(): Promise<T[]> {
99
- const rs = await this.client.execute(
100
- `SELECT item FROM ${this.tableName} ORDER BY id ASC`
101
- );
102
- return rs.rows.map(row => JSON.parse(row.item as string));
103
- }
104
-
105
- // Optional: Nur die letzten N Nachrichten holen (Kontext-Fenster!)
106
- async getRecent(limit: number): Promise<T[]> {
107
- // Trick: Erst sortieren DESC (neueste), limitieren, dann wieder ASC sortieren
108
- const rs = await this.client.execute({
109
- sql: `SELECT * FROM (
110
- SELECT item, id FROM ${this.tableName} ORDER BY id DESC LIMIT ?
111
- ) ORDER BY id ASC`,
112
- args: [limit]
113
- });
114
- return rs.rows.map(row => JSON.parse(row.item as string));
115
- }
116
-
117
- async count(): Promise<number> {
118
- const rs = await this.client.execute(
119
- `SELECT COUNT(*) as cnt FROM ${this.tableName}`
120
- );
121
- return Number(rs.rows[0]?.cnt ?? 0);
122
- }
70
+ private tableName = 'list_items';
123
71
 
124
- async clear() {
125
- await this.client.execute(`DELETE FROM ${this.tableName}`);
126
- // Reset autoincrement so IDs stay clean
127
- await this.client.execute(
128
- `DELETE FROM sqlite_sequence WHERE name = '${this.tableName}'`
129
- );
72
+ private constructor(url: string, authToken?: string) {
73
+ this.client = authToken ? createClient({ url, authToken }) : createClient({ url });
130
74
  }
131
- }
132
75
 
133
- // --- C. Skill History Storage (single table, partitioned by skill name) ---
134
- export class SkillHistoryStorage<T = any> {
135
- private client: Client;
136
-
137
- constructor(url: string, authToken?: string) {
138
- this.client = createClient({ url, authToken });
139
- this.init();
76
+ static async create<T>(url: string, authToken?: string) {
77
+ const s = new LibSqlListStorage(url, authToken);
78
+ await s.init();
79
+ return s;
140
80
  }
141
81
 
142
82
  private async init() {
143
83
  await this.client.execute(`
144
- CREATE TABLE IF NOT EXISTS skill_history (
84
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
145
85
  id INTEGER PRIMARY KEY AUTOINCREMENT,
146
86
  name TEXT NOT NULL,
147
- item TEXT,
87
+ item TEXT NOT NULL,
148
88
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
149
89
  )
150
90
  `);
151
91
  }
152
92
 
153
- async push(name: string, item: T) {
93
+ async push(name: string, item: T): Promise<void> {
154
94
  await this.client.execute({
155
- sql: `INSERT INTO skill_history (name, item) VALUES (?, ?)`,
95
+ sql: `INSERT INTO ${this.tableName} (name, item) VALUES (?, ?)`,
156
96
  args: [name, JSON.stringify(item)]
157
97
  });
158
98
  }
159
99
 
160
- async pushMany(name: string, items: T[]) {
161
- const promises = items.map(item => this.client.execute({
162
- sql: `INSERT INTO skill_history (name, item) VALUES (?, ?)`,
163
- args: [name, JSON.stringify(item)]
164
- }));
165
- await Promise.all(promises);
100
+ async pushMany(name: string, items: T[]): Promise<void> {
101
+ for (const item of items) {
102
+ this.push(name, item);
103
+ }
104
+ }
105
+
106
+ async replaceAll(name: string, items: T[]): Promise<void> {
107
+ const tx = await this.client.transaction();
108
+ try {
109
+ await tx.execute({
110
+ sql: `DELETE FROM ${this.tableName} WHERE name = ?`,
111
+ args: [name],
112
+ });
113
+ for (const item of items) {
114
+ await tx.execute({
115
+ sql: `INSERT INTO ${this.tableName} (name, item) VALUES (?, ?)`,
116
+ args: [name, JSON.stringify(item)]
117
+ });
118
+ }
119
+ await tx.commit();
120
+ } catch (e) {
121
+ await tx.rollback();
122
+ throw e;
123
+ }
166
124
  }
167
125
 
168
126
  async getAll(name: string): Promise<T[]> {
169
127
  const rs = await this.client.execute({
170
- sql: `SELECT item FROM skill_history WHERE name = ? ORDER BY id ASC`,
128
+ sql: `SELECT item FROM ${this.tableName} WHERE name = ? ORDER BY id ASC`,
171
129
  args: [name]
172
130
  });
173
131
  return rs.rows.map(row => JSON.parse(row.item as string));
174
132
  }
175
-
176
- async clear(name: string) {
177
- await this.client.execute({
178
- sql: `DELETE FROM skill_history WHERE name = ?`,
133
+
134
+ async count(name: string): Promise<number> {
135
+ const rs = await this.client.execute({
136
+ sql: `SELECT COUNT(*) as cnt FROM ${this.tableName} WHERE name = ?`,
179
137
  args: [name]
180
138
  });
139
+ return Number(rs.rows[0]?.cnt ?? 0);
181
140
  }
182
141
 
183
- async count(name: string): Promise<number> {
184
- const rs = await this.client.execute({
185
- sql: `SELECT COUNT(*) as cnt FROM skill_history WHERE name = ?`,
142
+ async clear(name: string): Promise<void> {
143
+ await this.client.execute({
144
+ sql: `DELETE FROM ${this.tableName} WHERE name = ?`,
186
145
  args: [name]
187
146
  });
188
- return Number(rs.rows[0]?.cnt ?? 0);
189
147
  }
190
148
  }
149
+
150
+
151
+ export class LibSqlFiFoStorage<T> {
152
+ private client: Client;
153
+ private tableName = 'queue_items';
154
+
155
+ constructor(url: string, authToken?: string) {
156
+ this.client = authToken ? createClient({ url, authToken }) : createClient({ url });
157
+ this.init();
158
+ }
159
+
160
+ private async init(): Promise<void> {
161
+ await this.client.execute(`
162
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
163
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
164
+ queue_name TEXT NOT NULL,
165
+ value TEXT NOT NULL,
166
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
167
+ );
168
+ `);
169
+ }
170
+
171
+ public async push(queue: string, item: T): Promise<void> {
172
+ await this.client.execute({
173
+ sql: `INSERT INTO ${this.tableName} (queue_name, value) VALUES (?, ?)`,
174
+ args: [queue, JSON.stringify(item)],
175
+ });
176
+ }
177
+
178
+ public async pushMany(queue: string, items: T[]): Promise<void> {
179
+ const tx = await this.client.transaction();
180
+ try {
181
+ for (const item of items) {
182
+ await tx.execute({
183
+ sql: `INSERT INTO ${this.tableName} (queue_name, value) VALUES (?, ?)`,
184
+ args: [queue, JSON.stringify(item)],
185
+ });
186
+ }
187
+ await tx.commit();
188
+ } catch (e) {
189
+ await tx.rollback();
190
+ throw e;
191
+ }
192
+ }
193
+
194
+ public async empty(queue: string): Promise<boolean> {
195
+ return (await this.count(queue)) === 0;
196
+ }
197
+
198
+ public async pop(queue: string): Promise<T | null> {
199
+ const tx = await this.client.transaction();
200
+ try {
201
+ const rs = await tx.execute({
202
+ sql: `SELECT id, value FROM ${this.tableName} WHERE queue_name = ? ORDER BY id ASC LIMIT 1`,
203
+ args: [queue],
204
+ });
205
+
206
+ if (rs.rows.length === 0) {
207
+ await tx.commit();
208
+ return null;
209
+ }
210
+
211
+ const row = rs.rows[0] as any;
212
+ const id = row.id;
213
+
214
+ await tx.execute({
215
+ sql: `DELETE FROM ${this.tableName} WHERE id = ?`,
216
+ args: [id!],
217
+ });
218
+
219
+ await tx.commit();
220
+
221
+ return JSON.parse(row.value as string) as T;
222
+ } catch (e) {
223
+ await tx.rollback();
224
+ throw e;
225
+ }
226
+ }
227
+
228
+ public async count(queue: string): Promise<number> {
229
+ const rs = await this.client.execute({
230
+ sql: `SELECT COUNT(*) as count FROM ${this.tableName} WHERE queue_name = ?`,
231
+ args: [queue],
232
+ });
233
+ return (rs.rows[0]?.count as number) ?? 0;
234
+ }
235
+
236
+ public async clear(queue: string): Promise<void> {
237
+ await this.client.execute({
238
+ sql: `DELETE FROM ${this.tableName} WHERE queue_name = ?`,
239
+ args: [queue],
240
+ });
241
+ }
242
+ }