opc-agent 2.0.2 → 3.0.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 +603 -545
- package/dist/channels/voice.d.ts +59 -0
- package/dist/channels/voice.js +351 -1
- package/dist/cli.js +284 -5
- package/dist/core/agent.d.ts +9 -0
- package/dist/core/agent.js +49 -0
- package/dist/core/collaboration.d.ts +89 -0
- package/dist/core/collaboration.js +201 -0
- package/dist/deploy/index.d.ts +40 -0
- package/dist/deploy/index.js +261 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +47 -3
- package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
- package/dist/mcp/servers/calculator-mcp.js +65 -0
- package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
- package/dist/mcp/servers/crypto-mcp.js +108 -0
- package/dist/mcp/servers/database-mcp.d.ts +3 -0
- package/dist/mcp/servers/database-mcp.js +73 -0
- package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
- package/dist/mcp/servers/datetime-mcp.js +71 -0
- package/dist/mcp/servers/filesystem.d.ts +3 -0
- package/dist/mcp/servers/filesystem.js +101 -0
- package/dist/mcp/servers/github-mcp.d.ts +3 -0
- package/dist/mcp/servers/github-mcp.js +60 -0
- package/dist/mcp/servers/index.d.ts +21 -0
- package/dist/mcp/servers/index.js +50 -0
- package/dist/mcp/servers/json-mcp.d.ts +3 -0
- package/dist/mcp/servers/json-mcp.js +126 -0
- package/dist/mcp/servers/memory-mcp.d.ts +3 -0
- package/dist/mcp/servers/memory-mcp.js +60 -0
- package/dist/mcp/servers/regex-mcp.d.ts +3 -0
- package/dist/mcp/servers/regex-mcp.js +56 -0
- package/dist/mcp/servers/web-mcp.d.ts +3 -0
- package/dist/mcp/servers/web-mcp.js +51 -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/dist/schema/oad.d.ts +292 -12
- package/dist/schema/oad.js +12 -1
- package/dist/security/guardrails.d.ts +50 -0
- package/dist/security/guardrails.js +197 -0
- package/dist/studio/server.d.ts +31 -1
- package/dist/studio/server.js +154 -3
- package/dist/studio-ui/index.html +1278 -662
- package/dist/tools/integrations/calendar.d.ts +3 -0
- package/dist/tools/integrations/calendar.js +73 -0
- package/dist/tools/integrations/code-exec.d.ts +3 -0
- package/dist/tools/integrations/code-exec.js +42 -0
- package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
- package/dist/tools/integrations/csv-analyzer.js +142 -0
- package/dist/tools/integrations/database.d.ts +3 -0
- package/dist/tools/integrations/database.js +44 -0
- package/dist/tools/integrations/email-send.d.ts +3 -0
- package/dist/tools/integrations/email-send.js +104 -0
- package/dist/tools/integrations/git-tool.d.ts +3 -0
- package/dist/tools/integrations/git-tool.js +49 -0
- package/dist/tools/integrations/github-tool.d.ts +3 -0
- package/dist/tools/integrations/github-tool.js +77 -0
- package/dist/tools/integrations/image-gen.d.ts +3 -0
- package/dist/tools/integrations/image-gen.js +58 -0
- package/dist/tools/integrations/index.d.ts +30 -0
- package/dist/tools/integrations/index.js +107 -0
- package/dist/tools/integrations/jira.d.ts +3 -0
- package/dist/tools/integrations/jira.js +85 -0
- package/dist/tools/integrations/notion.d.ts +3 -0
- package/dist/tools/integrations/notion.js +71 -0
- package/dist/tools/integrations/npm-tool.d.ts +3 -0
- package/dist/tools/integrations/npm-tool.js +49 -0
- package/dist/tools/integrations/pdf-reader.d.ts +3 -0
- package/dist/tools/integrations/pdf-reader.js +91 -0
- package/dist/tools/integrations/slack.d.ts +3 -0
- package/dist/tools/integrations/slack.js +67 -0
- package/dist/tools/integrations/summarizer.d.ts +3 -0
- package/dist/tools/integrations/summarizer.js +49 -0
- package/dist/tools/integrations/translator.d.ts +3 -0
- package/dist/tools/integrations/translator.js +48 -0
- package/dist/tools/integrations/trello.d.ts +3 -0
- package/dist/tools/integrations/trello.js +60 -0
- package/dist/tools/integrations/vector-search.d.ts +3 -0
- package/dist/tools/integrations/vector-search.js +44 -0
- package/dist/tools/integrations/web-scraper.d.ts +3 -0
- package/dist/tools/integrations/web-scraper.js +48 -0
- package/dist/tools/integrations/web-search.d.ts +3 -0
- package/dist/tools/integrations/web-search.js +60 -0
- package/dist/tools/integrations/webhook.d.ts +3 -0
- package/dist/tools/integrations/webhook.js +39 -0
- package/dist/ui/components.d.ts +10 -0
- package/dist/ui/components.js +123 -0
- package/package.json +1 -1
- package/src/channels/voice.ts +365 -0
- package/src/cli.ts +294 -6
- package/src/core/agent.ts +56 -0
- package/src/core/collaboration.ts +275 -0
- package/src/deploy/index.ts +255 -0
- package/src/index.ts +21 -1
- package/src/mcp/servers/calculator-mcp.ts +65 -0
- package/src/mcp/servers/crypto-mcp.ts +73 -0
- package/src/mcp/servers/database-mcp.ts +72 -0
- package/src/mcp/servers/datetime-mcp.ts +69 -0
- package/src/mcp/servers/filesystem.ts +66 -0
- package/src/mcp/servers/github-mcp.ts +58 -0
- package/src/mcp/servers/index.ts +63 -0
- package/src/mcp/servers/json-mcp.ts +102 -0
- package/src/mcp/servers/memory-mcp.ts +56 -0
- package/src/mcp/servers/regex-mcp.ts +53 -0
- package/src/mcp/servers/web-mcp.ts +49 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/seed-loader.ts +212 -0
- package/src/schema/oad.ts +13 -0
- package/src/security/guardrails.ts +248 -0
- package/src/studio/server.ts +166 -4
- package/src/studio-ui/index.html +1278 -662
- package/src/tools/integrations/calendar.ts +73 -0
- package/src/tools/integrations/code-exec.ts +39 -0
- package/src/tools/integrations/csv-analyzer.ts +92 -0
- package/src/tools/integrations/database.ts +44 -0
- package/src/tools/integrations/email-send.ts +76 -0
- package/src/tools/integrations/git-tool.ts +42 -0
- package/src/tools/integrations/github-tool.ts +76 -0
- package/src/tools/integrations/image-gen.ts +56 -0
- package/src/tools/integrations/index.ts +92 -0
- package/src/tools/integrations/jira.ts +83 -0
- package/src/tools/integrations/notion.ts +71 -0
- package/src/tools/integrations/npm-tool.ts +48 -0
- package/src/tools/integrations/pdf-reader.ts +58 -0
- package/src/tools/integrations/slack.ts +65 -0
- package/src/tools/integrations/summarizer.ts +49 -0
- package/src/tools/integrations/translator.ts +48 -0
- package/src/tools/integrations/trello.ts +60 -0
- package/src/tools/integrations/vector-search.ts +42 -0
- package/src/tools/integrations/web-scraper.ts +47 -0
- package/src/tools/integrations/web-search.ts +58 -0
- package/src/tools/integrations/webhook.ts +38 -0
- package/src/ui/components.ts +127 -0
- package/tests/brain-seed-extended.test.ts +490 -0
- package/tests/brain-seed.test.ts +239 -0
- package/tests/collaboration.test.ts +319 -0
- package/tests/deploy-and-dag.test.ts +196 -0
- package/tests/guardrails.test.ts +177 -0
- package/tests/integrations.test.ts +249 -0
- package/tests/mcp-servers.test.ts +260 -0
- package/tests/voice-enhanced.test.ts +169 -0
- package/dist/dtv/data.d.ts +0 -18
- package/dist/dtv/data.js +0 -25
- package/dist/dtv/trust.d.ts +0 -19
- package/dist/dtv/trust.js +0 -40
- package/dist/dtv/value.d.ts +0 -23
- package/dist/dtv/value.js +0 -38
- package/dist/marketplace/index.d.ts +0 -34
- package/dist/marketplace/index.js +0 -202
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DebatePattern, VotingPattern, PipelinePattern, HierarchyPattern, SharedContext, ConversationProtocol } from '../src/core/collaboration';
|
|
3
|
+
import { BaseAgent } from '../src/core/agent';
|
|
4
|
+
|
|
5
|
+
// Mock provider that returns deterministic responses
|
|
6
|
+
function createMockAgent(name: string, responses: string[] | ((prompt: string) => string)): BaseAgent {
|
|
7
|
+
const agent = new BaseAgent({ name, provider: 'echo' });
|
|
8
|
+
let callIndex = 0;
|
|
9
|
+
|
|
10
|
+
// Override handleMessage to return mock responses
|
|
11
|
+
agent.handleMessage = async (message) => {
|
|
12
|
+
let content: string;
|
|
13
|
+
if (typeof responses === 'function') {
|
|
14
|
+
content = responses(message.content);
|
|
15
|
+
} else {
|
|
16
|
+
content = responses[callIndex % responses.length];
|
|
17
|
+
callIndex++;
|
|
18
|
+
}
|
|
19
|
+
return { id: `resp-${Date.now()}`, role: 'assistant', content, timestamp: Date.now() };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return agent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── DebatePattern ──────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe('DebatePattern', () => {
|
|
28
|
+
it('2 agents debate for 2 rounds', async () => {
|
|
29
|
+
const alice = createMockAgent('alice', ['AI is beneficial', 'AI creates jobs']);
|
|
30
|
+
const bob = createMockAgent('bob', ['AI is risky', 'AI displaces workers']);
|
|
31
|
+
const judge = createMockAgent('judge', ['Both sides made good points. AI is nuanced.']);
|
|
32
|
+
|
|
33
|
+
const pattern = new DebatePattern([alice, bob], 2);
|
|
34
|
+
const result = await pattern.debate('Is AI good?', judge);
|
|
35
|
+
|
|
36
|
+
expect(result.topic).toBe('Is AI good?');
|
|
37
|
+
expect(result.rounds.length).toBe(4); // 2 agents × 2 rounds
|
|
38
|
+
expect(result.rounds[0].agent).toBe('alice');
|
|
39
|
+
expect(result.rounds[1].agent).toBe('bob');
|
|
40
|
+
expect(result.summary).toBeTruthy();
|
|
41
|
+
expect(result.judge).toBe('judge');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('debate without explicit judge uses first agent', async () => {
|
|
45
|
+
const a1 = createMockAgent('a1', ['arg1', 'summary']);
|
|
46
|
+
const a2 = createMockAgent('a2', ['arg2']);
|
|
47
|
+
|
|
48
|
+
const pattern = new DebatePattern([a1, a2], 1);
|
|
49
|
+
const result = await pattern.debate('topic');
|
|
50
|
+
|
|
51
|
+
expect(result.judge).toBe('a1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('single round debate', async () => {
|
|
55
|
+
const a = createMockAgent('a', ['yes']);
|
|
56
|
+
const b = createMockAgent('b', ['no']);
|
|
57
|
+
const pattern = new DebatePattern([a, b], 1);
|
|
58
|
+
const result = await pattern.debate('test');
|
|
59
|
+
|
|
60
|
+
expect(result.rounds.length).toBe(2);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── VotingPattern ──────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe('VotingPattern', () => {
|
|
67
|
+
it('3 agents vote, majority wins', async () => {
|
|
68
|
+
const a1 = createMockAgent('v1', ['TypeScript']);
|
|
69
|
+
const a2 = createMockAgent('v2', ['TypeScript']);
|
|
70
|
+
const a3 = createMockAgent('v3', ['Python']);
|
|
71
|
+
|
|
72
|
+
const pattern = new VotingPattern([a1, a2, a3]);
|
|
73
|
+
const result = await pattern.vote('Best language?', ['TypeScript', 'Python', 'Rust']);
|
|
74
|
+
|
|
75
|
+
expect(result.winner).toBe('TypeScript');
|
|
76
|
+
expect(result.tally['TypeScript']).toBe(2);
|
|
77
|
+
expect(result.tally['Python']).toBe(1);
|
|
78
|
+
expect(result.votes.length).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('unanimous vote', async () => {
|
|
82
|
+
const agents = [1, 2, 3].map(i => createMockAgent(`u${i}`, ['Rust']));
|
|
83
|
+
const pattern = new VotingPattern(agents);
|
|
84
|
+
const result = await pattern.vote('Pick one', ['Rust', 'Go']);
|
|
85
|
+
|
|
86
|
+
expect(result.winner).toBe('Rust');
|
|
87
|
+
expect(result.tally['Rust']).toBe(3);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('weighted vote with confidence', async () => {
|
|
91
|
+
const a1 = createMockAgent('w1', ['TypeScript|0.9']);
|
|
92
|
+
const a2 = createMockAgent('w2', ['Python|0.3']);
|
|
93
|
+
const a3 = createMockAgent('w3', ['Python|0.4']);
|
|
94
|
+
|
|
95
|
+
const pattern = new VotingPattern([a1, a2, a3]);
|
|
96
|
+
const result = await pattern.weightedVote('Best?', ['TypeScript', 'Python']);
|
|
97
|
+
|
|
98
|
+
expect(result.winner).toBe('TypeScript'); // 0.9 > 0.3+0.4=0.7
|
|
99
|
+
expect(result.votes[0].confidence).toBeCloseTo(0.9);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('weighted vote Python wins with higher total confidence', async () => {
|
|
103
|
+
const a1 = createMockAgent('w1', ['TypeScript|0.3']);
|
|
104
|
+
const a2 = createMockAgent('w2', ['Python|0.8']);
|
|
105
|
+
const a3 = createMockAgent('w3', ['Python|0.9']);
|
|
106
|
+
|
|
107
|
+
const pattern = new VotingPattern([a1, a2, a3]);
|
|
108
|
+
const result = await pattern.weightedVote('Best?', ['TypeScript', 'Python']);
|
|
109
|
+
|
|
110
|
+
expect(result.winner).toBe('Python');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('vote result includes question', async () => {
|
|
114
|
+
const a = createMockAgent('a', ['X']);
|
|
115
|
+
const pattern = new VotingPattern([a]);
|
|
116
|
+
const result = await pattern.vote('Q?', ['X', 'Y']);
|
|
117
|
+
expect(result.question).toBe('Q?');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─── PipelinePattern ────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe('PipelinePattern', () => {
|
|
124
|
+
it('3-stage pipeline processes sequentially', async () => {
|
|
125
|
+
const s1 = createMockAgent('stage1', (input) => `[s1:${input}]`);
|
|
126
|
+
const s2 = createMockAgent('stage2', (input) => `[s2:${input}]`);
|
|
127
|
+
const s3 = createMockAgent('stage3', (input) => `[s3:${input}]`);
|
|
128
|
+
|
|
129
|
+
const pattern = new PipelinePattern([
|
|
130
|
+
{ agent: s1 },
|
|
131
|
+
{ agent: s2 },
|
|
132
|
+
{ agent: s3 },
|
|
133
|
+
]);
|
|
134
|
+
const result = await pattern.process('hello');
|
|
135
|
+
|
|
136
|
+
expect(result.stages.length).toBe(3);
|
|
137
|
+
expect(result.stages[0].output).toBe('[s1:hello]');
|
|
138
|
+
expect(result.stages[1].input).toBe('[s1:hello]');
|
|
139
|
+
expect(result.finalOutput).toBe('[s3:[s2:[s1:hello]]]');
|
|
140
|
+
expect(result.totalDurationMs).toBeGreaterThanOrEqual(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('pipeline with transform function', async () => {
|
|
144
|
+
const agent = createMockAgent('a', (input) => input.toUpperCase());
|
|
145
|
+
const pattern = new PipelinePattern([
|
|
146
|
+
{ agent, transform: (s) => `prefix:${s}` },
|
|
147
|
+
]);
|
|
148
|
+
const result = await pattern.process('data');
|
|
149
|
+
|
|
150
|
+
expect(result.stages[0].input).toBe('prefix:data');
|
|
151
|
+
expect(result.finalOutput).toBe('PREFIX:DATA');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('single stage pipeline', async () => {
|
|
155
|
+
const agent = createMockAgent('only', ['processed']);
|
|
156
|
+
const pattern = new PipelinePattern([{ agent }]);
|
|
157
|
+
const result = await pattern.process('input');
|
|
158
|
+
|
|
159
|
+
expect(result.stages.length).toBe(1);
|
|
160
|
+
expect(result.finalOutput).toBe('processed');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('pipeline stage records duration', async () => {
|
|
164
|
+
const agent = createMockAgent('slow', ['done']);
|
|
165
|
+
const pattern = new PipelinePattern([{ agent }]);
|
|
166
|
+
const result = await pattern.process('x');
|
|
167
|
+
expect(result.stages[0].durationMs).toBeGreaterThanOrEqual(0);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── HierarchyPattern ───────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe('HierarchyPattern', () => {
|
|
174
|
+
it('leader decomposes, workers execute, leader synthesizes', async () => {
|
|
175
|
+
const leader = createMockAgent('leader', [
|
|
176
|
+
'Research the topic\nWrite the code', // decomposition
|
|
177
|
+
'Combined: research + code done.', // synthesis
|
|
178
|
+
]);
|
|
179
|
+
const w1 = createMockAgent('worker1', ['Research complete']);
|
|
180
|
+
const w2 = createMockAgent('worker2', ['Code written']);
|
|
181
|
+
|
|
182
|
+
const pattern = new HierarchyPattern(leader, [w1, w2]);
|
|
183
|
+
const result = await pattern.execute('Build a feature');
|
|
184
|
+
|
|
185
|
+
expect(result.task).toBe('Build a feature');
|
|
186
|
+
expect(result.subtasks.length).toBe(2);
|
|
187
|
+
expect(result.subtasks[0].agent).toBe('worker1');
|
|
188
|
+
expect(result.subtasks[1].agent).toBe('worker2');
|
|
189
|
+
expect(result.synthesis).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('single worker gets single subtask', async () => {
|
|
193
|
+
const leader = createMockAgent('boss', ['Do the thing', 'All done']);
|
|
194
|
+
const worker = createMockAgent('w', ['Done']);
|
|
195
|
+
|
|
196
|
+
const pattern = new HierarchyPattern(leader, [worker]);
|
|
197
|
+
const result = await pattern.execute('task');
|
|
198
|
+
|
|
199
|
+
expect(result.subtasks.length).toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('workers execute in parallel', async () => {
|
|
203
|
+
const timestamps: number[] = [];
|
|
204
|
+
const leader = createMockAgent('l', ['A\nB\nC', 'Synthesized']);
|
|
205
|
+
const workers = [1, 2, 3].map(i =>
|
|
206
|
+
createMockAgent(`w${i}`, () => { timestamps.push(Date.now()); return `result${i}`; })
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const pattern = new HierarchyPattern(leader, workers);
|
|
210
|
+
await pattern.execute('parallel task');
|
|
211
|
+
|
|
212
|
+
// All workers should start nearly simultaneously (within 50ms)
|
|
213
|
+
if (timestamps.length >= 2) {
|
|
214
|
+
const spread = Math.max(...timestamps) - Math.min(...timestamps);
|
|
215
|
+
expect(spread).toBeLessThan(200);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ─── SharedContext ───────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
describe('SharedContext', () => {
|
|
223
|
+
it('set and get values', () => {
|
|
224
|
+
const ctx = new SharedContext();
|
|
225
|
+
ctx.set('key', 'value');
|
|
226
|
+
expect(ctx.get('key')).toBe('value');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('get returns undefined for missing key', () => {
|
|
230
|
+
const ctx = new SharedContext();
|
|
231
|
+
expect(ctx.get('nope')).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('getAll returns all entries', () => {
|
|
235
|
+
const ctx = new SharedContext();
|
|
236
|
+
ctx.set('a', 1);
|
|
237
|
+
ctx.set('b', 2);
|
|
238
|
+
expect(ctx.getAll()).toEqual({ a: 1, b: 2 });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('onChange fires on set', () => {
|
|
242
|
+
const ctx = new SharedContext();
|
|
243
|
+
const values: any[] = [];
|
|
244
|
+
ctx.onChange('x', (v) => values.push(v));
|
|
245
|
+
ctx.set('x', 10);
|
|
246
|
+
ctx.set('x', 20);
|
|
247
|
+
expect(values).toEqual([10, 20]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('onChange only fires for subscribed key', () => {
|
|
251
|
+
const ctx = new SharedContext();
|
|
252
|
+
const fired: any[] = [];
|
|
253
|
+
ctx.onChange('a', (v) => fired.push(v));
|
|
254
|
+
ctx.set('b', 1);
|
|
255
|
+
expect(fired.length).toBe(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('multiple listeners on same key', () => {
|
|
259
|
+
const ctx = new SharedContext();
|
|
260
|
+
const r1: any[] = [];
|
|
261
|
+
const r2: any[] = [];
|
|
262
|
+
ctx.onChange('k', (v) => r1.push(v));
|
|
263
|
+
ctx.onChange('k', (v) => r2.push(v));
|
|
264
|
+
ctx.set('k', 'hello');
|
|
265
|
+
expect(r1).toEqual(['hello']);
|
|
266
|
+
expect(r2).toEqual(['hello']);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('typed get', () => {
|
|
270
|
+
const ctx = new SharedContext();
|
|
271
|
+
ctx.set('num', 42);
|
|
272
|
+
const val = ctx.get<number>('num');
|
|
273
|
+
expect(val).toBe(42);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ─── ConversationProtocol ───────────────────────────────────
|
|
278
|
+
|
|
279
|
+
describe('ConversationProtocol', () => {
|
|
280
|
+
it('roundRobin produces correct number of messages', async () => {
|
|
281
|
+
const agents = [
|
|
282
|
+
createMockAgent('a', ['hello from a']),
|
|
283
|
+
createMockAgent('b', ['hello from b']),
|
|
284
|
+
];
|
|
285
|
+
const proto = new ConversationProtocol();
|
|
286
|
+
const msgs = await proto.roundRobin(agents, 'test topic', 2);
|
|
287
|
+
|
|
288
|
+
expect(msgs.length).toBe(4); // 2 agents × 2 rounds
|
|
289
|
+
expect((msgs[0] as any).agent).toBe('a');
|
|
290
|
+
expect((msgs[1] as any).agent).toBe('b');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('roundRobin single round', async () => {
|
|
294
|
+
const agents = [createMockAgent('x', ['x says'])];
|
|
295
|
+
const proto = new ConversationProtocol();
|
|
296
|
+
const msgs = await proto.roundRobin(agents, 'topic', 1);
|
|
297
|
+
|
|
298
|
+
expect(msgs.length).toBe(1);
|
|
299
|
+
expect(msgs[0].content).toBe('x says');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('moderated discussion has opening, responses, and summary', async () => {
|
|
303
|
+
const mod = createMockAgent('mod', ['Welcome!', 'Great discussion.']);
|
|
304
|
+
const agents = [
|
|
305
|
+
createMockAgent('p1', ['My opinion is X']),
|
|
306
|
+
createMockAgent('p2', ['I think Y']),
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const proto = new ConversationProtocol();
|
|
310
|
+
const msgs = await proto.moderated(agents, mod, 'AI ethics');
|
|
311
|
+
|
|
312
|
+
// 1 opening + 2 responses + 1 summary = 4
|
|
313
|
+
expect(msgs.length).toBe(4);
|
|
314
|
+
expect((msgs[0] as any).agent).toBe('mod');
|
|
315
|
+
expect((msgs[1] as any).agent).toBe('p1');
|
|
316
|
+
expect((msgs[2] as any).agent).toBe('p2');
|
|
317
|
+
expect((msgs[3] as any).agent).toBe('mod');
|
|
318
|
+
});
|
|
319
|
+
});
|