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/tools.ts
CHANGED
|
@@ -18,6 +18,16 @@ const GENERATE_TEXT_MAX_STEPS = 30;
|
|
|
18
18
|
|
|
19
19
|
const turndownService = new TurndownService()
|
|
20
20
|
|
|
21
|
+
/** Index file content into the knowledge store. Silently catches errors so index failures don't break file operations. */
|
|
22
|
+
async function indexFileContent(memory: AgentMemory, path: string, content: string): Promise<void> {
|
|
23
|
+
try { await memory.knowledge.upsert(path, content); } catch { /* index failure is non-fatal */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Remove file from knowledge index. Silently catches errors. */
|
|
27
|
+
async function removeFromIndex(memory: AgentMemory, path: string): Promise<void> {
|
|
28
|
+
try { await memory.knowledge.remove(path); } catch { /* index failure is non-fatal */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// --- SETTINGS HELPERS ---
|
|
22
32
|
|
|
23
33
|
const SETTINGS_PATH = `${process.cwd()}/settings.json`;
|
|
@@ -96,7 +106,7 @@ async function buildSkillSystemPrompt(name: string, memory: AgentMemory, skillPe
|
|
|
96
106
|
|
|
97
107
|
// Build identity section from SOUL.md and IDENTITY.md
|
|
98
108
|
let identitySection = `# IDENTITY: Clawlet
|
|
99
|
-
You are "Clawlet", an autonomous agent defined by the file \`
|
|
109
|
+
You are "Clawlet", an autonomous agent defined by the file \`SYSTEM_INSTRUCTIONS.md\`.`;
|
|
100
110
|
|
|
101
111
|
if (identityDoc) {
|
|
102
112
|
identitySection += `\n\n## Identity Definition (IDENTITY.md)\n${identityDoc}`;
|
|
@@ -446,6 +456,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
446
456
|
logger.debug({ path }, 'FS writeFile');
|
|
447
457
|
try {
|
|
448
458
|
await memory.workspace.setItem(path, content);
|
|
459
|
+
await indexFileContent(memory, path, content);
|
|
449
460
|
return `Success: Wrote to ${path}`;
|
|
450
461
|
} catch (e: any) { return "Error writing file: " + e.message; }
|
|
451
462
|
}
|
|
@@ -480,6 +491,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
480
491
|
|
|
481
492
|
const newContent = fileText.replace(find, replace);
|
|
482
493
|
await memory.workspace.setItem(path, newContent);
|
|
494
|
+
await indexFileContent(memory, path, newContent);
|
|
483
495
|
return `Success: Edited "${path}". Replaced 1 occurrence.`;
|
|
484
496
|
} catch (e: any) { return "Error editing file: " + e.message; }
|
|
485
497
|
}
|
|
@@ -487,7 +499,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
487
499
|
|
|
488
500
|
'fs.appendFile': tool({
|
|
489
501
|
description: 'Apped specific string to a file (will create it if it does not exist yet). Use this for appending something to daily memory files and the likes.',
|
|
490
|
-
inputSchema: jsonSchema<{ path: string,
|
|
502
|
+
inputSchema: jsonSchema<{ path: string, content: string }>({
|
|
491
503
|
type: 'object',
|
|
492
504
|
properties: {
|
|
493
505
|
path: { type: 'string', description: 'Path/key of the file to edit' },
|
|
@@ -495,12 +507,14 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
495
507
|
},
|
|
496
508
|
required: ['path', 'content'],
|
|
497
509
|
}),
|
|
498
|
-
execute: async ({ path,
|
|
510
|
+
execute: async ({ path, content }) => {
|
|
499
511
|
logger.debug({ path }, 'FS appendFile');
|
|
500
512
|
try {
|
|
501
|
-
const
|
|
502
|
-
const fileText = String(
|
|
503
|
-
|
|
513
|
+
const existingContent = await memory.workspace.getItem(path);
|
|
514
|
+
const fileText = String(existingContent || '');
|
|
515
|
+
const finalContent = (fileText != '' ? (fileText + "\n") : '') + content;
|
|
516
|
+
await memory.workspace.setItem(path, finalContent);
|
|
517
|
+
await indexFileContent(memory, path, finalContent);
|
|
504
518
|
return `Success: Appended "${path}".`;
|
|
505
519
|
} catch (e: any) { return "Error appending file: " + e.message; }
|
|
506
520
|
}
|
|
@@ -524,6 +538,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
524
538
|
// Already in trash — hard delete
|
|
525
539
|
logger.debug({ path }, 'FS permanentDelete');
|
|
526
540
|
await memory.workspace.removeItem(path);
|
|
541
|
+
await removeFromIndex(memory, path);
|
|
527
542
|
return `Success: Permanently deleted ${path}`;
|
|
528
543
|
} else {
|
|
529
544
|
// Move to .trash/
|
|
@@ -531,6 +546,7 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
531
546
|
logger.debug({ path, trashPath }, 'FS softDelete');
|
|
532
547
|
await memory.workspace.setItem(trashPath, content);
|
|
533
548
|
await memory.workspace.removeItem(path);
|
|
549
|
+
await removeFromIndex(memory, path);
|
|
534
550
|
return `Success: Moved ${path} to ${trashPath}`;
|
|
535
551
|
}
|
|
536
552
|
} catch (e: any) { return "Error deleting file: " + e.message; }
|
|
@@ -554,6 +570,8 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
554
570
|
if (content === null || content === undefined) return `File not found: ${from}`;
|
|
555
571
|
await memory.workspace.setItem(to, content);
|
|
556
572
|
await memory.workspace.removeItem(from);
|
|
573
|
+
await removeFromIndex(memory, from);
|
|
574
|
+
await indexFileContent(memory, to, String(content));
|
|
557
575
|
return `Success: Moved ${from} to ${to}`;
|
|
558
576
|
} catch (e: any) { return "Error moving file: " + e.message; }
|
|
559
577
|
}
|
|
@@ -597,6 +615,393 @@ export function createTools(memory: AgentMemory, model: LanguageModel) {
|
|
|
597
615
|
}
|
|
598
616
|
}),
|
|
599
617
|
|
|
618
|
+
'fs.keywordSearch': tool({
|
|
619
|
+
description: 'Full-text search across all workspace files. Returns the best matching files ranked by relevance. Use this to find files by content keywords.',
|
|
620
|
+
inputSchema: jsonSchema<{ query: string; limit?: number }>({
|
|
621
|
+
type: 'object',
|
|
622
|
+
properties: {
|
|
623
|
+
query: { type: 'string', description: 'Search query (keywords to search for)' },
|
|
624
|
+
limit: { type: 'number', description: 'Max number of results (default: 10)' },
|
|
625
|
+
},
|
|
626
|
+
required: ['query'],
|
|
627
|
+
}),
|
|
628
|
+
execute: async ({ query, limit }) => {
|
|
629
|
+
logger.debug({ query, limit }, 'FS keywordSearch');
|
|
630
|
+
try {
|
|
631
|
+
const results = await memory.knowledge.searchFulltext(query, limit || 10);
|
|
632
|
+
if (results.length === 0) return "No results found.";
|
|
633
|
+
return results.map(r => {
|
|
634
|
+
const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
|
|
635
|
+
return `[${r.path}] (score: ${r.score})\n${snippet}`;
|
|
636
|
+
}).join('\n\n');
|
|
637
|
+
} catch (e: any) { return `Error searching: ${e.message}`; }
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
|
|
641
|
+
'fs.search': tool({
|
|
642
|
+
description: 'Federated search across all workspace files. Combines full-text keyword matching, semantic/conceptual similarity, and graph connections into a single ranked result set. Use this as the primary search tool — it automatically picks the best strategy for your query.',
|
|
643
|
+
inputSchema: jsonSchema<{ query: string; limit?: number }>({
|
|
644
|
+
type: 'object',
|
|
645
|
+
properties: {
|
|
646
|
+
query: { type: 'string', description: 'Search query — can be keywords, a question, or a concept.' },
|
|
647
|
+
limit: { type: 'number', description: 'Max number of results (default: 10).' },
|
|
648
|
+
},
|
|
649
|
+
required: ['query'],
|
|
650
|
+
}),
|
|
651
|
+
execute: async ({ query, limit }) => {
|
|
652
|
+
logger.debug({ query, limit }, 'FS search (federated)');
|
|
653
|
+
const maxResults = limit || 10;
|
|
654
|
+
|
|
655
|
+
// Collect results from multiple backends in parallel, each source tagged
|
|
656
|
+
interface ScoredResult { path: string; content: string; score: number; source: string; }
|
|
657
|
+
const allResults: ScoredResult[] = [];
|
|
658
|
+
|
|
659
|
+
// 1. FTS keyword search (exact term matches)
|
|
660
|
+
try {
|
|
661
|
+
const ftsResults = await memory.knowledge.searchFulltext(query, maxResults);
|
|
662
|
+
for (const r of ftsResults) {
|
|
663
|
+
// bm25 returns negative scores (more negative = better), normalize to 0..1
|
|
664
|
+
allResults.push({ ...r, score: Math.abs(r.score), source: 'keyword' });
|
|
665
|
+
}
|
|
666
|
+
} catch { /* FTS query syntax may fail on special chars — non-fatal */ }
|
|
667
|
+
|
|
668
|
+
// 2. Semantic search (OR-tokenized FTS to broaden recall)
|
|
669
|
+
try {
|
|
670
|
+
const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 2);
|
|
671
|
+
if (tokens.length > 1) {
|
|
672
|
+
const broadQuery = tokens.join(' OR ');
|
|
673
|
+
const semanticResults = await memory.knowledge.searchFulltext(broadQuery, maxResults);
|
|
674
|
+
for (const r of semanticResults) {
|
|
675
|
+
allResults.push({ ...r, score: Math.abs(r.score) * 0.8, source: 'semantic' });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} catch {}
|
|
679
|
+
|
|
680
|
+
// 3. Graph expansion — for top keyword hits, pull in their direct neighbors
|
|
681
|
+
try {
|
|
682
|
+
const topPaths = [...new Set(allResults.slice(0, 3).map(r => r.path))];
|
|
683
|
+
for (const p of topPaths) {
|
|
684
|
+
const neighbors = await memory.knowledge.graphTraverse(p, 'both', 1);
|
|
685
|
+
for (const n of neighbors) {
|
|
686
|
+
// Read the neighbor's content from the index
|
|
687
|
+
const existing = allResults.find(r => r.path === n.path);
|
|
688
|
+
if (!existing) {
|
|
689
|
+
// Fetch content from knowledge_entries to include a snippet
|
|
690
|
+
const neighborResults = await memory.knowledge.searchFulltext(n.path.replace(/[:.\/]/g, ' '), 1);
|
|
691
|
+
const content = neighborResults[0]?.content ?? '';
|
|
692
|
+
allResults.push({
|
|
693
|
+
path: n.path,
|
|
694
|
+
content,
|
|
695
|
+
score: 0.3, // lower base score for graph-discovered results
|
|
696
|
+
source: `graph (via ${p}, ${n.relation_type})`,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} catch {}
|
|
702
|
+
|
|
703
|
+
if (allResults.length === 0) return "No results found.";
|
|
704
|
+
|
|
705
|
+
// Deduplicate by path, keeping the highest score per path and merging sources
|
|
706
|
+
const byPath = new Map<string, ScoredResult & { sources: string[] }>();
|
|
707
|
+
for (const r of allResults) {
|
|
708
|
+
const existing = byPath.get(r.path);
|
|
709
|
+
if (existing) {
|
|
710
|
+
if (r.score > existing.score) {
|
|
711
|
+
existing.score = r.score;
|
|
712
|
+
existing.content = r.content || existing.content;
|
|
713
|
+
}
|
|
714
|
+
if (!existing.sources.includes(r.source)) existing.sources.push(r.source);
|
|
715
|
+
} else {
|
|
716
|
+
byPath.set(r.path, { ...r, sources: [r.source] });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Sort by score descending and take top N
|
|
721
|
+
const ranked = [...byPath.values()]
|
|
722
|
+
.sort((a, b) => b.score - a.score)
|
|
723
|
+
.slice(0, maxResults);
|
|
724
|
+
|
|
725
|
+
return ranked.map(r => {
|
|
726
|
+
const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
|
|
727
|
+
const sources = r.sources.join(', ');
|
|
728
|
+
return `[${r.path}] (score: ${r.score.toFixed(2)}, via: ${sources})\n${snippet}`;
|
|
729
|
+
}).join('\n\n');
|
|
730
|
+
}
|
|
731
|
+
}),
|
|
732
|
+
|
|
733
|
+
'fs.reindexKnowledge': tool({
|
|
734
|
+
description: 'Rebuild the full-text search index from scratch by reading and indexing every file in the workspace. Use this after manual file changes or to fix a corrupted index.',
|
|
735
|
+
inputSchema: jsonSchema<{}>({
|
|
736
|
+
type: 'object',
|
|
737
|
+
properties: {},
|
|
738
|
+
additionalProperties: false,
|
|
739
|
+
}),
|
|
740
|
+
execute: async () => {
|
|
741
|
+
logger.debug('FS reindexKnowledge');
|
|
742
|
+
try {
|
|
743
|
+
const keys = await memory.workspace.getKeys();
|
|
744
|
+
let indexed = 0;
|
|
745
|
+
let errors = 0;
|
|
746
|
+
for (const key of keys) {
|
|
747
|
+
try {
|
|
748
|
+
const content = await memory.workspace.getItem(key);
|
|
749
|
+
if (content === null || content === undefined) continue;
|
|
750
|
+
await memory.knowledge.upsert(key, String(content));
|
|
751
|
+
indexed++;
|
|
752
|
+
} catch {
|
|
753
|
+
errors++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return `Success: Reindexed ${indexed} files.${errors > 0 ? ` ${errors} files failed.` : ''}`;
|
|
757
|
+
} catch (e: any) { return `Error reindexing: ${e.message}`; }
|
|
758
|
+
}
|
|
759
|
+
}),
|
|
760
|
+
|
|
761
|
+
'fs.temporalSearch': tool({
|
|
762
|
+
description: 'Search for files based on time ranges. Use for questions like "What happened last week?" or "Upcoming deadlines".',
|
|
763
|
+
inputSchema: jsonSchema<{ start_date: string; end_date: string; type_filter?: string }>({
|
|
764
|
+
type: 'object',
|
|
765
|
+
properties: {
|
|
766
|
+
start_date: { type: 'string', description: 'ISO Start Date (YYYY-MM-DD).' },
|
|
767
|
+
end_date: { type: 'string', description: 'ISO End Date (YYYY-MM-DD).' },
|
|
768
|
+
type_filter: {
|
|
769
|
+
type: 'string',
|
|
770
|
+
enum: ['commitment', 'memory', 'decision', 'lesson'],
|
|
771
|
+
description: 'Optional filter by knowledge type.',
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
required: ['start_date', 'end_date'],
|
|
775
|
+
}),
|
|
776
|
+
execute: async ({ start_date, end_date, type_filter }) => {
|
|
777
|
+
logger.debug({ start_date, end_date, type_filter }, 'FS temporalSearch');
|
|
778
|
+
try {
|
|
779
|
+
const results = await memory.knowledge.searchTemporal(start_date, end_date, type_filter);
|
|
780
|
+
if (results.length === 0) return "No files found in the given time range.";
|
|
781
|
+
return results.map(r => {
|
|
782
|
+
const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
|
|
783
|
+
return `[${r.path}]\n${snippet}`;
|
|
784
|
+
}).join('\n\n');
|
|
785
|
+
} catch (e: any) { return `Error in temporal search: ${e.message}`; }
|
|
786
|
+
}
|
|
787
|
+
}),
|
|
788
|
+
|
|
789
|
+
'fs.conflictSearch': tool({
|
|
790
|
+
description: 'Checks for contradictions before storing new facts. MANDATORY before confirming critical decisions or changing profile data.',
|
|
791
|
+
inputSchema: jsonSchema<{ assertion: string; target_category: string }>({
|
|
792
|
+
type: 'object',
|
|
793
|
+
properties: {
|
|
794
|
+
assertion: { type: 'string', description: 'The new fact to be checked (e.g., "Jan uses Strapi now").' },
|
|
795
|
+
target_category: {
|
|
796
|
+
type: 'string',
|
|
797
|
+
enum: ['decision', 'preference', 'commitment'],
|
|
798
|
+
description: 'Where to look for conflicts.',
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
required: ['assertion', 'target_category'],
|
|
802
|
+
}),
|
|
803
|
+
execute: async ({ assertion, target_category }) => {
|
|
804
|
+
logger.debug({ assertion, target_category }, 'FS conflictSearch');
|
|
805
|
+
try {
|
|
806
|
+
const results = await memory.knowledge.searchConflicts(assertion, target_category, 3);
|
|
807
|
+
if (results.length === 0) return JSON.stringify({ conflict_found: false, matches: [] });
|
|
808
|
+
return JSON.stringify({
|
|
809
|
+
conflict_found: true,
|
|
810
|
+
matches: results.map(r => ({
|
|
811
|
+
conflicting_file: r.path,
|
|
812
|
+
snippet: r.content.length > 300 ? r.content.slice(0, 300) + '...' : r.content,
|
|
813
|
+
score: r.score,
|
|
814
|
+
})),
|
|
815
|
+
});
|
|
816
|
+
} catch (e: any) { return `Error in conflict search: ${e.message}`; }
|
|
817
|
+
}
|
|
818
|
+
}),
|
|
819
|
+
|
|
820
|
+
'fs.vectorSearch': tool({
|
|
821
|
+
description: 'Performs a semantic similarity search across the knowledge base. Use this when searching for concepts, meanings, or topics rather than exact keywords.',
|
|
822
|
+
inputSchema: jsonSchema<{
|
|
823
|
+
query: string;
|
|
824
|
+
topK?: number;
|
|
825
|
+
minSimilarity?: number;
|
|
826
|
+
filter?: { category?: string; path_prefix?: string };
|
|
827
|
+
}>({
|
|
828
|
+
type: 'object',
|
|
829
|
+
properties: {
|
|
830
|
+
query: { type: 'string', description: 'The conceptual query (e.g., "how to handle async errors").' },
|
|
831
|
+
topK: { type: 'number', description: 'Number of most similar results to return (1-20, default: 5).' },
|
|
832
|
+
minSimilarity: { type: 'number', description: 'Similarity threshold 0-1. Lower = more creative, higher = more precise. Default: 0.7.' },
|
|
833
|
+
filter: {
|
|
834
|
+
type: 'object',
|
|
835
|
+
properties: {
|
|
836
|
+
category: {
|
|
837
|
+
type: 'string',
|
|
838
|
+
enum: ['somebody', 'something', 'decision', 'lesson', 'commitment', 'preference'],
|
|
839
|
+
description: 'Limit search to a specific category.',
|
|
840
|
+
},
|
|
841
|
+
path_prefix: { type: 'string', description: 'Limit search to a specific directory (e.g., "decision:").' },
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
required: ['query'],
|
|
846
|
+
}),
|
|
847
|
+
execute: async ({ query, topK, minSimilarity, filter }) => {
|
|
848
|
+
logger.debug({ query, topK, minSimilarity, filter }, 'FS vectorSearch');
|
|
849
|
+
try {
|
|
850
|
+
// Until vector embeddings are implemented, fall back to FTS keyword search
|
|
851
|
+
// with path filtering to approximate semantic search
|
|
852
|
+
const limit = topK || 5;
|
|
853
|
+
const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 2);
|
|
854
|
+
if (tokens.length === 0) return "No meaningful search terms found in query.";
|
|
855
|
+
|
|
856
|
+
const ftsQuery = tokens.join(' OR ');
|
|
857
|
+
let results = await memory.knowledge.searchFulltext(ftsQuery, limit * 2);
|
|
858
|
+
|
|
859
|
+
// Apply path-based filters
|
|
860
|
+
if (filter?.category) {
|
|
861
|
+
results = results.filter(r => r.path.startsWith(`${filter.category}:`));
|
|
862
|
+
}
|
|
863
|
+
if (filter?.path_prefix) {
|
|
864
|
+
results = results.filter(r => r.path.startsWith(filter.path_prefix!));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
results = results.slice(0, limit);
|
|
868
|
+
|
|
869
|
+
if (results.length === 0) return "No similar entries found.";
|
|
870
|
+
return results.map(r => {
|
|
871
|
+
const snippet = r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content;
|
|
872
|
+
return `[${r.path}] (relevance: ${r.score})\n${snippet}`;
|
|
873
|
+
}).join('\n\n');
|
|
874
|
+
} catch (e: any) { return `Error in vector search: ${e.message}`; }
|
|
875
|
+
}
|
|
876
|
+
}),
|
|
877
|
+
|
|
878
|
+
'fs.graphSearch': tool({
|
|
879
|
+
description: 'Navigates relationships between entities. Use this to find connections, backlinks, or to traverse the knowledge graph (e.g., "Who is connected to Jan?").',
|
|
880
|
+
inputSchema: jsonSchema<{
|
|
881
|
+
startNode: string;
|
|
882
|
+
direction?: string;
|
|
883
|
+
maxDepth?: number;
|
|
884
|
+
relationshipType?: string;
|
|
885
|
+
}>({
|
|
886
|
+
type: 'object',
|
|
887
|
+
properties: {
|
|
888
|
+
startNode: { type: 'string', description: 'The path of the file to start the traversal from (e.g., "somebody:jan.md").' },
|
|
889
|
+
direction: {
|
|
890
|
+
type: 'string',
|
|
891
|
+
enum: ['outbound', 'inbound', 'both'],
|
|
892
|
+
description: 'outbound: files this file links to; inbound: files linking TO this file; both: full neighborhood. Default: both.',
|
|
893
|
+
},
|
|
894
|
+
maxDepth: { type: 'number', description: 'How many hops to follow in the graph. Depth 1 = direct neighbors. Max 3. Default: 1.' },
|
|
895
|
+
relationshipType: { type: 'string', description: 'Optional filter for the relation type (e.g., "works_at").' },
|
|
896
|
+
},
|
|
897
|
+
required: ['startNode'],
|
|
898
|
+
}),
|
|
899
|
+
execute: async ({ startNode, direction, maxDepth, relationshipType }) => {
|
|
900
|
+
logger.debug({ startNode, direction, maxDepth, relationshipType }, 'FS graphSearch');
|
|
901
|
+
try {
|
|
902
|
+
const dir = (direction as 'outbound' | 'inbound' | 'both') || 'both';
|
|
903
|
+
const depth = Math.min(Math.max(maxDepth || 1, 1), 3);
|
|
904
|
+
|
|
905
|
+
const results = await memory.knowledge.graphTraverse(startNode, dir, depth, relationshipType);
|
|
906
|
+
if (results.length === 0) return `No connections found from "${startNode}".`;
|
|
907
|
+
return results.map(r =>
|
|
908
|
+
`[depth ${r.depth}] ${r.path} (${r.relation_type})`
|
|
909
|
+
).join('\n');
|
|
910
|
+
} catch (e: any) { return `Error in graph search: ${e.message}`; }
|
|
911
|
+
}
|
|
912
|
+
}),
|
|
913
|
+
|
|
914
|
+
'fs.upsertKnowledge': tool({
|
|
915
|
+
description: 'Create or update a knowledge entry (somebody, something, decision, lesson, commitment). Generates a Markdown file with YAML frontmatter and writes it to the appropriate category directory. If the file already exists it is overwritten. The path is derived automatically: <category>/<slug>.md',
|
|
916
|
+
inputSchema: jsonSchema<{
|
|
917
|
+
category: string;
|
|
918
|
+
title: string;
|
|
919
|
+
metadata?: {
|
|
920
|
+
tags: string[];
|
|
921
|
+
relations?: { target: string; type: string }[];
|
|
922
|
+
decision_status?: string;
|
|
923
|
+
due_date?: string;
|
|
924
|
+
};
|
|
925
|
+
content: string;
|
|
926
|
+
reasoning?: string;
|
|
927
|
+
}>({
|
|
928
|
+
type: 'object',
|
|
929
|
+
properties: {
|
|
930
|
+
category: {
|
|
931
|
+
type: 'string',
|
|
932
|
+
enum: ['somebody', 'something', 'decision', 'lesson', 'commitment'],
|
|
933
|
+
description: 'Knowledge category determining the target directory',
|
|
934
|
+
},
|
|
935
|
+
title: { type: 'string', description: 'Name of the entity or title of the decision' },
|
|
936
|
+
metadata: {
|
|
937
|
+
type: 'object',
|
|
938
|
+
description: 'Structured data for the YAML header',
|
|
939
|
+
properties: {
|
|
940
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
941
|
+
relations: {
|
|
942
|
+
type: 'array',
|
|
943
|
+
items: {
|
|
944
|
+
type: 'object',
|
|
945
|
+
properties: {
|
|
946
|
+
target: { type: 'string' },
|
|
947
|
+
type: { type: 'string' },
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
decision_status: { type: 'string', enum: ['accepted', 'rejected', 'proposed'] },
|
|
952
|
+
due_date: { type: 'string', format: 'date' },
|
|
953
|
+
},
|
|
954
|
+
required: ['tags'],
|
|
955
|
+
},
|
|
956
|
+
content: { type: 'string', description: 'The body text (Markdown) WITHOUT frontmatter header.' },
|
|
957
|
+
reasoning: { type: 'string', description: 'Why is this being stored? (For audit log)' },
|
|
958
|
+
},
|
|
959
|
+
required: ['category', 'title', 'content'],
|
|
960
|
+
}),
|
|
961
|
+
execute: async ({ category, title, metadata, content, reasoning }) => {
|
|
962
|
+
logger.debug({ category, title }, 'FS upsertKnowledge');
|
|
963
|
+
try {
|
|
964
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
965
|
+
const path = `${category}:${slug}.md`;
|
|
966
|
+
|
|
967
|
+
// Build YAML frontmatter
|
|
968
|
+
const yamlLines: string[] = ['---', `type: ${category}`];
|
|
969
|
+
if (metadata?.tags?.length) {
|
|
970
|
+
yamlLines.push(`tags: [${metadata.tags.join(', ')}]`);
|
|
971
|
+
}
|
|
972
|
+
if (metadata?.relations?.length) {
|
|
973
|
+
yamlLines.push('relations:');
|
|
974
|
+
for (const rel of metadata.relations) {
|
|
975
|
+
yamlLines.push(` - target: ${rel.target}`);
|
|
976
|
+
yamlLines.push(` type: ${rel.type}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (metadata?.decision_status) {
|
|
980
|
+
yamlLines.push(`decision_status: ${metadata.decision_status}`);
|
|
981
|
+
}
|
|
982
|
+
if (metadata?.due_date) {
|
|
983
|
+
yamlLines.push(`due_date: ${metadata.due_date}`);
|
|
984
|
+
}
|
|
985
|
+
yamlLines.push('---');
|
|
986
|
+
|
|
987
|
+
const fileContent = yamlLines.join('\n') + '\n' + content;
|
|
988
|
+
const existed = await memory.workspace.hasItem(path);
|
|
989
|
+
await memory.workspace.setItem(path, fileContent);
|
|
990
|
+
await memory.knowledge.upsert(
|
|
991
|
+
path,
|
|
992
|
+
fileContent,
|
|
993
|
+
metadata?.relations?.map(r => ({ target: r.target, type: r.type }))
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
if (reasoning) {
|
|
997
|
+
logger.info({ path, reasoning }, 'Knowledge upsert reasoning');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return `Success: ${existed ? 'Updated' : 'Created'} knowledge entry at ${path}`;
|
|
1001
|
+
} catch (e: any) { return `Error upserting knowledge: ${e.message}`; }
|
|
1002
|
+
}
|
|
1003
|
+
}),
|
|
1004
|
+
|
|
600
1005
|
'skill.install': tool({
|
|
601
1006
|
description: 'Install a skill from a remote URL. Downloads SKILL.md, parses it for additional files to download, analyzes required tool permissions, and saves everything to workspace under skills/<name>/.',
|
|
602
1007
|
inputSchema: jsonSchema<{ name: string; url: string }>({
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SYSTEM OPERATING PROCEDURES
|
|
2
|
+
|
|
3
|
+
## 1. KERNEL MAINTENANCE (Root Access)
|
|
4
|
+
|
|
5
|
+
You operate on a configurable Kernel defined by four root files. Unlike the files graph (Cold Storage), these files are **injected into your active context window**.
|
|
6
|
+
|
|
7
|
+
### The Kernel Files
|
|
8
|
+
1. `IDENTITY.md` (Who you are): Professional persona and mode constraints.
|
|
9
|
+
2. `SOUL.md` (Why you exist): Stewardship and ethical protocols.
|
|
10
|
+
3. `USER.md` (Who you serve): User context and current focus.
|
|
11
|
+
4. `SYSTEM_INSTRUCTIONS.md` (How you function): **This file.**
|
|
12
|
+
|
|
13
|
+
### Self-Correction Protocol
|
|
14
|
+
You are authorized to update these files using `fs.editFile` or `fs.knowledgeUpsert` if:
|
|
15
|
+
- **Drift:** The user requests an adjustment to your communication style.
|
|
16
|
+
- **Focus Shift:** The user's priorities change (update `USER.md`).
|
|
17
|
+
- **Optimization:** A procedural rule causes friction (patch `SYSTEM_INSTRUCTIONS.md`).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. KNOWLEDGE ARCHITECTURE (The Rhizome)
|
|
22
|
+
You manage a persistent knowledge graph stored in Markdown files. Your "memory" is not the context window; it is the file system.
|
|
23
|
+
|
|
24
|
+
### File Structure & Types
|
|
25
|
+
All knowledge is stored in root-level category folders. **Do not create uncategorized files.**
|
|
26
|
+
|
|
27
|
+
- `somebody/*.md`: People (Attributes, Relationships).
|
|
28
|
+
- `something/*.md`: Companies, Tech, Places, Concepts.
|
|
29
|
+
- `decision/*.md`: Architecture decisions (Requires: `status`, `context`, `reasoning`).
|
|
30
|
+
- `lesson/*.md`: Post-mortems, debug logs, learned patterns.
|
|
31
|
+
- `commitment/*.md`: Deadlines, promises, active projects.
|
|
32
|
+
- `preference/*.md`: User constraints (Styling, defaults).
|
|
33
|
+
- `memory/YYYY-MM-DD/{HHmm}-{slug}.md`: **Sharded Daily Logs**.
|
|
34
|
+
|
|
35
|
+
### Linking & Provenance
|
|
36
|
+
- **Bidirectional Linking:** Connect dots. A fact without a link is lost. Connect new entities to existing ones.
|
|
37
|
+
- **Relative Paths:** Use `../` to switch categories (e.g., `../something/java.md`).
|
|
38
|
+
- **Provenance:** Build trust. Cite sources using footnotes `[^1]`.
|
|
39
|
+
- *Format:* `As decided in the planning meeting[^1]...` -> `[^1]: [Log 09:30](../memory/2026-02-16/0930-planning.md)`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. TOOL USAGE PROTOCOLS
|
|
44
|
+
|
|
45
|
+
### Retrieval (Search Cascade & Research Protocol)
|
|
46
|
+
To provide high-quality, grounded assistance, follow this research hierarchy. Always aim for a comprehensive understanding before proposing solutions.
|
|
47
|
+
|
|
48
|
+
1. **`fs.search` (Unified):** Your primary entry point. Use this to gather initial context via hybrid (vector + keyword) search. It is the most efficient way to scan the entire knowledge base for a topic.
|
|
49
|
+
2. **`fs.graphSearch` (Relational Mapping):** Once relevant files are found, use this to map dependencies. Investigate what a file links to and what links back to it. This ensures you understand the broader impact of any information or change.
|
|
50
|
+
3. **`fs.vectorSearch` (Conceptual Depth):** Utilize this when the user's request is thematic or abstract. It helps you find "hidden" connections that exact keywords might miss (e.g., finding "risk management" lessons when the user asks about "safety").
|
|
51
|
+
4. **`fs.keywordSearch` (Technical Precision):** Use for pinpointing specific technical identifiers, error codes, or unique terms that require 100% character-match accuracy.
|
|
52
|
+
5. **`fs.temporalSearch` (Contextual Timeline):** Essential for maintaining project momentum. Use this to review recent `memory/` logs or upcoming `commitment/` deadlines to provide chronologically relevant advice.
|
|
53
|
+
6. **`fs.conflictSearch` (Integrity Assurance):** **MANDATORY** before modifying `decision/`, `preference/`, or `somebody/` files. In Professional Mode, you are the guardian of consistency; you must identify and resolve contradictions before they are persisted.
|
|
54
|
+
|
|
55
|
+
### Storage (`fs.knowledgeUpsert`)
|
|
56
|
+
Use this for **CREATING** or **RESTRUCTURING** knowledge.
|
|
57
|
+
- **Input:** JSON. The tool handles formatting.
|
|
58
|
+
- **Memory Sharding:** Generate paths like `memory/YYYY-MM-DD/HHmm-topic.md`.
|
|
59
|
+
- **Quality Control:** Ensure `metadata` is complete. For decisions, include the `status` and `reasoning`.
|
|
60
|
+
|
|
61
|
+
### Maintenance (`fs.editFile`)
|
|
62
|
+
Use this for **REFINING** or **PATCHING**.
|
|
63
|
+
- **Use Case:** Updating a status, adding a cross-link, correcting a typo.
|
|
64
|
+
- **Constraint:** Send only the `search` block (context) and `replace` block.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 4. OPERATIONAL RULES
|
|
69
|
+
|
|
70
|
+
### Rule 1: Memory Sharding (Anti-Monolith)
|
|
71
|
+
- **Do NOT** append to a single daily file.
|
|
72
|
+
- Create a **NEW** memory file (`HHmm-{slug}.md`) for every distinct session or topic.
|
|
73
|
+
- *Reason:* Ensures precise retrieval and keeps files readable.
|
|
74
|
+
|
|
75
|
+
### Rule 2: The Capture Flow
|
|
76
|
+
1. **Log:** Record the interaction to `memory/YYYY-MM-DD/HHmm-{slug}.md`.
|
|
77
|
+
2. **Synthesize:** Extract key Facts/Decisions to `somebody/`, `decision/`, etc.
|
|
78
|
+
3. **Connect:** Link the permanent entity back to the memory log.
|
|
79
|
+
|
|
80
|
+
### Rule 3: Constructive Conflict
|
|
81
|
+
- If input contradicts `fs.search`, **PAUSE**.
|
|
82
|
+
- Politely flag the discrepancy: *"I noticed this contradicts our decision on PayloadCMS from last week. Shall we update the decision?"*
|
|
83
|
+
- Upon confirmation, update the record and log the rationale.
|
|
84
|
+
|
|
85
|
+
### Rule 4: Proactive Gardening
|
|
86
|
+
- When adding a new file, check if it should be added to an `index.md` or linked from a related project file. Keep the graph healthy.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 5. RESPONSE FORMAT
|
|
91
|
+
- **Structure:** Use clear Markdown headers, bullet points, and bold text for emphasis.
|
|
92
|
+
- **Tone:** Professional, articulate, and helpful.
|
|
93
|
+
- **Reasoning:** When presenting a solution, briefly explain *why* (Chain of Thought).
|
|
94
|
+
- **Action Oriented:** Conclude with a clear next step or confirmation of action.
|