opc-agent 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.
- package/dist/cli.js +112 -4
- package/dist/core/agent.d.ts +5 -0
- package/dist/core/agent.js +14 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +4 -1
- package/dist/memory/seed-loader.d.ts +51 -0
- package/dist/memory/seed-loader.js +200 -0
- package/package.json +1 -1
- package/src/cli.ts +118 -4
- package/src/core/agent.ts +18 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/seed-loader.ts +212 -0
- package/tests/brain-seed.test.ts +239 -0
package/dist/cli.js
CHANGED
|
@@ -188,9 +188,21 @@ program
|
|
|
188
188
|
fs.mkdirSync(dir, { recursive: true });
|
|
189
189
|
fs.mkdirSync(path.join(dir, 'src', 'skills'), { recursive: true });
|
|
190
190
|
fs.mkdirSync(path.join(dir, 'data'), { recursive: true });
|
|
191
|
+
fs.mkdirSync(path.join(dir, 'brain-seeds'), { recursive: true });
|
|
191
192
|
// Get system prompt content
|
|
192
193
|
const systemPromptContent = roleData.files['system-prompt.md'] || roleData.files['prompts/system.md'] || '';
|
|
193
|
-
//
|
|
194
|
+
// Generate brain-seeds/ files from role data
|
|
195
|
+
const brainSeedContent = roleData.files['brain-seed.md'] || '';
|
|
196
|
+
const industryMatch = brainSeedContent ? brainSeedContent.match(/# Industry Knowledge[\s\S]*?(?=# Job Knowledge|# Workstation Knowledge|$)/i) : null;
|
|
197
|
+
const jobMatch = brainSeedContent ? brainSeedContent.match(/# Job Knowledge[\s\S]*?(?=# Industry Knowledge|# Workstation Knowledge|$)/i) : null;
|
|
198
|
+
const workstationMatch = brainSeedContent ? brainSeedContent.match(/# Workstation Knowledge[\s\S]*?(?=# Industry Knowledge|# Job Knowledge|$)/i) : null;
|
|
199
|
+
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`);
|
|
200
|
+
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`);
|
|
201
|
+
// workstation.md: public workstation knowledge (tools, workflows, best practices)
|
|
202
|
+
// Company-specific knowledge belongs to Desk (closed-source), not here.
|
|
203
|
+
const workstationSeedFromRole = workstationMatch?.[0]?.trim() || '';
|
|
204
|
+
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`);
|
|
205
|
+
// agent.yaml with role system prompt and brain seeds
|
|
194
206
|
const firstLine = systemPromptContent.split('\n').find((l) => l.trim() && !l.startsWith('#'))?.trim() || 'You are a helpful AI assistant.';
|
|
195
207
|
fs.writeFileSync(path.join(dir, 'agent.yaml'), `apiVersion: opc/v1
|
|
196
208
|
kind: Agent
|
|
@@ -212,6 +224,15 @@ spec:
|
|
|
212
224
|
longTerm:
|
|
213
225
|
provider: deepbrain
|
|
214
226
|
database: ./data/brain.db
|
|
227
|
+
brain:
|
|
228
|
+
seeds:
|
|
229
|
+
- brain-seeds/industry.md
|
|
230
|
+
- brain-seeds/job.md
|
|
231
|
+
- brain-seeds/workstation.md
|
|
232
|
+
autoSeed: true
|
|
233
|
+
evolve:
|
|
234
|
+
enabled: true
|
|
235
|
+
direction: bottom-up
|
|
215
236
|
skills: []
|
|
216
237
|
`);
|
|
217
238
|
// SOUL.md from system-prompt.md
|
|
@@ -292,8 +313,12 @@ export class EchoSkill extends BaseSkill {
|
|
|
292
313
|
console.log(` ${icon.file} agent.yaml - Agent definition with role system prompt`);
|
|
293
314
|
console.log(` ${icon.file} SOUL.md - Role personality (${systemPromptContent.split('\n').length} lines)`);
|
|
294
315
|
console.log(` ${icon.file} CONTEXT.md - Role context & documentation`);
|
|
316
|
+
console.log(` ${icon.file} brain-seeds/ - 3-tier brain seed knowledge`);
|
|
317
|
+
console.log(` ${color.dim('├')} industry.md - Industry knowledge`);
|
|
318
|
+
console.log(` ${color.dim('├')} job.md - Job/role knowledge`);
|
|
319
|
+
console.log(` ${color.dim('└')} workstation.md - Workstation knowledge`);
|
|
295
320
|
if (roleData.files['brain-seed.md']) {
|
|
296
|
-
console.log(` ${icon.file} data/brain-seed.md - Role brain seed knowledge`);
|
|
321
|
+
console.log(` ${icon.file} data/brain-seed.md - Role brain seed knowledge (legacy)`);
|
|
297
322
|
}
|
|
298
323
|
console.log(` ${icon.file} src/index.ts - Entry point`);
|
|
299
324
|
console.log(` ${icon.file} package.json - Dependencies`);
|
|
@@ -1403,9 +1428,12 @@ program
|
|
|
1403
1428
|
}
|
|
1404
1429
|
});
|
|
1405
1430
|
// ── Brain command ────────────────────────────────────────────
|
|
1406
|
-
program
|
|
1431
|
+
const brainCmd = program
|
|
1407
1432
|
.command('brain')
|
|
1408
|
-
.description('
|
|
1433
|
+
.description('Manage agent brain (memory, seeds, evolve)');
|
|
1434
|
+
brainCmd
|
|
1435
|
+
.command('status')
|
|
1436
|
+
.description('Show brain stats (pages, tiers, last evolve)')
|
|
1409
1437
|
.option('--url <url>', 'DeepBrain server URL', 'http://localhost:3333')
|
|
1410
1438
|
.action(async (opts) => {
|
|
1411
1439
|
console.log(`\n${icon.gear} ${color.bold('DeepBrain Status')} — ${color.dim(opts.url)}\n`);
|
|
@@ -1438,6 +1466,86 @@ program
|
|
|
1438
1466
|
}
|
|
1439
1467
|
}
|
|
1440
1468
|
});
|
|
1469
|
+
brainCmd
|
|
1470
|
+
.command('seed')
|
|
1471
|
+
.description('Import brain seed files into memory')
|
|
1472
|
+
.option('-f, --file <file>', 'OAD file', 'agent.yaml')
|
|
1473
|
+
.option('--status', 'Check if seeds have been imported')
|
|
1474
|
+
.option('--reset', 'Re-import seeds (clear marker and re-seed)')
|
|
1475
|
+
.action(async (opts) => {
|
|
1476
|
+
const { BrainSeedLoader } = require('./memory/seed-loader');
|
|
1477
|
+
let config = {};
|
|
1478
|
+
try {
|
|
1479
|
+
config = yaml.load(fs.readFileSync(opts.file, 'utf-8'));
|
|
1480
|
+
}
|
|
1481
|
+
catch { /* ignore */ }
|
|
1482
|
+
const brainConfig = config?.spec?.brain;
|
|
1483
|
+
if (!brainConfig?.seeds?.length) {
|
|
1484
|
+
console.log(`${icon.info} No brain seeds configured in ${opts.file}.`);
|
|
1485
|
+
console.log(` Add spec.brain.seeds to your agent.yaml.`);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const loader = new BrainSeedLoader(process.cwd(), {
|
|
1489
|
+
seeds: brainConfig.seeds,
|
|
1490
|
+
autoSeed: brainConfig.autoSeed !== false,
|
|
1491
|
+
});
|
|
1492
|
+
if (opts.status) {
|
|
1493
|
+
const seeded = await loader.isSeeded();
|
|
1494
|
+
console.log(`\n Brain seed status: ${seeded ? color.green('seeded ✔') : color.yellow('not seeded')}`);
|
|
1495
|
+
console.log(` Seeds configured: ${brainConfig.seeds.map((s) => color.cyan(s)).join(', ')}\n`);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (opts.reset) {
|
|
1499
|
+
const markerPath = path.resolve(process.cwd(), '.brain-seeded');
|
|
1500
|
+
if (fs.existsSync(markerPath)) {
|
|
1501
|
+
fs.unlinkSync(markerPath);
|
|
1502
|
+
console.log(` ${icon.success} Cleared seed marker.`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (await loader.isSeeded() && !opts.reset) {
|
|
1506
|
+
console.log(`${icon.info} Brain already seeded. Use --reset to re-import.`);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
console.log(`\n${icon.gear} Importing brain seeds...\n`);
|
|
1510
|
+
// Use a simple mock brain that logs imports (real usage would connect to DeepBrain)
|
|
1511
|
+
const pages = [];
|
|
1512
|
+
const mockBrain = {
|
|
1513
|
+
learn: async (content, meta) => { pages.push(meta?.slug || 'unknown'); },
|
|
1514
|
+
};
|
|
1515
|
+
const result = await loader.seedBrain(mockBrain);
|
|
1516
|
+
await loader.markSeeded();
|
|
1517
|
+
console.log(` ${icon.success} Imported ${color.bold(String(result.imported))} pages from ${brainConfig.seeds.length} seed files.`);
|
|
1518
|
+
for (const p of result.pages) {
|
|
1519
|
+
console.log(` ${color.dim('•')} ${p}`);
|
|
1520
|
+
}
|
|
1521
|
+
console.log();
|
|
1522
|
+
});
|
|
1523
|
+
brainCmd
|
|
1524
|
+
.command('evolve')
|
|
1525
|
+
.description('Trigger manual knowledge evolution cycle')
|
|
1526
|
+
.option('--dry-run', 'Show what would be promoted without doing it')
|
|
1527
|
+
.action(async (opts) => {
|
|
1528
|
+
const { KnowledgeEvolver } = require('./memory/seed-loader');
|
|
1529
|
+
const evolver = new KnowledgeEvolver();
|
|
1530
|
+
console.log(`\n${icon.gear} ${color.bold('Knowledge Evolution')}\n`);
|
|
1531
|
+
console.log(` ${icon.info} Checking for promotion candidates...`);
|
|
1532
|
+
// Would connect to real brain in production
|
|
1533
|
+
const result = await evolver.checkPromotion(null);
|
|
1534
|
+
if (result.candidates.length === 0) {
|
|
1535
|
+
console.log(` ${icon.info} No knowledge ready for promotion yet.\n`);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
for (const c of result.candidates) {
|
|
1539
|
+
console.log(` ${color.cyan(c.slug)} → ${c.fromTier} → ${c.toTier} (confidence: ${(c.confidence * 100).toFixed(0)}%)`);
|
|
1540
|
+
}
|
|
1541
|
+
if (opts.dryRun) {
|
|
1542
|
+
console.log(`\n ${icon.info} Dry run — no changes made.\n`);
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
console.log(`\n ${icon.success} Promoted ${result.promoted} knowledge entries.\n`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1441
1549
|
// ── Logs command ─────────────────────────────────────────────
|
|
1442
1550
|
program
|
|
1443
1551
|
.command('logs')
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { MCPTool } from '../tools/mcp';
|
|
|
6
6
|
import { MCPToolRegistry } from '../tools/mcp';
|
|
7
7
|
import { type SubAgentConfig, type SubAgentResult } from './subagent';
|
|
8
8
|
import { Tracer } from '../telemetry';
|
|
9
|
+
import { type BrainSeedConfig } from '../memory/seed-loader';
|
|
9
10
|
export declare class BaseAgent extends EventEmitter implements IAgent {
|
|
10
11
|
readonly name: string;
|
|
11
12
|
private _state;
|
|
@@ -23,6 +24,8 @@ export declare class BaseAgent extends EventEmitter implements IAgent {
|
|
|
23
24
|
private longTermMemory?;
|
|
24
25
|
private longTermMemoryConfig;
|
|
25
26
|
private tracer?;
|
|
27
|
+
private brainSeedConfig?;
|
|
28
|
+
private agentDir;
|
|
26
29
|
constructor(options: {
|
|
27
30
|
name: string;
|
|
28
31
|
systemPrompt?: string;
|
|
@@ -38,6 +41,8 @@ export declare class BaseAgent extends EventEmitter implements IAgent {
|
|
|
38
41
|
};
|
|
39
42
|
maxToolRounds?: number;
|
|
40
43
|
tracer?: Tracer;
|
|
44
|
+
agentDir?: string;
|
|
45
|
+
brainSeedConfig?: BrainSeedConfig;
|
|
41
46
|
});
|
|
42
47
|
setLongTermMemory(brain: any, config?: {
|
|
43
48
|
autoLearn?: boolean;
|
package/dist/core/agent.js
CHANGED
|
@@ -7,6 +7,7 @@ const providers_1 = require("../providers");
|
|
|
7
7
|
const auto_learn_1 = require("../skills/auto-learn");
|
|
8
8
|
const mcp_1 = require("../tools/mcp");
|
|
9
9
|
const subagent_1 = require("./subagent");
|
|
10
|
+
const seed_loader_1 = require("../memory/seed-loader");
|
|
10
11
|
class BaseAgent extends events_1.EventEmitter {
|
|
11
12
|
name;
|
|
12
13
|
_state = 'init';
|
|
@@ -24,6 +25,8 @@ class BaseAgent extends events_1.EventEmitter {
|
|
|
24
25
|
longTermMemory;
|
|
25
26
|
longTermMemoryConfig = { autoLearn: true, autoRecall: true };
|
|
26
27
|
tracer;
|
|
28
|
+
brainSeedConfig;
|
|
29
|
+
agentDir;
|
|
27
30
|
constructor(options) {
|
|
28
31
|
super();
|
|
29
32
|
this.name = options.name;
|
|
@@ -41,6 +44,8 @@ class BaseAgent extends events_1.EventEmitter {
|
|
|
41
44
|
this.skillLearner = new auto_learn_1.SkillLearner(options.skillsDir);
|
|
42
45
|
}
|
|
43
46
|
this.tracer = options.tracer;
|
|
47
|
+
this.agentDir = options.agentDir ?? process.cwd();
|
|
48
|
+
this.brainSeedConfig = options.brainSeedConfig;
|
|
44
49
|
}
|
|
45
50
|
setLongTermMemory(brain, config) {
|
|
46
51
|
this.longTermMemory = brain;
|
|
@@ -93,6 +98,15 @@ class BaseAgent extends events_1.EventEmitter {
|
|
|
93
98
|
if (this.skillLearner) {
|
|
94
99
|
await this.skillLearner.loadLearnedSkills();
|
|
95
100
|
}
|
|
101
|
+
// Auto-seed brain if configured
|
|
102
|
+
if (this.brainSeedConfig?.autoSeed && this.longTermMemory) {
|
|
103
|
+
const loader = new seed_loader_1.BrainSeedLoader(this.agentDir, this.brainSeedConfig);
|
|
104
|
+
if (!await loader.isSeeded()) {
|
|
105
|
+
const result = await loader.seedBrain(this.longTermMemory);
|
|
106
|
+
this.emit('brain:seeded', result);
|
|
107
|
+
await loader.markSeeded();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
96
110
|
this.transition('ready');
|
|
97
111
|
}
|
|
98
112
|
async start() {
|
package/dist/memory/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Message, MemoryStore } from '../core/types';
|
|
2
|
+
export { BrainSeedLoader, KnowledgeEvolver } from './seed-loader';
|
|
3
|
+
export type { BrainSeedConfig, SeedPage, SeedResult, PromotionResult, PromotionCandidate } from './seed-loader';
|
|
2
4
|
export declare class InMemoryStore implements MemoryStore {
|
|
3
5
|
private store;
|
|
4
6
|
private conversations;
|
package/dist/memory/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InMemoryStore = void 0;
|
|
3
|
+
exports.InMemoryStore = exports.KnowledgeEvolver = exports.BrainSeedLoader = void 0;
|
|
4
|
+
var seed_loader_1 = require("./seed-loader");
|
|
5
|
+
Object.defineProperty(exports, "BrainSeedLoader", { enumerable: true, get: function () { return seed_loader_1.BrainSeedLoader; } });
|
|
6
|
+
Object.defineProperty(exports, "KnowledgeEvolver", { enumerable: true, get: function () { return seed_loader_1.KnowledgeEvolver; } });
|
|
4
7
|
class InMemoryStore {
|
|
5
8
|
store = new Map();
|
|
6
9
|
conversations = new Map();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface BrainSeedConfig {
|
|
2
|
+
seeds: string[];
|
|
3
|
+
autoSeed: boolean;
|
|
4
|
+
seedMarkerFile?: string;
|
|
5
|
+
evolve?: {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
direction: 'bottom-up' | 'top-down';
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface SeedPage {
|
|
11
|
+
slug: string;
|
|
12
|
+
content: string;
|
|
13
|
+
tier: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SeedResult {
|
|
16
|
+
imported: number;
|
|
17
|
+
pages: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface PromotionCandidate {
|
|
20
|
+
slug: string;
|
|
21
|
+
content: string;
|
|
22
|
+
fromTier: string;
|
|
23
|
+
toTier: string;
|
|
24
|
+
confidence: number;
|
|
25
|
+
}
|
|
26
|
+
export interface PromotionResult {
|
|
27
|
+
candidates: PromotionCandidate[];
|
|
28
|
+
promoted: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class BrainSeedLoader {
|
|
31
|
+
private agentDir;
|
|
32
|
+
private config;
|
|
33
|
+
private markerFile;
|
|
34
|
+
constructor(agentDir: string, config: BrainSeedConfig);
|
|
35
|
+
isSeeded(): Promise<boolean>;
|
|
36
|
+
seedBrain(brain: any): Promise<SeedResult>;
|
|
37
|
+
markSeeded(): Promise<void>;
|
|
38
|
+
parseSeedFile(filePath: string, tier: string): SeedPage[];
|
|
39
|
+
private inferTier;
|
|
40
|
+
private slugify;
|
|
41
|
+
}
|
|
42
|
+
export declare class KnowledgeEvolver {
|
|
43
|
+
private tierOrder;
|
|
44
|
+
checkPromotion(brain: any, options?: {
|
|
45
|
+
minInteractions?: number;
|
|
46
|
+
confidenceThreshold?: number;
|
|
47
|
+
}): Promise<PromotionResult>;
|
|
48
|
+
promoteToJob(brain: any, knowledge: string, jobSlug: string): Promise<void>;
|
|
49
|
+
promoteToIndustry(brain: any, knowledge: string, industrySlug: string): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=seed-loader.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.KnowledgeEvolver = exports.BrainSeedLoader = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class BrainSeedLoader {
|
|
40
|
+
agentDir;
|
|
41
|
+
config;
|
|
42
|
+
markerFile;
|
|
43
|
+
constructor(agentDir, config) {
|
|
44
|
+
this.agentDir = agentDir;
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.markerFile = config.seedMarkerFile
|
|
47
|
+
? path.resolve(agentDir, config.seedMarkerFile)
|
|
48
|
+
: path.resolve(agentDir, '.brain-seeded');
|
|
49
|
+
}
|
|
50
|
+
async isSeeded() {
|
|
51
|
+
return fs.existsSync(this.markerFile);
|
|
52
|
+
}
|
|
53
|
+
async seedBrain(brain) {
|
|
54
|
+
const allPages = [];
|
|
55
|
+
for (const seedPath of this.config.seeds) {
|
|
56
|
+
const fullPath = path.resolve(this.agentDir, seedPath);
|
|
57
|
+
if (!fs.existsSync(fullPath))
|
|
58
|
+
continue;
|
|
59
|
+
const tier = this.inferTier(seedPath);
|
|
60
|
+
const pages = this.parseSeedFile(fullPath, tier);
|
|
61
|
+
allPages.push(...pages);
|
|
62
|
+
}
|
|
63
|
+
const importedSlugs = [];
|
|
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
|
+
}
|
|
71
|
+
else if (brain && typeof brain.learn === 'function') {
|
|
72
|
+
await brain.learn(page.content, {
|
|
73
|
+
tags: ['brain-seed', page.tier],
|
|
74
|
+
slug: page.slug,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
importedSlugs.push(page.slug);
|
|
78
|
+
}
|
|
79
|
+
return { imported: importedSlugs.length, pages: importedSlugs };
|
|
80
|
+
}
|
|
81
|
+
async markSeeded() {
|
|
82
|
+
const dir = path.dirname(this.markerFile);
|
|
83
|
+
if (!fs.existsSync(dir))
|
|
84
|
+
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
|
+
parseSeedFile(filePath, tier) {
|
|
91
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
92
|
+
const pages = [];
|
|
93
|
+
const sections = content.split(/^## /m);
|
|
94
|
+
for (const section of sections) {
|
|
95
|
+
const trimmed = section.trim();
|
|
96
|
+
if (!trimmed)
|
|
97
|
+
continue;
|
|
98
|
+
const newlineIdx = trimmed.indexOf('\n');
|
|
99
|
+
if (newlineIdx === -1 && sections.indexOf(section) === 0 && !content.trimStart().startsWith('## ')) {
|
|
100
|
+
// This is preamble before any ## heading — skip or treat as intro
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
let title;
|
|
104
|
+
let body;
|
|
105
|
+
if (sections.indexOf(section) === 0 && !content.trimStart().startsWith('## ')) {
|
|
106
|
+
// Preamble (before first ##)
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (newlineIdx === -1) {
|
|
110
|
+
title = trimmed;
|
|
111
|
+
body = '';
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
title = trimmed.slice(0, newlineIdx).trim();
|
|
115
|
+
body = trimmed.slice(newlineIdx + 1).trim();
|
|
116
|
+
}
|
|
117
|
+
const slug = `seed/${tier}/${this.slugify(title)}`;
|
|
118
|
+
pages.push({ slug, content: `## ${title}\n\n${body}`, tier });
|
|
119
|
+
}
|
|
120
|
+
return pages;
|
|
121
|
+
}
|
|
122
|
+
inferTier(seedPath) {
|
|
123
|
+
const basename = path.basename(seedPath, path.extname(seedPath)).toLowerCase();
|
|
124
|
+
if (basename.includes('industry'))
|
|
125
|
+
return 'industry';
|
|
126
|
+
if (basename.includes('job'))
|
|
127
|
+
return 'job';
|
|
128
|
+
if (basename.includes('workstation'))
|
|
129
|
+
return 'workstation';
|
|
130
|
+
return 'workstation';
|
|
131
|
+
}
|
|
132
|
+
slugify(text) {
|
|
133
|
+
return text
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
|
136
|
+
.replace(/^-|-$/g, '');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.BrainSeedLoader = BrainSeedLoader;
|
|
140
|
+
class KnowledgeEvolver {
|
|
141
|
+
tierOrder = ['workstation', 'job', 'industry'];
|
|
142
|
+
async checkPromotion(brain, options = {}) {
|
|
143
|
+
const minInteractions = options.minInteractions ?? 50;
|
|
144
|
+
const confidenceThreshold = options.confidenceThreshold ?? 0.8;
|
|
145
|
+
const result = { candidates: [], promoted: 0 };
|
|
146
|
+
// Search for frequently referenced seed knowledge
|
|
147
|
+
if (!brain || typeof brain.search !== 'function')
|
|
148
|
+
return result;
|
|
149
|
+
try {
|
|
150
|
+
const seedPages = await brain.search('brain-seeds', 'seed/', 100);
|
|
151
|
+
if (!Array.isArray(seedPages))
|
|
152
|
+
return result;
|
|
153
|
+
for (const page of seedPages) {
|
|
154
|
+
const meta = page.metadata || {};
|
|
155
|
+
const usageCount = meta.usageCount ?? 0;
|
|
156
|
+
const tier = meta.tier || 'workstation';
|
|
157
|
+
if (usageCount < minInteractions)
|
|
158
|
+
continue;
|
|
159
|
+
const confidence = Math.min(usageCount / (minInteractions * 2), 1.0);
|
|
160
|
+
if (confidence < confidenceThreshold)
|
|
161
|
+
continue;
|
|
162
|
+
const tierIdx = this.tierOrder.indexOf(tier);
|
|
163
|
+
if (tierIdx <= 0)
|
|
164
|
+
continue; // already at highest tier or unknown
|
|
165
|
+
const toTier = this.tierOrder[tierIdx - 1];
|
|
166
|
+
result.candidates.push({
|
|
167
|
+
slug: page.id || page.slug,
|
|
168
|
+
content: page.content,
|
|
169
|
+
fromTier: tier,
|
|
170
|
+
toTier,
|
|
171
|
+
confidence,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Silent fail
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
async promoteToJob(brain, knowledge, jobSlug) {
|
|
181
|
+
if (brain && typeof brain.store === 'function') {
|
|
182
|
+
await brain.store('brain-seeds', jobSlug, knowledge, {
|
|
183
|
+
tier: 'job',
|
|
184
|
+
source: 'promotion',
|
|
185
|
+
promotedAt: new Date().toISOString(),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async promoteToIndustry(brain, knowledge, industrySlug) {
|
|
190
|
+
if (brain && typeof brain.store === 'function') {
|
|
191
|
+
await brain.store('brain-seeds', industrySlug, knowledge, {
|
|
192
|
+
tier: 'industry',
|
|
193
|
+
source: 'promotion',
|
|
194
|
+
promotedAt: new Date().toISOString(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.KnowledgeEvolver = KnowledgeEvolver;
|
|
200
|
+
//# sourceMappingURL=seed-loader.js.map
|
package/package.json
CHANGED
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
|
-
//
|
|
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('
|
|
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/memory/index.ts
CHANGED
|
@@ -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
|
+
});
|