jay-code
Version:
Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability
701 lines (586 loc) • 19.3 kB
text/typescript
/**
* Agentic Flow Hook Manager
*
* Central manager for all agentic-flow hooks, providing registration,
* execution, and lifecycle management.
*/
import { EventEmitter } from 'events';
import { Logger } from '../../core/logger.js';
import type {
AgenticHookContext,
AgenticHookType,
HookFilter,
HookHandler,
HookHandlerResult,
HookOptions,
HookPayload,
HookPipeline,
HookRegistration,
HookRegistry,
PipelineMetrics,
PipelineStage,
SideEffect,
} from './types.js';
const logger = new Logger({
level: 'info',
format: 'text',
destination: 'console'
}, { prefix: 'AgenticHookManager' });
export class AgenticHookManager extends EventEmitter implements HookRegistry {
private hooks: Map<AgenticHookType, HookRegistration[]> = new Map();
private pipelines: Map<string, HookPipeline> = new Map();
private metrics: Map<string, any> = new Map();
private activeExecutions: Set<string> = new Set();
constructor() {
super();
this.initializeMetrics();
}
/**
* Register a new hook
*/
register(registration: HookRegistration): void {
const { type, id } = registration;
// Validate registration
this.validateRegistration(registration);
// Get or create hook list for type
if (!this.hooks.has(type)) {
this.hooks.set(type, []);
}
const hookList = this.hooks.get(type)!;
// Check for duplicate ID
if (hookList.some(h => h.id === id)) {
throw new Error(`Hook with ID '${id}' already registered for type '${type}'`);
}
// Insert hook sorted by priority (higher priority first)
const insertIndex = hookList.findIndex(h => h.priority < registration.priority);
if (insertIndex === -1) {
hookList.push(registration);
} else {
hookList.splice(insertIndex, 0, registration);
}
logger.info(`Registered hook '${id}' for type '${type}' with priority ${registration.priority}`);
this.emit('hook:registered', { type, registration });
// Update metrics
this.updateMetric('hooks.registered', 1);
}
/**
* Unregister a hook
*/
unregister(id: string): void {
let found = false;
for (const [type, hookList] of this.hooks.entries()) {
const index = hookList.findIndex(h => h.id === id);
if (index !== -1) {
hookList.splice(index, 1);
found = true;
logger.info(`Unregistered hook '${id}' from type '${type}'`);
this.emit('hook:unregistered', { type, id });
// Clean up empty lists
if (hookList.length === 0) {
this.hooks.delete(type);
}
break;
}
}
if (!found) {
throw new Error(`Hook with ID '${id}' not found`);
}
this.updateMetric('hooks.unregistered', 1);
}
/**
* Get hooks by type with optional filtering
*/
getHooks(type: AgenticHookType, filter?: HookFilter): HookRegistration[] {
const hookList = this.hooks.get(type) || [];
if (!filter) {
return [...hookList];
}
return hookList.filter(hook => this.matchesFilter(hook, filter));
}
/**
* Execute hooks for a given type
*/
async executeHooks(
type: AgenticHookType,
payload: HookPayload,
context: AgenticHookContext
): Promise<HookHandlerResult[]> {
const executionId = this.generateExecutionId();
this.activeExecutions.add(executionId);
const startTime = Date.now();
const results: HookHandlerResult[] = [];
try {
// Get applicable hooks
const hooks = this.getHooks(type, this.createFilterFromPayload(payload));
logger.debug(`Executing ${hooks.length} hooks for type '${type}'`);
this.emit('hooks:executing', { type, count: hooks.length, executionId });
// Execute hooks in order
let modifiedPayload = payload;
for (const hook of hooks) {
try {
const result = await this.executeHook(hook, modifiedPayload, context);
results.push(result);
// Handle side effects
if (result.sideEffects) {
await this.processSideEffects(result.sideEffects, context);
}
// Update payload if modified
if (result.modified && result.payload) {
modifiedPayload = result.payload;
}
// Check if we should continue
if (!result.continue) {
logger.debug(`Hook '${hook.id}' halted execution chain`);
break;
}
} catch (error) {
await this.handleHookError(hook, error as Error, context);
// Determine if we should continue after error
if (hook.options?.errorHandler) {
hook.options.errorHandler(error as Error);
} else {
throw error; // Re-throw if no error handler
}
}
}
// Update metrics
const duration = Date.now() - startTime;
this.updateMetric('hooks.executions', 1);
this.updateMetric('hooks.totalDuration', duration);
this.updateMetric(`hooks.${type}.executions`, 1);
this.updateMetric(`hooks.${type}.duration`, duration);
this.emit('hooks:executed', {
type,
results,
duration,
executionId
});
return results;
} finally {
this.activeExecutions.delete(executionId);
}
}
/**
* Create a new hook pipeline
*/
createPipeline(config: Partial<HookPipeline>): HookPipeline {
const pipeline: HookPipeline = {
id: config.id || this.generatePipelineId(),
name: config.name || 'Unnamed Pipeline',
stages: config.stages || [],
errorStrategy: config.errorStrategy || 'fail-fast',
metrics: {
executions: 0,
avgDuration: 0,
errorRate: 0,
throughput: 0,
},
};
this.pipelines.set(pipeline.id, pipeline);
logger.info(`Created pipeline '${pipeline.name}' with ID '${pipeline.id}'`);
return pipeline;
}
/**
* Execute a pipeline
*/
async executePipeline(
pipelineId: string,
initialPayload: HookPayload,
context: AgenticHookContext
): Promise<HookHandlerResult[]> {
const pipeline = this.pipelines.get(pipelineId);
if (!pipeline) {
throw new Error(`Pipeline '${pipelineId}' not found`);
}
const startTime = Date.now();
const results: HookHandlerResult[] = [];
let currentPayload = initialPayload;
try {
for (const stage of pipeline.stages) {
// Check stage condition
if (stage.condition && !stage.condition(context)) {
logger.debug(`Skipping stage '${stage.name}' due to condition`);
continue;
}
// Execute stage hooks
const stageResults = await this.executeStage(
stage,
currentPayload,
context
);
// Apply stage transform if provided
if (stage.transform) {
for (let i = 0; i < stageResults.length; i++) {
stageResults[i] = stage.transform(stageResults[i]);
}
}
results.push(...stageResults);
// Update payload for next stage
const lastModified = stageResults
.reverse()
.find(r => r.modified && r.payload);
if (lastModified) {
currentPayload = lastModified.payload;
}
}
// Update pipeline metrics
this.updatePipelineMetrics(pipeline, Date.now() - startTime, false);
return results;
} catch (error) {
// Update error metrics
this.updatePipelineMetrics(pipeline, Date.now() - startTime, true);
// Handle error based on strategy
if (pipeline.errorStrategy === 'rollback') {
await this.rollbackPipeline(pipeline, results, context);
}
throw error;
}
}
/**
* Get current metrics
*/
getMetrics(): Record<string, any> {
const metrics: Record<string, any> = {};
for (const [key, value] of this.metrics.entries()) {
metrics[key] = value;
}
// Add computed metrics
metrics['hooks.count'] = this.getTotalHookCount();
metrics['hooks.types'] = Array.from(this.hooks.keys());
metrics['pipelines.count'] = this.pipelines.size;
metrics['executions.active'] = this.activeExecutions.size;
return metrics;
}
// ===== Private Methods =====
private validateRegistration(registration: HookRegistration): void {
if (!registration.id) {
throw new Error('Hook registration must have an ID');
}
if (!registration.type) {
throw new Error('Hook registration must have a type');
}
if (typeof registration.handler !== 'function') {
throw new Error('Hook registration must have a handler function');
}
if (registration.priority < 0) {
throw new Error('Hook priority must be non-negative');
}
}
private matchesFilter(hook: HookRegistration, filter: HookFilter): boolean {
if (!hook.filter) {
return true; // No filter means hook applies to all
}
// Check providers
if (filter.providers && hook.filter.providers) {
const hasProvider = filter.providers.some(p =>
hook.filter!.providers!.includes(p)
);
if (!hasProvider) return false;
}
// Check models
if (filter.models && hook.filter.models) {
const hasModel = filter.models.some(m =>
hook.filter!.models!.includes(m)
);
if (!hasModel) return false;
}
// Check patterns
if (filter.patterns && hook.filter.patterns) {
// Complex pattern matching logic here
// For now, simplified version
return true;
}
// Check conditions
if (filter.conditions && hook.filter.conditions) {
// Evaluate conditions
// Simplified for now
return true;
}
return true;
}
private createFilterFromPayload(payload: HookPayload): HookFilter | undefined {
const filter: HookFilter = {};
// Extract filter criteria from payload
if ('provider' in payload) {
filter.providers = [payload.provider];
}
if ('model' in payload) {
filter.models = [payload.model];
}
if ('operation' in payload) {
filter.operations = [payload.operation];
}
if ('namespace' in payload) {
filter.namespaces = [payload.namespace];
}
return Object.keys(filter).length > 0 ? filter : undefined;
}
private async executeHook(
hook: HookRegistration,
payload: HookPayload,
context: AgenticHookContext
): Promise<HookHandlerResult> {
const startTime = Date.now();
try {
// Check cache if enabled
if (hook.options?.cache?.enabled) {
const cacheKey = hook.options.cache.key(payload);
const cached = this.getCachedResult(hook.id, cacheKey);
if (cached) {
this.updateMetric('hooks.cacheHits', 1);
return cached;
}
}
// Execute with timeout if specified
let resultPromise = hook.handler(payload, context);
if (hook.options?.timeout) {
resultPromise = this.withTimeout(
resultPromise,
hook.options.timeout,
`Hook '${hook.id}' timed out`
);
}
const result = await resultPromise;
// Cache result if enabled
if (hook.options?.cache?.enabled && result) {
const cacheKey = hook.options.cache.key(payload);
this.cacheResult(hook.id, cacheKey, result, hook.options.cache.ttl);
}
// Update hook-specific metrics
const duration = Date.now() - startTime;
this.updateMetric(`hooks.${hook.id}.executions`, 1);
this.updateMetric(`hooks.${hook.id}.duration`, duration);
return result;
} catch (error) {
// Handle retries if configured
if (hook.options?.retries && hook.options.retries > 0) {
logger.warn(`Hook '${hook.id}' failed, retrying...`);
return this.retryHook(hook, payload, context, hook.options.retries);
}
// Use fallback if provided
if (hook.options?.fallback) {
logger.warn(`Hook '${hook.id}' failed, using fallback`);
return hook.options.fallback(payload, context);
}
throw error;
}
}
private async retryHook(
hook: HookRegistration,
payload: HookPayload,
context: AgenticHookContext,
retriesLeft: number
): Promise<HookHandlerResult> {
for (let i = 0; i < retriesLeft; i++) {
try {
await this.delay(Math.pow(2, i) * 1000); // Exponential backoff
return await hook.handler(payload, context);
} catch (error) {
if (i === retriesLeft - 1) {
throw error; // Last retry failed
}
}
}
// Should not reach here
throw new Error('Retry logic error');
}
private async processSideEffects(
sideEffects: SideEffect[],
context: AgenticHookContext
): Promise<void> {
for (const effect of sideEffects) {
try {
await this.processSideEffect(effect, context);
} catch (error) {
logger.error(`Failed to process side effect: ${effect.type}`, error);
// Continue processing other side effects
}
}
}
private async processSideEffect(
effect: SideEffect,
context: AgenticHookContext
): Promise<void> {
switch (effect.type) {
case 'memory':
await this.processMemorySideEffect(effect, context);
break;
case 'neural':
await this.processNeuralSideEffect(effect, context);
break;
case 'metric':
this.processMetricSideEffect(effect);
break;
case 'notification':
this.processNotificationSideEffect(effect);
break;
case 'log':
this.processLogSideEffect(effect);
break;
}
}
private async processMemorySideEffect(
effect: SideEffect,
context: AgenticHookContext
): Promise<void> {
// Implement memory side effect processing
// This would integrate with the memory service
logger.debug(`Processing memory side effect: ${effect.action}`, effect.data);
}
private async processNeuralSideEffect(
effect: SideEffect,
context: AgenticHookContext
): Promise<void> {
// Implement neural side effect processing
// This would integrate with the neural service
logger.debug(`Processing neural side effect: ${effect.action}`, effect.data);
}
private processMetricSideEffect(effect: SideEffect): void {
if (effect.action === 'update') {
this.updateMetric(effect.data.name, effect.data.value);
} else if (effect.action === 'increment') {
this.updateMetric(effect.data.name, 1);
}
}
private processNotificationSideEffect(effect: SideEffect): void {
this.emit('notification', effect.data);
}
private processLogSideEffect(effect: SideEffect): void {
const { level = 'info', message, data } = effect.data;
logger[level as keyof Logger](message, data);
}
private async handleHookError(
hook: HookRegistration,
error: Error,
context: AgenticHookContext
): Promise<void> {
logger.error(`Hook '${hook.id}' error:`, error);
this.updateMetric('hooks.errors', 1);
this.updateMetric(`hooks.${hook.id}.errors`, 1);
this.emit('hook:error', {
hookId: hook.id,
type: hook.type,
error,
context,
});
}
private async executeStage(
stage: PipelineStage,
payload: HookPayload,
context: AgenticHookContext
): Promise<HookHandlerResult[]> {
if (stage.parallel) {
// Execute hooks in parallel
const promises = stage.hooks.map(hook =>
this.executeHook(hook, payload, context)
);
return Promise.all(promises);
} else {
// Execute hooks sequentially
const results: HookHandlerResult[] = [];
let currentPayload = payload;
for (const hook of stage.hooks) {
const result = await this.executeHook(hook, currentPayload, context);
results.push(result);
if (result.modified && result.payload) {
currentPayload = result.payload;
}
if (!result.continue) {
break;
}
}
return results;
}
}
private updatePipelineMetrics(
pipeline: HookPipeline,
duration: number,
hasError: boolean
): void {
const metrics = pipeline.metrics;
metrics.executions++;
metrics.avgDuration =
(metrics.avgDuration * (metrics.executions - 1) + duration) /
metrics.executions;
if (hasError) {
metrics.errorRate =
(metrics.errorRate * (metrics.executions - 1) + 1) /
metrics.executions;
} else {
metrics.errorRate =
(metrics.errorRate * (metrics.executions - 1)) /
metrics.executions;
}
// Calculate throughput (executions per minute)
const timeWindow = 60000; // 1 minute
metrics.throughput = (metrics.executions / duration) * timeWindow;
}
private async rollbackPipeline(
pipeline: HookPipeline,
results: HookHandlerResult[],
context: AgenticHookContext
): Promise<void> {
logger.warn(`Rolling back pipeline '${pipeline.name}'`);
// Implement rollback logic based on side effects in results
// This is a placeholder for actual rollback implementation
}
private getTotalHookCount(): number {
let count = 0;
for (const hookList of this.hooks.values()) {
count += hookList.length;
}
return count;
}
private initializeMetrics(): void {
this.metrics.set('hooks.registered', 0);
this.metrics.set('hooks.unregistered', 0);
this.metrics.set('hooks.executions', 0);
this.metrics.set('hooks.errors', 0);
this.metrics.set('hooks.cacheHits', 0);
this.metrics.set('hooks.totalDuration', 0);
}
private updateMetric(key: string, value: number): void {
const current = this.metrics.get(key) || 0;
this.metrics.set(key, current + value);
}
private generateExecutionId(): string {
return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private generatePipelineId(): string {
return `pipe_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private getCachedResult(
hookId: string,
cacheKey: string
): HookHandlerResult | null {
// Implement cache retrieval
// This is a placeholder
return null;
}
private cacheResult(
hookId: string,
cacheKey: string,
result: HookHandlerResult,
ttl: number
): void {
// Implement cache storage
// This is a placeholder
}
private async withTimeout<T>(
promise: Promise<T>,
timeout: number,
message: string
): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(message)), timeout)
),
]);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Export singleton instance
export const agenticHookManager = new AgenticHookManager();