@soleri/core 2.0.1 → 2.1.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.
Files changed (68) hide show
  1. package/dist/brain/brain.d.ts +2 -49
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +1 -158
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +51 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -0
  7. package/dist/brain/intelligence.js +666 -0
  8. package/dist/brain/intelligence.js.map +1 -0
  9. package/dist/brain/types.d.ts +165 -0
  10. package/dist/brain/types.d.ts.map +1 -0
  11. package/dist/brain/types.js +2 -0
  12. package/dist/brain/types.js.map +1 -0
  13. package/dist/curator/curator.d.ts +28 -0
  14. package/dist/curator/curator.d.ts.map +1 -0
  15. package/dist/curator/curator.js +525 -0
  16. package/dist/curator/curator.js.map +1 -0
  17. package/dist/curator/types.d.ts +87 -0
  18. package/dist/curator/types.d.ts.map +1 -0
  19. package/dist/curator/types.js +3 -0
  20. package/dist/curator/types.js.map +1 -0
  21. package/dist/facades/types.d.ts +1 -1
  22. package/dist/index.d.ts +11 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +11 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/llm/llm-client.d.ts +28 -0
  27. package/dist/llm/llm-client.d.ts.map +1 -0
  28. package/dist/llm/llm-client.js +226 -0
  29. package/dist/llm/llm-client.js.map +1 -0
  30. package/dist/runtime/core-ops.d.ts +17 -0
  31. package/dist/runtime/core-ops.d.ts.map +1 -0
  32. package/dist/runtime/core-ops.js +613 -0
  33. package/dist/runtime/core-ops.js.map +1 -0
  34. package/dist/runtime/domain-ops.d.ts +25 -0
  35. package/dist/runtime/domain-ops.d.ts.map +1 -0
  36. package/dist/runtime/domain-ops.js +130 -0
  37. package/dist/runtime/domain-ops.js.map +1 -0
  38. package/dist/runtime/runtime.d.ts +19 -0
  39. package/dist/runtime/runtime.d.ts.map +1 -0
  40. package/dist/runtime/runtime.js +66 -0
  41. package/dist/runtime/runtime.js.map +1 -0
  42. package/dist/runtime/types.d.ts +41 -0
  43. package/dist/runtime/types.d.ts.map +1 -0
  44. package/dist/runtime/types.js +2 -0
  45. package/dist/runtime/types.js.map +1 -0
  46. package/dist/text/similarity.d.ts +8 -0
  47. package/dist/text/similarity.d.ts.map +1 -0
  48. package/dist/text/similarity.js +161 -0
  49. package/dist/text/similarity.js.map +1 -0
  50. package/package.json +6 -2
  51. package/src/__tests__/brain-intelligence.test.ts +623 -0
  52. package/src/__tests__/core-ops.test.ts +218 -0
  53. package/src/__tests__/curator.test.ts +574 -0
  54. package/src/__tests__/domain-ops.test.ts +160 -0
  55. package/src/__tests__/llm-client.test.ts +69 -0
  56. package/src/__tests__/runtime.test.ts +95 -0
  57. package/src/brain/brain.ts +27 -221
  58. package/src/brain/intelligence.ts +1061 -0
  59. package/src/brain/types.ts +176 -0
  60. package/src/curator/curator.ts +699 -0
  61. package/src/curator/types.ts +114 -0
  62. package/src/index.ts +55 -1
  63. package/src/llm/llm-client.ts +310 -0
  64. package/src/runtime/core-ops.ts +665 -0
  65. package/src/runtime/domain-ops.ts +144 -0
  66. package/src/runtime/runtime.ts +76 -0
  67. package/src/runtime/types.ts +39 -0
  68. package/src/text/similarity.ts +168 -0
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { createAgentRuntime } from '../runtime/runtime.js';
3
+ import { createDomainFacade, createDomainFacades } from '../runtime/domain-ops.js';
4
+ import type { AgentRuntime } from '../runtime/types.js';
5
+ import type { IntelligenceEntry } from '../intelligence/types.js';
6
+
7
+ describe('createDomainFacade', () => {
8
+ let runtime: AgentRuntime;
9
+
10
+ beforeEach(() => {
11
+ runtime = createAgentRuntime({
12
+ agentId: 'test-domain',
13
+ vaultPath: ':memory:',
14
+ });
15
+ });
16
+
17
+ afterEach(() => {
18
+ runtime.close();
19
+ });
20
+
21
+ it('should create facade with correct name', () => {
22
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
23
+ expect(facade.name).toBe('test-domain_security');
24
+ });
25
+
26
+ it('should create facade with 5 ops', () => {
27
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
28
+ expect(facade.ops.length).toBe(5);
29
+ const names = facade.ops.map((o) => o.name);
30
+ expect(names).toContain('get_patterns');
31
+ expect(names).toContain('search');
32
+ expect(names).toContain('get_entry');
33
+ expect(names).toContain('capture');
34
+ expect(names).toContain('remove');
35
+ });
36
+
37
+ it('should handle kebab-case domain names', () => {
38
+ const facade = createDomainFacade(runtime, 'test-domain', 'api-design');
39
+ expect(facade.name).toBe('test-domain_api_design');
40
+ });
41
+
42
+ it('get_patterns should scope to domain', async () => {
43
+ runtime.vault.seed([
44
+ {
45
+ id: 'sec-1',
46
+ type: 'pattern',
47
+ domain: 'security',
48
+ title: 'Auth',
49
+ severity: 'warning',
50
+ description: 'Auth.',
51
+ tags: ['auth'],
52
+ },
53
+ {
54
+ id: 'api-1',
55
+ type: 'pattern',
56
+ domain: 'api-design',
57
+ title: 'REST',
58
+ severity: 'warning',
59
+ description: 'REST.',
60
+ tags: ['rest'],
61
+ },
62
+ ]);
63
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
64
+ const op = facade.ops.find((o) => o.name === 'get_patterns')!;
65
+ const results = (await op.handler({})) as IntelligenceEntry[];
66
+ expect(results.every((e) => e.domain === 'security')).toBe(true);
67
+ });
68
+
69
+ it('capture should add entry with correct domain', async () => {
70
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
71
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
72
+ await captureOp.handler({
73
+ id: 'cap-1',
74
+ type: 'pattern',
75
+ title: 'Captured Pattern',
76
+ severity: 'warning',
77
+ description: 'Test capture.',
78
+ tags: ['test'],
79
+ });
80
+ const entry = runtime.vault.get('cap-1');
81
+ expect(entry).not.toBeNull();
82
+ expect(entry!.domain).toBe('security');
83
+ });
84
+
85
+ it('remove should delete entry', async () => {
86
+ runtime.vault.seed([
87
+ {
88
+ id: 'rm-1',
89
+ type: 'pattern',
90
+ domain: 'security',
91
+ title: 'Remove me',
92
+ severity: 'warning',
93
+ description: 'Remove.',
94
+ tags: ['test'],
95
+ },
96
+ ]);
97
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
98
+ const removeOp = facade.ops.find((o) => o.name === 'remove')!;
99
+ const result = (await removeOp.handler({ id: 'rm-1' })) as { removed: boolean };
100
+ expect(result.removed).toBe(true);
101
+ expect(runtime.vault.get('rm-1')).toBeNull();
102
+ });
103
+
104
+ it('get_entry should return specific entry', async () => {
105
+ runtime.vault.seed([
106
+ {
107
+ id: 'ge-1',
108
+ type: 'pattern',
109
+ domain: 'security',
110
+ title: 'Get me',
111
+ severity: 'warning',
112
+ description: 'Get.',
113
+ tags: ['test'],
114
+ },
115
+ ]);
116
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
117
+ const getOp = facade.ops.find((o) => o.name === 'get_entry')!;
118
+ const result = (await getOp.handler({ id: 'ge-1' })) as IntelligenceEntry;
119
+ expect(result.id).toBe('ge-1');
120
+ });
121
+
122
+ it('get_entry should return error for missing entry', async () => {
123
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
124
+ const getOp = facade.ops.find((o) => o.name === 'get_entry')!;
125
+ const result = (await getOp.handler({ id: 'nope' })) as { error: string };
126
+ expect(result.error).toBeDefined();
127
+ });
128
+ });
129
+
130
+ describe('createDomainFacades', () => {
131
+ let runtime: AgentRuntime;
132
+
133
+ beforeEach(() => {
134
+ runtime = createAgentRuntime({
135
+ agentId: 'test-multi',
136
+ vaultPath: ':memory:',
137
+ });
138
+ });
139
+
140
+ afterEach(() => {
141
+ runtime.close();
142
+ });
143
+
144
+ it('should create one facade per domain', () => {
145
+ const facades = createDomainFacades(runtime, 'test-multi', [
146
+ 'security',
147
+ 'api-design',
148
+ 'testing',
149
+ ]);
150
+ expect(facades.length).toBe(3);
151
+ expect(facades[0].name).toBe('test-multi_security');
152
+ expect(facades[1].name).toBe('test-multi_api_design');
153
+ expect(facades[2].name).toBe('test-multi_testing');
154
+ });
155
+
156
+ it('should return empty array for no domains', () => {
157
+ const facades = createDomainFacades(runtime, 'test-multi', []);
158
+ expect(facades.length).toBe(0);
159
+ });
160
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { LLMClient } from '../llm/llm-client.js';
3
+ import { KeyPool } from '../llm/key-pool.js';
4
+
5
+ describe('LLMClient', () => {
6
+ it('should create with empty key pools', () => {
7
+ const openai = new KeyPool({ keys: [] });
8
+ const anthropic = new KeyPool({ keys: [] });
9
+ const client = new LLMClient(openai, anthropic);
10
+
11
+ expect(client.isAvailable()).toEqual({ openai: false, anthropic: false });
12
+ });
13
+
14
+ it('should report availability when keys present', () => {
15
+ const openai = new KeyPool({ keys: ['sk-test'] });
16
+ const anthropic = new KeyPool({ keys: ['sk-ant-test'] });
17
+ const client = new LLMClient(openai, anthropic);
18
+
19
+ expect(client.isAvailable()).toEqual({ openai: true, anthropic: true });
20
+ });
21
+
22
+ it('should accept agentId parameter', () => {
23
+ const openai = new KeyPool({ keys: [] });
24
+ const anthropic = new KeyPool({ keys: [] });
25
+ // Should not throw — agentId is used for model routing config
26
+ const client = new LLMClient(openai, anthropic, 'test-agent');
27
+ expect(client.isAvailable()).toEqual({ openai: false, anthropic: false });
28
+ });
29
+
30
+ it('should return empty routes by default', () => {
31
+ const openai = new KeyPool({ keys: [] });
32
+ const anthropic = new KeyPool({ keys: [] });
33
+ const client = new LLMClient(openai, anthropic);
34
+
35
+ expect(client.getRoutes()).toEqual([]);
36
+ });
37
+
38
+ it('should throw on callOpenAI without keys', async () => {
39
+ const openai = new KeyPool({ keys: [] });
40
+ const anthropic = new KeyPool({ keys: [] });
41
+ const client = new LLMClient(openai, anthropic);
42
+
43
+ await expect(
44
+ client.complete({
45
+ provider: 'openai',
46
+ model: 'gpt-4o-mini',
47
+ systemPrompt: 'test',
48
+ userPrompt: 'test',
49
+ caller: 'test',
50
+ }),
51
+ ).rejects.toThrow('OpenAI API key not configured');
52
+ });
53
+
54
+ it('should throw on callAnthropic without keys', async () => {
55
+ const openai = new KeyPool({ keys: [] });
56
+ const anthropic = new KeyPool({ keys: [] });
57
+ const client = new LLMClient(openai, anthropic);
58
+
59
+ await expect(
60
+ client.complete({
61
+ provider: 'anthropic',
62
+ model: 'claude-sonnet-4-20250514',
63
+ systemPrompt: 'test',
64
+ userPrompt: 'test',
65
+ caller: 'test',
66
+ }),
67
+ ).rejects.toThrow('Anthropic API key not configured');
68
+ });
69
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { createAgentRuntime } from '../runtime/runtime.js';
3
+ import type { AgentRuntime } from '../runtime/types.js';
4
+
5
+ describe('createAgentRuntime', () => {
6
+ let runtime: AgentRuntime | null = null;
7
+
8
+ afterEach(() => {
9
+ runtime?.close();
10
+ runtime = null;
11
+ });
12
+
13
+ it('should create a runtime with all modules initialized', () => {
14
+ runtime = createAgentRuntime({
15
+ agentId: 'test-agent',
16
+ vaultPath: ':memory:',
17
+ });
18
+
19
+ expect(runtime.config.agentId).toBe('test-agent');
20
+ expect(runtime.vault).toBeDefined();
21
+ expect(runtime.brain).toBeDefined();
22
+ expect(runtime.planner).toBeDefined();
23
+ expect(runtime.curator).toBeDefined();
24
+ expect(runtime.keyPool.openai).toBeDefined();
25
+ expect(runtime.keyPool.anthropic).toBeDefined();
26
+ expect(runtime.llmClient).toBeDefined();
27
+ });
28
+
29
+ it('should use :memory: vault when specified', () => {
30
+ runtime = createAgentRuntime({
31
+ agentId: 'test-mem',
32
+ vaultPath: ':memory:',
33
+ });
34
+
35
+ const stats = runtime.vault.stats();
36
+ expect(stats.totalEntries).toBe(0);
37
+ });
38
+
39
+ it('should preserve config on runtime', () => {
40
+ runtime = createAgentRuntime({
41
+ agentId: 'test-cfg',
42
+ vaultPath: ':memory:',
43
+ dataDir: '/nonexistent',
44
+ });
45
+
46
+ expect(runtime.config.agentId).toBe('test-cfg');
47
+ expect(runtime.config.vaultPath).toBe(':memory:');
48
+ expect(runtime.config.dataDir).toBe('/nonexistent');
49
+ });
50
+
51
+ it('close() should not throw', () => {
52
+ runtime = createAgentRuntime({
53
+ agentId: 'test-close',
54
+ vaultPath: ':memory:',
55
+ });
56
+
57
+ expect(() => runtime!.close()).not.toThrow();
58
+ runtime = null; // already closed
59
+ });
60
+
61
+ it('brain should be wired to vault', async () => {
62
+ runtime = createAgentRuntime({
63
+ agentId: 'test-brain-wire',
64
+ vaultPath: ':memory:',
65
+ });
66
+
67
+ // Seed some data through vault
68
+ runtime.vault.seed([
69
+ {
70
+ id: 'rt-1',
71
+ type: 'pattern',
72
+ domain: 'testing',
73
+ title: 'Runtime test pattern',
74
+ severity: 'warning',
75
+ description: 'A test.',
76
+ tags: ['test'],
77
+ },
78
+ ]);
79
+
80
+ // Brain should find it
81
+ runtime.brain.rebuildVocabulary();
82
+ const results = await runtime.brain.intelligentSearch('runtime test', { limit: 5 });
83
+ expect(results.length).toBeGreaterThan(0);
84
+ });
85
+
86
+ it('curator should be wired to vault', () => {
87
+ runtime = createAgentRuntime({
88
+ agentId: 'test-curator-wire',
89
+ vaultPath: ':memory:',
90
+ });
91
+
92
+ const status = runtime.curator.getStatus();
93
+ expect(status.initialized).toBe(true);
94
+ });
95
+ });
@@ -1,228 +1,34 @@
1
1
  import type { Vault } from '../vault/vault.js';
2
2
  import type { SearchResult } from '../vault/vault.js';
3
3
  import type { IntelligenceEntry } from '../intelligence/types.js';
4
+ import {
5
+ tokenize,
6
+ calculateTf,
7
+ calculateTfIdf,
8
+ cosineSimilarity,
9
+ jaccardSimilarity,
10
+ } from '../text/similarity.js';
4
11
  import type { CogneeClient } from '../cognee/client.js';
5
-
6
- // ─── Types ───────────────────────────────────────────────────────────
7
-
8
- export interface ScoringWeights {
9
- semantic: number;
10
- vector: number;
11
- severity: number;
12
- recency: number;
13
- tagOverlap: number;
14
- domainMatch: number;
15
- }
16
-
17
- export interface ScoreBreakdown {
18
- semantic: number;
19
- vector: number;
20
- severity: number;
21
- recency: number;
22
- tagOverlap: number;
23
- domainMatch: number;
24
- total: number;
25
- }
26
-
27
- export interface RankedResult {
28
- entry: IntelligenceEntry;
29
- score: number;
30
- breakdown: ScoreBreakdown;
31
- }
32
-
33
- export interface SearchOptions {
34
- domain?: string;
35
- type?: string;
36
- severity?: string;
37
- limit?: number;
38
- tags?: string[];
39
- }
40
-
41
- export interface CaptureResult {
42
- captured: boolean;
43
- id: string;
44
- autoTags: string[];
45
- duplicate?: { id: string; similarity: number };
46
- blocked?: boolean;
47
- }
48
-
49
- export interface BrainStats {
50
- vocabularySize: number;
51
- feedbackCount: number;
52
- weights: ScoringWeights;
53
- }
54
-
55
- export interface QueryContext {
56
- query: string;
57
- domain?: string;
58
- tags?: string[];
59
- }
60
-
61
- type SparseVector = Map<string, number>;
62
-
63
- // ─── Stopwords ─────────────────────────────────────────────────────
64
-
65
- const STOPWORDS = new Set([
66
- 'a',
67
- 'an',
68
- 'the',
69
- 'and',
70
- 'or',
71
- 'but',
72
- 'in',
73
- 'on',
74
- 'at',
75
- 'to',
76
- 'for',
77
- 'of',
78
- 'with',
79
- 'by',
80
- 'from',
81
- 'as',
82
- 'is',
83
- 'was',
84
- 'are',
85
- 'were',
86
- 'been',
87
- 'be',
88
- 'have',
89
- 'has',
90
- 'had',
91
- 'do',
92
- 'does',
93
- 'did',
94
- 'will',
95
- 'would',
96
- 'could',
97
- 'should',
98
- 'may',
99
- 'might',
100
- 'shall',
101
- 'can',
102
- 'need',
103
- 'must',
104
- 'it',
105
- 'its',
106
- 'this',
107
- 'that',
108
- 'these',
109
- 'those',
110
- 'i',
111
- 'you',
112
- 'he',
113
- 'she',
114
- 'we',
115
- 'they',
116
- 'me',
117
- 'him',
118
- 'her',
119
- 'us',
120
- 'them',
121
- 'my',
122
- 'your',
123
- 'his',
124
- 'our',
125
- 'their',
126
- 'what',
127
- 'which',
128
- 'who',
129
- 'whom',
130
- 'when',
131
- 'where',
132
- 'why',
133
- 'how',
134
- 'all',
135
- 'each',
136
- 'every',
137
- 'both',
138
- 'few',
139
- 'more',
140
- 'most',
141
- 'other',
142
- 'some',
143
- 'such',
144
- 'no',
145
- 'not',
146
- 'only',
147
- 'same',
148
- 'so',
149
- 'than',
150
- 'too',
151
- 'very',
152
- 'just',
153
- 'because',
154
- 'if',
155
- 'then',
156
- 'else',
157
- 'about',
158
- 'up',
159
- 'out',
160
- 'into',
161
- ]);
162
-
163
- // ─── Text Processing (pure functions) ────────────────────────────
164
-
165
- function tokenize(text: string): string[] {
166
- return text
167
- .toLowerCase()
168
- .replace(/[^a-z0-9\s-]/g, ' ')
169
- .split(/\s+/)
170
- .filter((t) => t.length > 2 && !STOPWORDS.has(t));
171
- }
172
-
173
- function calculateTf(tokens: string[]): SparseVector {
174
- const tf: SparseVector = new Map();
175
- for (const token of tokens) {
176
- tf.set(token, (tf.get(token) ?? 0) + 1);
177
- }
178
- const len = tokens.length || 1;
179
- for (const [term, count] of tf) {
180
- tf.set(term, count / len);
181
- }
182
- return tf;
183
- }
184
-
185
- function calculateTfIdf(tokens: string[], vocabulary: Map<string, number>): SparseVector {
186
- const tf = calculateTf(tokens);
187
- const tfidf: SparseVector = new Map();
188
- for (const [term, tfValue] of tf) {
189
- const idf = vocabulary.get(term) ?? 0;
190
- if (idf > 0) {
191
- tfidf.set(term, tfValue * idf);
192
- }
193
- }
194
- return tfidf;
195
- }
196
-
197
- function cosineSimilarity(a: SparseVector, b: SparseVector): number {
198
- let dot = 0;
199
- let normA = 0;
200
- let normB = 0;
201
- for (const [term, valA] of a) {
202
- normA += valA * valA;
203
- const valB = b.get(term);
204
- if (valB !== undefined) {
205
- dot += valA * valB;
206
- }
207
- }
208
- for (const [, valB] of b) {
209
- normB += valB * valB;
210
- }
211
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
212
- return denom === 0 ? 0 : dot / denom;
213
- }
214
-
215
- function jaccardSimilarity(a: string[], b: string[]): number {
216
- if (a.length === 0 && b.length === 0) return 0;
217
- const setA = new Set(a);
218
- const setB = new Set(b);
219
- let intersection = 0;
220
- for (const item of setA) {
221
- if (setB.has(item)) intersection++;
222
- }
223
- const union = new Set([...a, ...b]).size;
224
- return union === 0 ? 0 : intersection / union;
225
- }
12
+ import type {
13
+ ScoringWeights,
14
+ ScoreBreakdown,
15
+ RankedResult,
16
+ SearchOptions,
17
+ CaptureResult,
18
+ BrainStats,
19
+ QueryContext,
20
+ } from './types.js';
21
+
22
+ // Re-export types for backward compatibility
23
+ export type {
24
+ ScoringWeights,
25
+ ScoreBreakdown,
26
+ RankedResult,
27
+ SearchOptions,
28
+ CaptureResult,
29
+ BrainStats,
30
+ QueryContext,
31
+ } from './types.js';
226
32
 
227
33
  // ─── Severity scoring ──────────────────────────────────────────────
228
34