pmp-gywd 3.4.0 → 4.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.
@@ -0,0 +1,449 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Orchestrator
5
+ *
6
+ * Manages multiple agents, handles context sharing, and aggregates results.
7
+ * Part of Phase 29: Agent Runtime.
8
+ */
9
+
10
+ const { EventEmitter } = require('events');
11
+ const { AGENT_STATE, AGENT_PRIORITY } = require('./base-agent');
12
+
13
+ /**
14
+ * Orchestration strategies
15
+ */
16
+ const ORCHESTRATION_STRATEGY = {
17
+ SEQUENTIAL: 'sequential', // Run agents one after another
18
+ PARALLEL: 'parallel', // Run all agents simultaneously
19
+ PRIORITY: 'priority', // Run by priority, highest first
20
+ PIPELINE: 'pipeline', // Each agent's output feeds next agent's input
21
+ };
22
+
23
+ /**
24
+ * Result aggregation strategies
25
+ */
26
+ const AGGREGATION_STRATEGY = {
27
+ MERGE: 'merge', // Merge all results into one object
28
+ ARRAY: 'array', // Collect results as array
29
+ VOTE: 'vote', // Majority voting on common fields
30
+ WEIGHTED: 'weighted', // Weight results by agent priority
31
+ };
32
+
33
+ /**
34
+ * Agent Orchestrator class
35
+ */
36
+ class AgentOrchestrator extends EventEmitter {
37
+ constructor(options = {}) {
38
+ super();
39
+
40
+ this.agents = new Map();
41
+ this.strategy = options.strategy || ORCHESTRATION_STRATEGY.PARALLEL;
42
+ this.aggregation = options.aggregation || AGGREGATION_STRATEGY.MERGE;
43
+ this.timeout = options.timeout || 60000;
44
+ this.maxConcurrent = options.maxConcurrent || 5;
45
+
46
+ this.results = new Map();
47
+ this.sharedContext = {};
48
+ this.isRunning = false;
49
+ }
50
+
51
+ /**
52
+ * Register an agent with the orchestrator
53
+ * @param {BaseAgent} agent - Agent to register
54
+ * @returns {string} Agent ID
55
+ */
56
+ register(agent) {
57
+ if (this.agents.has(agent.id)) {
58
+ throw new Error(`Agent ${agent.id} already registered`);
59
+ }
60
+
61
+ this.agents.set(agent.id, agent);
62
+
63
+ // Set up event forwarding
64
+ agent.on('spawned', (data) => this.emit('agentSpawned', data));
65
+ agent.on('started', (data) => this.emit('agentStarted', data));
66
+ agent.on('completed', (data) => this.emit('agentCompleted', data));
67
+ agent.on('error', (data) => this.emit('agentError', data));
68
+
69
+ this.emit('agentRegistered', { agentId: agent.id, name: agent.name });
70
+ return agent.id;
71
+ }
72
+
73
+ /**
74
+ * Unregister an agent
75
+ * @param {string} agentId - Agent ID to unregister
76
+ */
77
+ unregister(agentId) {
78
+ const agent = this.agents.get(agentId);
79
+ if (agent) {
80
+ agent.removeAllListeners();
81
+ this.agents.delete(agentId);
82
+ this.emit('agentUnregistered', { agentId });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Set shared context for all agents
88
+ * @param {object} context - Context to share
89
+ */
90
+ setSharedContext(context) {
91
+ this.sharedContext = { ...this.sharedContext, ...context };
92
+ this.emit('contextUpdated', { keys: Object.keys(context) });
93
+ }
94
+
95
+ /**
96
+ * Run all registered agents
97
+ * @param {object} input - Input to pass to all agents
98
+ * @returns {Promise<object>}
99
+ */
100
+ async runAll(input = {}) {
101
+ if (this.isRunning) {
102
+ throw new Error('Orchestrator is already running');
103
+ }
104
+
105
+ this.isRunning = true;
106
+ this.results.clear();
107
+
108
+ const combinedInput = { ...this.sharedContext, ...input };
109
+
110
+ this.emit('started', {
111
+ agentCount: this.agents.size,
112
+ strategy: this.strategy,
113
+ });
114
+
115
+ try {
116
+ let results;
117
+
118
+ switch (this.strategy) {
119
+ case ORCHESTRATION_STRATEGY.SEQUENTIAL:
120
+ results = await this._runSequential(combinedInput);
121
+ break;
122
+ case ORCHESTRATION_STRATEGY.PARALLEL:
123
+ results = await this._runParallel(combinedInput);
124
+ break;
125
+ case ORCHESTRATION_STRATEGY.PRIORITY:
126
+ results = await this._runByPriority(combinedInput);
127
+ break;
128
+ case ORCHESTRATION_STRATEGY.PIPELINE:
129
+ results = await this._runPipeline(combinedInput);
130
+ break;
131
+ default:
132
+ results = await this._runParallel(combinedInput);
133
+ }
134
+
135
+ const aggregated = this._aggregateResults(results);
136
+
137
+ this.emit('completed', {
138
+ agentCount: this.agents.size,
139
+ successCount: results.filter(r => r.success).length,
140
+ failureCount: results.filter(r => !r.success).length,
141
+ });
142
+
143
+ return aggregated;
144
+ } finally {
145
+ this.isRunning = false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Run agents sequentially
151
+ * @param {object} input
152
+ * @returns {Promise<Array>}
153
+ */
154
+ async _runSequential(input) {
155
+ const results = [];
156
+
157
+ for (const [agentId, agent] of this.agents) {
158
+ try {
159
+ const result = await agent.run(input);
160
+ results.push({ agentId, success: true, result });
161
+ this.results.set(agentId, result);
162
+ } catch (error) {
163
+ results.push({ agentId, success: false, error: error.message });
164
+ }
165
+ }
166
+
167
+ return results;
168
+ }
169
+
170
+ /**
171
+ * Run agents in parallel
172
+ * @param {object} input
173
+ * @returns {Promise<Array>}
174
+ */
175
+ async _runParallel(input) {
176
+ const agentArray = Array.from(this.agents.entries());
177
+ const results = [];
178
+
179
+ // Run in batches to respect maxConcurrent
180
+ for (let i = 0; i < agentArray.length; i += this.maxConcurrent) {
181
+ const batch = agentArray.slice(i, i + this.maxConcurrent);
182
+
183
+ const batchResults = await Promise.allSettled(
184
+ batch.map(async ([agentId, agent]) => {
185
+ const result = await agent.run(input);
186
+ this.results.set(agentId, result);
187
+ return { agentId, result };
188
+ })
189
+ );
190
+
191
+ for (const settledResult of batchResults) {
192
+ if (settledResult.status === 'fulfilled') {
193
+ results.push({
194
+ agentId: settledResult.value.agentId,
195
+ success: true,
196
+ result: settledResult.value.result,
197
+ });
198
+ } else {
199
+ results.push({
200
+ agentId: 'unknown',
201
+ success: false,
202
+ error: settledResult.reason.message,
203
+ });
204
+ }
205
+ }
206
+ }
207
+
208
+ return results;
209
+ }
210
+
211
+ /**
212
+ * Run agents by priority (highest first)
213
+ * @param {object} input
214
+ * @returns {Promise<Array>}
215
+ */
216
+ async _runByPriority(input) {
217
+ const sortedAgents = Array.from(this.agents.entries())
218
+ .sort(([, a], [, b]) => a.priority - b.priority);
219
+
220
+ const results = [];
221
+
222
+ for (const [agentId, agent] of sortedAgents) {
223
+ try {
224
+ const result = await agent.run(input);
225
+ results.push({ agentId, success: true, result, priority: agent.priority });
226
+ this.results.set(agentId, result);
227
+ } catch (error) {
228
+ results.push({ agentId, success: false, error: error.message, priority: agent.priority });
229
+ }
230
+ }
231
+
232
+ return results;
233
+ }
234
+
235
+ /**
236
+ * Run agents in pipeline (output → input)
237
+ * @param {object} input
238
+ * @returns {Promise<Array>}
239
+ */
240
+ async _runPipeline(input) {
241
+ const results = [];
242
+ let currentInput = { ...input };
243
+
244
+ for (const [agentId, agent] of this.agents) {
245
+ try {
246
+ const result = await agent.run(currentInput);
247
+ results.push({ agentId, success: true, result });
248
+ this.results.set(agentId, result);
249
+
250
+ // Pass result as input to next agent
251
+ currentInput = { ...currentInput, previousResult: result };
252
+ } catch (error) {
253
+ results.push({ agentId, success: false, error: error.message });
254
+ // Stop pipeline on failure
255
+ break;
256
+ }
257
+ }
258
+
259
+ return results;
260
+ }
261
+
262
+ /**
263
+ * Aggregate results based on strategy
264
+ * @param {Array} results
265
+ * @returns {object}
266
+ */
267
+ _aggregateResults(results) {
268
+ const successfulResults = results.filter(r => r.success);
269
+
270
+ switch (this.aggregation) {
271
+ case AGGREGATION_STRATEGY.MERGE:
272
+ return this._mergeResults(successfulResults);
273
+ case AGGREGATION_STRATEGY.ARRAY:
274
+ return this._arrayResults(successfulResults);
275
+ case AGGREGATION_STRATEGY.VOTE:
276
+ return this._voteResults(successfulResults);
277
+ case AGGREGATION_STRATEGY.WEIGHTED:
278
+ return this._weightedResults(successfulResults);
279
+ default:
280
+ return this._mergeResults(successfulResults);
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Merge all results into single object
286
+ * @param {Array} results
287
+ * @returns {object}
288
+ */
289
+ _mergeResults(results) {
290
+ const merged = {
291
+ _meta: {
292
+ agentCount: results.length,
293
+ strategy: this.aggregation,
294
+ timestamp: new Date().toISOString(),
295
+ },
296
+ };
297
+
298
+ for (const { agentId, result } of results) {
299
+ const agent = this.agents.get(agentId);
300
+ const key = agent ? agent.type : agentId;
301
+ merged[key] = result;
302
+ }
303
+
304
+ return merged;
305
+ }
306
+
307
+ /**
308
+ * Collect results as array
309
+ * @param {Array} results
310
+ * @returns {object}
311
+ */
312
+ _arrayResults(results) {
313
+ return {
314
+ _meta: {
315
+ agentCount: results.length,
316
+ strategy: this.aggregation,
317
+ timestamp: new Date().toISOString(),
318
+ },
319
+ results: results.map(({ agentId, result }) => {
320
+ const agent = this.agents.get(agentId);
321
+ return {
322
+ agent: agent ? agent.name : agentId,
323
+ type: agent ? agent.type : 'unknown',
324
+ result,
325
+ };
326
+ }),
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Majority voting on common fields
332
+ * @param {Array} results
333
+ * @returns {object}
334
+ */
335
+ _voteResults(results) {
336
+ const votes = {};
337
+ const finalResult = {};
338
+
339
+ // Collect votes for each field
340
+ for (const { result } of results) {
341
+ if (typeof result === 'object' && result !== null) {
342
+ for (const [key, value] of Object.entries(result)) {
343
+ if (!votes[key]) votes[key] = new Map();
344
+
345
+ const valueKey = JSON.stringify(value);
346
+ const currentCount = votes[key].get(valueKey) || 0;
347
+ votes[key].set(valueKey, currentCount + 1);
348
+ }
349
+ }
350
+ }
351
+
352
+ // Select majority for each field
353
+ for (const [key, valueMap] of Object.entries(votes)) {
354
+ let maxCount = 0;
355
+ let winner = null;
356
+
357
+ for (const [valueKey, count] of valueMap.entries()) {
358
+ if (count > maxCount) {
359
+ maxCount = count;
360
+ winner = JSON.parse(valueKey);
361
+ }
362
+ }
363
+
364
+ finalResult[key] = winner;
365
+ }
366
+
367
+ return {
368
+ _meta: {
369
+ agentCount: results.length,
370
+ strategy: this.aggregation,
371
+ timestamp: new Date().toISOString(),
372
+ },
373
+ ...finalResult,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Weight results by agent priority
379
+ * @param {Array} results
380
+ * @returns {object}
381
+ */
382
+ _weightedResults(results) {
383
+ const weighted = {
384
+ _meta: {
385
+ agentCount: results.length,
386
+ strategy: this.aggregation,
387
+ timestamp: new Date().toISOString(),
388
+ },
389
+ byPriority: {},
390
+ };
391
+
392
+ // Group by priority
393
+ for (const { agentId, result } of results) {
394
+ const agent = this.agents.get(agentId);
395
+ const priority = agent ? agent.priority : AGENT_PRIORITY.NORMAL;
396
+ const priorityKey = `priority_${priority}`;
397
+
398
+ if (!weighted.byPriority[priorityKey]) {
399
+ weighted.byPriority[priorityKey] = [];
400
+ }
401
+
402
+ weighted.byPriority[priorityKey].push({
403
+ agent: agent ? agent.name : agentId,
404
+ result,
405
+ });
406
+ }
407
+
408
+ return weighted;
409
+ }
410
+
411
+ /**
412
+ * Get status of all agents
413
+ * @returns {Array}
414
+ */
415
+ getStatus() {
416
+ return Array.from(this.agents.values()).map(agent => agent.getStatus());
417
+ }
418
+
419
+ /**
420
+ * Cancel all running agents
421
+ * @param {string} reason
422
+ */
423
+ cancelAll(reason = 'Orchestrator cancelled') {
424
+ for (const agent of this.agents.values()) {
425
+ if (agent.state === AGENT_STATE.RUNNING || agent.state === AGENT_STATE.SPAWNING) {
426
+ agent.cancel(reason);
427
+ }
428
+ }
429
+
430
+ this.isRunning = false;
431
+ this.emit('cancelled', { reason });
432
+ }
433
+
434
+ /**
435
+ * Clear all agents and results
436
+ */
437
+ clear() {
438
+ this.cancelAll('Clearing orchestrator');
439
+ this.agents.clear();
440
+ this.results.clear();
441
+ this.sharedContext = {};
442
+ }
443
+ }
444
+
445
+ module.exports = {
446
+ AgentOrchestrator,
447
+ ORCHESTRATION_STRATEGY,
448
+ AGGREGATION_STRATEGY,
449
+ };