import { ZodSchema } from 'zod'; import type { RetryPolicy } from '../types'; import type { ExpressionContext } from '../expressionEngine'; export type ActionId = string; export type ActionIdempotency = | { mode: 'engineProvided' } | { mode: 'actionProvided'; key: (input: any, ctx: ActionContext) => string }; export type ActionUI = { label: string; description?: string; category?: string; icon?: string; }; export type ActionDef = { id: ActionId; version: number; inputSchema: ZodSchema; outputSchema: ZodSchema; sideEffectful: boolean; retryHint?: RetryPolicy; idempotency: ActionIdempotency; ui?: ActionUI; examples?: Record; handler: (input: I, ctx: ActionContext) => Promise; }; export type ActionContext = { runId: string; stepPath: string; stepConfig?: unknown; expressionContext?: ExpressionContext; tenantId?: string | null; idempotencyKey: string; attempt: number; logger?: { info: (msg: string, meta?: unknown) => void; warn: (msg: string, meta?: unknown) => void; error: (msg: string, meta?: unknown) => void }; nowIso: () => string; env: Record; knex?: any; }; export type ActionMeta = { id: string; version: number; sideEffectful: boolean; retryHint?: RetryPolicy; idempotency: { mode: 'engineProvided' | 'actionProvided' }; ui?: ActionUI; inputSchema: ZodSchema; outputSchema: ZodSchema; examples?: Record; }; export class ActionRegistry { private actions = new Map>(); register(def: ActionDef): void { if (!def.id || !def.version) { throw new Error('ActionRegistry.register requires id and version'); } if (!def.inputSchema || !def.outputSchema) { throw new Error(`Action ${def.id}@${def.version} must include inputSchema and outputSchema`); } if (def.sideEffectful && !def.idempotency) { throw new Error(`Action ${def.id}@${def.version} must define idempotency strategy`); } const key = this.key(def.id, def.version); if (this.actions.has(key)) { throw new Error(`ActionRegistry already has ${key}`); } this.actions.set(key, def); } get(id: string, version: number): ActionDef | undefined { return this.actions.get(this.key(id, version)); } list(): ActionMeta[] { return Array.from(this.actions.values()).map((action) => ({ id: action.id, version: action.version, sideEffectful: action.sideEffectful, retryHint: action.retryHint, idempotency: { mode: action.idempotency.mode }, ui: action.ui, inputSchema: action.inputSchema, outputSchema: action.outputSchema, examples: action.examples })); } listById(id: string): ActionDef[] { return Array.from(this.actions.values()).filter((action) => action.id === id); } private key(id: string, version: number): string { return `${id}@${version}`; } } let registryInstance: ActionRegistry | null = null; export function getActionRegistryV2(): ActionRegistry { if (!registryInstance) { registryInstance = new ActionRegistry(); } return registryInstance; }