opc-agent 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.
package/src/cli.ts CHANGED
@@ -170,11 +170,25 @@ program
170
170
  fs.mkdirSync(dir, { recursive: true });
171
171
  fs.mkdirSync(path.join(dir, 'src', 'skills'), { recursive: true });
172
172
  fs.mkdirSync(path.join(dir, 'data'), { recursive: true });
173
+ fs.mkdirSync(path.join(dir, 'brain-seeds'), { recursive: true });
173
174
 
174
175
  // Get system prompt content
175
176
  const systemPromptContent = roleData.files['system-prompt.md'] || roleData.files['prompts/system.md'] || '';
176
177
 
177
- // agent.yaml with role system prompt
178
+ // Generate brain-seeds/ files from role data
179
+ const brainSeedContent = roleData.files['brain-seed.md'] || '';
180
+ const industryMatch = brainSeedContent ? brainSeedContent.match(/# Industry Knowledge[\s\S]*?(?=# Job Knowledge|# Workstation Knowledge|$)/i) : null;
181
+ const jobMatch = brainSeedContent ? brainSeedContent.match(/# Job Knowledge[\s\S]*?(?=# Industry Knowledge|# Workstation Knowledge|$)/i) : null;
182
+ const workstationMatch = brainSeedContent ? brainSeedContent.match(/# Workstation Knowledge[\s\S]*?(?=# Industry Knowledge|# Job Knowledge|$)/i) : null;
183
+
184
+ fs.writeFileSync(path.join(dir, 'brain-seeds', 'industry.md'), industryMatch?.[0]?.trim() || `# Industry Knowledge\n\n## Overview\n\nAdd industry-specific knowledge for your domain.\n`);
185
+ fs.writeFileSync(path.join(dir, 'brain-seeds', 'job.md'), jobMatch?.[0]?.trim() || `# Job Knowledge\n\n## Core Skills\n\nAdd role-specific knowledge for ${roleDisplayName}.\n`);
186
+ // workstation.md: public workstation knowledge (tools, workflows, best practices)
187
+ // Company-specific knowledge belongs to Desk (closed-source), not here.
188
+ const workstationSeedFromRole = workstationMatch?.[0]?.trim() || '';
189
+ fs.writeFileSync(path.join(dir, 'brain-seeds', 'workstation.md'), workstationSeedFromRole || `# Workstation Knowledge\n\n## Tools & Environment\n\nCommon tools and setup for this workstation role.\n\n## Workflows\n\nStandard operating procedures and workflows.\n\n## Best Practices\n\nIndustry best practices for this role.\n`);
190
+
191
+ // agent.yaml with role system prompt and brain seeds
178
192
  const firstLine = systemPromptContent.split('\n').find((l: string) => l.trim() && !l.startsWith('#'))?.trim() || 'You are a helpful AI assistant.';
179
193
  fs.writeFileSync(
180
194
  path.join(dir, 'agent.yaml'),
@@ -198,6 +212,15 @@ spec:
198
212
  longTerm:
199
213
  provider: deepbrain
200
214
  database: ./data/brain.db
215
+ brain:
216
+ seeds:
217
+ - brain-seeds/industry.md
218
+ - brain-seeds/job.md
219
+ - brain-seeds/workstation.md
220
+ autoSeed: true
221
+ evolve:
222
+ enabled: true
223
+ direction: bottom-up
201
224
  skills: []
202
225
  `,
203
226
  );
@@ -317,8 +340,12 @@ export class EchoSkill extends BaseSkill {
317
340
  console.log(` ${icon.file} agent.yaml - Agent definition with role system prompt`);
318
341
  console.log(` ${icon.file} SOUL.md - Role personality (${systemPromptContent.split('\n').length} lines)`);
319
342
  console.log(` ${icon.file} CONTEXT.md - Role context & documentation`);
343
+ console.log(` ${icon.file} brain-seeds/ - 3-tier brain seed knowledge`);
344
+ console.log(` ${color.dim('├')} industry.md - Industry knowledge`);
345
+ console.log(` ${color.dim('├')} job.md - Job/role knowledge`);
346
+ console.log(` ${color.dim('└')} workstation.md - Workstation knowledge`);
320
347
  if (roleData.files['brain-seed.md']) {
321
- console.log(` ${icon.file} data/brain-seed.md - Role brain seed knowledge`);
348
+ console.log(` ${icon.file} data/brain-seed.md - Role brain seed knowledge (legacy)`);
322
349
  }
323
350
  console.log(` ${icon.file} src/index.ts - Entry point`);
324
351
  console.log(` ${icon.file} package.json - Dependencies`);
@@ -1516,9 +1543,13 @@ program
1516
1543
 
1517
1544
  // ── Brain command ────────────────────────────────────────────
1518
1545
 
1519
- program
1546
+ const brainCmd = program
1520
1547
  .command('brain')
1521
- .description('Show agent memory/brain status from DeepBrain')
1548
+ .description('Manage agent brain (memory, seeds, evolve)');
1549
+
1550
+ brainCmd
1551
+ .command('status')
1552
+ .description('Show brain stats (pages, tiers, last evolve)')
1522
1553
  .option('--url <url>', 'DeepBrain server URL', 'http://localhost:3333')
1523
1554
  .action(async (opts: { url: string }) => {
1524
1555
  console.log(`\n${icon.gear} ${color.bold('DeepBrain Status')} — ${color.dim(opts.url)}\n`);
@@ -1549,6 +1580,89 @@ program
1549
1580
  }
1550
1581
  });
1551
1582
 
1583
+ brainCmd
1584
+ .command('seed')
1585
+ .description('Import brain seed files into memory')
1586
+ .option('-f, --file <file>', 'OAD file', 'agent.yaml')
1587
+ .option('--status', 'Check if seeds have been imported')
1588
+ .option('--reset', 'Re-import seeds (clear marker and re-seed)')
1589
+ .action(async (opts: { file: string; status?: boolean; reset?: boolean }) => {
1590
+ const { BrainSeedLoader } = require('./memory/seed-loader');
1591
+ let config: any = {};
1592
+ try { config = yaml.load(fs.readFileSync(opts.file, 'utf-8')) as any; } catch { /* ignore */ }
1593
+ const brainConfig = config?.spec?.brain;
1594
+ if (!brainConfig?.seeds?.length) {
1595
+ console.log(`${icon.info} No brain seeds configured in ${opts.file}.`);
1596
+ console.log(` Add spec.brain.seeds to your agent.yaml.`);
1597
+ return;
1598
+ }
1599
+
1600
+ const loader = new BrainSeedLoader(process.cwd(), {
1601
+ seeds: brainConfig.seeds,
1602
+ autoSeed: brainConfig.autoSeed !== false,
1603
+ });
1604
+
1605
+ if (opts.status) {
1606
+ const seeded = await loader.isSeeded();
1607
+ console.log(`\n Brain seed status: ${seeded ? color.green('seeded ✔') : color.yellow('not seeded')}`);
1608
+ console.log(` Seeds configured: ${brainConfig.seeds.map((s: string) => color.cyan(s)).join(', ')}\n`);
1609
+ return;
1610
+ }
1611
+
1612
+ if (opts.reset) {
1613
+ const markerPath = path.resolve(process.cwd(), '.brain-seeded');
1614
+ if (fs.existsSync(markerPath)) {
1615
+ fs.unlinkSync(markerPath);
1616
+ console.log(` ${icon.success} Cleared seed marker.`);
1617
+ }
1618
+ }
1619
+
1620
+ if (await loader.isSeeded() && !opts.reset) {
1621
+ console.log(`${icon.info} Brain already seeded. Use --reset to re-import.`);
1622
+ return;
1623
+ }
1624
+
1625
+ console.log(`\n${icon.gear} Importing brain seeds...\n`);
1626
+ // Use a simple mock brain that logs imports (real usage would connect to DeepBrain)
1627
+ const pages: string[] = [];
1628
+ const mockBrain = {
1629
+ learn: async (content: string, meta: any) => { pages.push(meta?.slug || 'unknown'); },
1630
+ };
1631
+ const result = await loader.seedBrain(mockBrain);
1632
+ await loader.markSeeded();
1633
+
1634
+ console.log(` ${icon.success} Imported ${color.bold(String(result.imported))} pages from ${brainConfig.seeds.length} seed files.`);
1635
+ for (const p of result.pages) {
1636
+ console.log(` ${color.dim('•')} ${p}`);
1637
+ }
1638
+ console.log();
1639
+ });
1640
+
1641
+ brainCmd
1642
+ .command('evolve')
1643
+ .description('Trigger manual knowledge evolution cycle')
1644
+ .option('--dry-run', 'Show what would be promoted without doing it')
1645
+ .action(async (opts: { dryRun?: boolean }) => {
1646
+ const { KnowledgeEvolver } = require('./memory/seed-loader');
1647
+ const evolver = new KnowledgeEvolver();
1648
+ console.log(`\n${icon.gear} ${color.bold('Knowledge Evolution')}\n`);
1649
+ console.log(` ${icon.info} Checking for promotion candidates...`);
1650
+ // Would connect to real brain in production
1651
+ const result = await evolver.checkPromotion(null);
1652
+ if (result.candidates.length === 0) {
1653
+ console.log(` ${icon.info} No knowledge ready for promotion yet.\n`);
1654
+ } else {
1655
+ for (const c of result.candidates) {
1656
+ console.log(` ${color.cyan(c.slug)} → ${c.fromTier} → ${c.toTier} (confidence: ${(c.confidence * 100).toFixed(0)}%)`);
1657
+ }
1658
+ if (opts.dryRun) {
1659
+ console.log(`\n ${icon.info} Dry run — no changes made.\n`);
1660
+ } else {
1661
+ console.log(`\n ${icon.success} Promoted ${result.promoted} knowledge entries.\n`);
1662
+ }
1663
+ }
1664
+ });
1665
+
1552
1666
  // ── Logs command ─────────────────────────────────────────────
1553
1667
 
1554
1668
  program
package/src/core/agent.ts CHANGED
@@ -8,6 +8,7 @@ import { MCPToolRegistry } from '../tools/mcp';
8
8
  import { SubAgentManager, type SubAgentConfig, type SubAgentResult } from './subagent';
9
9
  import { Tracer } from '../telemetry';
10
10
  import type { Span as TelemetrySpan } from '../telemetry';
11
+ import { BrainSeedLoader, type BrainSeedConfig } from '../memory/seed-loader';
11
12
 
12
13
  export class BaseAgent extends EventEmitter implements IAgent {
13
14
  readonly name: string;
@@ -26,6 +27,8 @@ export class BaseAgent extends EventEmitter implements IAgent {
26
27
  private longTermMemory?: any;
27
28
  private longTermMemoryConfig: { autoLearn: boolean; autoRecall: boolean } = { autoLearn: true, autoRecall: true };
28
29
  private tracer?: Tracer;
30
+ private brainSeedConfig?: BrainSeedConfig;
31
+ private agentDir: string;
29
32
 
30
33
  constructor(options: {
31
34
  name: string;
@@ -42,6 +45,8 @@ export class BaseAgent extends EventEmitter implements IAgent {
42
45
  };
43
46
  maxToolRounds?: number;
44
47
  tracer?: Tracer;
48
+ agentDir?: string;
49
+ brainSeedConfig?: BrainSeedConfig;
45
50
  }) {
46
51
  super();
47
52
  this.name = options.name;
@@ -59,6 +64,8 @@ export class BaseAgent extends EventEmitter implements IAgent {
59
64
  this.skillLearner = new SkillLearner(options.skillsDir);
60
65
  }
61
66
  this.tracer = options.tracer;
67
+ this.agentDir = options.agentDir ?? process.cwd();
68
+ this.brainSeedConfig = options.brainSeedConfig;
62
69
  }
63
70
 
64
71
  setLongTermMemory(brain: any, config?: { autoLearn?: boolean; autoRecall?: boolean }): void {
@@ -125,6 +132,17 @@ export class BaseAgent extends EventEmitter implements IAgent {
125
132
  if (this.skillLearner) {
126
133
  await this.skillLearner.loadLearnedSkills();
127
134
  }
135
+
136
+ // Auto-seed brain if configured
137
+ if (this.brainSeedConfig?.autoSeed && this.longTermMemory) {
138
+ const loader = new BrainSeedLoader(this.agentDir, this.brainSeedConfig);
139
+ if (!await loader.isSeeded()) {
140
+ const result = await loader.seedBrain(this.longTermMemory);
141
+ this.emit('brain:seeded', result);
142
+ await loader.markSeeded();
143
+ }
144
+ }
145
+
128
146
  this.transition('ready');
129
147
  }
130
148
 
package/src/index.ts CHANGED
@@ -128,6 +128,10 @@ export type { Span as TraceSpan, SpanEvent as TraceSpanEvent, TraceExporter as I
128
128
  export { Tracer, ConsoleExporter, FileExporter, OTLPHttpExporter, generateTraceId, generateSpanId } from './telemetry';
129
129
  export type { Span, SpanEvent, Metric, TraceExporter } from './telemetry';
130
130
 
131
+ // v1.3.1 — Sub-agent management
132
+ export { SubAgentManager } from './core/subagent';
133
+ export type { SubAgentConfig, SubAgentResult } from './core/subagent';
134
+
131
135
  // v1.4.0 modules
132
136
  export { Scheduler, parseCron, cronMatches } from './core/scheduler';
133
137
  export type { CronJob, JobHandler } from './core/scheduler';
@@ -1,5 +1,8 @@
1
1
  import type { Message, MemoryStore } from '../core/types';
2
2
 
3
+ export { BrainSeedLoader, KnowledgeEvolver } from './seed-loader';
4
+ export type { BrainSeedConfig, SeedPage, SeedResult, PromotionResult, PromotionCandidate } from './seed-loader';
5
+
3
6
  export class InMemoryStore implements MemoryStore {
4
7
  private store: Map<string, unknown> = new Map();
5
8
  private conversations: Map<string, Message[]> = new Map();
@@ -0,0 +1,212 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface BrainSeedConfig {
5
+ seeds: string[];
6
+ autoSeed: boolean;
7
+ seedMarkerFile?: string;
8
+ evolve?: {
9
+ enabled: boolean;
10
+ direction: 'bottom-up' | 'top-down';
11
+ };
12
+ }
13
+
14
+ export interface SeedPage {
15
+ slug: string;
16
+ content: string;
17
+ tier: string;
18
+ }
19
+
20
+ export interface SeedResult {
21
+ imported: number;
22
+ pages: string[];
23
+ }
24
+
25
+ export interface PromotionCandidate {
26
+ slug: string;
27
+ content: string;
28
+ fromTier: string;
29
+ toTier: string;
30
+ confidence: number;
31
+ }
32
+
33
+ export interface PromotionResult {
34
+ candidates: PromotionCandidate[];
35
+ promoted: number;
36
+ }
37
+
38
+ export class BrainSeedLoader {
39
+ private markerFile: string;
40
+
41
+ constructor(private agentDir: string, private config: BrainSeedConfig) {
42
+ this.markerFile = config.seedMarkerFile
43
+ ? path.resolve(agentDir, config.seedMarkerFile)
44
+ : path.resolve(agentDir, '.brain-seeded');
45
+ }
46
+
47
+ async isSeeded(): Promise<boolean> {
48
+ return fs.existsSync(this.markerFile);
49
+ }
50
+
51
+ async seedBrain(brain: any): Promise<SeedResult> {
52
+ const allPages: SeedPage[] = [];
53
+
54
+ for (const seedPath of this.config.seeds) {
55
+ const fullPath = path.resolve(this.agentDir, seedPath);
56
+ if (!fs.existsSync(fullPath)) continue;
57
+
58
+ const tier = this.inferTier(seedPath);
59
+ const pages = this.parseSeedFile(fullPath, tier);
60
+ allPages.push(...pages);
61
+ }
62
+
63
+ const importedSlugs: string[] = [];
64
+ for (const page of allPages) {
65
+ if (brain && typeof brain.store === 'function') {
66
+ await brain.store('brain-seeds', page.slug, page.content, {
67
+ tier: page.tier,
68
+ source: 'brain-seed',
69
+ });
70
+ } else if (brain && typeof brain.learn === 'function') {
71
+ await brain.learn(page.content, {
72
+ tags: ['brain-seed', page.tier],
73
+ slug: page.slug,
74
+ });
75
+ }
76
+ importedSlugs.push(page.slug);
77
+ }
78
+
79
+ return { imported: importedSlugs.length, pages: importedSlugs };
80
+ }
81
+
82
+ async markSeeded(): Promise<void> {
83
+ const dir = path.dirname(this.markerFile);
84
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
85
+ fs.writeFileSync(this.markerFile, JSON.stringify({
86
+ seededAt: new Date().toISOString(),
87
+ seeds: this.config.seeds,
88
+ }, null, 2));
89
+ }
90
+
91
+ parseSeedFile(filePath: string, tier: string): SeedPage[] {
92
+ const content = fs.readFileSync(filePath, 'utf-8');
93
+ const pages: SeedPage[] = [];
94
+ const sections = content.split(/^## /m);
95
+
96
+ for (const section of sections) {
97
+ const trimmed = section.trim();
98
+ if (!trimmed) continue;
99
+
100
+ const newlineIdx = trimmed.indexOf('\n');
101
+ if (newlineIdx === -1 && sections.indexOf(section) === 0 && !content.trimStart().startsWith('## ')) {
102
+ // This is preamble before any ## heading — skip or treat as intro
103
+ continue;
104
+ }
105
+
106
+ let title: string;
107
+ let body: string;
108
+
109
+ if (sections.indexOf(section) === 0 && !content.trimStart().startsWith('## ')) {
110
+ // Preamble (before first ##)
111
+ continue;
112
+ }
113
+
114
+ if (newlineIdx === -1) {
115
+ title = trimmed;
116
+ body = '';
117
+ } else {
118
+ title = trimmed.slice(0, newlineIdx).trim();
119
+ body = trimmed.slice(newlineIdx + 1).trim();
120
+ }
121
+
122
+ const slug = `seed/${tier}/${this.slugify(title)}`;
123
+ pages.push({ slug, content: `## ${title}\n\n${body}`, tier });
124
+ }
125
+
126
+ return pages;
127
+ }
128
+
129
+ private inferTier(seedPath: string): string {
130
+ const basename = path.basename(seedPath, path.extname(seedPath)).toLowerCase();
131
+ if (basename.includes('industry')) return 'industry';
132
+ if (basename.includes('job')) return 'job';
133
+ if (basename.includes('workstation')) return 'workstation';
134
+ return 'workstation';
135
+ }
136
+
137
+ private slugify(text: string): string {
138
+ return text
139
+ .toLowerCase()
140
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
141
+ .replace(/^-|-$/g, '');
142
+ }
143
+ }
144
+
145
+ export class KnowledgeEvolver {
146
+ private tierOrder = ['workstation', 'job', 'industry'];
147
+
148
+ async checkPromotion(brain: any, options: {
149
+ minInteractions?: number;
150
+ confidenceThreshold?: number;
151
+ } = {}): Promise<PromotionResult> {
152
+ const minInteractions = options.minInteractions ?? 50;
153
+ const confidenceThreshold = options.confidenceThreshold ?? 0.8;
154
+
155
+ const result: PromotionResult = { candidates: [], promoted: 0 };
156
+
157
+ // Search for frequently referenced seed knowledge
158
+ if (!brain || typeof brain.search !== 'function') return result;
159
+
160
+ try {
161
+ const seedPages = await brain.search('brain-seeds', 'seed/', 100);
162
+ if (!Array.isArray(seedPages)) return result;
163
+
164
+ for (const page of seedPages) {
165
+ const meta = page.metadata || {};
166
+ const usageCount = meta.usageCount ?? 0;
167
+ const tier = meta.tier || 'workstation';
168
+
169
+ if (usageCount < minInteractions) continue;
170
+
171
+ const confidence = Math.min(usageCount / (minInteractions * 2), 1.0);
172
+ if (confidence < confidenceThreshold) continue;
173
+
174
+ const tierIdx = this.tierOrder.indexOf(tier);
175
+ if (tierIdx <= 0) continue; // already at highest tier or unknown
176
+
177
+ const toTier = this.tierOrder[tierIdx - 1];
178
+ result.candidates.push({
179
+ slug: page.id || page.slug,
180
+ content: page.content,
181
+ fromTier: tier,
182
+ toTier,
183
+ confidence,
184
+ });
185
+ }
186
+ } catch {
187
+ // Silent fail
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ async promoteToJob(brain: any, knowledge: string, jobSlug: string): Promise<void> {
194
+ if (brain && typeof brain.store === 'function') {
195
+ await brain.store('brain-seeds', jobSlug, knowledge, {
196
+ tier: 'job',
197
+ source: 'promotion',
198
+ promotedAt: new Date().toISOString(),
199
+ });
200
+ }
201
+ }
202
+
203
+ async promoteToIndustry(brain: any, knowledge: string, industrySlug: string): Promise<void> {
204
+ if (brain && typeof brain.store === 'function') {
205
+ await brain.store('brain-seeds', industrySlug, knowledge, {
206
+ tier: 'industry',
207
+ source: 'promotion',
208
+ promotedAt: new Date().toISOString(),
209
+ });
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,239 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { BrainSeedLoader, KnowledgeEvolver } from '../src/memory/seed-loader';
6
+ import type { BrainSeedConfig } from '../src/memory/seed-loader';
7
+
8
+ function makeTmpDir(): string {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opc-seed-test-'));
10
+ return dir;
11
+ }
12
+
13
+ describe('BrainSeedLoader', () => {
14
+ let tmpDir: string;
15
+
16
+ beforeEach(() => {
17
+ tmpDir = makeTmpDir();
18
+ fs.mkdirSync(path.join(tmpDir, 'brain-seeds'), { recursive: true });
19
+ });
20
+
21
+ afterEach(() => {
22
+ fs.rmSync(tmpDir, { recursive: true, force: true });
23
+ });
24
+
25
+ it('parseSeedFile splits ## sections into pages', () => {
26
+ const seedFile = path.join(tmpDir, 'brain-seeds', 'industry.md');
27
+ fs.writeFileSync(seedFile, `# Industry Knowledge
28
+
29
+ ## E-commerce Basics
30
+
31
+ Online retail fundamentals.
32
+
33
+ ## Payment Systems
34
+
35
+ How payments work in e-commerce.
36
+
37
+ ## Logistics
38
+
39
+ Shipping and fulfillment.
40
+ `);
41
+ const loader = new BrainSeedLoader(tmpDir, { seeds: ['brain-seeds/industry.md'], autoSeed: true });
42
+ const pages = loader.parseSeedFile(seedFile, 'industry');
43
+
44
+ expect(pages).toHaveLength(3);
45
+ expect(pages[0].slug).toBe('seed/industry/e-commerce-basics');
46
+ expect(pages[0].tier).toBe('industry');
47
+ expect(pages[0].content).toContain('E-commerce Basics');
48
+ expect(pages[0].content).toContain('Online retail fundamentals.');
49
+ expect(pages[1].slug).toBe('seed/industry/payment-systems');
50
+ expect(pages[2].slug).toBe('seed/industry/logistics');
51
+ });
52
+
53
+ it('isSeeded returns false when no marker file', async () => {
54
+ const loader = new BrainSeedLoader(tmpDir, { seeds: [], autoSeed: true });
55
+ expect(await loader.isSeeded()).toBe(false);
56
+ });
57
+
58
+ it('isSeeded returns true after markSeeded', async () => {
59
+ const loader = new BrainSeedLoader(tmpDir, { seeds: [], autoSeed: true });
60
+ await loader.markSeeded();
61
+ expect(await loader.isSeeded()).toBe(true);
62
+ // Check marker file content
63
+ const marker = JSON.parse(fs.readFileSync(path.join(tmpDir, '.brain-seeded'), 'utf-8'));
64
+ expect(marker.seededAt).toBeDefined();
65
+ });
66
+
67
+ it('seedBrain imports pages with learn()', async () => {
68
+ const seedFile = path.join(tmpDir, 'brain-seeds', 'job.md');
69
+ fs.writeFileSync(seedFile, `# Job Knowledge
70
+
71
+ ## Customer Handling
72
+
73
+ How to handle customers.
74
+
75
+ ## Escalation
76
+
77
+ When to escalate.
78
+ `);
79
+
80
+ const learned: { content: string; meta: any }[] = [];
81
+ const mockBrain = {
82
+ learn: async (content: string, meta: any) => { learned.push({ content, meta }); },
83
+ };
84
+
85
+ const loader = new BrainSeedLoader(tmpDir, { seeds: ['brain-seeds/job.md'], autoSeed: true });
86
+ const result = await loader.seedBrain(mockBrain);
87
+
88
+ expect(result.imported).toBe(2);
89
+ expect(result.pages).toContain('seed/job/customer-handling');
90
+ expect(result.pages).toContain('seed/job/escalation');
91
+ expect(learned).toHaveLength(2);
92
+ expect(learned[0].meta.tags).toContain('brain-seed');
93
+ expect(learned[0].meta.tags).toContain('job');
94
+ });
95
+
96
+ it('seedBrain imports pages with store()', async () => {
97
+ const seedFile = path.join(tmpDir, 'brain-seeds', 'industry.md');
98
+ fs.writeFileSync(seedFile, `## Topic One\n\nContent one.\n`);
99
+
100
+ const stored: any[] = [];
101
+ const mockBrain = {
102
+ store: async (collection: string, slug: string, content: string, meta: any) => {
103
+ stored.push({ collection, slug, content, meta });
104
+ },
105
+ };
106
+
107
+ const loader = new BrainSeedLoader(tmpDir, { seeds: ['brain-seeds/industry.md'], autoSeed: true });
108
+ const result = await loader.seedBrain(mockBrain);
109
+
110
+ expect(result.imported).toBe(1);
111
+ expect(stored[0].collection).toBe('brain-seeds');
112
+ expect(stored[0].meta.tier).toBe('industry');
113
+ expect(stored[0].meta.source).toBe('brain-seed');
114
+ });
115
+
116
+ it('skips import if already seeded', async () => {
117
+ const seedFile = path.join(tmpDir, 'brain-seeds', 'industry.md');
118
+ fs.writeFileSync(seedFile, `## Topic\n\nContent.\n`);
119
+
120
+ const loader = new BrainSeedLoader(tmpDir, { seeds: ['brain-seeds/industry.md'], autoSeed: true });
121
+ await loader.markSeeded();
122
+
123
+ expect(await loader.isSeeded()).toBe(true);
124
+ // seedBrain would still work if called, but the guard is in agent.ts
125
+ });
126
+
127
+ it('handles missing seed files gracefully', async () => {
128
+ const loader = new BrainSeedLoader(tmpDir, {
129
+ seeds: ['brain-seeds/nonexistent.md'],
130
+ autoSeed: true,
131
+ });
132
+
133
+ const mockBrain = { learn: async () => {} };
134
+ const result = await loader.seedBrain(mockBrain);
135
+ expect(result.imported).toBe(0);
136
+ expect(result.pages).toHaveLength(0);
137
+ });
138
+
139
+ it('infers tier from filename', () => {
140
+ // Write all three seed files
141
+ fs.writeFileSync(path.join(tmpDir, 'brain-seeds', 'industry.md'), '## Ind Topic\n\nContent.\n');
142
+ fs.writeFileSync(path.join(tmpDir, 'brain-seeds', 'job.md'), '## Job Topic\n\nContent.\n');
143
+ fs.writeFileSync(path.join(tmpDir, 'brain-seeds', 'workstation.md'), '## WS Topic\n\nContent.\n');
144
+
145
+ const loader = new BrainSeedLoader(tmpDir, {
146
+ seeds: ['brain-seeds/industry.md', 'brain-seeds/job.md', 'brain-seeds/workstation.md'],
147
+ autoSeed: true,
148
+ });
149
+
150
+ const indPages = loader.parseSeedFile(path.join(tmpDir, 'brain-seeds', 'industry.md'), 'industry');
151
+ const jobPages = loader.parseSeedFile(path.join(tmpDir, 'brain-seeds', 'job.md'), 'job');
152
+ const wsPages = loader.parseSeedFile(path.join(tmpDir, 'brain-seeds', 'workstation.md'), 'workstation');
153
+
154
+ expect(indPages[0].tier).toBe('industry');
155
+ expect(indPages[0].slug).toContain('seed/industry/');
156
+ expect(jobPages[0].tier).toBe('job');
157
+ expect(wsPages[0].tier).toBe('workstation');
158
+ });
159
+
160
+ it('uses custom seedMarkerFile', async () => {
161
+ const loader = new BrainSeedLoader(tmpDir, {
162
+ seeds: [],
163
+ autoSeed: true,
164
+ seedMarkerFile: '.custom-marker',
165
+ });
166
+ await loader.markSeeded();
167
+ expect(fs.existsSync(path.join(tmpDir, '.custom-marker'))).toBe(true);
168
+ expect(await loader.isSeeded()).toBe(true);
169
+ });
170
+ });
171
+
172
+ describe('KnowledgeEvolver', () => {
173
+ it('checkPromotion returns empty when brain is null', async () => {
174
+ const evolver = new KnowledgeEvolver();
175
+ const result = await evolver.checkPromotion(null);
176
+ expect(result.candidates).toHaveLength(0);
177
+ expect(result.promoted).toBe(0);
178
+ });
179
+
180
+ it('promoteToJob calls brain.store with job tier', async () => {
181
+ const stored: any[] = [];
182
+ const mockBrain = {
183
+ store: async (col: string, slug: string, content: string, meta: any) => {
184
+ stored.push({ col, slug, content, meta });
185
+ },
186
+ };
187
+
188
+ const evolver = new KnowledgeEvolver();
189
+ await evolver.promoteToJob(mockBrain, 'Important knowledge', 'seed/job/promoted-topic');
190
+
191
+ expect(stored).toHaveLength(1);
192
+ expect(stored[0].meta.tier).toBe('job');
193
+ expect(stored[0].meta.source).toBe('promotion');
194
+ expect(stored[0].slug).toBe('seed/job/promoted-topic');
195
+ });
196
+
197
+ it('promoteToIndustry calls brain.store with industry tier', async () => {
198
+ const stored: any[] = [];
199
+ const mockBrain = {
200
+ store: async (col: string, slug: string, content: string, meta: any) => {
201
+ stored.push({ col, slug, content, meta });
202
+ },
203
+ };
204
+
205
+ const evolver = new KnowledgeEvolver();
206
+ await evolver.promoteToIndustry(mockBrain, 'Cross-role knowledge', 'seed/industry/common-pattern');
207
+
208
+ expect(stored).toHaveLength(1);
209
+ expect(stored[0].meta.tier).toBe('industry');
210
+ expect(stored[0].meta.source).toBe('promotion');
211
+ });
212
+ });
213
+
214
+ describe('OAD spec.brain.seeds config parsing', () => {
215
+ it('parses brain seed config from YAML', () => {
216
+ const yaml = require('js-yaml');
217
+ const config = yaml.load(`
218
+ apiVersion: opc/v1
219
+ kind: Agent
220
+ metadata:
221
+ name: test
222
+ spec:
223
+ brain:
224
+ seeds:
225
+ - brain-seeds/industry.md
226
+ - brain-seeds/job.md
227
+ - brain-seeds/workstation.md
228
+ autoSeed: true
229
+ evolve:
230
+ enabled: true
231
+ direction: bottom-up
232
+ `) as any;
233
+
234
+ expect(config.spec.brain.seeds).toHaveLength(3);
235
+ expect(config.spec.brain.autoSeed).toBe(true);
236
+ expect(config.spec.brain.evolve.enabled).toBe(true);
237
+ expect(config.spec.brain.evolve.direction).toBe('bottom-up');
238
+ });
239
+ });