@defai.digital/agent-domain 13.0.3

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 (77) hide show
  1. package/LICENSE +214 -0
  2. package/dist/enhanced-executor.d.ts +170 -0
  3. package/dist/enhanced-executor.d.ts.map +1 -0
  4. package/dist/enhanced-executor.js +1072 -0
  5. package/dist/enhanced-executor.js.map +1 -0
  6. package/dist/executor.d.ts +120 -0
  7. package/dist/executor.d.ts.map +1 -0
  8. package/dist/executor.js +929 -0
  9. package/dist/executor.js.map +1 -0
  10. package/dist/index.d.ts +25 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +34 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/loader.d.ts +50 -0
  15. package/dist/loader.d.ts.map +1 -0
  16. package/dist/loader.js +160 -0
  17. package/dist/loader.js.map +1 -0
  18. package/dist/persistent-registry.d.ts +105 -0
  19. package/dist/persistent-registry.d.ts.map +1 -0
  20. package/dist/persistent-registry.js +183 -0
  21. package/dist/persistent-registry.js.map +1 -0
  22. package/dist/production-factories.d.ts +70 -0
  23. package/dist/production-factories.d.ts.map +1 -0
  24. package/dist/production-factories.js +434 -0
  25. package/dist/production-factories.js.map +1 -0
  26. package/dist/prompt-executor.d.ts +119 -0
  27. package/dist/prompt-executor.d.ts.map +1 -0
  28. package/dist/prompt-executor.js +211 -0
  29. package/dist/prompt-executor.js.map +1 -0
  30. package/dist/registry.d.ts +57 -0
  31. package/dist/registry.d.ts.map +1 -0
  32. package/dist/registry.js +123 -0
  33. package/dist/registry.js.map +1 -0
  34. package/dist/selection-service.d.ts +74 -0
  35. package/dist/selection-service.d.ts.map +1 -0
  36. package/dist/selection-service.js +322 -0
  37. package/dist/selection-service.js.map +1 -0
  38. package/dist/selector.d.ts +51 -0
  39. package/dist/selector.d.ts.map +1 -0
  40. package/dist/selector.js +249 -0
  41. package/dist/selector.js.map +1 -0
  42. package/dist/stub-checkpoint.d.ts +23 -0
  43. package/dist/stub-checkpoint.d.ts.map +1 -0
  44. package/dist/stub-checkpoint.js +137 -0
  45. package/dist/stub-checkpoint.js.map +1 -0
  46. package/dist/stub-delegation-tracker.d.ts +25 -0
  47. package/dist/stub-delegation-tracker.d.ts.map +1 -0
  48. package/dist/stub-delegation-tracker.js +118 -0
  49. package/dist/stub-delegation-tracker.js.map +1 -0
  50. package/dist/stub-parallel-executor.d.ts +19 -0
  51. package/dist/stub-parallel-executor.d.ts.map +1 -0
  52. package/dist/stub-parallel-executor.js +176 -0
  53. package/dist/stub-parallel-executor.js.map +1 -0
  54. package/dist/types.d.ts +614 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +15 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/workflow-templates.d.ts +117 -0
  59. package/dist/workflow-templates.d.ts.map +1 -0
  60. package/dist/workflow-templates.js +342 -0
  61. package/dist/workflow-templates.js.map +1 -0
  62. package/package.json +51 -0
  63. package/src/enhanced-executor.ts +1395 -0
  64. package/src/executor.ts +1153 -0
  65. package/src/index.ts +172 -0
  66. package/src/loader.ts +191 -0
  67. package/src/persistent-registry.ts +235 -0
  68. package/src/production-factories.ts +613 -0
  69. package/src/prompt-executor.ts +310 -0
  70. package/src/registry.ts +167 -0
  71. package/src/selection-service.ts +411 -0
  72. package/src/selector.ts +299 -0
  73. package/src/stub-checkpoint.ts +187 -0
  74. package/src/stub-delegation-tracker.ts +161 -0
  75. package/src/stub-parallel-executor.ts +224 -0
  76. package/src/types.ts +784 -0
  77. package/src/workflow-templates.ts +393 -0
@@ -0,0 +1,1153 @@
1
+ /**
2
+ * Agent Executor Implementation
3
+ *
4
+ * Executes agent workflows with step-by-step processing.
5
+ * Integrates with DelegationTracker for INV-DT-001 (depth limits) and INV-DT-002 (no circular).
6
+ */
7
+
8
+ import {
9
+ type AgentResult,
10
+ type AgentRunOptions,
11
+ type AgentError,
12
+ type AgentWorkflowStep,
13
+ AgentErrorCode,
14
+ AgentRunOptionsSchema,
15
+ LIMIT_ABILITY_TOKENS_AGENT,
16
+ } from '@defai.digital/contracts';
17
+ import type {
18
+ AgentExecutor,
19
+ AgentRegistry,
20
+ ExecutionStatus,
21
+ StepExecutionContext,
22
+ StepExecutionResult,
23
+ StepExecutorFn,
24
+ AgentDomainConfig,
25
+ PromptExecutor,
26
+ PromptStepConfig,
27
+ AbilityManagerLike,
28
+ ToolExecutor,
29
+ DelegationTrackerFactory,
30
+ } from './types.js';
31
+ import { createStubPromptExecutor } from './prompt-executor.js';
32
+ import { stubDelegationTrackerFactory } from './stub-delegation-tracker.js';
33
+
34
+ /**
35
+ * Default agent executor implementation
36
+ *
37
+ * Supports optional ability injection via AbilityManager.
38
+ * INV-AGT-ABL-001: When abilityManager is provided, abilities are injected before prompt execution
39
+ * INV-AGT-ABL-002: Core abilities from agent profile are always included
40
+ * INV-AGT-ABL-003: Token limits are respected during injection
41
+ */
42
+ export class DefaultAgentExecutor implements AgentExecutor {
43
+ private readonly executions = new Map<string, ExecutionState>();
44
+ private readonly stepExecutors = new Map<string, StepExecutorFn>();
45
+ private readonly promptExecutor: PromptExecutor;
46
+ private readonly toolExecutor: ToolExecutor | undefined;
47
+ private readonly abilityManager: AbilityManagerLike | undefined;
48
+ private readonly enableAbilityInjection: boolean;
49
+ private readonly maxAbilityTokens: number;
50
+ private readonly delegationTrackerFactory: DelegationTrackerFactory;
51
+
52
+ constructor(
53
+ private readonly registry: AgentRegistry,
54
+ private readonly config: AgentDomainConfig
55
+ ) {
56
+ // Use provided prompt executor or create a stub for testing
57
+ this.promptExecutor = config.promptExecutor ?? createStubPromptExecutor(
58
+ config.defaultProvider ?? 'claude'
59
+ );
60
+
61
+ // Tool executor for executing MCP tools (optional)
62
+ this.toolExecutor = config.toolExecutor;
63
+
64
+ // Ability injection configuration
65
+ this.abilityManager = config.abilityManager;
66
+ this.enableAbilityInjection = config.enableAbilityInjection ?? (config.abilityManager !== undefined);
67
+ this.maxAbilityTokens = config.maxAbilityTokens ?? LIMIT_ABILITY_TOKENS_AGENT;
68
+
69
+ // Delegation tracker factory (use stub if not provided)
70
+ this.delegationTrackerFactory = config.delegationTrackerFactory ?? stubDelegationTrackerFactory;
71
+
72
+ // Register default step executors
73
+ this.registerDefaultExecutors();
74
+ }
75
+
76
+ /**
77
+ * Execute an agent with the given input
78
+ * Validates AgentRunOptions at entry point per contract
79
+ */
80
+ async execute(
81
+ agentId: string,
82
+ input: unknown,
83
+ options?: AgentRunOptions
84
+ ): Promise<AgentResult> {
85
+ const startTime = Date.now();
86
+ const executionId = crypto.randomUUID();
87
+
88
+ // Validate options at entry point
89
+ let validatedOptions: AgentRunOptions | undefined;
90
+ if (options !== undefined) {
91
+ try {
92
+ validatedOptions = AgentRunOptionsSchema.parse(options);
93
+ } catch (error) {
94
+ return this.createErrorResult(
95
+ agentId,
96
+ startTime,
97
+ AgentErrorCode.AGENT_VALIDATION_ERROR,
98
+ `Invalid run options: ${error instanceof Error ? error.message : 'Unknown error'}`
99
+ );
100
+ }
101
+ }
102
+
103
+ // Get agent profile
104
+ const agent = await this.registry.get(agentId);
105
+ if (agent === undefined) {
106
+ return this.createErrorResult(
107
+ agentId,
108
+ startTime,
109
+ AgentErrorCode.AGENT_NOT_FOUND,
110
+ `Agent "${agentId}" not found`
111
+ );
112
+ }
113
+
114
+ // Check if agent is enabled
115
+ if (!agent.enabled) {
116
+ return this.createErrorResult(
117
+ agentId,
118
+ startTime,
119
+ AgentErrorCode.AGENT_PERMISSION_DENIED,
120
+ `Agent "${agentId}" is disabled`
121
+ );
122
+ }
123
+
124
+ // Initialize execution state
125
+ const state: ExecutionState = {
126
+ executionId,
127
+ agentId,
128
+ status: 'running',
129
+ startedAt: new Date().toISOString(),
130
+ stepResults: [],
131
+ previousOutputs: {},
132
+ };
133
+ this.executions.set(executionId, state);
134
+
135
+ try {
136
+ // Execute workflow steps
137
+ const workflow = agent.workflow ?? [];
138
+ const stepResults: StepResult[] = [];
139
+
140
+ // Handle agents with no workflow defined
141
+ if (workflow.length === 0) {
142
+ state.status = 'completed';
143
+ state.completedAt = new Date().toISOString();
144
+
145
+ return {
146
+ success: true,
147
+ agentId,
148
+ sessionId: validatedOptions?.sessionId,
149
+ output: {
150
+ message: `Agent "${agentId}" has no workflow steps defined. Register the agent with a workflow to enable execution.`,
151
+ agent: {
152
+ agentId: agent.agentId,
153
+ description: agent.description,
154
+ capabilities: agent.capabilities,
155
+ systemPrompt: agent.systemPrompt ? '(defined)' : undefined,
156
+ },
157
+ input,
158
+ },
159
+ stepResults: [],
160
+ totalDurationMs: Date.now() - startTime,
161
+ };
162
+ }
163
+
164
+ for (const step of workflow) {
165
+ // Check for cancellation
166
+ if (state.status === 'cancelled') {
167
+ break;
168
+ }
169
+
170
+ state.currentStep = step.stepId;
171
+
172
+ // Check dependencies
173
+ const dependenciesMet = this.checkDependencies(
174
+ step,
175
+ stepResults
176
+ );
177
+ if (!dependenciesMet) {
178
+ stepResults.push({
179
+ stepId: step.stepId,
180
+ success: false,
181
+ durationMs: 0,
182
+ retryCount: 0,
183
+ skipped: true,
184
+ error: {
185
+ code: AgentErrorCode.AGENT_DEPENDENCY_FAILED,
186
+ message: 'Dependencies not met',
187
+ },
188
+ });
189
+ continue;
190
+ }
191
+
192
+ // Check condition
193
+ if (step.condition !== undefined) {
194
+ const conditionMet = this.evaluateCondition(
195
+ step.condition,
196
+ state.previousOutputs
197
+ );
198
+ if (!conditionMet) {
199
+ stepResults.push({
200
+ stepId: step.stepId,
201
+ success: true,
202
+ durationMs: 0,
203
+ retryCount: 0,
204
+ skipped: true,
205
+ error: undefined,
206
+ });
207
+ continue;
208
+ }
209
+ }
210
+
211
+ // Execute step with retries
212
+ const result = await this.executeStep(
213
+ step,
214
+ {
215
+ agentId,
216
+ executionId,
217
+ sessionId: validatedOptions?.sessionId,
218
+ input,
219
+ previousOutputs: state.previousOutputs,
220
+ memory: undefined,
221
+ provider: validatedOptions?.provider,
222
+ model: validatedOptions?.model,
223
+ delegationContext: validatedOptions?.delegationContext,
224
+ },
225
+ step.retryPolicy?.maxAttempts ?? 1
226
+ );
227
+
228
+ stepResults.push({
229
+ stepId: step.stepId,
230
+ success: result.success,
231
+ output: result.output,
232
+ durationMs: result.durationMs,
233
+ retryCount: result.retryCount,
234
+ skipped: false,
235
+ error: result.error,
236
+ });
237
+
238
+ // Store output for dependencies
239
+ if (result.success && result.output !== undefined) {
240
+ state.previousOutputs[step.stepId] = result.output;
241
+ }
242
+
243
+ // Stop on failure (unless parallel execution)
244
+ if (!result.success && !step.parallel) {
245
+ break;
246
+ }
247
+ }
248
+
249
+ // Determine final status
250
+ const allSuccess = stepResults.every((r) => r.success || r.skipped);
251
+ const finalOutput = this.collectOutputs(stepResults, state.previousOutputs);
252
+
253
+ state.status = allSuccess ? 'completed' : 'failed';
254
+ state.completedAt = new Date().toISOString();
255
+ state.stepResults = stepResults;
256
+
257
+ return {
258
+ success: allSuccess,
259
+ agentId,
260
+ sessionId: validatedOptions?.sessionId,
261
+ output: finalOutput,
262
+ stepResults,
263
+ totalDurationMs: Date.now() - startTime,
264
+ error: allSuccess
265
+ ? undefined
266
+ : stepResults.find((r) => r.error !== undefined)?.error,
267
+ };
268
+ } catch (error) {
269
+ state.status = 'failed';
270
+ state.completedAt = new Date().toISOString();
271
+
272
+ return this.createErrorResult(
273
+ agentId,
274
+ startTime,
275
+ AgentErrorCode.AGENT_STAGE_FAILED,
276
+ error instanceof Error ? error.message : 'Unknown error'
277
+ );
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Cancel a running agent execution
283
+ */
284
+ async cancel(executionId: string): Promise<void> {
285
+ const state = this.executions.get(executionId);
286
+ if (state?.status === 'running') {
287
+ state.status = 'cancelled';
288
+ state.completedAt = new Date().toISOString();
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Get execution status
294
+ */
295
+ async getStatus(executionId: string): Promise<ExecutionStatus | undefined> {
296
+ const state = this.executions.get(executionId);
297
+ if (state === undefined) {
298
+ return undefined;
299
+ }
300
+
301
+ const completedSteps = state.stepResults.filter(
302
+ (r) => r.success || r.skipped
303
+ ).length;
304
+ const totalSteps = state.stepResults.length;
305
+
306
+ return {
307
+ executionId: state.executionId,
308
+ agentId: state.agentId,
309
+ status: state.status,
310
+ currentStep: state.currentStep,
311
+ startedAt: state.startedAt,
312
+ completedAt: state.completedAt,
313
+ progress: {
314
+ totalSteps,
315
+ completedSteps,
316
+ currentStepName: state.currentStep,
317
+ percentComplete:
318
+ totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0,
319
+ },
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Register a step executor for a specific step type
325
+ */
326
+ registerStepExecutor(type: string, executor: StepExecutorFn): void {
327
+ this.stepExecutors.set(type, executor);
328
+ }
329
+
330
+ /**
331
+ * Execute a single step with retry logic
332
+ */
333
+ private async executeStep(
334
+ step: AgentWorkflowStep,
335
+ context: StepExecutionContext,
336
+ maxAttempts: number
337
+ ): Promise<StepExecutionResult> {
338
+ const executor = this.stepExecutors.get(step.type);
339
+ if (executor === undefined) {
340
+ return {
341
+ success: false,
342
+ error: {
343
+ code: AgentErrorCode.AGENT_STAGE_FAILED,
344
+ message: `No executor for step type: ${step.type}`,
345
+ stepId: step.stepId,
346
+ },
347
+ durationMs: 0,
348
+ retryCount: 0,
349
+ };
350
+ }
351
+
352
+ let lastError: AgentError | undefined;
353
+ let retryCount = 0;
354
+
355
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
356
+ // Apply timeout with proper cleanup
357
+ const timeout = step.timeoutMs ?? this.config.defaultTimeout;
358
+ const { promise: timeoutPromise, cancel: cancelTimeout } = this.createTimeout(timeout, step.stepId);
359
+
360
+ try {
361
+ const result = await Promise.race([
362
+ executor(step, context),
363
+ timeoutPromise,
364
+ ]);
365
+ cancelTimeout(); // Clean up the timer
366
+
367
+ if (result.success) {
368
+ return {
369
+ ...result,
370
+ retryCount,
371
+ };
372
+ }
373
+
374
+ lastError = result.error;
375
+ retryCount++;
376
+
377
+ // Apply backoff
378
+ if (attempt < maxAttempts - 1 && step.retryPolicy !== undefined) {
379
+ const backoff =
380
+ step.retryPolicy.backoffMs *
381
+ Math.pow(step.retryPolicy.backoffMultiplier, attempt);
382
+ await this.sleep(backoff);
383
+ }
384
+ } catch (error) {
385
+ cancelTimeout(); // Clean up the timer on error too
386
+ lastError = {
387
+ code: AgentErrorCode.AGENT_STAGE_FAILED,
388
+ message: error instanceof Error ? error.message : 'Unknown error',
389
+ stepId: step.stepId,
390
+ };
391
+ retryCount++;
392
+ }
393
+ }
394
+
395
+ return {
396
+ success: false,
397
+ error: lastError,
398
+ durationMs: 0,
399
+ retryCount,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Check if step dependencies are met
405
+ */
406
+ private checkDependencies(
407
+ step: AgentWorkflowStep,
408
+ results: StepResult[]
409
+ ): boolean {
410
+ if (step.dependencies === undefined || step.dependencies.length === 0) {
411
+ return true;
412
+ }
413
+
414
+ return step.dependencies.every((depId) => {
415
+ const depResult = results.find((r) => r.stepId === depId);
416
+ return depResult !== undefined && depResult.success;
417
+ });
418
+ }
419
+
420
+ /**
421
+ * Evaluate a condition expression safely without using eval/Function
422
+ *
423
+ * Supports patterns:
424
+ * - ${variable} === value
425
+ * - ${variable} !== value
426
+ * - ${variable} > value
427
+ * - ${variable} < value
428
+ * - ${variable} >= value
429
+ * - ${variable} <= value
430
+ * - ${variable} (truthy check)
431
+ * - !${variable} (falsy check)
432
+ * - condition && condition
433
+ * - condition || condition
434
+ */
435
+ private evaluateCondition(
436
+ condition: string,
437
+ outputs: Record<string, unknown>
438
+ ): boolean {
439
+ try {
440
+ // Handle logical operators by recursively evaluating sub-conditions
441
+ // Check for || first (lower precedence)
442
+ const orParts = this.splitByOperator(condition, '||');
443
+ if (orParts.length > 1) {
444
+ return orParts.some((part) => this.evaluateCondition(part.trim(), outputs));
445
+ }
446
+
447
+ // Check for && (higher precedence than ||)
448
+ const andParts = this.splitByOperator(condition, '&&');
449
+ if (andParts.length > 1) {
450
+ return andParts.every((part) => this.evaluateCondition(part.trim(), outputs));
451
+ }
452
+
453
+ // Handle negation
454
+ const trimmed = condition.trim();
455
+ if (trimmed.startsWith('!')) {
456
+ return !this.evaluateCondition(trimmed.slice(1).trim(), outputs);
457
+ }
458
+
459
+ // Handle parentheses
460
+ if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
461
+ return this.evaluateCondition(trimmed.slice(1, -1), outputs);
462
+ }
463
+
464
+ // Parse comparison expression: ${variable} op value
465
+ const comparisonMatch = /^\$\{(\w+(?:\.\w+)*)\}\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/.exec(trimmed);
466
+ if (comparisonMatch) {
467
+ const [, keyPath, op, rawValue] = comparisonMatch;
468
+ const actualValue = this.getConditionValue(outputs, keyPath!);
469
+ const expectedValue = this.parseValue(rawValue!.trim());
470
+ return this.compareValues(actualValue, expectedValue, op!);
471
+ }
472
+
473
+ // Handle simple variable reference (truthy check): ${variable}
474
+ const varMatch = /^\$\{(\w+(?:\.\w+)*)\}$/.exec(trimmed);
475
+ if (varMatch) {
476
+ const value = this.getConditionValue(outputs, varMatch[1]!);
477
+ return Boolean(value);
478
+ }
479
+
480
+ // Handle literal boolean
481
+ if (trimmed === 'true') return true;
482
+ if (trimmed === 'false') return false;
483
+
484
+ // Unknown pattern - fail safely
485
+ console.warn(`[evaluateCondition] Unknown condition pattern: ${condition}`);
486
+ return false;
487
+ } catch (error) {
488
+ console.warn(`[evaluateCondition] Error evaluating condition: ${condition}`, error);
489
+ return false;
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Split condition by operator, respecting parentheses
495
+ */
496
+ private splitByOperator(condition: string, operator: string): string[] {
497
+ const parts: string[] = [];
498
+ let current = '';
499
+ let depth = 0;
500
+ let i = 0;
501
+
502
+ while (i < condition.length) {
503
+ const char = condition[i];
504
+
505
+ if (char === '(') {
506
+ depth++;
507
+ current += char;
508
+ } else if (char === ')') {
509
+ depth--;
510
+ current += char;
511
+ } else if (depth === 0 && condition.slice(i, i + operator.length) === operator) {
512
+ parts.push(current);
513
+ current = '';
514
+ i += operator.length;
515
+ continue;
516
+ } else {
517
+ current += char;
518
+ }
519
+ i++;
520
+ }
521
+
522
+ if (current) {
523
+ parts.push(current);
524
+ }
525
+
526
+ return parts;
527
+ }
528
+
529
+ /**
530
+ * Get nested value from object using dot notation (for condition evaluation)
531
+ */
532
+ private getConditionValue(obj: Record<string, unknown>, path: string): unknown {
533
+ const parts = path.split('.');
534
+ let current: unknown = obj;
535
+
536
+ for (const part of parts) {
537
+ if (current === null || current === undefined) {
538
+ return undefined;
539
+ }
540
+ current = (current as Record<string, unknown>)[part];
541
+ }
542
+
543
+ return current;
544
+ }
545
+
546
+ /**
547
+ * Parse a value string into its actual type
548
+ */
549
+ private parseValue(value: string): unknown {
550
+ // String literal
551
+ if ((value.startsWith('"') && value.endsWith('"')) ||
552
+ (value.startsWith("'") && value.endsWith("'"))) {
553
+ return value.slice(1, -1);
554
+ }
555
+
556
+ // Null/undefined
557
+ if (value === 'null') return null;
558
+ if (value === 'undefined') return undefined;
559
+
560
+ // Boolean
561
+ if (value === 'true') return true;
562
+ if (value === 'false') return false;
563
+
564
+ // Number
565
+ const num = Number(value);
566
+ if (!isNaN(num)) return num;
567
+
568
+ // Default to string
569
+ return value;
570
+ }
571
+
572
+ /**
573
+ * Compare two values using the specified operator
574
+ */
575
+ private compareValues(actual: unknown, expected: unknown, op: string): boolean {
576
+ switch (op) {
577
+ case '===':
578
+ return actual === expected;
579
+ case '!==':
580
+ return actual !== expected;
581
+ case '==':
582
+ return actual == expected;
583
+ case '!=':
584
+ return actual != expected;
585
+ case '>':
586
+ return (actual as number) > (expected as number);
587
+ case '<':
588
+ return (actual as number) < (expected as number);
589
+ case '>=':
590
+ return (actual as number) >= (expected as number);
591
+ case '<=':
592
+ return (actual as number) <= (expected as number);
593
+ default:
594
+ return false;
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Collect final outputs from step results
600
+ */
601
+ private collectOutputs(
602
+ results: StepResult[],
603
+ _previousOutputs: Record<string, unknown>
604
+ ): Record<string, unknown> {
605
+ const output: Record<string, unknown> = {};
606
+
607
+ for (const result of results) {
608
+ if (result.success && result.output !== undefined) {
609
+ output[result.stepId] = result.output;
610
+ }
611
+ }
612
+
613
+ return output;
614
+ }
615
+
616
+ /**
617
+ * Create error result
618
+ */
619
+ private createErrorResult(
620
+ agentId: string,
621
+ startTime: number,
622
+ code: string,
623
+ message: string
624
+ ): AgentResult {
625
+ return {
626
+ success: false,
627
+ agentId,
628
+ error: { code, message },
629
+ totalDurationMs: Date.now() - startTime,
630
+ };
631
+ }
632
+
633
+ /**
634
+ * Create timeout promise with cancellation support
635
+ */
636
+ private createTimeout(ms: number, stepId: string): {
637
+ promise: Promise<StepExecutionResult>;
638
+ cancel: () => void;
639
+ } {
640
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
641
+
642
+ const promise = new Promise<StepExecutionResult>((_, reject) => {
643
+ timeoutId = setTimeout(
644
+ () =>
645
+ { reject({
646
+ success: false,
647
+ error: {
648
+ code: AgentErrorCode.AGENT_STAGE_FAILED,
649
+ message: `Step "${stepId}" timed out after ${ms}ms`,
650
+ stepId,
651
+ retryable: true,
652
+ },
653
+ durationMs: ms,
654
+ retryCount: 0,
655
+ }); },
656
+ ms
657
+ );
658
+ });
659
+
660
+ const cancel = (): void => {
661
+ if (timeoutId !== undefined) {
662
+ clearTimeout(timeoutId);
663
+ }
664
+ };
665
+
666
+ return { promise, cancel };
667
+ }
668
+
669
+ /**
670
+ * Sleep for specified duration
671
+ */
672
+ private sleep(ms: number): Promise<void> {
673
+ return new Promise((resolve) => setTimeout(resolve, ms));
674
+ }
675
+
676
+ /**
677
+ * Substitute template variables in a string
678
+ * Supports: ${input}, ${input.field}, ${previousOutputs.stepId}, ${agent.field}
679
+ */
680
+ private substituteVariables(
681
+ template: string,
682
+ context: StepExecutionContext,
683
+ agentProfile: { description?: string; systemPrompt?: string; agentId: string }
684
+ ): string {
685
+ return template.replace(/\$\{([^}]+)\}/g, (match, path) => {
686
+ const parts = path.split('.');
687
+ let value: unknown;
688
+
689
+ switch (parts[0]) {
690
+ case 'input':
691
+ if (parts.length === 1) {
692
+ value = typeof context.input === 'string'
693
+ ? context.input
694
+ : JSON.stringify(context.input, null, 2);
695
+ } else {
696
+ value = this.getNestedValue(context.input, parts.slice(1));
697
+ }
698
+ break;
699
+ case 'previousOutputs':
700
+ if (parts.length > 1) {
701
+ value = this.getNestedValue(context.previousOutputs, parts.slice(1));
702
+ }
703
+ break;
704
+ case 'agent':
705
+ if (parts.length > 1) {
706
+ value = this.getNestedValue(agentProfile, parts.slice(1));
707
+ }
708
+ break;
709
+ default:
710
+ // Try to get from input directly
711
+ value = this.getNestedValue(context.input, parts);
712
+ }
713
+
714
+ if (value === undefined) {
715
+ return match; // Keep original if not found
716
+ }
717
+
718
+ return typeof value === 'string' ? value : JSON.stringify(value);
719
+ });
720
+ }
721
+
722
+ /**
723
+ * Get nested value from object using path array
724
+ */
725
+ private getNestedValue(obj: unknown, path: string[]): unknown {
726
+ let current: unknown = obj;
727
+ for (const key of path) {
728
+ if (current === null || current === undefined) {
729
+ return undefined;
730
+ }
731
+ if (typeof current !== 'object') {
732
+ return undefined;
733
+ }
734
+ current = (current as Record<string, unknown>)[key];
735
+ }
736
+ return current;
737
+ }
738
+
739
+ /**
740
+ * Register default step executors
741
+ */
742
+ private registerDefaultExecutors(): void {
743
+ // Prompt step executor - executes real LLM calls via promptExecutor
744
+ // INV-AGT-ABL-001: Abilities injected before prompt execution
745
+ this.stepExecutors.set('prompt', async (step, context) => {
746
+ const startTime = Date.now();
747
+ const config = (step.config ?? {}) as PromptStepConfig;
748
+
749
+ // Get agent profile for system prompt and variable substitution
750
+ const agent = await this.registry.get(context.agentId);
751
+ const agentProfile: { description?: string; systemPrompt?: string; agentId: string } = {
752
+ agentId: context.agentId,
753
+ };
754
+ if (agent?.description !== undefined) {
755
+ agentProfile.description = agent.description;
756
+ }
757
+ if (agent?.systemPrompt !== undefined) {
758
+ agentProfile.systemPrompt = agent.systemPrompt;
759
+ }
760
+
761
+ // INV-AGT-ABL-001: Inject abilities if ability manager is available
762
+ // INV-AGT-ABL-002: Core abilities from agent profile are always included
763
+ // INV-AGT-ABL-003: Token limits are respected
764
+ let abilityContent = '';
765
+ if (this.enableAbilityInjection && this.abilityManager !== undefined) {
766
+ try {
767
+ // Get core abilities from agent profile
768
+ const coreAbilities = agent?.abilities?.core ?? [];
769
+
770
+ // Determine task from input for relevance matching
771
+ const task = typeof context.input === 'string'
772
+ ? context.input
773
+ : typeof context.input === 'object' && context.input !== null && 'task' in context.input
774
+ ? String((context.input as { task: unknown }).task)
775
+ : typeof context.input === 'object' && context.input !== null && 'prompt' in context.input
776
+ ? String((context.input as { prompt: unknown }).prompt)
777
+ : agent?.description ?? '';
778
+
779
+ // Inject abilities
780
+ const injectionResult = await this.abilityManager.injectAbilities(
781
+ context.agentId,
782
+ task,
783
+ coreAbilities,
784
+ {
785
+ maxTokens: this.maxAbilityTokens,
786
+ includeMetadata: true,
787
+ }
788
+ );
789
+
790
+ if (injectionResult.combinedContent.length > 0) {
791
+ abilityContent = `\n\n## Relevant Knowledge & Abilities\n\n${injectionResult.combinedContent}\n\n---\n\n`;
792
+ }
793
+ } catch {
794
+ // Ability injection failure should not block execution
795
+ // Just log and continue without abilities
796
+ }
797
+ }
798
+
799
+ // Build the prompt from config or input
800
+ let promptText: string;
801
+ if (config.prompt) {
802
+ // Use template from config with variable substitution
803
+ promptText = this.substituteVariables(config.prompt, context, agentProfile);
804
+ } else if (typeof context.input === 'string') {
805
+ // Use input directly as prompt
806
+ promptText = context.input;
807
+ } else if (
808
+ context.input !== null &&
809
+ typeof context.input === 'object' &&
810
+ 'prompt' in context.input &&
811
+ typeof (context.input as { prompt: unknown }).prompt === 'string'
812
+ ) {
813
+ // Use input.prompt
814
+ promptText = (context.input as { prompt: string }).prompt;
815
+ } else {
816
+ // Default: stringify input as the prompt
817
+ promptText = JSON.stringify(context.input, null, 2);
818
+ }
819
+
820
+ // Determine system prompt with injected abilities
821
+ const baseSystemPrompt = config.systemPrompt ?? agent?.systemPrompt ?? '';
822
+ const systemPrompt = abilityContent
823
+ ? `${baseSystemPrompt}${abilityContent}`
824
+ : baseSystemPrompt || undefined;
825
+
826
+ // Execute the prompt
827
+ const response = await this.promptExecutor.execute({
828
+ prompt: promptText,
829
+ systemPrompt: systemPrompt || undefined,
830
+ provider: config.provider ?? context.provider,
831
+ model: config.model ?? context.model,
832
+ maxTokens: config.maxTokens,
833
+ temperature: config.temperature,
834
+ timeout: config.timeout ?? step.timeoutMs,
835
+ });
836
+
837
+ if (response.success) {
838
+ return {
839
+ success: true,
840
+ output: {
841
+ content: response.content,
842
+ provider: response.provider,
843
+ model: response.model,
844
+ usage: response.usage,
845
+ },
846
+ error: undefined,
847
+ durationMs: Date.now() - startTime,
848
+ retryCount: 0,
849
+ };
850
+ } else {
851
+ return {
852
+ success: false,
853
+ error: {
854
+ code: response.errorCode ?? AgentErrorCode.AGENT_STAGE_FAILED,
855
+ message: response.error ?? 'Prompt execution failed',
856
+ stepId: step.stepId,
857
+ retryable: true,
858
+ },
859
+ durationMs: Date.now() - startTime,
860
+ retryCount: 0,
861
+ };
862
+ }
863
+ });
864
+
865
+ // Tool step executor (INV-TOOL-001, INV-TOOL-002, INV-TOOL-003)
866
+ this.stepExecutors.set('tool', async (step, context) => {
867
+ const startTime = Date.now();
868
+ const config = step.config as {
869
+ toolName?: string;
870
+ toolInput?: Record<string, unknown>;
871
+ } | undefined;
872
+
873
+ const toolName = config?.toolName;
874
+ const toolInput = (config?.toolInput ?? context.input ?? {}) as Record<string, unknown>;
875
+
876
+ // If no tool executor is configured, return placeholder result
877
+ if (this.toolExecutor === undefined) {
878
+ return {
879
+ success: true,
880
+ output: {
881
+ step: step.stepId,
882
+ type: 'tool',
883
+ toolName: toolName ?? 'unknown',
884
+ status: 'no_executor',
885
+ message: 'Tool execution requires a ToolExecutor. Configure AgentDomainConfig.toolExecutor.',
886
+ },
887
+ error: undefined,
888
+ durationMs: Date.now() - startTime,
889
+ retryCount: 0,
890
+ };
891
+ }
892
+
893
+ // Check if tool name is provided
894
+ if (toolName === undefined || toolName.trim() === '') {
895
+ return {
896
+ success: false,
897
+ output: undefined,
898
+ error: {
899
+ code: 'TOOL_CONFIG_ERROR',
900
+ message: `Tool step "${step.stepId}" requires toolName in config`,
901
+ stepId: step.stepId,
902
+ },
903
+ durationMs: Date.now() - startTime,
904
+ retryCount: 0,
905
+ };
906
+ }
907
+
908
+ // INV-TOOL-001: Check if tool is available
909
+ if (!this.toolExecutor.isToolAvailable(toolName)) {
910
+ return {
911
+ success: false,
912
+ output: undefined,
913
+ error: {
914
+ code: 'TOOL_NOT_FOUND',
915
+ message: `Tool "${toolName}" is not available`,
916
+ stepId: step.stepId,
917
+ },
918
+ durationMs: Date.now() - startTime,
919
+ retryCount: 0,
920
+ };
921
+ }
922
+
923
+ // Execute the tool
924
+ try {
925
+ const result = await this.toolExecutor.execute(toolName, toolInput);
926
+ return {
927
+ success: result.success,
928
+ output: {
929
+ step: step.stepId,
930
+ type: 'tool',
931
+ toolName,
932
+ toolOutput: result.output,
933
+ },
934
+ error: result.success ? undefined : {
935
+ code: result.errorCode ?? 'TOOL_EXECUTION_ERROR',
936
+ message: result.error ?? 'Tool execution failed',
937
+ stepId: step.stepId,
938
+ retryable: result.retryable ?? false,
939
+ },
940
+ durationMs: Date.now() - startTime,
941
+ retryCount: 0,
942
+ };
943
+ } catch (error) {
944
+ return {
945
+ success: false,
946
+ output: undefined,
947
+ error: {
948
+ code: 'TOOL_EXECUTION_ERROR',
949
+ message: error instanceof Error ? error.message : 'Unknown tool execution error',
950
+ stepId: step.stepId,
951
+ retryable: true,
952
+ },
953
+ durationMs: Date.now() - startTime,
954
+ retryCount: 0,
955
+ };
956
+ }
957
+ });
958
+
959
+ // Conditional step executor
960
+ this.stepExecutors.set('conditional', async (step, _context) => {
961
+ const startTime = Date.now();
962
+ return {
963
+ success: true,
964
+ output: { step: step.stepId, type: 'conditional' },
965
+ error: undefined,
966
+ durationMs: Date.now() - startTime,
967
+ retryCount: 0,
968
+ };
969
+ });
970
+
971
+ // Loop step executor
972
+ this.stepExecutors.set('loop', async (step, _context) => {
973
+ const startTime = Date.now();
974
+ return {
975
+ success: true,
976
+ output: { step: step.stepId, type: 'loop' },
977
+ error: undefined,
978
+ durationMs: Date.now() - startTime,
979
+ retryCount: 0,
980
+ };
981
+ });
982
+
983
+ // Parallel step executor
984
+ this.stepExecutors.set('parallel', async (step, _context) => {
985
+ const startTime = Date.now();
986
+ return {
987
+ success: true,
988
+ output: { step: step.stepId, type: 'parallel' },
989
+ error: undefined,
990
+ durationMs: Date.now() - startTime,
991
+ retryCount: 0,
992
+ };
993
+ });
994
+
995
+ // Delegate step executor - integrates with DelegationTracker
996
+ // INV-DT-001: Depth never exceeds maxDepth
997
+ // INV-DT-002: No circular delegations
998
+ this.stepExecutors.set('delegate', async (step, context) => {
999
+ const startTime = Date.now();
1000
+ const config = step.config as {
1001
+ targetAgentId?: string;
1002
+ task?: string;
1003
+ input?: unknown;
1004
+ } | undefined;
1005
+
1006
+ if (!config?.targetAgentId) {
1007
+ return {
1008
+ success: false,
1009
+ error: {
1010
+ code: AgentErrorCode.AGENT_STAGE_FAILED,
1011
+ message: 'Delegate step requires targetAgentId in config',
1012
+ stepId: step.stepId,
1013
+ },
1014
+ durationMs: Date.now() - startTime,
1015
+ retryCount: 0,
1016
+ };
1017
+ }
1018
+
1019
+ // Create delegation tracker using injected factory
1020
+ const delegationTracker = this.delegationTrackerFactory(
1021
+ context.agentId,
1022
+ context.delegationContext,
1023
+ this.config.maxDelegationDepth
1024
+ );
1025
+
1026
+ // Check if delegation is allowed (INV-DT-001, INV-DT-002)
1027
+ const checkResult = delegationTracker.canDelegate(config.targetAgentId);
1028
+ if (!checkResult.allowed) {
1029
+ return {
1030
+ success: false,
1031
+ error: {
1032
+ code: checkResult.reason === 'MAX_DEPTH_EXCEEDED'
1033
+ ? AgentErrorCode.AGENT_DELEGATION_DEPTH_EXCEEDED
1034
+ : AgentErrorCode.AGENT_STAGE_FAILED,
1035
+ message: checkResult.message ?? `Cannot delegate to ${config.targetAgentId}`,
1036
+ stepId: step.stepId,
1037
+ retryable: false,
1038
+ },
1039
+ durationMs: Date.now() - startTime,
1040
+ retryCount: 0,
1041
+ };
1042
+ }
1043
+
1044
+ // Get target agent profile
1045
+ const targetAgent = await this.registry.get(config.targetAgentId);
1046
+ if (targetAgent === undefined) {
1047
+ return {
1048
+ success: false,
1049
+ error: {
1050
+ code: AgentErrorCode.AGENT_NOT_FOUND,
1051
+ message: `Delegation target "${config.targetAgentId}" not found`,
1052
+ stepId: step.stepId,
1053
+ },
1054
+ durationMs: Date.now() - startTime,
1055
+ retryCount: 0,
1056
+ };
1057
+ }
1058
+
1059
+ // Create child context for the delegated agent
1060
+ const childContext = delegationTracker.createChildContext(config.targetAgentId);
1061
+
1062
+ // Execute the delegated agent recursively
1063
+ try {
1064
+ const delegateResult = await this.execute(
1065
+ config.targetAgentId,
1066
+ config.input ?? context.input,
1067
+ {
1068
+ sessionId: context.sessionId,
1069
+ provider: context.provider,
1070
+ model: context.model,
1071
+ delegationContext: childContext,
1072
+ }
1073
+ );
1074
+
1075
+ // Record the delegation result
1076
+ delegationTracker.recordResult({
1077
+ success: delegateResult.success,
1078
+ handledBy: config.targetAgentId,
1079
+ result: delegateResult.output,
1080
+ durationMs: delegateResult.totalDurationMs,
1081
+ finalDepth: childContext.currentDepth,
1082
+ error: delegateResult.error ? {
1083
+ code: delegateResult.error.code,
1084
+ message: delegateResult.error.message,
1085
+ retryable: false,
1086
+ } : undefined,
1087
+ });
1088
+
1089
+ return {
1090
+ success: delegateResult.success,
1091
+ output: {
1092
+ step: step.stepId,
1093
+ type: 'delegate',
1094
+ delegatedTo: config.targetAgentId,
1095
+ result: delegateResult.output,
1096
+ delegationDepth: childContext.currentDepth,
1097
+ },
1098
+ error: delegateResult.error,
1099
+ durationMs: Date.now() - startTime,
1100
+ retryCount: 0,
1101
+ };
1102
+ } catch (error) {
1103
+ return {
1104
+ success: false,
1105
+ error: {
1106
+ code: AgentErrorCode.AGENT_STAGE_FAILED,
1107
+ message: error instanceof Error ? error.message : 'Delegation failed',
1108
+ stepId: step.stepId,
1109
+ },
1110
+ durationMs: Date.now() - startTime,
1111
+ retryCount: 0,
1112
+ };
1113
+ }
1114
+ });
1115
+ }
1116
+ }
1117
+
1118
+ /**
1119
+ * Execution state (internal)
1120
+ */
1121
+ interface ExecutionState {
1122
+ executionId: string;
1123
+ agentId: string;
1124
+ status: 'running' | 'completed' | 'failed' | 'cancelled';
1125
+ startedAt: string;
1126
+ completedAt?: string;
1127
+ currentStep?: string;
1128
+ stepResults: StepResult[];
1129
+ previousOutputs: Record<string, unknown>;
1130
+ }
1131
+
1132
+ /**
1133
+ * Step result
1134
+ */
1135
+ interface StepResult {
1136
+ stepId: string;
1137
+ success: boolean;
1138
+ output?: unknown;
1139
+ durationMs: number;
1140
+ retryCount: number;
1141
+ skipped: boolean;
1142
+ error: AgentError | undefined;
1143
+ }
1144
+
1145
+ /**
1146
+ * Creates a new agent executor
1147
+ */
1148
+ export function createAgentExecutor(
1149
+ registry: AgentRegistry,
1150
+ config: AgentDomainConfig
1151
+ ): AgentExecutor {
1152
+ return new DefaultAgentExecutor(registry, config);
1153
+ }