clawlet 0.7.0 → 0.8.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 +7 -3
- package/package.json +4 -2
- package/src/agent.eval.test.ts +4 -1
- package/src/agent.ts +84 -81
- package/src/evals/connection_auth.yaml +9 -1
- package/src/evals/create_python_file.yaml +9 -1
- package/src/evals/directory_traversal.yaml +9 -1
- package/src/evals/empty_directory.yaml +9 -1
- package/src/evals/extend_agents_md.yaml +9 -126
- package/src/evals/external_data.yaml +10 -1
- package/src/evals/file_not_found.yaml +8 -0
- package/src/evals/knowledge.yaml +23 -0
- package/src/evals/memory_persistence.yaml +9 -0
- package/src/evals/move_and_rename.yaml +8 -0
- package/src/evals/needle_in_haystack.yaml +8 -0
- package/src/evals/persona_tone.yaml +6 -0
- package/src/evals/rag_user.yaml +5 -0
- package/src/evals/reasoning_multi_step.yaml +8 -0
- package/src/evals/refactoring_edit.yaml +8 -0
- package/src/evals/rewrite_agents_md.yaml +9 -126
- package/src/evals/skill_system_installation.yaml +9 -1
- package/src/evals/soft_delete.yaml +8 -0
- package/src/evals/stat_check.yaml +8 -0
- package/src/evals/workflow_cleanup.yaml +8 -0
- package/src/evals/write_complex_json.yaml +10 -2
- package/src/llm.ts +212 -4
- package/src/memory.ts +17 -4
- package/src/storage.ts +344 -0
- package/src/tools.ts +411 -6
- package/template/SYSTEM_INSTRUCTIONS.template +94 -0
- package/template/AGENTS.template +0 -122
package/src/storage.ts
CHANGED
|
@@ -211,3 +211,347 @@ export class LibSqlFiFoStorage<T> {
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
// --- D. Knowledge Storage (FTS + Graph Index) ---
|
|
217
|
+
|
|
218
|
+
export interface KnowledgeSearchResult {
|
|
219
|
+
path: string;
|
|
220
|
+
content: string;
|
|
221
|
+
score: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface KnowledgeEdge {
|
|
225
|
+
source_path: string;
|
|
226
|
+
target_path: string;
|
|
227
|
+
relation_type: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface KnowledgeRelation {
|
|
231
|
+
target: string;
|
|
232
|
+
type: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export class LibSqlKnowledgeStorage {
|
|
236
|
+
private client: Client;
|
|
237
|
+
|
|
238
|
+
private constructor(url: string, authToken?: string) {
|
|
239
|
+
this.client = authToken ? createClient({ url, authToken }) : createClient({ url });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static async create(url: string, authToken?: string): Promise<LibSqlKnowledgeStorage> {
|
|
243
|
+
const s = new LibSqlKnowledgeStorage(url, authToken);
|
|
244
|
+
await s.init();
|
|
245
|
+
return s;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async init(): Promise<void> {
|
|
249
|
+
await this.client.execute(`
|
|
250
|
+
CREATE TABLE IF NOT EXISTS knowledge_entries (
|
|
251
|
+
path TEXT PRIMARY KEY,
|
|
252
|
+
content TEXT NOT NULL,
|
|
253
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
254
|
+
)
|
|
255
|
+
`);
|
|
256
|
+
|
|
257
|
+
await this.client.execute(`
|
|
258
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts
|
|
259
|
+
USING fts5(path, content)
|
|
260
|
+
`);
|
|
261
|
+
|
|
262
|
+
await this.client.execute(`
|
|
263
|
+
CREATE TABLE IF NOT EXISTS knowledge_edges (
|
|
264
|
+
source_path TEXT NOT NULL,
|
|
265
|
+
target_path TEXT NOT NULL,
|
|
266
|
+
relation_type TEXT NOT NULL,
|
|
267
|
+
PRIMARY KEY (source_path, target_path, relation_type)
|
|
268
|
+
)
|
|
269
|
+
`);
|
|
270
|
+
|
|
271
|
+
await this.client.execute(`
|
|
272
|
+
CREATE INDEX IF NOT EXISTS knowledge_edges_target_idx
|
|
273
|
+
ON knowledge_edges (target_path)
|
|
274
|
+
`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async upsert(path: string, content: string, relations?: KnowledgeRelation[]): Promise<void> {
|
|
278
|
+
// 1. Upsert main entry
|
|
279
|
+
await this.client.execute({
|
|
280
|
+
sql: `INSERT INTO knowledge_entries (path, content, updated_at)
|
|
281
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
282
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
283
|
+
content = excluded.content,
|
|
284
|
+
updated_at = excluded.updated_at`,
|
|
285
|
+
args: [path, content]
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// 2. Sync FTS (delete old, insert new)
|
|
289
|
+
await this.client.execute({
|
|
290
|
+
sql: `DELETE FROM knowledge_fts WHERE path = ?`,
|
|
291
|
+
args: [path]
|
|
292
|
+
});
|
|
293
|
+
await this.client.execute({
|
|
294
|
+
sql: `INSERT INTO knowledge_fts (path, content) VALUES (?, ?)`,
|
|
295
|
+
args: [path, content]
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// 3. Sync edges (delete old outgoing, insert new)
|
|
299
|
+
await this.client.execute({
|
|
300
|
+
sql: `DELETE FROM knowledge_edges WHERE source_path = ?`,
|
|
301
|
+
args: [path]
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (relations && relations.length > 0) {
|
|
305
|
+
for (const rel of relations) {
|
|
306
|
+
await this.client.execute({
|
|
307
|
+
sql: `INSERT OR IGNORE INTO knowledge_edges (source_path, target_path, relation_type)
|
|
308
|
+
VALUES (?, ?, ?)`,
|
|
309
|
+
args: [path, rel.target, rel.type]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async remove(path: string): Promise<void> {
|
|
316
|
+
await this.client.execute({
|
|
317
|
+
sql: `DELETE FROM knowledge_fts WHERE path = ?`,
|
|
318
|
+
args: [path]
|
|
319
|
+
});
|
|
320
|
+
await this.client.execute({
|
|
321
|
+
sql: `DELETE FROM knowledge_edges WHERE source_path = ? OR target_path = ?`,
|
|
322
|
+
args: [path, path]
|
|
323
|
+
});
|
|
324
|
+
await this.client.execute({
|
|
325
|
+
sql: `DELETE FROM knowledge_entries WHERE path = ?`,
|
|
326
|
+
args: [path]
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async searchFulltext(query: string, limit: number = 10): Promise<KnowledgeSearchResult[]> {
|
|
331
|
+
const rs = await this.client.execute({
|
|
332
|
+
sql: `SELECT path, content, bm25(knowledge_fts) as score
|
|
333
|
+
FROM knowledge_fts
|
|
334
|
+
WHERE knowledge_fts MATCH ?
|
|
335
|
+
ORDER BY score
|
|
336
|
+
LIMIT ?`,
|
|
337
|
+
args: [query, limit]
|
|
338
|
+
});
|
|
339
|
+
return rs.rows.map(row => ({
|
|
340
|
+
path: row.path as string,
|
|
341
|
+
content: row.content as string,
|
|
342
|
+
score: row.score as number,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async getRelated(
|
|
347
|
+
path: string,
|
|
348
|
+
direction: 'outgoing' | 'incoming' | 'both' = 'both',
|
|
349
|
+
relationType?: string
|
|
350
|
+
): Promise<KnowledgeEdge[]> {
|
|
351
|
+
let sql: string;
|
|
352
|
+
const args: string[] = [];
|
|
353
|
+
|
|
354
|
+
if (direction === 'outgoing') {
|
|
355
|
+
sql = `SELECT source_path, target_path, relation_type FROM knowledge_edges WHERE source_path = ?`;
|
|
356
|
+
args.push(path);
|
|
357
|
+
} else if (direction === 'incoming') {
|
|
358
|
+
sql = `SELECT source_path, target_path, relation_type FROM knowledge_edges WHERE target_path = ?`;
|
|
359
|
+
args.push(path);
|
|
360
|
+
} else {
|
|
361
|
+
sql = `SELECT source_path, target_path, relation_type FROM knowledge_edges WHERE source_path = ? OR target_path = ?`;
|
|
362
|
+
args.push(path, path);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (relationType) {
|
|
366
|
+
sql += ` AND relation_type = ?`;
|
|
367
|
+
args.push(relationType);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const rs = await this.client.execute({ sql, args });
|
|
371
|
+
return rs.rows.map(row => ({
|
|
372
|
+
source_path: row.source_path as string,
|
|
373
|
+
target_path: row.target_path as string,
|
|
374
|
+
relation_type: row.relation_type as string,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async list(): Promise<Array<{ path: string }>> {
|
|
379
|
+
const rs = await this.client.execute(`SELECT path FROM knowledge_entries ORDER BY path`);
|
|
380
|
+
return rs.rows.map(row => ({ path: row.path as string }));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async searchTemporal(
|
|
384
|
+
startDate: string,
|
|
385
|
+
endDate: string,
|
|
386
|
+
typeFilter?: string
|
|
387
|
+
): Promise<KnowledgeSearchResult[]> {
|
|
388
|
+
const startTs = `${startDate}T00:00:00`;
|
|
389
|
+
const endTs = `${endDate}T23:59:59`;
|
|
390
|
+
|
|
391
|
+
let sql = `SELECT path, content, updated_at FROM knowledge_entries
|
|
392
|
+
WHERE updated_at BETWEEN ? AND ?`;
|
|
393
|
+
const args: (string)[] = [startTs, endTs];
|
|
394
|
+
|
|
395
|
+
if (typeFilter) {
|
|
396
|
+
sql += ` AND path LIKE ?`;
|
|
397
|
+
args.push(`${typeFilter}:%`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
sql += ` ORDER BY updated_at DESC`;
|
|
401
|
+
|
|
402
|
+
const rs = await this.client.execute({ sql, args });
|
|
403
|
+
return rs.rows.map(row => ({
|
|
404
|
+
path: row.path as string,
|
|
405
|
+
content: row.content as string,
|
|
406
|
+
score: 0,
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async searchConflicts(
|
|
411
|
+
assertion: string,
|
|
412
|
+
targetCategory: string,
|
|
413
|
+
limit: number = 3
|
|
414
|
+
): Promise<KnowledgeSearchResult[]> {
|
|
415
|
+
// Use FTS to find entries in the target category that are semantically close to the assertion
|
|
416
|
+
// FTS5 tokenizes the assertion and matches against content
|
|
417
|
+
const tokens = assertion
|
|
418
|
+
.toLowerCase()
|
|
419
|
+
.split(/[^a-z0-9]+/)
|
|
420
|
+
.filter(t => t.length >= 2);
|
|
421
|
+
|
|
422
|
+
if (tokens.length === 0) return [];
|
|
423
|
+
|
|
424
|
+
// Build an OR query so any overlapping term surfaces potential conflicts
|
|
425
|
+
const ftsQuery = tokens.join(' OR ');
|
|
426
|
+
|
|
427
|
+
const rs = await this.client.execute({
|
|
428
|
+
sql: `SELECT path, content, bm25(knowledge_fts) as score
|
|
429
|
+
FROM knowledge_fts
|
|
430
|
+
WHERE knowledge_fts MATCH ? AND path LIKE ?
|
|
431
|
+
ORDER BY score
|
|
432
|
+
LIMIT ?`,
|
|
433
|
+
args: [ftsQuery, `${targetCategory}:%`, limit]
|
|
434
|
+
});
|
|
435
|
+
return rs.rows.map(row => ({
|
|
436
|
+
path: row.path as string,
|
|
437
|
+
content: row.content as string,
|
|
438
|
+
score: row.score as number,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async graphTraverse(
|
|
443
|
+
startNode: string,
|
|
444
|
+
direction: 'outbound' | 'inbound' | 'both' = 'both',
|
|
445
|
+
maxDepth: number = 1,
|
|
446
|
+
relationshipType?: string
|
|
447
|
+
): Promise<Array<{ path: string; depth: number; relation_type: string }>> {
|
|
448
|
+
// For depth 1, use a simple query; for depth > 1, use a recursive CTE
|
|
449
|
+
if (maxDepth <= 1) {
|
|
450
|
+
return this.graphTraverseSimple(startNode, direction, relationshipType);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Recursive CTE for multi-hop traversal
|
|
454
|
+
let cte: string;
|
|
455
|
+
const args: string[] = [];
|
|
456
|
+
|
|
457
|
+
if (direction === 'outbound') {
|
|
458
|
+
cte = `
|
|
459
|
+
WITH RECURSIVE traverse(node, depth, relation_type) AS (
|
|
460
|
+
SELECT target_path, 1, relation_type FROM knowledge_edges
|
|
461
|
+
WHERE source_path = ?${relationshipType ? ' AND relation_type = ?' : ''}
|
|
462
|
+
UNION ALL
|
|
463
|
+
SELECT e.target_path, t.depth + 1, e.relation_type
|
|
464
|
+
FROM knowledge_edges e
|
|
465
|
+
JOIN traverse t ON e.source_path = t.node
|
|
466
|
+
WHERE t.depth < ?${relationshipType ? ' AND e.relation_type = ?' : ''}
|
|
467
|
+
)
|
|
468
|
+
SELECT DISTINCT node as path, depth, relation_type FROM traverse ORDER BY depth, path`;
|
|
469
|
+
args.push(startNode);
|
|
470
|
+
if (relationshipType) args.push(relationshipType);
|
|
471
|
+
args.push(String(maxDepth));
|
|
472
|
+
if (relationshipType) args.push(relationshipType);
|
|
473
|
+
} else if (direction === 'inbound') {
|
|
474
|
+
cte = `
|
|
475
|
+
WITH RECURSIVE traverse(node, depth, relation_type) AS (
|
|
476
|
+
SELECT source_path, 1, relation_type FROM knowledge_edges
|
|
477
|
+
WHERE target_path = ?${relationshipType ? ' AND relation_type = ?' : ''}
|
|
478
|
+
UNION ALL
|
|
479
|
+
SELECT e.source_path, t.depth + 1, e.relation_type
|
|
480
|
+
FROM knowledge_edges e
|
|
481
|
+
JOIN traverse t ON e.target_path = t.node
|
|
482
|
+
WHERE t.depth < ?${relationshipType ? ' AND e.relation_type = ?' : ''}
|
|
483
|
+
)
|
|
484
|
+
SELECT DISTINCT node as path, depth, relation_type FROM traverse ORDER BY depth, path`;
|
|
485
|
+
args.push(startNode);
|
|
486
|
+
if (relationshipType) args.push(relationshipType);
|
|
487
|
+
args.push(String(maxDepth));
|
|
488
|
+
if (relationshipType) args.push(relationshipType);
|
|
489
|
+
} else {
|
|
490
|
+
// Both directions
|
|
491
|
+
cte = `
|
|
492
|
+
WITH RECURSIVE traverse(node, depth, relation_type) AS (
|
|
493
|
+
SELECT target_path, 1, relation_type FROM knowledge_edges
|
|
494
|
+
WHERE source_path = ?${relationshipType ? ' AND relation_type = ?' : ''}
|
|
495
|
+
UNION ALL
|
|
496
|
+
SELECT source_path, 1, relation_type FROM knowledge_edges
|
|
497
|
+
WHERE target_path = ?${relationshipType ? ' AND relation_type = ?' : ''}
|
|
498
|
+
UNION ALL
|
|
499
|
+
SELECT CASE WHEN e.source_path = t.node THEN e.target_path ELSE e.source_path END,
|
|
500
|
+
t.depth + 1, e.relation_type
|
|
501
|
+
FROM knowledge_edges e
|
|
502
|
+
JOIN traverse t ON (e.source_path = t.node OR e.target_path = t.node)
|
|
503
|
+
WHERE t.depth < ?${relationshipType ? ' AND e.relation_type = ?' : ''}
|
|
504
|
+
)
|
|
505
|
+
SELECT DISTINCT node as path, depth, relation_type FROM traverse WHERE node != ? ORDER BY depth, path`;
|
|
506
|
+
args.push(startNode);
|
|
507
|
+
if (relationshipType) args.push(relationshipType);
|
|
508
|
+
args.push(startNode);
|
|
509
|
+
if (relationshipType) args.push(relationshipType);
|
|
510
|
+
args.push(String(maxDepth));
|
|
511
|
+
if (relationshipType) args.push(relationshipType);
|
|
512
|
+
args.push(startNode); // exclude startNode from results
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const rs = await this.client.execute({ sql: cte, args });
|
|
516
|
+
return rs.rows.map(row => ({
|
|
517
|
+
path: row.path as string,
|
|
518
|
+
depth: Number(row.depth),
|
|
519
|
+
relation_type: row.relation_type as string,
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async graphTraverseSimple(
|
|
524
|
+
startNode: string,
|
|
525
|
+
direction: 'outbound' | 'inbound' | 'both',
|
|
526
|
+
relationshipType?: string
|
|
527
|
+
): Promise<Array<{ path: string; depth: number; relation_type: string }>> {
|
|
528
|
+
let sql: string;
|
|
529
|
+
const args: string[] = [];
|
|
530
|
+
|
|
531
|
+
if (direction === 'outbound') {
|
|
532
|
+
sql = `SELECT target_path as path, relation_type FROM knowledge_edges WHERE source_path = ?`;
|
|
533
|
+
args.push(startNode);
|
|
534
|
+
} else if (direction === 'inbound') {
|
|
535
|
+
sql = `SELECT source_path as path, relation_type FROM knowledge_edges WHERE target_path = ?`;
|
|
536
|
+
args.push(startNode);
|
|
537
|
+
} else {
|
|
538
|
+
sql = `SELECT CASE WHEN source_path = ? THEN target_path ELSE source_path END as path, relation_type
|
|
539
|
+
FROM knowledge_edges WHERE source_path = ? OR target_path = ?`;
|
|
540
|
+
args.push(startNode, startNode, startNode);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (relationshipType) {
|
|
544
|
+
sql += ` AND relation_type = ?`;
|
|
545
|
+
args.push(relationshipType);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
sql += ` ORDER BY path`;
|
|
549
|
+
|
|
550
|
+
const rs = await this.client.execute({ sql, args });
|
|
551
|
+
return rs.rows.map(row => ({
|
|
552
|
+
path: row.path as string,
|
|
553
|
+
depth: 1,
|
|
554
|
+
relation_type: row.relation_type as string,
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
}
|