@soleri/core 2.0.2 → 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 (58) hide show
  1. package/dist/brain/brain.d.ts +12 -50
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +147 -12
  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/cognee/client.d.ts +35 -0
  14. package/dist/cognee/client.d.ts.map +1 -0
  15. package/dist/cognee/client.js +291 -0
  16. package/dist/cognee/client.js.map +1 -0
  17. package/dist/cognee/types.d.ts +46 -0
  18. package/dist/cognee/types.d.ts.map +1 -0
  19. package/dist/cognee/types.js +3 -0
  20. package/dist/cognee/types.js.map +1 -0
  21. package/dist/curator/curator.d.ts.map +1 -1
  22. package/dist/curator/curator.js +7 -5
  23. package/dist/curator/curator.js.map +1 -1
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/llm/llm-client.d.ts.map +1 -1
  29. package/dist/llm/llm-client.js +9 -2
  30. package/dist/llm/llm-client.js.map +1 -1
  31. package/dist/runtime/core-ops.d.ts +3 -3
  32. package/dist/runtime/core-ops.d.ts.map +1 -1
  33. package/dist/runtime/core-ops.js +180 -15
  34. package/dist/runtime/core-ops.js.map +1 -1
  35. package/dist/runtime/runtime.d.ts.map +1 -1
  36. package/dist/runtime/runtime.js +4 -0
  37. package/dist/runtime/runtime.js.map +1 -1
  38. package/dist/runtime/types.d.ts +2 -0
  39. package/dist/runtime/types.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/brain-intelligence.test.ts +623 -0
  42. package/src/__tests__/brain.test.ts +265 -27
  43. package/src/__tests__/cognee-client.test.ts +524 -0
  44. package/src/__tests__/core-ops.test.ts +77 -49
  45. package/src/__tests__/curator.test.ts +126 -31
  46. package/src/__tests__/domain-ops.test.ts +45 -9
  47. package/src/__tests__/runtime.test.ts +13 -11
  48. package/src/brain/brain.ts +194 -65
  49. package/src/brain/intelligence.ts +1061 -0
  50. package/src/brain/types.ts +176 -0
  51. package/src/cognee/client.ts +352 -0
  52. package/src/cognee/types.ts +62 -0
  53. package/src/curator/curator.ts +52 -15
  54. package/src/index.ts +26 -1
  55. package/src/llm/llm-client.ts +18 -24
  56. package/src/runtime/core-ops.ts +219 -26
  57. package/src/runtime/runtime.ts +5 -0
  58. package/src/runtime/types.ts +2 -0
@@ -188,7 +188,13 @@ export class Curator {
188
188
  JSON.stringify(dedupedTags),
189
189
  entryId,
190
190
  );
191
- this.logChange('normalize_tags', entryId, JSON.stringify(entry.tags), JSON.stringify(dedupedTags), 'Tag normalization');
191
+ this.logChange(
192
+ 'normalize_tags',
193
+ entryId,
194
+ JSON.stringify(entry.tags),
195
+ JSON.stringify(dedupedTags),
196
+ 'Tag normalization',
197
+ );
192
198
  }
193
199
 
194
200
  return results;
@@ -245,13 +251,13 @@ export class Curator {
245
251
  // Build vectors for all entries
246
252
  const vectors = new Map<string, SparseVector>();
247
253
  for (const entry of entries) {
248
- const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(' ');
254
+ const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(
255
+ ' ',
256
+ );
249
257
  vectors.set(entry.id, calculateTfIdf(tokenize(text), vocabulary));
250
258
  }
251
259
 
252
- const targetEntries = entryId
253
- ? entries.filter((e) => e.id === entryId)
254
- : entries;
260
+ const targetEntries = entryId ? entries.filter((e) => e.id === entryId) : entries;
255
261
 
256
262
  const results: DuplicateDetectionResult[] = [];
257
263
 
@@ -428,7 +434,8 @@ export class Curator {
428
434
  const dryRun = options?.dryRun ?? true;
429
435
  const staleDaysThreshold = options?.staleDaysThreshold ?? DEFAULT_STALE_DAYS;
430
436
  const duplicateThreshold = options?.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
431
- const contradictionThreshold = options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
437
+ const contradictionThreshold =
438
+ options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
432
439
 
433
440
  // Detect duplicates
434
441
  const duplicates = this.detectDuplicates(undefined, duplicateThreshold);
@@ -455,7 +462,13 @@ export class Curator {
455
462
  VALUES (?, 'archived', unixepoch())
456
463
  ON CONFLICT(entry_id) DO UPDATE SET status = 'archived', last_groomed_at = unixepoch()`,
457
464
  ).run(entryId);
458
- this.logChange('archive', entryId, 'active', 'archived', 'Stale entry archived during consolidation');
465
+ this.logChange(
466
+ 'archive',
467
+ entryId,
468
+ 'active',
469
+ 'archived',
470
+ 'Stale entry archived during consolidation',
471
+ );
459
472
  mutations++;
460
473
  }
461
474
 
@@ -526,9 +539,21 @@ export class Curator {
526
539
  const hasAntiPatterns = typeCount['anti-pattern'] > 0;
527
540
  const hasRules = typeCount.rule > 0;
528
541
  let coverageScore = 1;
529
- if (!hasPatterns) { score -= 10; coverageScore -= 0.33; recommendations.push('No patterns found — add patterns to improve coverage.'); }
530
- if (!hasAntiPatterns) { score -= 5; coverageScore -= 0.17; recommendations.push('No anti-patterns found — add anti-patterns to detect contradictions.'); }
531
- if (!hasRules) { score -= 5; coverageScore -= 0.17; recommendations.push('No rules found — add rules for completeness.'); }
542
+ if (!hasPatterns) {
543
+ score -= 10;
544
+ coverageScore -= 0.33;
545
+ recommendations.push('No patterns found — add patterns to improve coverage.');
546
+ }
547
+ if (!hasAntiPatterns) {
548
+ score -= 5;
549
+ coverageScore -= 0.17;
550
+ recommendations.push('No anti-patterns found — add anti-patterns to detect contradictions.');
551
+ }
552
+ if (!hasRules) {
553
+ score -= 5;
554
+ coverageScore -= 0.17;
555
+ recommendations.push('No rules found — add rules for completeness.');
556
+ }
532
557
  coverageScore = Math.max(0, coverageScore);
533
558
 
534
559
  // Freshness: penalize stale entries
@@ -536,14 +561,18 @@ export class Curator {
536
561
  const now = Math.floor(Date.now() / 1000);
537
562
  const staleThreshold = now - DEFAULT_STALE_DAYS * 86400;
538
563
  const staleCount = (
539
- db.prepare('SELECT COUNT(*) as count FROM entries WHERE updated_at < ?').get(staleThreshold) as { count: number }
564
+ db
565
+ .prepare('SELECT COUNT(*) as count FROM entries WHERE updated_at < ?')
566
+ .get(staleThreshold) as { count: number }
540
567
  ).count;
541
568
  const staleRatio = staleCount / entries.length;
542
569
  const freshnessScore = 1 - staleRatio;
543
570
  if (staleRatio > 0.3) {
544
571
  const penalty = Math.min(20, Math.round(staleRatio * 30));
545
572
  score -= penalty;
546
- recommendations.push(`${staleCount} stale entries (${Math.round(staleRatio * 100)}%) — run grooming to update.`);
573
+ recommendations.push(
574
+ `${staleCount} stale entries (${Math.round(staleRatio * 100)}%) — run grooming to update.`,
575
+ );
547
576
  }
548
577
 
549
578
  // Quality: penalize duplicates and contradictions
@@ -571,12 +600,18 @@ export class Curator {
571
600
  if (lowTagRatio > 0.3) {
572
601
  const penalty = Math.min(10, Math.round(lowTagRatio * 15));
573
602
  score -= penalty;
574
- recommendations.push(`${lowTagEntries.length} entries have fewer than 2 tags — improve tagging.`);
603
+ recommendations.push(
604
+ `${lowTagEntries.length} entries have fewer than 2 tags — improve tagging.`,
605
+ );
575
606
  }
576
607
 
577
608
  // Penalize ungroomed entries
578
609
  const groomedCount = (
579
- db.prepare('SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL').get() as { count: number }
610
+ db
611
+ .prepare(
612
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
613
+ )
614
+ .get() as { count: number }
580
615
  ).count;
581
616
  if (groomedCount < entries.length) {
582
617
  const ungroomed = entries.length - groomedCount;
@@ -609,7 +644,9 @@ export class Curator {
609
644
  const docCount = entries.length;
610
645
  const termDocFreq = new Map<string, number>();
611
646
  for (const entry of entries) {
612
- const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(' ');
647
+ const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(
648
+ ' ',
649
+ );
613
650
  const tokens = new Set(tokenize(text));
614
651
  for (const token of tokens) {
615
652
  termDocFreq.set(token, (termDocFreq.get(token) ?? 0) + 1);
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export type {
39
39
 
40
40
  // ─── Brain ───────────────────────────────────────────────────────────
41
41
  export { Brain } from './brain/brain.js';
42
+ export { BrainIntelligence } from './brain/intelligence.js';
42
43
  export type {
43
44
  ScoringWeights,
44
45
  ScoreBreakdown,
@@ -47,7 +48,31 @@ export type {
47
48
  CaptureResult,
48
49
  BrainStats,
49
50
  QueryContext,
50
- } 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';
65
+
66
+ // ─── Cognee ─────────────────────────────────────────────────────────
67
+ export { CogneeClient } from './cognee/client.js';
68
+ export type {
69
+ CogneeConfig,
70
+ CogneeSearchResult,
71
+ CogneeSearchType,
72
+ CogneeStatus,
73
+ CogneeAddResult,
74
+ CogneeCognifyResult,
75
+ } from './cognee/types.js';
51
76
 
52
77
  // ─── Planning ────────────────────────────────────────────────────────
53
78
  export { Planner } from './planning/planner.js';
@@ -11,12 +11,7 @@ import * as path from 'node:path';
11
11
  import { homedir } from 'node:os';
12
12
  import { LLMError } from './types.js';
13
13
  import { CircuitBreaker, retry, parseRateLimitHeaders } from './utils.js';
14
- import type {
15
- LLMCallOptions,
16
- LLMCallResult,
17
- RouteEntry,
18
- RoutingConfig,
19
- } from './types.js';
14
+ import type { LLMCallOptions, LLMCallResult, RouteEntry, RoutingConfig } from './types.js';
20
15
  import type { KeyPool } from './key-pool.js';
21
16
 
22
17
  // =============================================================================
@@ -69,7 +64,11 @@ class ModelRouter {
69
64
  private config: RoutingConfig;
70
65
 
71
66
  constructor(config?: RoutingConfig) {
72
- this.config = config ?? { routes: [], defaultOpenAIModel: 'gpt-4o-mini', defaultAnthropicModel: 'claude-sonnet-4-20250514' };
67
+ this.config = config ?? {
68
+ routes: [],
69
+ defaultOpenAIModel: 'gpt-4o-mini',
70
+ defaultAnthropicModel: 'claude-sonnet-4-20250514',
71
+ };
73
72
  }
74
73
 
75
74
  resolve(
@@ -78,17 +77,13 @@ class ModelRouter {
78
77
  originalModel?: string,
79
78
  ): { model: string; provider: 'openai' | 'anthropic' } {
80
79
  if (task) {
81
- const exactMatch = this.config.routes.find(
82
- (r) => r.caller === caller && r.task === task,
83
- );
80
+ const exactMatch = this.config.routes.find((r) => r.caller === caller && r.task === task);
84
81
  if (exactMatch) {
85
82
  return { model: exactMatch.model, provider: exactMatch.provider };
86
83
  }
87
84
  }
88
85
 
89
- const callerMatch = this.config.routes.find(
90
- (r) => r.caller === caller && !r.task,
91
- );
86
+ const callerMatch = this.config.routes.find((r) => r.caller === caller && !r.task);
92
87
  if (callerMatch) {
93
88
  return { model: callerMatch.model, provider: callerMatch.provider };
94
89
  }
@@ -148,11 +143,7 @@ export class LLMClient {
148
143
  }
149
144
 
150
145
  async complete(options: LLMCallOptions): Promise<LLMCallResult> {
151
- const routed = this.router.resolve(
152
- options.caller,
153
- options.task,
154
- options.model,
155
- );
146
+ const routed = this.router.resolve(options.caller, options.task, options.model);
156
147
  const resolvedOptions = { ...options, model: routed.model, provider: routed.provider };
157
148
 
158
149
  return resolvedOptions.provider === 'anthropic'
@@ -219,10 +210,10 @@ export class LLMClient {
219
210
  }
220
211
 
221
212
  const errorBody = await response.text();
222
- throw new LLMError(
223
- `OpenAI API error: ${response.status} - ${errorBody}`,
224
- { retryable: response.status === 429 || response.status >= 500, statusCode: response.status },
225
- );
213
+ throw new LLMError(`OpenAI API error: ${response.status} - ${errorBody}`, {
214
+ retryable: response.status === 429 || response.status >= 500,
215
+ statusCode: response.status,
216
+ });
226
217
  }
227
218
 
228
219
  const data = (await response.json()) as {
@@ -270,7 +261,8 @@ export class LLMClient {
270
261
 
271
262
  const text = response.content
272
263
  .filter(
273
- (block): block is { type: 'text'; text: string } => block.type === 'text' && typeof block.text === 'string',
264
+ (block): block is { type: 'text'; text: string } =>
265
+ block.type === 'text' && typeof block.text === 'string',
274
266
  )
275
267
  .map((block) => block.text)
276
268
  .join('\n');
@@ -305,7 +297,9 @@ export class LLMClient {
305
297
  try {
306
298
  // Dynamic import — @anthropic-ai/sdk is an optional peer dep.
307
299
  // eslint-disable-next-line @typescript-eslint/no-require-imports
308
- const mod = await (Function('return import("@anthropic-ai/sdk")')() as Promise<{ default: new (opts: { apiKey: string }) => AnthropicClient }>);
300
+ const mod = await (Function('return import("@anthropic-ai/sdk")')() as Promise<{
301
+ default: new (opts: { apiKey: string }) => AnthropicClient;
302
+ }>);
309
303
  this.anthropicClient = new mod.default({ apiKey: currentKey });
310
304
  return this.anthropicClient;
311
305
  } catch {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generic core operations factory — 26 ops that every agent gets.
2
+ * Generic core operations factory — 37 ops that every agent gets.
3
3
  *
4
4
  * These ops are agent-agnostic (no persona, no activation).
5
5
  * The 5 agent-specific ops (health, identity, activate, inject_claude_md, setup)
@@ -12,19 +12,20 @@ import type { IntelligenceEntry } from '../intelligence/types.js';
12
12
  import type { AgentRuntime } from './types.js';
13
13
 
14
14
  /**
15
- * Create the 26 generic core operations for an agent runtime.
15
+ * Create the 37 generic core operations for an agent runtime.
16
16
  *
17
17
  * Groups: search/vault (4), memory (4), export (1), planning (5),
18
- * brain (4), curator (8).
18
+ * brain (4), brain intelligence (11), curator (8).
19
19
  */
20
20
  export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
21
- const { vault, brain, planner, curator, llmClient, keyPool } = runtime;
21
+ const { vault, brain, brainIntelligence, planner, curator, llmClient, keyPool } = runtime;
22
22
 
23
23
  return [
24
24
  // ─── Search / Vault ──────────────────────────────────────────
25
25
  {
26
26
  name: 'search',
27
- description: 'Search across all knowledge domains. Results ranked by TF-IDF + severity + recency + tag overlap + domain match.',
27
+ description:
28
+ 'Search across all knowledge domains. Results ranked by TF-IDF + severity + recency + tag overlap + domain match.',
28
29
  auth: 'read',
29
30
  schema: z.object({
30
31
  query: z.string(),
@@ -75,7 +76,8 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
75
76
  },
76
77
  {
77
78
  name: 'register',
78
- description: 'Register a project for this session. Call on every new session to track usage and get context.',
79
+ description:
80
+ 'Register a project for this session. Call on every new session to track usage and get context.',
79
81
  auth: 'write',
80
82
  schema: z.object({
81
83
  projectPath: z.string().optional().default('.'),
@@ -167,7 +169,8 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
167
169
  },
168
170
  {
169
171
  name: 'session_capture',
170
- description: 'Capture a session summary before context compaction. Called automatically by PreCompact hook.',
172
+ description:
173
+ 'Capture a session summary before context compaction. Called automatically by PreCompact hook.',
171
174
  auth: 'write',
172
175
  schema: z.object({
173
176
  projectPath: z.string().optional().default('.'),
@@ -195,17 +198,17 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
195
198
  // ─── Export ───────────────────────────────────────────────────
196
199
  {
197
200
  name: 'export',
198
- description: 'Export vault entries as JSON intelligence bundles — one per domain. Enables version control and sharing.',
201
+ description:
202
+ 'Export vault entries as JSON intelligence bundles — one per domain. Enables version control and sharing.',
199
203
  auth: 'read',
200
204
  schema: z.object({
201
205
  domain: z.string().optional().describe('Export only this domain. Omit to export all.'),
202
206
  }),
203
207
  handler: async (params) => {
204
208
  const stats = vault.stats();
205
- const domains = params.domain
206
- ? [params.domain as string]
207
- : Object.keys(stats.byDomain);
208
- const bundles: Array<{ domain: string; version: string; entries: IntelligenceEntry[] }> = [];
209
+ const domains = params.domain ? [params.domain as string] : Object.keys(stats.byDomain);
210
+ const bundles: Array<{ domain: string; version: string; entries: IntelligenceEntry[] }> =
211
+ [];
209
212
  for (const d of domains) {
210
213
  const entries = vault.list({ domain: d, limit: 10000 });
211
214
  bundles.push({ domain: d, version: '1.0.0', entries });
@@ -222,13 +225,17 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
222
225
  // ─── Planning ────────────────────────────────────────────────
223
226
  {
224
227
  name: 'create_plan',
225
- description: 'Create a new plan in draft status. Plans track multi-step tasks with decisions and sub-tasks.',
228
+ description:
229
+ 'Create a new plan in draft status. Plans track multi-step tasks with decisions and sub-tasks.',
226
230
  auth: 'write',
227
231
  schema: z.object({
228
232
  objective: z.string().describe('What the plan aims to achieve'),
229
233
  scope: z.string().describe('Which parts of the codebase are affected'),
230
234
  decisions: z.array(z.string()).optional().default([]),
231
- tasks: z.array(z.object({ title: z.string(), description: z.string() })).optional().default([]),
235
+ tasks: z
236
+ .array(z.object({ title: z.string(), description: z.string() }))
237
+ .optional()
238
+ .default([]),
232
239
  }),
233
240
  handler: async (params) => {
234
241
  const plan = planner.create({
@@ -262,7 +269,11 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
262
269
  auth: 'write',
263
270
  schema: z.object({
264
271
  planId: z.string(),
265
- startExecution: z.boolean().optional().default(false).describe('If true, immediately start execution after approval'),
272
+ startExecution: z
273
+ .boolean()
274
+ .optional()
275
+ .default(false)
276
+ .describe('If true, immediately start execution after approval'),
266
277
  }),
267
278
  handler: async (params) => {
268
279
  let plan = planner.approve(params.planId as string);
@@ -313,7 +324,8 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
313
324
  // ─── Brain ───────────────────────────────────────────────────
314
325
  {
315
326
  name: 'record_feedback',
316
- description: 'Record feedback on a search result — accepted or dismissed. Used for adaptive weight tuning.',
327
+ description:
328
+ 'Record feedback on a search result — accepted or dismissed. Used for adaptive weight tuning.',
317
329
  auth: 'write',
318
330
  schema: z.object({
319
331
  query: z.string().describe('The original search query'),
@@ -326,7 +338,12 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
326
338
  params.entryId as string,
327
339
  params.action as 'accepted' | 'dismissed',
328
340
  );
329
- return { recorded: true, query: params.query, entryId: params.entryId, action: params.action };
341
+ return {
342
+ recorded: true,
343
+ query: params.query,
344
+ entryId: params.entryId,
345
+ action: params.action,
346
+ };
330
347
  },
331
348
  },
332
349
  {
@@ -340,15 +357,19 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
340
357
  },
341
358
  {
342
359
  name: 'brain_stats',
343
- description: 'Get brain intelligence stats — vocabulary size, feedback count, current scoring weights.',
360
+ description:
361
+ 'Get brain intelligence stats — vocabulary size, feedback count, current scoring weights, intelligence pipeline stats.',
344
362
  auth: 'read',
345
363
  handler: async () => {
346
- return brain.getStats();
364
+ const base = brain.getStats();
365
+ const intelligence = brainIntelligence.getStats();
366
+ return { ...base, intelligence };
347
367
  },
348
368
  },
349
369
  {
350
370
  name: 'llm_status',
351
- description: 'LLM client status — provider availability, key pool status, model routing config.',
371
+ description:
372
+ 'LLM client status — provider availability, key pool status, model routing config.',
352
373
  auth: 'read',
353
374
  handler: async () => {
354
375
  const available = llmClient.isAvailable();
@@ -368,6 +389,165 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
368
389
  },
369
390
  },
370
391
 
392
+ // ─── Brain Intelligence ───────────────────────────────────────
393
+ {
394
+ name: 'brain_session_context',
395
+ description:
396
+ 'Get recent session context — sessions, tool usage frequency, file change frequency.',
397
+ auth: 'read',
398
+ schema: z.object({
399
+ limit: z.number().optional().describe('Number of recent sessions. Default 10.'),
400
+ }),
401
+ handler: async (params) => {
402
+ return brainIntelligence.getSessionContext((params.limit as number) ?? 10);
403
+ },
404
+ },
405
+ {
406
+ name: 'brain_strengths',
407
+ description:
408
+ 'Get pattern strength scores. 4-signal scoring: usage (0-25) + spread (0-25) + success (0-25) + recency (0-25).',
409
+ auth: 'read',
410
+ schema: z.object({
411
+ domain: z.string().optional(),
412
+ minStrength: z.number().optional().describe('Minimum strength score (0-100).'),
413
+ limit: z.number().optional(),
414
+ }),
415
+ handler: async (params) => {
416
+ return brainIntelligence.getStrengths({
417
+ domain: params.domain as string | undefined,
418
+ minStrength: params.minStrength as number | undefined,
419
+ limit: (params.limit as number) ?? 50,
420
+ });
421
+ },
422
+ },
423
+ {
424
+ name: 'brain_global_patterns',
425
+ description:
426
+ 'Get cross-domain pattern registry — patterns that appear across multiple domains.',
427
+ auth: 'read',
428
+ schema: z.object({
429
+ limit: z.number().optional(),
430
+ }),
431
+ handler: async (params) => {
432
+ return brainIntelligence.getGlobalPatterns((params.limit as number) ?? 20);
433
+ },
434
+ },
435
+ {
436
+ name: 'brain_recommend',
437
+ description:
438
+ 'Get pattern recommendations for a task context. Matches domain and task terms against known strengths.',
439
+ auth: 'read',
440
+ schema: z.object({
441
+ domain: z.string().optional(),
442
+ task: z.string().optional().describe('Task description for contextual matching.'),
443
+ limit: z.number().optional(),
444
+ }),
445
+ handler: async (params) => {
446
+ return brainIntelligence.recommend({
447
+ domain: params.domain as string | undefined,
448
+ task: params.task as string | undefined,
449
+ limit: (params.limit as number) ?? 5,
450
+ });
451
+ },
452
+ },
453
+ {
454
+ name: 'brain_build_intelligence',
455
+ description:
456
+ 'Run the full intelligence pipeline: compute strengths → build global registry → build domain profiles.',
457
+ auth: 'write',
458
+ handler: async () => {
459
+ return brainIntelligence.buildIntelligence();
460
+ },
461
+ },
462
+ {
463
+ name: 'brain_export',
464
+ description:
465
+ 'Export all brain intelligence data — strengths, sessions, proposals, global patterns, domain profiles.',
466
+ auth: 'read',
467
+ handler: async () => {
468
+ return brainIntelligence.exportData();
469
+ },
470
+ },
471
+ {
472
+ name: 'brain_import',
473
+ description: 'Import brain intelligence data from a previous export.',
474
+ auth: 'write',
475
+ schema: z.object({
476
+ data: z.any().describe('BrainExportData object from brain_export.'),
477
+ }),
478
+ handler: async (params) => {
479
+ return brainIntelligence.importData(
480
+ params.data as import('../brain/types.js').BrainExportData,
481
+ );
482
+ },
483
+ },
484
+ {
485
+ name: 'brain_extract_knowledge',
486
+ description:
487
+ 'Extract knowledge proposals from a session using 6 heuristic rules (repeated tools, multi-file edits, long sessions, plan outcomes, feedback ratios).',
488
+ auth: 'write',
489
+ schema: z.object({
490
+ sessionId: z.string().describe('Session ID to extract knowledge from.'),
491
+ }),
492
+ handler: async (params) => {
493
+ return brainIntelligence.extractKnowledge(params.sessionId as string);
494
+ },
495
+ },
496
+ {
497
+ name: 'brain_archive_sessions',
498
+ description: 'Archive (delete) completed sessions older than N days.',
499
+ auth: 'write',
500
+ schema: z.object({
501
+ olderThanDays: z.number().optional().describe('Days threshold. Default 30.'),
502
+ }),
503
+ handler: async (params) => {
504
+ return brainIntelligence.archiveSessions((params.olderThanDays as number) ?? 30);
505
+ },
506
+ },
507
+ {
508
+ name: 'brain_promote_proposals',
509
+ description:
510
+ 'Promote knowledge proposals to vault entries. Creates intelligence entries from auto-extracted patterns.',
511
+ auth: 'write',
512
+ schema: z.object({
513
+ proposalIds: z.array(z.string()).describe('IDs of proposals to promote.'),
514
+ }),
515
+ handler: async (params) => {
516
+ return brainIntelligence.promoteProposals(params.proposalIds as string[]);
517
+ },
518
+ },
519
+ {
520
+ name: 'brain_lifecycle',
521
+ description:
522
+ 'Start or end a brain session. Sessions track tool usage, file changes, and plan context.',
523
+ auth: 'write',
524
+ schema: z.object({
525
+ action: z.enum(['start', 'end']),
526
+ sessionId: z
527
+ .string()
528
+ .optional()
529
+ .describe('Required for end. Auto-generated for start if omitted.'),
530
+ domain: z.string().optional(),
531
+ context: z.string().optional(),
532
+ toolsUsed: z.array(z.string()).optional(),
533
+ filesModified: z.array(z.string()).optional(),
534
+ planId: z.string().optional(),
535
+ planOutcome: z.string().optional(),
536
+ }),
537
+ handler: async (params) => {
538
+ return brainIntelligence.lifecycle({
539
+ action: params.action as 'start' | 'end',
540
+ sessionId: params.sessionId as string | undefined,
541
+ domain: params.domain as string | undefined,
542
+ context: params.context as string | undefined,
543
+ toolsUsed: params.toolsUsed as string[] | undefined,
544
+ filesModified: params.filesModified as string[] | undefined,
545
+ planId: params.planId as string | undefined,
546
+ planOutcome: params.planOutcome as string | undefined,
547
+ });
548
+ },
549
+ },
550
+
371
551
  // ─── Curator ─────────────────────────────────────────────────
372
552
  {
373
553
  name: 'curator_status',
@@ -404,7 +584,9 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
404
584
  if (params.detect) {
405
585
  curator.detectContradictions();
406
586
  }
407
- return curator.getContradictions(params.status as 'open' | 'resolved' | 'dismissed' | undefined);
587
+ return curator.getContradictions(
588
+ params.status as 'open' | 'resolved' | 'dismissed' | undefined,
589
+ );
408
590
  },
409
591
  },
410
592
  {
@@ -443,13 +625,23 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
443
625
  },
444
626
  {
445
627
  name: 'curator_consolidate',
446
- description: 'Consolidate vault — find duplicates, stale entries, contradictions. Dry-run by default.',
628
+ description:
629
+ 'Consolidate vault — find duplicates, stale entries, contradictions. Dry-run by default.',
447
630
  auth: 'write',
448
631
  schema: z.object({
449
632
  dryRun: z.boolean().optional().describe('Default true. Set false to apply mutations.'),
450
- staleDaysThreshold: z.number().optional().describe('Days before entry is stale. Default 90.'),
451
- duplicateThreshold: z.number().optional().describe('Cosine similarity threshold. Default 0.45.'),
452
- contradictionThreshold: z.number().optional().describe('Contradiction threshold. Default 0.4.'),
633
+ staleDaysThreshold: z
634
+ .number()
635
+ .optional()
636
+ .describe('Days before entry is stale. Default 90.'),
637
+ duplicateThreshold: z
638
+ .number()
639
+ .optional()
640
+ .describe('Cosine similarity threshold. Default 0.45.'),
641
+ contradictionThreshold: z
642
+ .number()
643
+ .optional()
644
+ .describe('Contradiction threshold. Default 0.4.'),
453
645
  }),
454
646
  handler: async (params) => {
455
647
  return curator.consolidate({
@@ -462,7 +654,8 @@ export function createCoreOps(runtime: AgentRuntime): OpDefinition[] {
462
654
  },
463
655
  {
464
656
  name: 'curator_health_audit',
465
- description: 'Audit vault health — score (0-100), coverage, freshness, quality, tag health, recommendations.',
657
+ description:
658
+ 'Audit vault health — score (0-100), coverage, freshness, quality, tag health, recommendations.',
466
659
  auth: 'read',
467
660
  handler: async () => {
468
661
  return curator.healthAudit();
@@ -11,6 +11,7 @@ import { join } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
12
  import { Vault } from '../vault/vault.js';
13
13
  import { Brain } from '../brain/brain.js';
14
+ import { BrainIntelligence } from '../brain/intelligence.js';
14
15
  import { Planner } from '../planning/planner.js';
15
16
  import { Curator } from '../curator/curator.js';
16
17
  import { KeyPool, loadKeyPoolConfig } from '../llm/key-pool.js';
@@ -49,6 +50,9 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
49
50
  // Brain — intelligence layer (TF-IDF scoring, auto-tagging, dedup)
50
51
  const brain = new Brain(vault);
51
52
 
53
+ // Brain Intelligence — pattern strengths, session knowledge, intelligence pipeline
54
+ const brainIntelligence = new BrainIntelligence(vault, brain);
55
+
52
56
  // Curator — vault self-maintenance (dedup, contradictions, grooming, health)
53
57
  const curator = new Curator(vault);
54
58
 
@@ -62,6 +66,7 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
62
66
  config,
63
67
  vault,
64
68
  brain,
69
+ brainIntelligence,
65
70
  planner,
66
71
  curator,
67
72
  keyPool: { openai: openaiKeyPool, anthropic: anthropicKeyPool },
@@ -1,5 +1,6 @@
1
1
  import type { Vault } from '../vault/vault.js';
2
2
  import type { Brain } from '../brain/brain.js';
3
+ import type { BrainIntelligence } from '../brain/intelligence.js';
3
4
  import type { Planner } from '../planning/planner.js';
4
5
  import type { Curator } from '../curator/curator.js';
5
6
  import type { KeyPool } from '../llm/key-pool.js';
@@ -28,6 +29,7 @@ export interface AgentRuntime {
28
29
  config: AgentRuntimeConfig;
29
30
  vault: Vault;
30
31
  brain: Brain;
32
+ brainIntelligence: BrainIntelligence;
31
33
  planner: Planner;
32
34
  curator: Curator;
33
35
  keyPool: { openai: KeyPool; anthropic: KeyPool };