@defai.digital/agent-parallel 13.4.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,238 @@
1
+ /**
2
+ * Result Aggregator
3
+ *
4
+ * Combines results from parallel agent execution based on configured strategy.
5
+ *
6
+ * Invariants:
7
+ * - INV-APE-004: Result aggregation follows configured strategy
8
+ */
9
+
10
+ import type { AgentParallelTaskResult } from '@defai.digital/contracts';
11
+ import type {
12
+ ResultAggregator,
13
+ ResultAggregatorOptions,
14
+ CustomAggregator,
15
+ AggregationStrategy,
16
+ } from './types.js';
17
+
18
+ /**
19
+ * Deep merge two objects
20
+ * Later values override earlier values
21
+ */
22
+ function deepMerge(
23
+ target: Record<string, unknown>,
24
+ source: Record<string, unknown>
25
+ ): Record<string, unknown> {
26
+ const result = { ...target };
27
+
28
+ for (const [key, value] of Object.entries(source)) {
29
+ if (
30
+ value &&
31
+ typeof value === 'object' &&
32
+ !Array.isArray(value) &&
33
+ result[key] &&
34
+ typeof result[key] === 'object' &&
35
+ !Array.isArray(result[key])
36
+ ) {
37
+ // Recursively merge objects
38
+ result[key] = deepMerge(
39
+ result[key] as Record<string, unknown>,
40
+ value as Record<string, unknown>
41
+ );
42
+ } else {
43
+ // Override with new value
44
+ result[key] = value;
45
+ }
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Merge strategy: Combine all outputs into single object
53
+ * INV-APE-004: Later tasks override earlier for same keys
54
+ */
55
+ function mergeResults(results: AgentParallelTaskResult[]): unknown {
56
+ const merged: Record<string, unknown> = {};
57
+
58
+ // Sort by layer then completion time to ensure deterministic merge order
59
+ const sortedResults = [...results].sort((a, b) => {
60
+ // First by layer
61
+ const layerDiff = (a.layer ?? 0) - (b.layer ?? 0);
62
+ if (layerDiff !== 0) return layerDiff;
63
+
64
+ // Then by completion time
65
+ const aTime = a.completedAt ? new Date(a.completedAt).getTime() : 0;
66
+ const bTime = b.completedAt ? new Date(b.completedAt).getTime() : 0;
67
+ return aTime - bTime;
68
+ });
69
+
70
+ for (const result of sortedResults) {
71
+ if (result.success && result.output !== undefined) {
72
+ if (typeof result.output === 'object' && result.output !== null && !Array.isArray(result.output)) {
73
+ // Deep merge object outputs
74
+ Object.assign(merged, deepMerge(merged, result.output as Record<string, unknown>));
75
+ } else {
76
+ // For non-object outputs, use agent ID as key
77
+ merged[result.agentId] = result.output;
78
+ }
79
+ }
80
+ }
81
+
82
+ return Object.keys(merged).length > 0 ? merged : undefined;
83
+ }
84
+
85
+ /**
86
+ * List strategy: Return array of individual results
87
+ * INV-APE-004: Ordered by task definition order (taskId)
88
+ */
89
+ function listResults(results: AgentParallelTaskResult[]): unknown {
90
+ return results
91
+ .filter((r) => r.success)
92
+ .sort((a, b) => a.taskId.localeCompare(b.taskId))
93
+ .map((r) => ({
94
+ taskId: r.taskId,
95
+ agentId: r.agentId,
96
+ output: r.output,
97
+ durationMs: r.durationMs,
98
+ }));
99
+ }
100
+
101
+ /**
102
+ * First success strategy: Return first successful result
103
+ * INV-APE-004: First by layer, then by completion time
104
+ */
105
+ function firstSuccessResult(results: AgentParallelTaskResult[]): unknown {
106
+ const successResults = results
107
+ .filter((r) => r.success && r.output !== undefined)
108
+ .sort((a, b) => {
109
+ // First by layer
110
+ const layerDiff = (a.layer ?? 0) - (b.layer ?? 0);
111
+ if (layerDiff !== 0) return layerDiff;
112
+
113
+ // Then by completion time
114
+ const aTime = a.completedAt ? new Date(a.completedAt).getTime() : 0;
115
+ const bTime = b.completedAt ? new Date(b.completedAt).getTime() : 0;
116
+ return aTime - bTime;
117
+ });
118
+
119
+ return successResults[0]?.output;
120
+ }
121
+
122
+ /**
123
+ * Creates a result aggregator
124
+ */
125
+ export function createResultAggregator(): ResultAggregator {
126
+ return {
127
+ /**
128
+ * Aggregate task results based on strategy
129
+ * INV-APE-004: Follows configured strategy exactly
130
+ */
131
+ aggregate(
132
+ results: AgentParallelTaskResult[],
133
+ options: ResultAggregatorOptions
134
+ ): unknown {
135
+ const { strategy, customAggregator } = options;
136
+
137
+ switch (strategy) {
138
+ case 'merge':
139
+ return mergeResults(results);
140
+
141
+ case 'list':
142
+ return listResults(results);
143
+
144
+ case 'firstSuccess':
145
+ return firstSuccessResult(results);
146
+
147
+ case 'custom':
148
+ if (!customAggregator) {
149
+ throw new Error(
150
+ 'Custom aggregation strategy requires a customAggregator function'
151
+ );
152
+ }
153
+ return customAggregator(results);
154
+
155
+ default:
156
+ // Exhaustive check
157
+ const _exhaustive: never = strategy;
158
+ throw new Error(`Unknown aggregation strategy: ${_exhaustive}`);
159
+ }
160
+ },
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Built-in aggregation strategies for convenience
166
+ */
167
+ export const AggregationStrategies = {
168
+ /**
169
+ * Merge all successful outputs into single object
170
+ */
171
+ merge: mergeResults,
172
+
173
+ /**
174
+ * Return array of all successful results
175
+ */
176
+ list: listResults,
177
+
178
+ /**
179
+ * Return first successful result only
180
+ */
181
+ firstSuccess: firstSuccessResult,
182
+
183
+ /**
184
+ * Create custom strategy from function
185
+ */
186
+ custom: (fn: CustomAggregator) => fn,
187
+ } as const;
188
+
189
+ /**
190
+ * Utility: Create a keyed aggregator that groups results by a key
191
+ */
192
+ export function createKeyedAggregator(
193
+ keyFn: (result: AgentParallelTaskResult) => string
194
+ ): CustomAggregator {
195
+ return (results: AgentParallelTaskResult[]) => {
196
+ const grouped: Record<string, unknown[]> = {};
197
+
198
+ for (const result of results) {
199
+ if (result.success && result.output !== undefined) {
200
+ const key = keyFn(result);
201
+ if (!grouped[key]) {
202
+ grouped[key] = [];
203
+ }
204
+ grouped[key].push(result.output);
205
+ }
206
+ }
207
+
208
+ return grouped;
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Utility: Create aggregator that filters by success and transforms
214
+ */
215
+ export function createTransformAggregator<T>(
216
+ transform: (output: unknown, result: AgentParallelTaskResult) => T
217
+ ): CustomAggregator {
218
+ return (results: AgentParallelTaskResult[]) => {
219
+ return results
220
+ .filter((r) => r.success && r.output !== undefined)
221
+ .map((r) => transform(r.output, r));
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Get aggregation strategy from string
227
+ */
228
+ export function getAggregationStrategy(
229
+ name: string
230
+ ): AggregationStrategy {
231
+ const valid: AggregationStrategy[] = ['merge', 'list', 'firstSuccess', 'custom'];
232
+ if (valid.includes(name as AggregationStrategy)) {
233
+ return name as AggregationStrategy;
234
+ }
235
+ throw new Error(
236
+ `Invalid aggregation strategy: ${name}. Valid strategies: ${valid.join(', ')}`
237
+ );
238
+ }
package/src/types.ts ADDED
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Parallel Agent Execution Types
3
+ *
4
+ * Port interfaces and type definitions for parallel agent execution.
5
+ */
6
+
7
+ import type {
8
+ AgentParallelTask,
9
+ AgentParallelTaskResult,
10
+ AgentParallelGroupResult,
11
+ AgentParallelExecutionConfig,
12
+ ExecutionPlan,
13
+ SharedContext,
14
+ } from '@defai.digital/contracts';
15
+
16
+ // ============================================================================
17
+ // Agent Executor Port
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Agent execution request
22
+ */
23
+ export interface AgentExecuteRequest {
24
+ agentId: string;
25
+ input: unknown;
26
+ provider?: string;
27
+ model?: string;
28
+ timeout?: number;
29
+ sessionId?: string;
30
+ }
31
+
32
+ /**
33
+ * Agent execution result
34
+ */
35
+ export interface AgentExecuteResult {
36
+ success: boolean;
37
+ agentId: string;
38
+ output?: unknown;
39
+ error?: string;
40
+ errorCode?: string;
41
+ durationMs: number;
42
+ }
43
+
44
+ /**
45
+ * Port interface for agent execution
46
+ * Implementations inject actual agent executor at runtime
47
+ */
48
+ export interface AgentExecutorPort {
49
+ /**
50
+ * Execute a single agent with input
51
+ */
52
+ execute(request: AgentExecuteRequest): Promise<AgentExecuteResult>;
53
+
54
+ /**
55
+ * Check if an agent exists
56
+ */
57
+ exists(agentId: string): Promise<boolean>;
58
+ }
59
+
60
+ // ============================================================================
61
+ // Parallel Orchestrator Interface
62
+ // ============================================================================
63
+
64
+ /**
65
+ * Parallel agent orchestrator interface
66
+ */
67
+ export interface AgentParallelOrchestrator {
68
+ /**
69
+ * Execute multiple agents in parallel with DAG-based dependency management
70
+ * INV-APE-001: Respects maxConcurrentAgents
71
+ * INV-APE-002: Honors dependencies
72
+ * INV-APE-003: Shared context immutable
73
+ */
74
+ executeParallel(
75
+ tasks: AgentParallelTask[],
76
+ config?: Partial<AgentParallelExecutionConfig>,
77
+ sharedContext?: Record<string, unknown>
78
+ ): Promise<AgentParallelGroupResult>;
79
+
80
+ /**
81
+ * Build execution plan without executing
82
+ * Returns DAG analysis showing execution layers
83
+ */
84
+ buildExecutionPlan(tasks: AgentParallelTask[]): ExecutionPlan;
85
+
86
+ /**
87
+ * Cancel ongoing execution
88
+ */
89
+ cancel(): void;
90
+
91
+ /**
92
+ * Get current configuration
93
+ */
94
+ getConfig(): AgentParallelExecutionConfig;
95
+ }
96
+
97
+ // ============================================================================
98
+ // DAG Analyzer Interface
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Execution layer - tasks at same level can run in parallel
103
+ */
104
+ export interface TaskLayer {
105
+ index: number;
106
+ tasks: AgentParallelTask[];
107
+ }
108
+
109
+ /**
110
+ * DAG analysis result
111
+ */
112
+ export interface DAGAnalysisResult {
113
+ layers: TaskLayer[];
114
+ totalLayers: number;
115
+ maxParallelism: number;
116
+ hasCycles: boolean;
117
+ cycleNodes?: string[];
118
+ }
119
+
120
+ /**
121
+ * DAG analyzer interface
122
+ */
123
+ export interface DAGAnalyzer {
124
+ /**
125
+ * Analyze tasks and build execution layers
126
+ * INV-APE-002: Ensures dependencies honored
127
+ * INV-APE-200: Detects circular dependencies
128
+ */
129
+ analyze(tasks: AgentParallelTask[]): DAGAnalysisResult;
130
+
131
+ /**
132
+ * Validate DAG structure
133
+ */
134
+ validate(tasks: AgentParallelTask[]): { valid: boolean; errors: string[] };
135
+ }
136
+
137
+ // ============================================================================
138
+ // Context Manager Interface
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Context manager for shared immutable context
143
+ * INV-APE-003: Context immutable during execution
144
+ */
145
+ export interface ContextManager {
146
+ /**
147
+ * Create frozen shared context
148
+ */
149
+ create(data: Record<string, unknown>): SharedContext;
150
+
151
+ /**
152
+ * Get read-only view of context
153
+ */
154
+ get(): SharedContext | null;
155
+
156
+ /**
157
+ * Check if context is frozen/immutable
158
+ */
159
+ isFrozen(): boolean;
160
+
161
+ /**
162
+ * Clear context (for cleanup)
163
+ */
164
+ clear(): void;
165
+ }
166
+
167
+ // ============================================================================
168
+ // Result Aggregator Interface
169
+ // ============================================================================
170
+
171
+ /**
172
+ * Result aggregation strategy
173
+ */
174
+ export type AggregationStrategy = 'merge' | 'list' | 'firstSuccess' | 'custom';
175
+
176
+ /**
177
+ * Custom aggregation function
178
+ */
179
+ export type CustomAggregator = (
180
+ results: AgentParallelTaskResult[]
181
+ ) => unknown;
182
+
183
+ /**
184
+ * Result aggregator options
185
+ */
186
+ export interface ResultAggregatorOptions {
187
+ strategy: AggregationStrategy;
188
+ customAggregator?: CustomAggregator;
189
+ }
190
+
191
+ /**
192
+ * Result aggregator interface
193
+ * INV-APE-004: Aggregation follows configured strategy
194
+ */
195
+ export interface ResultAggregator {
196
+ /**
197
+ * Aggregate task results based on strategy
198
+ */
199
+ aggregate(
200
+ results: AgentParallelTaskResult[],
201
+ options: ResultAggregatorOptions
202
+ ): unknown;
203
+ }
204
+
205
+ // ============================================================================
206
+ // Progress Tracking
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Progress event types
211
+ */
212
+ export type ParallelProgressEventType =
213
+ | 'execution.started'
214
+ | 'layer.started'
215
+ | 'layer.completed'
216
+ | 'task.started'
217
+ | 'task.completed'
218
+ | 'task.failed'
219
+ | 'task.skipped'
220
+ | 'execution.completed'
221
+ | 'execution.cancelled';
222
+
223
+ /**
224
+ * Progress event
225
+ */
226
+ export interface ParallelProgressEvent {
227
+ type: ParallelProgressEventType;
228
+ timestamp: string;
229
+ groupId: string;
230
+ layerIndex?: number;
231
+ taskId?: string;
232
+ agentId?: string;
233
+ totalTasks?: number;
234
+ completedTasks?: number;
235
+ failedTasks?: number;
236
+ message?: string;
237
+ }
238
+
239
+ /**
240
+ * Progress callback
241
+ */
242
+ export type ParallelProgressCallback = (event: ParallelProgressEvent) => void;
243
+
244
+ // ============================================================================
245
+ // Orchestrator Options
246
+ // ============================================================================
247
+
248
+ /**
249
+ * Options for creating parallel orchestrator
250
+ */
251
+ export interface AgentParallelOrchestratorOptions {
252
+ /**
253
+ * Agent executor port (for dependency injection)
254
+ */
255
+ agentExecutor: AgentExecutorPort;
256
+
257
+ /**
258
+ * Default configuration
259
+ */
260
+ defaultConfig?: Partial<AgentParallelExecutionConfig>;
261
+
262
+ /**
263
+ * Progress callback for execution updates
264
+ */
265
+ onProgress?: ParallelProgressCallback;
266
+ }
267
+
268
+ // ============================================================================
269
+ // Stub Implementation (for testing)
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Stub agent executor for testing
274
+ */
275
+ export class StubAgentExecutor implements AgentExecutorPort {
276
+ private results: Map<string, AgentExecuteResult> = new Map();
277
+ private existingAgents: Set<string> = new Set();
278
+
279
+ /**
280
+ * Set expected result for an agent
281
+ */
282
+ setResult(agentId: string, result: Partial<AgentExecuteResult>): void {
283
+ this.results.set(agentId, {
284
+ success: true,
285
+ agentId,
286
+ durationMs: 100,
287
+ ...result,
288
+ });
289
+ }
290
+
291
+ /**
292
+ * Set agent as existing
293
+ */
294
+ setExists(agentId: string, exists: boolean): void {
295
+ if (exists) {
296
+ this.existingAgents.add(agentId);
297
+ } else {
298
+ this.existingAgents.delete(agentId);
299
+ }
300
+ }
301
+
302
+ async execute(request: AgentExecuteRequest): Promise<AgentExecuteResult> {
303
+ const result = this.results.get(request.agentId);
304
+ if (result) {
305
+ return result;
306
+ }
307
+
308
+ // Default: return success with echo output
309
+ return {
310
+ success: true,
311
+ agentId: request.agentId,
312
+ output: { input: request.input, echo: true },
313
+ durationMs: 50,
314
+ };
315
+ }
316
+
317
+ async exists(agentId: string): Promise<boolean> {
318
+ return this.existingAgents.has(agentId) || this.results.has(agentId);
319
+ }
320
+ }