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/README.md +545 -365
- package/dist/cli.js +112 -4
- package/dist/core/agent.d.ts +5 -0
- package/dist/core/agent.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- 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/index.ts +4 -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/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/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';
|
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
|
+
});
|