@soleri/core 9.14.4 → 9.15.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/dist/brain/brain.d.ts +9 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +11 -1
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +24 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/chat/chat-session.d.ts +6 -0
- package/dist/chat/chat-session.d.ts.map +1 -1
- package/dist/chat/chat-session.js +68 -17
- package/dist/chat/chat-session.js.map +1 -1
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +138 -0
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/types.d.ts +10 -0
- package/dist/curator/types.d.ts.map +1 -1
- package/dist/engine/bin/soleri-engine.js +0 -0
- package/dist/flows/types.d.ts +16 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +10 -4
- package/dist/intake/content-classifier.d.ts.map +1 -1
- package/dist/intake/content-classifier.js +19 -5
- package/dist/intake/content-classifier.js.map +1 -1
- package/dist/intake/text-ingester.d.ts +18 -0
- package/dist/intake/text-ingester.d.ts.map +1 -1
- package/dist/intake/text-ingester.js +37 -13
- package/dist/intake/text-ingester.js.map +1 -1
- package/dist/planning/planner.d.ts +3 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +43 -4
- package/dist/planning/planner.js.map +1 -1
- package/dist/plugins/types.d.ts +2 -2
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +59 -20
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +28 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +16 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +19 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/validate-skills.d.ts +32 -0
- package/dist/skills/validate-skills.d.ts.map +1 -0
- package/dist/skills/validate-skills.js +396 -0
- package/dist/skills/validate-skills.js.map +1 -0
- package/dist/vault/default-canonical-tags.d.ts +15 -0
- package/dist/vault/default-canonical-tags.d.ts.map +1 -0
- package/dist/vault/default-canonical-tags.js +65 -0
- package/dist/vault/default-canonical-tags.js.map +1 -0
- package/dist/vault/tag-normalizer.d.ts +42 -0
- package/dist/vault/tag-normalizer.d.ts.map +1 -0
- package/dist/vault/tag-normalizer.js +157 -0
- package/dist/vault/tag-normalizer.js.map +1 -0
- package/package.json +5 -1
- package/src/__tests__/embeddings.test.ts +3 -3
- package/src/brain/brain.ts +25 -1
- package/src/brain/intelligence.ts +25 -0
- package/src/brain/types.ts +1 -0
- package/src/chat/chat-session.ts +75 -17
- package/src/chat/chat-transport.test.ts +31 -1
- package/src/curator/curator.ts +180 -0
- package/src/curator/types.ts +10 -0
- package/src/index.ts +7 -0
- package/src/intake/content-classifier.ts +22 -4
- package/src/intake/text-ingester.ts +61 -12
- package/src/planning/planner.test.ts +86 -90
- package/src/planning/planner.ts +48 -4
- package/src/runtime/admin-setup-ops.test.ts +44 -0
- package/src/runtime/admin-setup-ops.ts +59 -20
- package/src/runtime/facades/orchestrate-facade.ts +27 -1
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/types.ts +19 -0
- package/src/skills/validate-skills.test.ts +205 -0
- package/src/skills/validate-skills.ts +470 -0
- package/src/vault/default-canonical-tags.ts +64 -0
- package/src/vault/tag-normalizer.test.ts +214 -0
- package/src/vault/tag-normalizer.ts +188 -0
- package/dist/embeddings/index.d.ts +0 -5
- package/dist/embeddings/index.d.ts.map +0 -1
- package/dist/embeddings/index.js +0 -3
- package/dist/embeddings/index.js.map +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default canonical tag taxonomy for Soleri agents.
|
|
3
|
+
*
|
|
4
|
+
* These tags represent the most common knowledge domains. When a vault is
|
|
5
|
+
* configured with tagConstraintMode 'suggest' or 'enforce', incoming tags
|
|
6
|
+
* are mapped to the nearest entry in this list via edit-distance matching.
|
|
7
|
+
*
|
|
8
|
+
* To use these defaults in your agent runtime config:
|
|
9
|
+
* import { DEFAULT_CANONICAL_TAGS } from '@soleri/core';
|
|
10
|
+
* // ...
|
|
11
|
+
* canonicalTags: DEFAULT_CANONICAL_TAGS,
|
|
12
|
+
* tagConstraintMode: 'suggest',
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_CANONICAL_TAGS = [
|
|
15
|
+
'architecture',
|
|
16
|
+
'typescript',
|
|
17
|
+
'react',
|
|
18
|
+
'testing',
|
|
19
|
+
'workflow',
|
|
20
|
+
'design-tokens',
|
|
21
|
+
'accessibility',
|
|
22
|
+
'performance',
|
|
23
|
+
'security',
|
|
24
|
+
'planning',
|
|
25
|
+
'soleri',
|
|
26
|
+
'vault',
|
|
27
|
+
'mcp',
|
|
28
|
+
'claude-code',
|
|
29
|
+
'ai',
|
|
30
|
+
'learning',
|
|
31
|
+
'gamification',
|
|
32
|
+
'education',
|
|
33
|
+
'adhd',
|
|
34
|
+
'routing',
|
|
35
|
+
'orchestration',
|
|
36
|
+
'skills',
|
|
37
|
+
'automation',
|
|
38
|
+
'git',
|
|
39
|
+
'database',
|
|
40
|
+
'api',
|
|
41
|
+
'authentication',
|
|
42
|
+
'subagent',
|
|
43
|
+
'design-system',
|
|
44
|
+
'component',
|
|
45
|
+
'frontend',
|
|
46
|
+
'backend',
|
|
47
|
+
'tooling',
|
|
48
|
+
'monorepo',
|
|
49
|
+
'refactoring',
|
|
50
|
+
'debugging',
|
|
51
|
+
'deployment',
|
|
52
|
+
'configuration',
|
|
53
|
+
'documentation',
|
|
54
|
+
'pattern',
|
|
55
|
+
'anti-pattern',
|
|
56
|
+
'principle',
|
|
57
|
+
'decision',
|
|
58
|
+
'migration',
|
|
59
|
+
'plugin',
|
|
60
|
+
'hook',
|
|
61
|
+
'schema',
|
|
62
|
+
'pipeline',
|
|
63
|
+
'ingestion',
|
|
64
|
+
];
|
|
65
|
+
//# sourceMappingURL=default-canonical-tags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-canonical-tags.js","sourceRoot":"","sources":["../../src/vault/default-canonical-tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAa;IAC9C,cAAc;IACd,YAAY;IACZ,OAAO;IACP,SAAS;IACT,UAAU;IACV,eAAe;IACf,eAAe;IACf,aAAa;IACb,UAAU;IACV,UAAU;IACV,QAAQ;IACR,OAAO;IACP,KAAK;IACL,aAAa;IACb,IAAI;IACJ,UAAU;IACV,cAAc;IACd,WAAW;IACX,MAAM;IACN,SAAS;IACT,eAAe;IACf,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,UAAU;IACV,KAAK;IACL,gBAAgB;IAChB,UAAU;IACV,eAAe;IACf,WAAW;IACX,UAAU;IACV,SAAS;IACT,SAAS;IACT,UAAU;IACV,aAAa;IACb,WAAW;IACX,YAAY;IACZ,eAAe;IACf,eAAe;IACf,SAAS;IACT,cAAc;IACd,WAAW;IACX,UAAU;IACV,WAAW;IACX,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,UAAU;IACV,WAAW;CACZ,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag Normalizer — canonical tag taxonomy enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Maps raw tags to nearest canonical via Levenshtein edit-distance.
|
|
5
|
+
* Strips noise words (version strings, single generic words).
|
|
6
|
+
* Respects metadata tag prefixes (e.g. 'source:').
|
|
7
|
+
*
|
|
8
|
+
* Three modes:
|
|
9
|
+
* - 'enforce': must match within edit-distance 3, else drop (return null)
|
|
10
|
+
* - 'suggest': map to nearest canonical within edit-distance 2 (passthrough if no match)
|
|
11
|
+
* - 'off': no normalization — return tag as-is
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Compute Levenshtein edit distance between two strings.
|
|
15
|
+
* O(m*n) time, O(n) space using two-row DP.
|
|
16
|
+
*/
|
|
17
|
+
export declare function computeEditDistance(a: string, b: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Returns true if the tag starts with any of the given prefixes.
|
|
20
|
+
* Metadata tags (e.g. 'source:article') are exempt from canonical normalization.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isMetadataTag(tag: string, prefixes: string[]): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a single tag against a canonical list.
|
|
25
|
+
*
|
|
26
|
+
* @param tag - Raw tag to normalize
|
|
27
|
+
* @param canonical - Canonical tag list to map against
|
|
28
|
+
* @param mode - Constraint mode: 'enforce' | 'suggest' | 'off'
|
|
29
|
+
* @returns Normalized tag string, or null if the tag should be dropped.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeTag(tag: string, canonical: string[], mode: 'enforce' | 'suggest' | 'off'): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a batch of tags against a canonical list.
|
|
34
|
+
* Filters out nulls (dropped tags). Deduplicates the result.
|
|
35
|
+
*
|
|
36
|
+
* @param tags - Raw tags
|
|
37
|
+
* @param canonical - Canonical tag list
|
|
38
|
+
* @param mode - Constraint mode
|
|
39
|
+
* @param metadataPrefixes - Tags with these prefixes bypass normalization
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeTags(tags: string[], canonical: string[], mode: 'enforce' | 'suggest' | 'off', metadataPrefixes?: string[]): string[];
|
|
42
|
+
//# sourceMappingURL=tag-normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-normalizer.d.ts","sourceRoot":"","sources":["../../src/vault/tag-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqCH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAsBhE;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAEtE;AAID;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,EACnB,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,KAAK,GAClC,MAAM,GAAG,IAAI,CA6Cf;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,EAAE,EACnB,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,KAAK,EACnC,gBAAgB,GAAE,MAAM,EAAgB,GACvC,MAAM,EAAE,CAwBV"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag Normalizer — canonical tag taxonomy enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Maps raw tags to nearest canonical via Levenshtein edit-distance.
|
|
5
|
+
* Strips noise words (version strings, single generic words).
|
|
6
|
+
* Respects metadata tag prefixes (e.g. 'source:').
|
|
7
|
+
*
|
|
8
|
+
* Three modes:
|
|
9
|
+
* - 'enforce': must match within edit-distance 3, else drop (return null)
|
|
10
|
+
* - 'suggest': map to nearest canonical within edit-distance 2 (passthrough if no match)
|
|
11
|
+
* - 'off': no normalization — return tag as-is
|
|
12
|
+
*/
|
|
13
|
+
// ─── Noise filter ────────────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Version-string pattern: v1.2, v10, v1.2.3, etc.
|
|
16
|
+
*/
|
|
17
|
+
const VERSION_PATTERN = /^v\d+(\.\d+)*/i;
|
|
18
|
+
/**
|
|
19
|
+
* Generic single words that add no signal.
|
|
20
|
+
*/
|
|
21
|
+
const NOISE_WORDS = new Set([
|
|
22
|
+
'one',
|
|
23
|
+
'via',
|
|
24
|
+
'new',
|
|
25
|
+
'full',
|
|
26
|
+
'actual',
|
|
27
|
+
'raw',
|
|
28
|
+
'the',
|
|
29
|
+
'and',
|
|
30
|
+
'for',
|
|
31
|
+
'with',
|
|
32
|
+
'this',
|
|
33
|
+
'that',
|
|
34
|
+
'from',
|
|
35
|
+
'into',
|
|
36
|
+
]);
|
|
37
|
+
function isNoisy(tag) {
|
|
38
|
+
if (VERSION_PATTERN.test(tag))
|
|
39
|
+
return true;
|
|
40
|
+
if (NOISE_WORDS.has(tag.toLowerCase()))
|
|
41
|
+
return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// ─── Levenshtein edit distance ───────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Compute Levenshtein edit distance between two strings.
|
|
47
|
+
* O(m*n) time, O(n) space using two-row DP.
|
|
48
|
+
*/
|
|
49
|
+
export function computeEditDistance(a, b) {
|
|
50
|
+
if (a === b)
|
|
51
|
+
return 0;
|
|
52
|
+
if (a.length === 0)
|
|
53
|
+
return b.length;
|
|
54
|
+
if (b.length === 0)
|
|
55
|
+
return a.length;
|
|
56
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
57
|
+
let curr = Array.from({ length: b.length + 1 });
|
|
58
|
+
for (let i = 1; i <= a.length; i++) {
|
|
59
|
+
curr[0] = i;
|
|
60
|
+
for (let j = 1; j <= b.length; j++) {
|
|
61
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
62
|
+
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
63
|
+
prev[j] + 1, // deletion
|
|
64
|
+
prev[j - 1] + cost);
|
|
65
|
+
}
|
|
66
|
+
[prev, curr] = [curr, prev];
|
|
67
|
+
}
|
|
68
|
+
return prev[b.length];
|
|
69
|
+
}
|
|
70
|
+
// ─── Metadata tag check ──────────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Returns true if the tag starts with any of the given prefixes.
|
|
73
|
+
* Metadata tags (e.g. 'source:article') are exempt from canonical normalization.
|
|
74
|
+
*/
|
|
75
|
+
export function isMetadataTag(tag, prefixes) {
|
|
76
|
+
return prefixes.some((prefix) => tag.startsWith(prefix));
|
|
77
|
+
}
|
|
78
|
+
// ─── Single tag normalization ────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Normalize a single tag against a canonical list.
|
|
81
|
+
*
|
|
82
|
+
* @param tag - Raw tag to normalize
|
|
83
|
+
* @param canonical - Canonical tag list to map against
|
|
84
|
+
* @param mode - Constraint mode: 'enforce' | 'suggest' | 'off'
|
|
85
|
+
* @returns Normalized tag string, or null if the tag should be dropped.
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeTag(tag, canonical, mode) {
|
|
88
|
+
if (mode === 'off')
|
|
89
|
+
return tag;
|
|
90
|
+
const lower = tag.toLowerCase().trim();
|
|
91
|
+
// Always drop noise words
|
|
92
|
+
if (isNoisy(lower))
|
|
93
|
+
return null;
|
|
94
|
+
// Derive lowercase canonical for matching; preserve original casing for return
|
|
95
|
+
const canonicalLower = canonical.map((x) => x.toLowerCase());
|
|
96
|
+
// Exact match in canonical list — return canonical form (original casing)
|
|
97
|
+
const exactIdx = canonicalLower.indexOf(lower);
|
|
98
|
+
if (exactIdx !== -1)
|
|
99
|
+
return canonical[exactIdx];
|
|
100
|
+
if (canonical.length === 0) {
|
|
101
|
+
// No canonical list configured — pass through in suggest, drop in enforce
|
|
102
|
+
return mode === 'enforce' ? null : lower;
|
|
103
|
+
}
|
|
104
|
+
// Find nearest canonical by edit distance
|
|
105
|
+
let bestMatch = null;
|
|
106
|
+
let bestDist = Infinity;
|
|
107
|
+
for (let i = 0; i < canonicalLower.length; i++) {
|
|
108
|
+
const dist = computeEditDistance(lower, canonicalLower[i]);
|
|
109
|
+
if (dist < bestDist) {
|
|
110
|
+
bestDist = dist;
|
|
111
|
+
bestMatch = canonical[i];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const threshold = mode === 'enforce' ? 3 : 2;
|
|
115
|
+
if (bestDist <= threshold && bestMatch !== null) {
|
|
116
|
+
return bestMatch;
|
|
117
|
+
}
|
|
118
|
+
// No close match found
|
|
119
|
+
if (mode === 'enforce') {
|
|
120
|
+
return null; // drop the tag
|
|
121
|
+
}
|
|
122
|
+
// 'suggest' mode — keep original tag unchanged (passthrough)
|
|
123
|
+
return lower;
|
|
124
|
+
}
|
|
125
|
+
// ─── Batch tag normalization ─────────────────────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Normalize a batch of tags against a canonical list.
|
|
128
|
+
* Filters out nulls (dropped tags). Deduplicates the result.
|
|
129
|
+
*
|
|
130
|
+
* @param tags - Raw tags
|
|
131
|
+
* @param canonical - Canonical tag list
|
|
132
|
+
* @param mode - Constraint mode
|
|
133
|
+
* @param metadataPrefixes - Tags with these prefixes bypass normalization
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeTags(tags, canonical, mode, metadataPrefixes = ['source:']) {
|
|
136
|
+
if (mode === 'off')
|
|
137
|
+
return tags;
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
const result = [];
|
|
140
|
+
for (const tag of tags) {
|
|
141
|
+
// Metadata tags bypass canonical normalization but are still kept
|
|
142
|
+
if (isMetadataTag(tag, metadataPrefixes)) {
|
|
143
|
+
if (!seen.has(tag)) {
|
|
144
|
+
seen.add(tag);
|
|
145
|
+
result.push(tag);
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const normalized = normalizeTag(tag, canonical, mode);
|
|
150
|
+
if (normalized !== null && !seen.has(normalized)) {
|
|
151
|
+
seen.add(normalized);
|
|
152
|
+
result.push(normalized);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=tag-normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-normalizer.js","sourceRoot":"","sources":["../../src/vault/tag-normalizer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soleri/core",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.15.0",
|
|
4
4
|
"description": "Shared engine for Soleri agents — vault, brain, planner, LLM utilities, and facade infrastructure.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"./personas": {
|
|
40
40
|
"types": "./dist/persona/defaults.d.ts",
|
|
41
41
|
"import": "./dist/persona/defaults.js"
|
|
42
|
+
},
|
|
43
|
+
"./skills/validate-skills": {
|
|
44
|
+
"types": "./dist/skills/validate-skills.d.ts",
|
|
45
|
+
"import": "./dist/skills/validate-skills.js"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"publishConfig": {
|
|
@@ -388,7 +388,7 @@ describe('EmbeddingPipeline', () => {
|
|
|
388
388
|
|
|
389
389
|
expect(result.embedded).toBe(2);
|
|
390
390
|
expect(result.failed).toBe(0);
|
|
391
|
-
expect(result.tokensUsed).
|
|
391
|
+
expect(result.tokensUsed).toBe(10); // mock provider returns texts.length * 5 = 2 * 5
|
|
392
392
|
|
|
393
393
|
// Both entries should now have vectors
|
|
394
394
|
expect(getVector(persistence, 'e1')).toBeTruthy();
|
|
@@ -405,7 +405,7 @@ describe('EmbeddingPipeline', () => {
|
|
|
405
405
|
onProgress: (completed, total) => progress.push([completed, total]),
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
-
expect(progress.length).
|
|
408
|
+
expect(progress.length).toBe(1); // default batchSize=100 processes both entries in one batch, firing onProgress once
|
|
409
409
|
// Last progress call should have completed == total
|
|
410
410
|
const last = progress[progress.length - 1];
|
|
411
411
|
expect(last[0]).toBe(last[1]);
|
|
@@ -513,7 +513,7 @@ describe('Brain hybrid search compatibility', () => {
|
|
|
513
513
|
|
|
514
514
|
// Should not throw — returns results from FTS only
|
|
515
515
|
expect(Array.isArray(results)).toBe(true);
|
|
516
|
-
expect(results.length).
|
|
516
|
+
expect(results.length).toBe(1); // single seeded entry matches FTS query 'backward compatibility'
|
|
517
517
|
});
|
|
518
518
|
|
|
519
519
|
it('Brain.setEmbeddingProvider can set and clear provider', () => {
|
package/src/brain/brain.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { SearchResult } from '../vault/vault.js';
|
|
|
3
3
|
import type { VaultManager } from '../vault/vault-manager.js';
|
|
4
4
|
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
5
5
|
import { computeContentHash } from '../vault/content-hash.js';
|
|
6
|
+
import { normalizeTags as normalizeTagsCanonical } from '../vault/tag-normalizer.js';
|
|
6
7
|
import {
|
|
7
8
|
tokenize,
|
|
8
9
|
calculateTf,
|
|
@@ -76,12 +77,20 @@ const DUPLICATE_BLOCK_THRESHOLD = 0.8;
|
|
|
76
77
|
const DUPLICATE_WARN_THRESHOLD = 0.6;
|
|
77
78
|
const RECENCY_HALF_LIFE_DAYS = 365;
|
|
78
79
|
|
|
80
|
+
/** Canonical tag taxonomy config, injected from AgentRuntimeConfig. */
|
|
81
|
+
export interface CanonicalTagConfig {
|
|
82
|
+
canonicalTags: string[];
|
|
83
|
+
tagConstraintMode: 'enforce' | 'suggest' | 'off';
|
|
84
|
+
metadataTagPrefixes: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
export class Brain {
|
|
80
88
|
private vault: Vault;
|
|
81
89
|
private vaultManager: VaultManager | undefined;
|
|
82
90
|
private embeddingProvider: EmbeddingProvider | undefined;
|
|
83
91
|
private vocabulary: Map<string, number> = new Map();
|
|
84
92
|
private weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
|
|
93
|
+
private canonicalTagConfig: CanonicalTagConfig | undefined;
|
|
85
94
|
|
|
86
95
|
constructor(vault: Vault, vaultManager?: VaultManager, embeddingProvider?: EmbeddingProvider) {
|
|
87
96
|
this.vault = vault;
|
|
@@ -91,6 +100,11 @@ export class Brain {
|
|
|
91
100
|
this.recomputeWeights();
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
/** Configure canonical tag taxonomy. Called by createAgentRuntime when config provides canonicalTags. */
|
|
104
|
+
setCanonicalTagConfig(cfg: CanonicalTagConfig): void {
|
|
105
|
+
this.canonicalTagConfig = cfg;
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
/** Set or replace the embedding provider at runtime. */
|
|
95
109
|
setEmbeddingProvider(provider: EmbeddingProvider | undefined): void {
|
|
96
110
|
this.embeddingProvider = provider;
|
|
@@ -260,7 +274,17 @@ export class Brain {
|
|
|
260
274
|
},
|
|
261
275
|
): CaptureResult {
|
|
262
276
|
const autoTags = this.generateTags(entry.title, entry.description, entry.context);
|
|
263
|
-
|
|
277
|
+
let mergedTags = Array.from(new Set([...(entry.tags ?? []), ...autoTags]));
|
|
278
|
+
|
|
279
|
+
// Apply canonical tag normalization if configured
|
|
280
|
+
if (this.canonicalTagConfig && this.canonicalTagConfig.tagConstraintMode !== 'off') {
|
|
281
|
+
mergedTags = normalizeTagsCanonical(
|
|
282
|
+
mergedTags,
|
|
283
|
+
this.canonicalTagConfig.canonicalTags,
|
|
284
|
+
this.canonicalTagConfig.tagConstraintMode,
|
|
285
|
+
this.canonicalTagConfig.metadataTagPrefixes,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
264
288
|
|
|
265
289
|
const duplicate = this.detectDuplicate(entry.title, entry.domain);
|
|
266
290
|
|
|
@@ -1129,6 +1129,30 @@ export class BrainIntelligence {
|
|
|
1129
1129
|
// ─── Intelligence Pipeline ────────────────────────────────────────
|
|
1130
1130
|
|
|
1131
1131
|
buildIntelligence(): BuildIntelligenceResult {
|
|
1132
|
+
// Step 0: GC — close orphaned sessions with no execution signal older than 24h
|
|
1133
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
1134
|
+
const cutoff = new Date(Date.now() - TTL_MS).toISOString();
|
|
1135
|
+
const activeSessions = this.listSessions({ active: true, limit: 1000 });
|
|
1136
|
+
let gcClosed = 0;
|
|
1137
|
+
for (const s of activeSessions) {
|
|
1138
|
+
const isOld = s.startedAt < cutoff;
|
|
1139
|
+
const hasNoSignal =
|
|
1140
|
+
s.toolsUsed.length === 0 && s.filesModified.length === 0 && s.planOutcome === null;
|
|
1141
|
+
if (isOld && hasNoSignal) {
|
|
1142
|
+
try {
|
|
1143
|
+
this.lifecycle({
|
|
1144
|
+
action: 'end',
|
|
1145
|
+
sessionId: s.id,
|
|
1146
|
+
planOutcome: 'abandoned',
|
|
1147
|
+
context: 'auto-gc: no execution signal after TTL',
|
|
1148
|
+
});
|
|
1149
|
+
gcClosed++;
|
|
1150
|
+
} catch {
|
|
1151
|
+
// GC must never break the intelligence pipeline
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1132
1156
|
// Step 1: Compute and persist strengths
|
|
1133
1157
|
const strengths = this.computeStrengths();
|
|
1134
1158
|
|
|
@@ -1154,6 +1178,7 @@ export class BrainIntelligence {
|
|
|
1154
1178
|
strengthsComputed: strengths.length,
|
|
1155
1179
|
globalPatterns,
|
|
1156
1180
|
domainProfiles,
|
|
1181
|
+
gcClosed,
|
|
1157
1182
|
};
|
|
1158
1183
|
}
|
|
1159
1184
|
|
package/src/brain/types.ts
CHANGED
package/src/chat/chat-session.ts
CHANGED
|
@@ -16,6 +16,7 @@ const DEFAULT_TTL_MS = 7_200_000; // 2 hours
|
|
|
16
16
|
const DEFAULT_COMPACTION_THRESHOLD = 100;
|
|
17
17
|
const DEFAULT_COMPACTION_KEEP = 40;
|
|
18
18
|
const REAPER_INTERVAL_MS = 60_000; // 1 minute
|
|
19
|
+
const SESSION_SUBDIR = 'sessions';
|
|
19
20
|
|
|
20
21
|
export class ChatSessionManager {
|
|
21
22
|
private sessions = new Map<string, ChatSession>();
|
|
@@ -31,6 +32,7 @@ export class ChatSessionManager {
|
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
mkdirSync(this.config.storageDir, { recursive: true });
|
|
35
|
+
mkdirSync(this.sessionDir(), { recursive: true });
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
@@ -78,7 +80,8 @@ export class ChatSessionManager {
|
|
|
78
80
|
*/
|
|
79
81
|
has(sessionId: string): boolean {
|
|
80
82
|
if (this.sessions.has(sessionId)) return true;
|
|
81
|
-
|
|
83
|
+
if (existsSync(this.sessionPath(sessionId))) return true;
|
|
84
|
+
return this.loadSessionFile(this.legacySessionPath(sessionId)) !== undefined;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
// ─── Message Management ─────────────────────────────────────────
|
|
@@ -159,15 +162,11 @@ export class ChatSessionManager {
|
|
|
159
162
|
*/
|
|
160
163
|
listAll(): string[] {
|
|
161
164
|
const memoryIds = new Set(this.sessions.keys());
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// Directory may not exist yet
|
|
165
|
+
for (const id of this.readPersistedSessionIds(this.sessionDir())) {
|
|
166
|
+
memoryIds.add(id);
|
|
167
|
+
}
|
|
168
|
+
for (const id of this.readPersistedSessionIds(this.config.storageDir)) {
|
|
169
|
+
memoryIds.add(id);
|
|
171
170
|
}
|
|
172
171
|
return [...memoryIds];
|
|
173
172
|
}
|
|
@@ -246,38 +245,97 @@ export class ChatSessionManager {
|
|
|
246
245
|
session.messages = session.messages.slice(-keep);
|
|
247
246
|
}
|
|
248
247
|
|
|
248
|
+
private sessionDir(): string {
|
|
249
|
+
return join(this.config.storageDir, SESSION_SUBDIR);
|
|
250
|
+
}
|
|
251
|
+
|
|
249
252
|
private sessionPath(sessionId: string): string {
|
|
250
253
|
// Sanitize ID for filesystem safety
|
|
254
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
255
|
+
return join(this.sessionDir(), `${safe}.json`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private legacySessionPath(sessionId: string): string {
|
|
251
259
|
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
252
260
|
return join(this.config.storageDir, `${safe}.json`);
|
|
253
261
|
}
|
|
254
262
|
|
|
255
263
|
private persistToDisk(session: ChatSession): void {
|
|
256
264
|
try {
|
|
265
|
+
mkdirSync(this.sessionDir(), { recursive: true });
|
|
257
266
|
writeFileSync(this.sessionPath(session.id), JSON.stringify(session), 'utf-8');
|
|
267
|
+
|
|
268
|
+
// Clean up valid legacy session files after migrating to the namespaced path.
|
|
269
|
+
const legacyPath = this.legacySessionPath(session.id);
|
|
270
|
+
if (this.loadSessionFile(legacyPath)) {
|
|
271
|
+
this.removeFile(legacyPath);
|
|
272
|
+
}
|
|
258
273
|
} catch {
|
|
259
274
|
// Disk write failure is non-critical — session lives in memory
|
|
260
275
|
}
|
|
261
276
|
}
|
|
262
277
|
|
|
263
278
|
private loadFromDisk(sessionId: string): ChatSession | undefined {
|
|
264
|
-
const
|
|
279
|
+
const current = this.loadSessionFile(this.sessionPath(sessionId));
|
|
280
|
+
if (current) return current;
|
|
281
|
+
|
|
282
|
+
const legacyPath = this.legacySessionPath(sessionId);
|
|
283
|
+
const legacy = this.loadSessionFile(legacyPath);
|
|
284
|
+
if (legacy) {
|
|
285
|
+
this.persistToDisk(legacy);
|
|
286
|
+
return legacy;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private removeFromDisk(sessionId: string): void {
|
|
293
|
+
this.removeFile(this.sessionPath(sessionId));
|
|
294
|
+
const legacyPath = this.legacySessionPath(sessionId);
|
|
295
|
+
if (this.loadSessionFile(legacyPath)) {
|
|
296
|
+
this.removeFile(legacyPath);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private removeFile(path: string): void {
|
|
301
|
+
try {
|
|
302
|
+
rmSync(path, { force: true });
|
|
303
|
+
} catch {
|
|
304
|
+
// Removal failure is non-critical
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private loadSessionFile(path: string): ChatSession | undefined {
|
|
265
309
|
if (!existsSync(path)) return undefined;
|
|
266
310
|
|
|
267
311
|
try {
|
|
268
|
-
const data = readFileSync(path, 'utf-8');
|
|
269
|
-
return
|
|
312
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
313
|
+
return this.isChatSession(data) ? data : undefined;
|
|
270
314
|
} catch {
|
|
271
315
|
return undefined;
|
|
272
316
|
}
|
|
273
317
|
}
|
|
274
318
|
|
|
275
|
-
private
|
|
276
|
-
const path = this.sessionPath(sessionId);
|
|
319
|
+
private readPersistedSessionIds(dir: string): string[] {
|
|
277
320
|
try {
|
|
278
|
-
|
|
321
|
+
return readdirSync(dir).flatMap((file) => {
|
|
322
|
+
if (!file.endsWith('.json')) return [];
|
|
323
|
+
const session = this.loadSessionFile(join(dir, file));
|
|
324
|
+
return session ? [session.id] : [];
|
|
325
|
+
});
|
|
279
326
|
} catch {
|
|
280
|
-
|
|
327
|
+
return [];
|
|
281
328
|
}
|
|
282
329
|
}
|
|
330
|
+
|
|
331
|
+
private isChatSession(value: unknown): value is ChatSession {
|
|
332
|
+
if (!value || typeof value !== 'object') return false;
|
|
333
|
+
const session = value as Partial<ChatSession>;
|
|
334
|
+
return (
|
|
335
|
+
typeof session.id === 'string' &&
|
|
336
|
+
Array.isArray(session.messages) &&
|
|
337
|
+
typeof session.createdAt === 'number' &&
|
|
338
|
+
typeof session.lastActiveAt === 'number'
|
|
339
|
+
);
|
|
340
|
+
}
|
|
283
341
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
7
|
+
import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { ChatSessionManager } from './chat-session.js';
|
|
@@ -136,6 +136,19 @@ describe('ChatSessionManager', () => {
|
|
|
136
136
|
expect(all).toContain('chat-2');
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
test('listAll ignores non-session JSON files in the storage root', () => {
|
|
140
|
+
manager.getOrCreate('chat-1');
|
|
141
|
+
writeFileSync(
|
|
142
|
+
join(dir, 'plans.json'),
|
|
143
|
+
JSON.stringify({ version: '1.0', plans: [] }),
|
|
144
|
+
'utf-8',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const all = manager.listAll();
|
|
148
|
+
expect(all).toContain('chat-1');
|
|
149
|
+
expect(all).not.toContain('plans');
|
|
150
|
+
});
|
|
151
|
+
|
|
139
152
|
test('setMeta updates metadata', () => {
|
|
140
153
|
manager.getOrCreate('chat-1');
|
|
141
154
|
manager.setMeta('chat-1', { mood: 'happy' });
|
|
@@ -162,6 +175,23 @@ describe('ChatSessionManager', () => {
|
|
|
162
175
|
manager2.close();
|
|
163
176
|
});
|
|
164
177
|
|
|
178
|
+
test('session files are namespaced away from plans.json collisions', () => {
|
|
179
|
+
writeFileSync(
|
|
180
|
+
join(dir, 'plans.json'),
|
|
181
|
+
JSON.stringify({ version: '1.0', plans: [{ id: 'plan-1' }] }),
|
|
182
|
+
'utf-8',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const session = manager.getOrCreate('plans');
|
|
186
|
+
|
|
187
|
+
expect(session.messages).toEqual([]);
|
|
188
|
+
expect(JSON.parse(readFileSync(join(dir, 'plans.json'), 'utf-8'))).toEqual({
|
|
189
|
+
version: '1.0',
|
|
190
|
+
plans: [{ id: 'plan-1' }],
|
|
191
|
+
});
|
|
192
|
+
expect(existsSync(join(dir, 'sessions', 'plans.json'))).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
165
195
|
test('delete removes from disk', () => {
|
|
166
196
|
manager.getOrCreate('chat-1');
|
|
167
197
|
manager.delete('chat-1');
|