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.
Files changed (151) hide show
  1. package/README.md +603 -545
  2. package/dist/channels/voice.d.ts +59 -0
  3. package/dist/channels/voice.js +351 -1
  4. package/dist/cli.js +284 -5
  5. package/dist/core/agent.d.ts +9 -0
  6. package/dist/core/agent.js +49 -0
  7. package/dist/core/collaboration.d.ts +89 -0
  8. package/dist/core/collaboration.js +201 -0
  9. package/dist/deploy/index.d.ts +40 -0
  10. package/dist/deploy/index.js +261 -0
  11. package/dist/index.d.ts +7 -1
  12. package/dist/index.js +47 -3
  13. package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
  14. package/dist/mcp/servers/calculator-mcp.js +65 -0
  15. package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
  16. package/dist/mcp/servers/crypto-mcp.js +108 -0
  17. package/dist/mcp/servers/database-mcp.d.ts +3 -0
  18. package/dist/mcp/servers/database-mcp.js +73 -0
  19. package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
  20. package/dist/mcp/servers/datetime-mcp.js +71 -0
  21. package/dist/mcp/servers/filesystem.d.ts +3 -0
  22. package/dist/mcp/servers/filesystem.js +101 -0
  23. package/dist/mcp/servers/github-mcp.d.ts +3 -0
  24. package/dist/mcp/servers/github-mcp.js +60 -0
  25. package/dist/mcp/servers/index.d.ts +21 -0
  26. package/dist/mcp/servers/index.js +50 -0
  27. package/dist/mcp/servers/json-mcp.d.ts +3 -0
  28. package/dist/mcp/servers/json-mcp.js +126 -0
  29. package/dist/mcp/servers/memory-mcp.d.ts +3 -0
  30. package/dist/mcp/servers/memory-mcp.js +60 -0
  31. package/dist/mcp/servers/regex-mcp.d.ts +3 -0
  32. package/dist/mcp/servers/regex-mcp.js +56 -0
  33. package/dist/mcp/servers/web-mcp.d.ts +3 -0
  34. package/dist/mcp/servers/web-mcp.js +51 -0
  35. package/dist/memory/index.d.ts +2 -0
  36. package/dist/memory/index.js +4 -1
  37. package/dist/memory/seed-loader.d.ts +51 -0
  38. package/dist/memory/seed-loader.js +200 -0
  39. package/dist/schema/oad.d.ts +292 -12
  40. package/dist/schema/oad.js +12 -1
  41. package/dist/security/guardrails.d.ts +50 -0
  42. package/dist/security/guardrails.js +197 -0
  43. package/dist/studio/server.d.ts +31 -1
  44. package/dist/studio/server.js +154 -3
  45. package/dist/studio-ui/index.html +1278 -662
  46. package/dist/tools/integrations/calendar.d.ts +3 -0
  47. package/dist/tools/integrations/calendar.js +73 -0
  48. package/dist/tools/integrations/code-exec.d.ts +3 -0
  49. package/dist/tools/integrations/code-exec.js +42 -0
  50. package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
  51. package/dist/tools/integrations/csv-analyzer.js +142 -0
  52. package/dist/tools/integrations/database.d.ts +3 -0
  53. package/dist/tools/integrations/database.js +44 -0
  54. package/dist/tools/integrations/email-send.d.ts +3 -0
  55. package/dist/tools/integrations/email-send.js +104 -0
  56. package/dist/tools/integrations/git-tool.d.ts +3 -0
  57. package/dist/tools/integrations/git-tool.js +49 -0
  58. package/dist/tools/integrations/github-tool.d.ts +3 -0
  59. package/dist/tools/integrations/github-tool.js +77 -0
  60. package/dist/tools/integrations/image-gen.d.ts +3 -0
  61. package/dist/tools/integrations/image-gen.js +58 -0
  62. package/dist/tools/integrations/index.d.ts +30 -0
  63. package/dist/tools/integrations/index.js +107 -0
  64. package/dist/tools/integrations/jira.d.ts +3 -0
  65. package/dist/tools/integrations/jira.js +85 -0
  66. package/dist/tools/integrations/notion.d.ts +3 -0
  67. package/dist/tools/integrations/notion.js +71 -0
  68. package/dist/tools/integrations/npm-tool.d.ts +3 -0
  69. package/dist/tools/integrations/npm-tool.js +49 -0
  70. package/dist/tools/integrations/pdf-reader.d.ts +3 -0
  71. package/dist/tools/integrations/pdf-reader.js +91 -0
  72. package/dist/tools/integrations/slack.d.ts +3 -0
  73. package/dist/tools/integrations/slack.js +67 -0
  74. package/dist/tools/integrations/summarizer.d.ts +3 -0
  75. package/dist/tools/integrations/summarizer.js +49 -0
  76. package/dist/tools/integrations/translator.d.ts +3 -0
  77. package/dist/tools/integrations/translator.js +48 -0
  78. package/dist/tools/integrations/trello.d.ts +3 -0
  79. package/dist/tools/integrations/trello.js +60 -0
  80. package/dist/tools/integrations/vector-search.d.ts +3 -0
  81. package/dist/tools/integrations/vector-search.js +44 -0
  82. package/dist/tools/integrations/web-scraper.d.ts +3 -0
  83. package/dist/tools/integrations/web-scraper.js +48 -0
  84. package/dist/tools/integrations/web-search.d.ts +3 -0
  85. package/dist/tools/integrations/web-search.js +60 -0
  86. package/dist/tools/integrations/webhook.d.ts +3 -0
  87. package/dist/tools/integrations/webhook.js +39 -0
  88. package/dist/ui/components.d.ts +10 -0
  89. package/dist/ui/components.js +123 -0
  90. package/package.json +1 -1
  91. package/src/channels/voice.ts +365 -0
  92. package/src/cli.ts +294 -6
  93. package/src/core/agent.ts +56 -0
  94. package/src/core/collaboration.ts +275 -0
  95. package/src/deploy/index.ts +255 -0
  96. package/src/index.ts +21 -1
  97. package/src/mcp/servers/calculator-mcp.ts +65 -0
  98. package/src/mcp/servers/crypto-mcp.ts +73 -0
  99. package/src/mcp/servers/database-mcp.ts +72 -0
  100. package/src/mcp/servers/datetime-mcp.ts +69 -0
  101. package/src/mcp/servers/filesystem.ts +66 -0
  102. package/src/mcp/servers/github-mcp.ts +58 -0
  103. package/src/mcp/servers/index.ts +63 -0
  104. package/src/mcp/servers/json-mcp.ts +102 -0
  105. package/src/mcp/servers/memory-mcp.ts +56 -0
  106. package/src/mcp/servers/regex-mcp.ts +53 -0
  107. package/src/mcp/servers/web-mcp.ts +49 -0
  108. package/src/memory/index.ts +3 -0
  109. package/src/memory/seed-loader.ts +212 -0
  110. package/src/schema/oad.ts +13 -0
  111. package/src/security/guardrails.ts +248 -0
  112. package/src/studio/server.ts +166 -4
  113. package/src/studio-ui/index.html +1278 -662
  114. package/src/tools/integrations/calendar.ts +73 -0
  115. package/src/tools/integrations/code-exec.ts +39 -0
  116. package/src/tools/integrations/csv-analyzer.ts +92 -0
  117. package/src/tools/integrations/database.ts +44 -0
  118. package/src/tools/integrations/email-send.ts +76 -0
  119. package/src/tools/integrations/git-tool.ts +42 -0
  120. package/src/tools/integrations/github-tool.ts +76 -0
  121. package/src/tools/integrations/image-gen.ts +56 -0
  122. package/src/tools/integrations/index.ts +92 -0
  123. package/src/tools/integrations/jira.ts +83 -0
  124. package/src/tools/integrations/notion.ts +71 -0
  125. package/src/tools/integrations/npm-tool.ts +48 -0
  126. package/src/tools/integrations/pdf-reader.ts +58 -0
  127. package/src/tools/integrations/slack.ts +65 -0
  128. package/src/tools/integrations/summarizer.ts +49 -0
  129. package/src/tools/integrations/translator.ts +48 -0
  130. package/src/tools/integrations/trello.ts +60 -0
  131. package/src/tools/integrations/vector-search.ts +42 -0
  132. package/src/tools/integrations/web-scraper.ts +47 -0
  133. package/src/tools/integrations/web-search.ts +58 -0
  134. package/src/tools/integrations/webhook.ts +38 -0
  135. package/src/ui/components.ts +127 -0
  136. package/tests/brain-seed-extended.test.ts +490 -0
  137. package/tests/brain-seed.test.ts +239 -0
  138. package/tests/collaboration.test.ts +319 -0
  139. package/tests/deploy-and-dag.test.ts +196 -0
  140. package/tests/guardrails.test.ts +177 -0
  141. package/tests/integrations.test.ts +249 -0
  142. package/tests/mcp-servers.test.ts +260 -0
  143. package/tests/voice-enhanced.test.ts +169 -0
  144. package/dist/dtv/data.d.ts +0 -18
  145. package/dist/dtv/data.js +0 -25
  146. package/dist/dtv/trust.d.ts +0 -19
  147. package/dist/dtv/trust.js +0 -40
  148. package/dist/dtv/value.d.ts +0 -23
  149. package/dist/dtv/value.js +0 -38
  150. package/dist/marketplace/index.d.ts +0 -34
  151. 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
+ });