@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,114 @@
1
+ // ─── Curator Types ──────────────────────────────────────────────────
2
+
3
+ export type EntryStatus = 'active' | 'stale' | 'archived';
4
+ export type EntrySource = 'manual' | 'capture' | 'seed' | 'unknown';
5
+
6
+ // ─── Tag Normalization ──────────────────────────────────────────────
7
+
8
+ export interface TagNormalizationResult {
9
+ original: string;
10
+ normalized: string;
11
+ wasAliased: boolean;
12
+ }
13
+
14
+ export interface CanonicalTag {
15
+ tag: string;
16
+ description: string | null;
17
+ usageCount: number;
18
+ aliasCount: number;
19
+ }
20
+
21
+ // ─── Duplicate Detection ────────────────────────────────────────────
22
+
23
+ export interface DuplicateCandidate {
24
+ entryId: string;
25
+ title: string;
26
+ similarity: number;
27
+ suggestMerge: boolean;
28
+ }
29
+
30
+ export interface DuplicateDetectionResult {
31
+ entryId: string;
32
+ matches: DuplicateCandidate[];
33
+ scannedCount: number;
34
+ }
35
+
36
+ // ─── Contradictions ─────────────────────────────────────────────────
37
+
38
+ export type ContradictionStatus = 'open' | 'resolved' | 'dismissed';
39
+
40
+ export interface Contradiction {
41
+ id: number;
42
+ patternId: string;
43
+ antipatternId: string;
44
+ similarity: number;
45
+ status: ContradictionStatus;
46
+ createdAt: number;
47
+ resolvedAt: number | null;
48
+ }
49
+
50
+ // ─── Grooming ───────────────────────────────────────────────────────
51
+
52
+ export interface GroomResult {
53
+ entryId: string;
54
+ tagsNormalized: TagNormalizationResult[];
55
+ stale: boolean;
56
+ lastGroomedAt: number;
57
+ }
58
+
59
+ export interface GroomAllResult {
60
+ totalEntries: number;
61
+ groomedCount: number;
62
+ tagsNormalized: number;
63
+ staleCount: number;
64
+ durationMs: number;
65
+ }
66
+
67
+ // ─── Consolidation ──────────────────────────────────────────────────
68
+
69
+ export interface ConsolidationOptions {
70
+ dryRun?: boolean;
71
+ staleDaysThreshold?: number;
72
+ duplicateThreshold?: number;
73
+ contradictionThreshold?: number;
74
+ }
75
+
76
+ export interface ConsolidationResult {
77
+ dryRun: boolean;
78
+ duplicates: DuplicateDetectionResult[];
79
+ staleEntries: string[];
80
+ contradictions: Contradiction[];
81
+ mutations: number;
82
+ durationMs: number;
83
+ }
84
+
85
+ // ─── Changelog & Health ─────────────────────────────────────────────
86
+
87
+ export interface ChangelogEntry {
88
+ id: number;
89
+ action: string;
90
+ entryId: string;
91
+ beforeValue: string | null;
92
+ afterValue: string | null;
93
+ reason: string;
94
+ createdAt: number;
95
+ }
96
+
97
+ export interface HealthMetrics {
98
+ coverage: number;
99
+ freshness: number;
100
+ quality: number;
101
+ tagHealth: number;
102
+ }
103
+
104
+ export interface HealthAuditResult {
105
+ score: number;
106
+ metrics: HealthMetrics;
107
+ recommendations: string[];
108
+ }
109
+
110
+ export interface CuratorStatus {
111
+ initialized: boolean;
112
+ tables: Record<string, number>;
113
+ lastGroomedAt: number | null;
114
+ }
package/src/index.ts CHANGED
@@ -6,8 +6,40 @@ export { loadIntelligenceData } from './intelligence/loader.js';
6
6
  export { Vault } from './vault/vault.js';
7
7
  export type { SearchResult, VaultStats, ProjectInfo, Memory, MemoryStats } from './vault/vault.js';
8
8
 
9
+ // ─── Text Utilities ─────────────────────────────────────────────────
10
+ export {
11
+ tokenize,
12
+ calculateTf,
13
+ calculateTfIdf,
14
+ cosineSimilarity,
15
+ jaccardSimilarity,
16
+ } from './text/similarity.js';
17
+ export type { SparseVector } from './text/similarity.js';
18
+
19
+ // ─── Curator ────────────────────────────────────────────────────────
20
+ export { Curator } from './curator/curator.js';
21
+ export type {
22
+ EntryStatus,
23
+ EntrySource,
24
+ TagNormalizationResult,
25
+ CanonicalTag,
26
+ DuplicateCandidate,
27
+ DuplicateDetectionResult,
28
+ Contradiction,
29
+ ContradictionStatus,
30
+ GroomResult,
31
+ GroomAllResult,
32
+ ConsolidationOptions,
33
+ ConsolidationResult,
34
+ ChangelogEntry,
35
+ HealthMetrics,
36
+ HealthAuditResult,
37
+ CuratorStatus,
38
+ } from './curator/types.js';
39
+
9
40
  // ─── Brain ───────────────────────────────────────────────────────────
10
41
  export { Brain } from './brain/brain.js';
42
+ export { BrainIntelligence } from './brain/intelligence.js';
11
43
  export type {
12
44
  ScoringWeights,
13
45
  ScoreBreakdown,
@@ -16,7 +48,20 @@ export type {
16
48
  CaptureResult,
17
49
  BrainStats,
18
50
  QueryContext,
19
- } from './brain/brain.js';
51
+ PatternStrength,
52
+ StrengthsQuery,
53
+ BrainSession,
54
+ SessionLifecycleInput,
55
+ KnowledgeProposal,
56
+ ExtractionResult,
57
+ GlobalPattern,
58
+ DomainProfile,
59
+ BuildIntelligenceResult,
60
+ BrainIntelligenceStats,
61
+ SessionContext,
62
+ BrainExportData,
63
+ BrainImportResult,
64
+ } from './brain/types.js';
20
65
 
21
66
  // ─── Cognee ─────────────────────────────────────────────────────────
22
67
  export { CogneeClient } from './cognee/client.js';
@@ -73,3 +118,12 @@ export type {
73
118
  FacadeResponse,
74
119
  FacadeInput,
75
120
  } from './facades/types.js';
121
+
122
+ // ─── LLM Client ─────────────────────────────────────────────────────
123
+ export { LLMClient } from './llm/llm-client.js';
124
+
125
+ // ─── Runtime Factory ────────────────────────────────────────────────
126
+ export { createAgentRuntime } from './runtime/runtime.js';
127
+ export { createCoreOps } from './runtime/core-ops.js';
128
+ export { createDomainFacade, createDomainFacades } from './runtime/domain-ops.js';
129
+ export type { AgentRuntimeConfig, AgentRuntime } from './runtime/types.js';
@@ -0,0 +1,310 @@
1
+ /**
2
+ * LLM Client — Unified OpenAI/Anthropic caller with key pool rotation,
3
+ * circuit breaker, retry, and model routing.
4
+ *
5
+ * Anthropic SDK is loaded via dynamic import on first use, keeping it
6
+ * an optional peer dependency.
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ import { LLMError } from './types.js';
13
+ import { CircuitBreaker, retry, parseRateLimitHeaders } from './utils.js';
14
+ import type { LLMCallOptions, LLMCallResult, RouteEntry, RoutingConfig } from './types.js';
15
+ import type { KeyPool } from './key-pool.js';
16
+
17
+ // =============================================================================
18
+ // CONSTANTS
19
+ // =============================================================================
20
+
21
+ const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
22
+
23
+ // =============================================================================
24
+ // MODEL ROUTER
25
+ // =============================================================================
26
+
27
+ function loadRoutingConfig(agentId: string): RoutingConfig {
28
+ const defaultConfig: RoutingConfig = {
29
+ routes: [],
30
+ defaultOpenAIModel: 'gpt-4o-mini',
31
+ defaultAnthropicModel: 'claude-sonnet-4-20250514',
32
+ };
33
+
34
+ const configPath = path.join(homedir(), `.${agentId}`, 'model-routing.json');
35
+
36
+ try {
37
+ if (fs.existsSync(configPath)) {
38
+ const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Partial<RoutingConfig>;
39
+ if (data.routes && Array.isArray(data.routes)) {
40
+ defaultConfig.routes = data.routes;
41
+ }
42
+ if (data.defaultOpenAIModel) {
43
+ defaultConfig.defaultOpenAIModel = data.defaultOpenAIModel;
44
+ }
45
+ if (data.defaultAnthropicModel) {
46
+ defaultConfig.defaultAnthropicModel = data.defaultAnthropicModel;
47
+ }
48
+ }
49
+ } catch {
50
+ // Config not available — use defaults
51
+ }
52
+
53
+ return defaultConfig;
54
+ }
55
+
56
+ function inferProvider(model: string): 'openai' | 'anthropic' {
57
+ if (model.startsWith('claude-') || model.startsWith('anthropic/')) {
58
+ return 'anthropic';
59
+ }
60
+ return 'openai';
61
+ }
62
+
63
+ class ModelRouter {
64
+ private config: RoutingConfig;
65
+
66
+ constructor(config?: RoutingConfig) {
67
+ this.config = config ?? {
68
+ routes: [],
69
+ defaultOpenAIModel: 'gpt-4o-mini',
70
+ defaultAnthropicModel: 'claude-sonnet-4-20250514',
71
+ };
72
+ }
73
+
74
+ resolve(
75
+ caller: string,
76
+ task?: string,
77
+ originalModel?: string,
78
+ ): { model: string; provider: 'openai' | 'anthropic' } {
79
+ if (task) {
80
+ const exactMatch = this.config.routes.find((r) => r.caller === caller && r.task === task);
81
+ if (exactMatch) {
82
+ return { model: exactMatch.model, provider: exactMatch.provider };
83
+ }
84
+ }
85
+
86
+ const callerMatch = this.config.routes.find((r) => r.caller === caller && !r.task);
87
+ if (callerMatch) {
88
+ return { model: callerMatch.model, provider: callerMatch.provider };
89
+ }
90
+
91
+ if (originalModel) {
92
+ const provider = inferProvider(originalModel);
93
+ return { model: originalModel, provider };
94
+ }
95
+
96
+ return { model: this.config.defaultOpenAIModel, provider: 'openai' };
97
+ }
98
+
99
+ getRoutes(): RouteEntry[] {
100
+ return [...this.config.routes];
101
+ }
102
+ }
103
+
104
+ // =============================================================================
105
+ // LLM CLIENT
106
+ // =============================================================================
107
+
108
+ // Anthropic SDK type — we only need the messages.create method shape
109
+ interface AnthropicClient {
110
+ messages: {
111
+ create(
112
+ params: {
113
+ model: string;
114
+ max_tokens: number;
115
+ system: string;
116
+ messages: Array<{ role: string; content: string }>;
117
+ },
118
+ options?: { timeout: number },
119
+ ): Promise<{
120
+ content: Array<{ type: string; text?: string }>;
121
+ usage?: { input_tokens?: number; output_tokens?: number };
122
+ }>;
123
+ };
124
+ }
125
+
126
+ export class LLMClient {
127
+ private openaiKeyPool: KeyPool;
128
+ private anthropicKeyPool: KeyPool;
129
+ private anthropicClient: AnthropicClient | null = null;
130
+ private anthropicBreaker: CircuitBreaker;
131
+ private anthropicKeyFingerprint: string = '';
132
+ private router: ModelRouter;
133
+
134
+ constructor(openaiKeyPool: KeyPool, anthropicKeyPool: KeyPool, agentId?: string) {
135
+ this.openaiKeyPool = openaiKeyPool;
136
+ this.anthropicKeyPool = anthropicKeyPool;
137
+ this.anthropicBreaker = new CircuitBreaker({
138
+ name: 'llm-anthropic',
139
+ failureThreshold: 5,
140
+ resetTimeoutMs: 60_000,
141
+ });
142
+ this.router = new ModelRouter(agentId ? loadRoutingConfig(agentId) : undefined);
143
+ }
144
+
145
+ async complete(options: LLMCallOptions): Promise<LLMCallResult> {
146
+ const routed = this.router.resolve(options.caller, options.task, options.model);
147
+ const resolvedOptions = { ...options, model: routed.model, provider: routed.provider };
148
+
149
+ return resolvedOptions.provider === 'anthropic'
150
+ ? this.callAnthropic(resolvedOptions)
151
+ : this.callOpenAI(resolvedOptions);
152
+ }
153
+
154
+ isAvailable(): { openai: boolean; anthropic: boolean } {
155
+ return {
156
+ openai: this.openaiKeyPool.hasKeys,
157
+ anthropic: this.anthropicKeyPool.hasKeys,
158
+ };
159
+ }
160
+
161
+ getRoutes(): RouteEntry[] {
162
+ return this.router.getRoutes();
163
+ }
164
+
165
+ // ===========================================================================
166
+ // OPENAI
167
+ // ===========================================================================
168
+
169
+ private async callOpenAI(options: LLMCallOptions): Promise<LLMCallResult> {
170
+ const keyPool = this.openaiKeyPool.hasKeys ? this.openaiKeyPool : null;
171
+
172
+ if (!keyPool) {
173
+ throw new LLMError('OpenAI API key not configured', { retryable: false });
174
+ }
175
+
176
+ const start = Date.now();
177
+
178
+ const doRequest = async (): Promise<LLMCallResult> => {
179
+ const apiKey = keyPool.getActiveKey().expose();
180
+ const keyIndex = keyPool.activeKeyIndex;
181
+
182
+ const response = await fetch(OPENAI_API_URL, {
183
+ method: 'POST',
184
+ headers: {
185
+ 'Content-Type': 'application/json',
186
+ Authorization: `Bearer ${apiKey}`,
187
+ },
188
+ body: JSON.stringify({
189
+ model: options.model,
190
+ messages: [
191
+ { role: 'system', content: options.systemPrompt },
192
+ { role: 'user', content: options.userPrompt },
193
+ ],
194
+ temperature: options.temperature ?? 0.3,
195
+ max_completion_tokens: options.maxTokens ?? 500,
196
+ }),
197
+ });
198
+
199
+ if (response.headers) {
200
+ const rateLimits = parseRateLimitHeaders(response.headers);
201
+ if (rateLimits.remaining !== null) {
202
+ keyPool.updateQuota(keyIndex, rateLimits.remaining);
203
+ keyPool.rotatePreemptive();
204
+ }
205
+ }
206
+
207
+ if (!response.ok) {
208
+ if (response.status === 429 && keyPool.poolSize > 1) {
209
+ keyPool.rotateOnError();
210
+ }
211
+
212
+ const errorBody = await response.text();
213
+ throw new LLMError(`OpenAI API error: ${response.status} - ${errorBody}`, {
214
+ retryable: response.status === 429 || response.status >= 500,
215
+ statusCode: response.status,
216
+ });
217
+ }
218
+
219
+ const data = (await response.json()) as {
220
+ choices: Array<{ message: { content: string } }>;
221
+ usage?: { prompt_tokens?: number; completion_tokens?: number };
222
+ };
223
+
224
+ return {
225
+ text: data.choices[0]?.message?.content || '',
226
+ model: options.model,
227
+ provider: 'openai' as const,
228
+ inputTokens: data.usage?.prompt_tokens,
229
+ outputTokens: data.usage?.completion_tokens,
230
+ durationMs: Date.now() - start,
231
+ };
232
+ };
233
+
234
+ return retry(doRequest, { maxAttempts: 3 });
235
+ }
236
+
237
+ // ===========================================================================
238
+ // ANTHROPIC
239
+ // ===========================================================================
240
+
241
+ private async callAnthropic(options: LLMCallOptions): Promise<LLMCallResult> {
242
+ const client = await this.getAnthropicClient();
243
+ if (!client) {
244
+ throw new LLMError('Anthropic API key not configured', { retryable: false });
245
+ }
246
+
247
+ const start = Date.now();
248
+
249
+ return this.anthropicBreaker.call(() =>
250
+ retry(
251
+ async () => {
252
+ const response = await client.messages.create(
253
+ {
254
+ model: options.model,
255
+ max_tokens: options.maxTokens ?? 1024,
256
+ system: options.systemPrompt,
257
+ messages: [{ role: 'user', content: options.userPrompt }],
258
+ },
259
+ { timeout: 60_000 },
260
+ );
261
+
262
+ const text = response.content
263
+ .filter(
264
+ (block): block is { type: 'text'; text: string } =>
265
+ block.type === 'text' && typeof block.text === 'string',
266
+ )
267
+ .map((block) => block.text)
268
+ .join('\n');
269
+
270
+ return {
271
+ text,
272
+ model: options.model,
273
+ provider: 'anthropic' as const,
274
+ inputTokens: response.usage?.input_tokens,
275
+ outputTokens: response.usage?.output_tokens,
276
+ durationMs: Date.now() - start,
277
+ };
278
+ },
279
+ { maxAttempts: 2 },
280
+ ),
281
+ );
282
+ }
283
+
284
+ private async getAnthropicClient(): Promise<AnthropicClient | null> {
285
+ if (!this.anthropicKeyPool.hasKeys) return null;
286
+
287
+ const currentKey = this.anthropicKeyPool.getActiveKey().expose();
288
+ const currentFingerprint = currentKey.slice(-8);
289
+
290
+ if (currentFingerprint !== this.anthropicKeyFingerprint) {
291
+ this.anthropicClient = null;
292
+ this.anthropicKeyFingerprint = currentFingerprint;
293
+ }
294
+
295
+ if (this.anthropicClient) return this.anthropicClient;
296
+
297
+ try {
298
+ // Dynamic import — @anthropic-ai/sdk is an optional peer dep.
299
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
300
+ const mod = await (Function('return import("@anthropic-ai/sdk")')() as Promise<{
301
+ default: new (opts: { apiKey: string }) => AnthropicClient;
302
+ }>);
303
+ this.anthropicClient = new mod.default({ apiKey: currentKey });
304
+ return this.anthropicClient;
305
+ } catch {
306
+ // SDK not installed — Anthropic provider unavailable
307
+ return null;
308
+ }
309
+ }
310
+ }