@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,194 @@
1
+ /**
2
+ * Context Manager
3
+ *
4
+ * Manages shared immutable context for parallel agent execution.
5
+ * Ensures context is frozen before execution and cannot be modified.
6
+ *
7
+ * Invariants:
8
+ * - INV-APE-003: Shared context immutable during execution
9
+ * - INV-APE-300: Context snapshot timing (frozen before first task)
10
+ */
11
+
12
+ import type { SharedContext } from '@defai.digital/contracts';
13
+ import { ParallelExecutionErrorCodes } from '@defai.digital/contracts';
14
+ import type { ContextManager } from './types.js';
15
+
16
+ /**
17
+ * Error thrown when context mutation is attempted
18
+ */
19
+ export class ContextMutationError extends Error {
20
+ constructor(message: string) {
21
+ super(message);
22
+ this.name = 'ContextMutationError';
23
+ }
24
+
25
+ static readonly code = ParallelExecutionErrorCodes.CONTEXT_MUTATION;
26
+ }
27
+
28
+ /**
29
+ * Deep freeze an object and all nested objects
30
+ * INV-APE-003: Ensures true immutability
31
+ */
32
+ function deepFreeze<T extends object>(obj: T): T {
33
+ // Get property names
34
+ const propNames = Object.getOwnPropertyNames(obj) as (keyof T)[];
35
+
36
+ // Freeze nested objects first
37
+ for (const name of propNames) {
38
+ const value = obj[name];
39
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
40
+ deepFreeze(value as object);
41
+ }
42
+ }
43
+
44
+ // Freeze self
45
+ return Object.freeze(obj);
46
+ }
47
+
48
+ /**
49
+ * Creates a context manager for shared immutable context
50
+ */
51
+ export function createContextManager(): ContextManager {
52
+ let context: SharedContext | null = null;
53
+ let frozen = false;
54
+
55
+ return {
56
+ /**
57
+ * Create frozen shared context
58
+ * INV-APE-300: Context snapshot timing
59
+ */
60
+ create(data: Record<string, unknown>): SharedContext {
61
+ // Deep clone to prevent external mutation
62
+ const clonedData = JSON.parse(JSON.stringify(data)) as Record<string, unknown>;
63
+
64
+ // Create context object
65
+ const newContext: SharedContext = {
66
+ data: clonedData,
67
+ createdAt: new Date().toISOString(),
68
+ version: '1',
69
+ };
70
+
71
+ // Deep freeze the entire context
72
+ // INV-APE-003: Immutable during execution
73
+ context = deepFreeze(newContext);
74
+ frozen = true;
75
+
76
+ return context;
77
+ },
78
+
79
+ /**
80
+ * Get read-only view of context
81
+ */
82
+ get(): SharedContext | null {
83
+ return context;
84
+ },
85
+
86
+ /**
87
+ * Check if context is frozen/immutable
88
+ */
89
+ isFrozen(): boolean {
90
+ return frozen && context !== null && Object.isFrozen(context);
91
+ },
92
+
93
+ /**
94
+ * Clear context (for cleanup after execution)
95
+ */
96
+ clear(): void {
97
+ context = null;
98
+ frozen = false;
99
+ },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Creates a proxy that throws on any mutation attempt
105
+ * Alternative approach for stricter runtime enforcement
106
+ */
107
+ export function createImmutableContextProxy<T extends Record<string, unknown>>(
108
+ data: T
109
+ ): Readonly<T> {
110
+ const handler: ProxyHandler<T> = {
111
+ get(target, prop, receiver) {
112
+ const value = Reflect.get(target, prop, receiver);
113
+ // Recursively wrap nested objects
114
+ if (value && typeof value === 'object') {
115
+ return createImmutableContextProxy(value as Record<string, unknown>);
116
+ }
117
+ return value;
118
+ },
119
+
120
+ set(_target, prop) {
121
+ throw new ContextMutationError(
122
+ `Cannot modify shared context: attempted to set "${String(prop)}". ` +
123
+ 'Shared context is immutable during parallel execution (INV-APE-003).'
124
+ );
125
+ },
126
+
127
+ deleteProperty(_target, prop) {
128
+ throw new ContextMutationError(
129
+ `Cannot modify shared context: attempted to delete "${String(prop)}". ` +
130
+ 'Shared context is immutable during parallel execution (INV-APE-003).'
131
+ );
132
+ },
133
+
134
+ defineProperty(_target, prop) {
135
+ throw new ContextMutationError(
136
+ `Cannot modify shared context: attempted to define "${String(prop)}". ` +
137
+ 'Shared context is immutable during parallel execution (INV-APE-003).'
138
+ );
139
+ },
140
+
141
+ setPrototypeOf() {
142
+ throw new ContextMutationError(
143
+ 'Cannot modify shared context prototype. ' +
144
+ 'Shared context is immutable during parallel execution (INV-APE-003).'
145
+ );
146
+ },
147
+ };
148
+
149
+ return new Proxy(data, handler) as Readonly<T>;
150
+ }
151
+
152
+ /**
153
+ * Validates that context data is JSON-serializable
154
+ */
155
+ export function validateContextData(
156
+ data: unknown
157
+ ): { valid: boolean; errors: string[] } {
158
+ const errors: string[] = [];
159
+
160
+ try {
161
+ // Check JSON serializability
162
+ JSON.stringify(data);
163
+ } catch (e) {
164
+ errors.push(
165
+ `Context data is not JSON-serializable: ${e instanceof Error ? e.message : 'Unknown error'}`
166
+ );
167
+ return { valid: false, errors };
168
+ }
169
+
170
+ // Check for functions (not serializable)
171
+ function checkForFunctions(obj: unknown, path: string): void {
172
+ if (typeof obj === 'function') {
173
+ errors.push(`Context contains function at ${path}`);
174
+ } else if (obj && typeof obj === 'object') {
175
+ if (Array.isArray(obj)) {
176
+ obj.forEach((item, index) => checkForFunctions(item, `${path}[${index}]`));
177
+ } else {
178
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
179
+ checkForFunctions(value, path ? `${path}.${key}` : key);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ checkForFunctions(data, '');
186
+
187
+ // Check for circular references (JSON.stringify would have failed)
188
+ // Already caught above
189
+
190
+ return {
191
+ valid: errors.length === 0,
192
+ errors,
193
+ };
194
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * DAG Analyzer
3
+ *
4
+ * Analyzes task dependencies and builds execution layers using topological sort.
5
+ * Detects circular dependencies and validates DAG structure.
6
+ *
7
+ * Invariants:
8
+ * - INV-APE-002: Dependencies honored (DAG ordering)
9
+ * - INV-APE-200: Circular dependencies detected before execution
10
+ */
11
+
12
+ import type { AgentParallelTask } from '@defai.digital/contracts';
13
+ import { ParallelExecutionErrorCodes } from '@defai.digital/contracts';
14
+ import type { DAGAnalyzer, DAGAnalysisResult, TaskLayer } from './types.js';
15
+
16
+ /**
17
+ * Error thrown when DAG analysis fails
18
+ */
19
+ export class DAGAnalysisError extends Error {
20
+ constructor(
21
+ public readonly code: string,
22
+ message: string,
23
+ public readonly cycleNodes?: string[]
24
+ ) {
25
+ super(message);
26
+ this.name = 'DAGAnalysisError';
27
+ }
28
+
29
+ static circularDependency(nodeIds: string[]): DAGAnalysisError {
30
+ return new DAGAnalysisError(
31
+ ParallelExecutionErrorCodes.CIRCULAR_DEPENDENCY,
32
+ `Circular dependency detected: ${nodeIds.join(' -> ')}`,
33
+ nodeIds
34
+ );
35
+ }
36
+
37
+ static invalidDependency(taskId: string, depId: string): DAGAnalysisError {
38
+ return new DAGAnalysisError(
39
+ ParallelExecutionErrorCodes.INVALID_PLAN,
40
+ `Task "${taskId}" depends on non-existent task "${depId}"`
41
+ );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Creates a DAG analyzer for parallel task execution
47
+ * Uses Kahn's algorithm for topological sorting
48
+ */
49
+ export function createDAGAnalyzer(): DAGAnalyzer {
50
+ /**
51
+ * Build execution layers using Kahn's algorithm
52
+ * INV-APE-002: Ensures dependencies honored
53
+ * INV-APE-200: Detects cycles via incomplete processing
54
+ */
55
+ function buildLayers(tasks: AgentParallelTask[]): DAGAnalysisResult {
56
+ // Handle empty input
57
+ if (tasks.length === 0) {
58
+ return {
59
+ layers: [],
60
+ totalLayers: 0,
61
+ maxParallelism: 0,
62
+ hasCycles: false,
63
+ };
64
+ }
65
+
66
+ // Build task lookup map
67
+ const taskMap = new Map<string, AgentParallelTask>();
68
+ for (const task of tasks) {
69
+ taskMap.set(task.taskId, task);
70
+ }
71
+
72
+ // Validate all dependencies exist
73
+ for (const task of tasks) {
74
+ for (const depId of task.dependencies) {
75
+ if (!taskMap.has(depId)) {
76
+ throw DAGAnalysisError.invalidDependency(task.taskId, depId);
77
+ }
78
+ }
79
+ }
80
+
81
+ // Calculate in-degree for each task
82
+ const inDegree = new Map<string, number>();
83
+ for (const task of tasks) {
84
+ inDegree.set(task.taskId, task.dependencies.length);
85
+ }
86
+
87
+ // Build reverse dependency map (who depends on me)
88
+ const dependents = new Map<string, string[]>();
89
+ for (const task of tasks) {
90
+ dependents.set(task.taskId, []);
91
+ }
92
+ for (const task of tasks) {
93
+ for (const depId of task.dependencies) {
94
+ const depList = dependents.get(depId) ?? [];
95
+ depList.push(task.taskId);
96
+ dependents.set(depId, depList);
97
+ }
98
+ }
99
+
100
+ // Build layers using BFS
101
+ const layers: TaskLayer[] = [];
102
+ let processedCount = 0;
103
+
104
+ // Start with tasks that have no dependencies
105
+ let currentLayerTaskIds = Array.from(inDegree.entries())
106
+ .filter(([_, degree]) => degree === 0)
107
+ .map(([id]) => id);
108
+
109
+ let layerIndex = 0;
110
+
111
+ while (currentLayerTaskIds.length > 0) {
112
+ // Get tasks for current layer, sorted by priority (descending)
113
+ const layerTasks = currentLayerTaskIds
114
+ .map((id) => taskMap.get(id)!)
115
+ .sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
116
+
117
+ layers.push({
118
+ index: layerIndex,
119
+ tasks: layerTasks,
120
+ });
121
+
122
+ processedCount += layerTasks.length;
123
+
124
+ // Find next layer
125
+ const nextLayerTaskIds: string[] = [];
126
+ for (const taskId of currentLayerTaskIds) {
127
+ const deps = dependents.get(taskId) ?? [];
128
+ for (const depId of deps) {
129
+ const degree = (inDegree.get(depId) ?? 0) - 1;
130
+ inDegree.set(depId, degree);
131
+
132
+ if (degree === 0) {
133
+ nextLayerTaskIds.push(depId);
134
+ }
135
+ }
136
+ }
137
+
138
+ currentLayerTaskIds = nextLayerTaskIds;
139
+ layerIndex++;
140
+ }
141
+
142
+ // Check for cycles - if we didn't process all tasks, there's a cycle
143
+ const hasCycles = processedCount !== tasks.length;
144
+ let cycleNodes: string[] | undefined;
145
+
146
+ if (hasCycles) {
147
+ // Find nodes involved in cycle (those not processed)
148
+ cycleNodes = tasks
149
+ .filter((t) => (inDegree.get(t.taskId) ?? 0) > 0)
150
+ .map((t) => t.taskId);
151
+ }
152
+
153
+ // Calculate max parallelism (largest layer)
154
+ const maxParallelism = Math.max(0, ...layers.map((l) => l.tasks.length));
155
+
156
+ const result: DAGAnalysisResult = {
157
+ layers,
158
+ totalLayers: layers.length,
159
+ maxParallelism,
160
+ hasCycles,
161
+ };
162
+
163
+ // Only include cycleNodes if there are cycles
164
+ if (cycleNodes) {
165
+ result.cycleNodes = cycleNodes;
166
+ }
167
+
168
+ return result;
169
+ }
170
+
171
+ /**
172
+ * Validate DAG structure
173
+ */
174
+ function validate(tasks: AgentParallelTask[]): { valid: boolean; errors: string[] } {
175
+ const errors: string[] = [];
176
+
177
+ // Check for duplicate task IDs
178
+ const taskIds = new Set<string>();
179
+ for (const task of tasks) {
180
+ if (taskIds.has(task.taskId)) {
181
+ errors.push(`Duplicate task ID: ${task.taskId}`);
182
+ }
183
+ taskIds.add(task.taskId);
184
+ }
185
+
186
+ // Check for self-dependencies
187
+ for (const task of tasks) {
188
+ if (task.dependencies.includes(task.taskId)) {
189
+ errors.push(`Task "${task.taskId}" depends on itself`);
190
+ }
191
+ }
192
+
193
+ // Check for missing dependencies
194
+ for (const task of tasks) {
195
+ for (const depId of task.dependencies) {
196
+ if (!taskIds.has(depId)) {
197
+ errors.push(`Task "${task.taskId}" depends on non-existent task "${depId}"`);
198
+ }
199
+ }
200
+ }
201
+
202
+ // Check for cycles
203
+ if (errors.length === 0) {
204
+ const result = buildLayers(tasks);
205
+ if (result.hasCycles) {
206
+ errors.push(`Circular dependency detected involving: ${result.cycleNodes?.join(', ')}`);
207
+ }
208
+ }
209
+
210
+ return {
211
+ valid: errors.length === 0,
212
+ errors,
213
+ };
214
+ }
215
+
216
+ return {
217
+ analyze(tasks: AgentParallelTask[]): DAGAnalysisResult {
218
+ const result = buildLayers(tasks);
219
+
220
+ // Throw if cycles detected
221
+ if (result.hasCycles && result.cycleNodes) {
222
+ throw DAGAnalysisError.circularDependency(result.cycleNodes);
223
+ }
224
+
225
+ return result;
226
+ },
227
+
228
+ validate,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Utility: Find tasks in a cycle using DFS
234
+ */
235
+ export function findCyclePath(tasks: AgentParallelTask[]): string[] | null {
236
+ const taskMap = new Map<string, AgentParallelTask>();
237
+ for (const task of tasks) {
238
+ taskMap.set(task.taskId, task);
239
+ }
240
+
241
+ const visited = new Set<string>();
242
+ const recursionStack = new Set<string>();
243
+ const path: string[] = [];
244
+
245
+ function dfs(taskId: string): boolean {
246
+ visited.add(taskId);
247
+ recursionStack.add(taskId);
248
+ path.push(taskId);
249
+
250
+ const task = taskMap.get(taskId);
251
+ if (task) {
252
+ for (const depId of task.dependencies) {
253
+ if (!visited.has(depId)) {
254
+ if (dfs(depId)) {
255
+ return true;
256
+ }
257
+ } else if (recursionStack.has(depId)) {
258
+ // Found cycle
259
+ path.push(depId);
260
+ return true;
261
+ }
262
+ }
263
+ }
264
+
265
+ recursionStack.delete(taskId);
266
+ path.pop();
267
+ return false;
268
+ }
269
+
270
+ for (const task of tasks) {
271
+ if (!visited.has(task.taskId)) {
272
+ if (dfs(task.taskId)) {
273
+ // Trim path to only include cycle
274
+ const lastNode = path[path.length - 1];
275
+ if (lastNode) {
276
+ const cycleStart = path.indexOf(lastNode);
277
+ return path.slice(cycleStart);
278
+ }
279
+ return path;
280
+ }
281
+ }
282
+ }
283
+
284
+ return null;
285
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @defai.digital/agent-parallel
3
+ *
4
+ * Parallel Agent Execution Domain
5
+ *
6
+ * Provides orchestration for executing multiple agents in parallel
7
+ * with DAG-based dependency management, shared immutable context,
8
+ * and configurable result aggregation.
9
+ *
10
+ * @module @defai.digital/agent-parallel
11
+ */
12
+
13
+ // Main Orchestrator
14
+ export {
15
+ createAgentParallelOrchestrator,
16
+ ParallelExecutionError,
17
+ } from './orchestrator.js';
18
+
19
+ // DAG Analyzer
20
+ export {
21
+ createDAGAnalyzer,
22
+ DAGAnalysisError,
23
+ findCyclePath,
24
+ } from './dag-analyzer.js';
25
+
26
+ // Context Manager
27
+ export {
28
+ createContextManager,
29
+ createImmutableContextProxy,
30
+ validateContextData,
31
+ ContextMutationError,
32
+ } from './context-manager.js';
33
+
34
+ // Result Aggregator
35
+ export {
36
+ createResultAggregator,
37
+ AggregationStrategies,
38
+ createKeyedAggregator,
39
+ createTransformAggregator,
40
+ getAggregationStrategy,
41
+ } from './result-aggregator.js';
42
+
43
+ // Types and Interfaces
44
+ export type {
45
+ // Orchestrator types
46
+ AgentParallelOrchestrator,
47
+ AgentParallelOrchestratorOptions,
48
+ // Agent executor port
49
+ AgentExecutorPort,
50
+ AgentExecuteRequest,
51
+ AgentExecuteResult,
52
+ // DAG types
53
+ DAGAnalyzer,
54
+ DAGAnalysisResult,
55
+ TaskLayer,
56
+ // Context types
57
+ ContextManager,
58
+ // Result aggregator types
59
+ ResultAggregator,
60
+ ResultAggregatorOptions,
61
+ AggregationStrategy,
62
+ CustomAggregator,
63
+ // Progress types
64
+ ParallelProgressEvent,
65
+ ParallelProgressEventType,
66
+ ParallelProgressCallback,
67
+ } from './types.js';
68
+
69
+ // Stub implementation for testing
70
+ export { StubAgentExecutor } from './types.js';