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/README.md +13 -2
- package/package.json +10 -3
- package/src/agent.eval.test.ts +218 -0
- package/src/agent.ts +52 -1004
- package/src/cli.ts +3 -1
- package/src/evals/bootstrap_trigger.yaml +14 -0
- package/src/evals/connection_auth.yaml +12 -0
- package/src/evals/create_python_file.yaml +11 -0
- package/src/evals/directory_traversal.yaml +13 -0
- package/src/evals/empty_directory.yaml +12 -0
- package/src/evals/extend_agents_md.yaml +161 -0
- package/src/evals/external_data.yaml +16 -0
- package/src/evals/file_not_found.yaml +15 -0
- package/src/evals/memory_persistence.yaml +19 -0
- package/src/evals/move_and_rename.yaml +13 -0
- package/src/evals/needle_in_haystack.yaml +16 -0
- package/src/evals/persona_tone.yaml +16 -0
- package/src/evals/rag_user.yaml +17 -0
- package/src/evals/reasoning_multi_step.yaml +13 -0
- package/src/evals/refactoring_edit.yaml +14 -0
- package/src/evals/skill_sandbox_execution.yaml +19 -0
- package/src/evals/skill_system_installation.yaml +14 -0
- package/src/evals/soft_delete.yaml +17 -0
- package/src/evals/stat_check.yaml +16 -0
- package/src/evals/workflow_cleanup.yaml +17 -0
- package/src/evals/write_complex_json.yaml +15 -0
- package/src/llm.ts +35 -0
- package/src/logger.ts +39 -0
- package/src/memory.ts +95 -27
- package/src/storage.ts +147 -95
- package/src/tools.ts +1044 -0
- package/template/AGENTS.template +1 -1
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
|
|
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.
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
|
177
|
-
await this.client.execute({
|
|
178
|
-
sql: `
|
|
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
|
|
184
|
-
|
|
185
|
-
sql: `
|
|
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
|
+
}
|