UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

818 lines 27.4 kB
/** * CLM Action Monad - Composable Actions following Monad Laws. * * This module implements the Action monad for CLM, enabling composable * agent actions that follow the three Monad Laws: * * 1. Left Identity: return a >>= f ≡ f a * 2. Right Identity: m >>= return ≡ m * 3. Associativity: (m >>= f) >>= g ≡ m >>= (λx. f x >>= g) * * Actions are the atomic units of computation in CLM workflows. * Each action transforms context and produces a result while * accumulating effects (memory updates, tool calls, logs). * * REPL Paradigm Integration (prep → exec → post → await): * - prep: Load context, validate preconditions (V_pre) * - exec: Execute action logic * - post: Verify postconditions, generate VCard (V_post) * - await: Record effects, prepare for next cycle * * Aligned with the Lambda Calculus interpretation: * - α-conversion: Action renaming (identity preservation) * - β-reduction: Action execution (input substitution) * - η-conversion: Behavioral equivalence (same I/O behavior) */ import { OpenTelemetrySidecar, REPLPhase } from '../OpenTelemetrySidecar.js'; // ============ Types ============ /** * Status of an action execution. */ export var ActionStatus; (function (ActionStatus) { ActionStatus["PENDING"] = "pending"; ActionStatus["RUNNING"] = "running"; ActionStatus["SUCCESS"] = "success"; ActionStatus["FAILURE"] = "failure"; ActionStatus["CANCELLED"] = "cancelled"; })(ActionStatus || (ActionStatus = {})); /** * Create a default ActionContext. */ export function createActionContext(partial = {}) { return { sessionId: partial.sessionId ?? '', agentId: partial.agentId ?? '', config: partial.config ?? {}, secrets: partial.secrets ?? {}, params: partial.params ?? {}, timestamp: partial.timestamp ?? Date.now(), traceId: partial.traceId ?? '' }; } /** * Create a new context with updated params. */ export function withParams(ctx, params) { return { ...ctx, params: { ...ctx.params, ...params } }; } /** * Create an empty effect (monoid identity). */ export function emptyEffect() { return { memoryUpdates: [], toolCalls: [], logs: [], tokens: { prompt: 0, completion: 0 }, executionTime: 0 }; } /** * Combine two effects (monoid append). */ export function combineEffects(a, b) { return { memoryUpdates: [...a.memoryUpdates, ...b.memoryUpdates], toolCalls: [...a.toolCalls, ...b.toolCalls], logs: [...a.logs, ...b.logs], tokens: { prompt: a.tokens.prompt + b.tokens.prompt, completion: a.tokens.completion + b.tokens.completion }, executionTime: a.executionTime + b.executionTime }; } /** * Create a successful result with a value. */ export function pureResult(value) { return { success: true, value, effects: emptyEffect() }; } /** * Create a failed result with an error. */ export function failResult(error) { return { success: false, error, effects: emptyEffect() }; } /** * Functor map: Apply function to value if successful. */ export function mapResult(result, f) { if (result.success && result.value !== undefined) { try { return { success: true, value: f(result.value), effects: result.effects }; } catch (e) { return { success: false, error: String(e), effects: result.effects }; } } return { success: false, error: result.error, effects: result.effects }; } /** * The Action Monad - A composable unit of computation. * * An Action encapsulates: * - An async function from context to result * - The ability to compose with other actions (bind/flatMap) * - Effects accumulated during execution (Writer) * - Context propagation (Reader) * - Error handling (Either) * * Satisfies the Monad Laws: * * 1. Left Identity: Action.pure(a).bind(f) == f(a) * 2. Right Identity: m.bind(Action.pure) == m * 3. Associativity: m.bind(f).bind(g) == m.bind(x => f(x).bind(g)) */ export class Action { _run; /** * Create an action from a function. * * @param run - Async function (ActionContext -> ActionResult<A>) */ constructor(run) { this._run = run; } /** * Execute this action with the given context. * * @param ctx - The execution context * @returns The result of the action */ async execute(ctx) { const startTime = Date.now(); try { const result = await this._run(ctx); // Add execution time to effects result.effects.executionTime += Date.now() - startTime; return result; } catch (e) { const elapsed = Date.now() - startTime; return { success: false, error: String(e), effects: { ...emptyEffect(), executionTime: elapsed, logs: [`Exception: ${e}`] } }; } } /** * Monadic bind (>>=, flatMap). * * Sequences this action with a function that produces * another action based on this action's result. * * This is the KEY operation that makes Action a monad. * * @param f - Function from A to Action<B> * @returns A new action that chains the computations */ bind(f) { const self = this; const boundRun = async (ctx) => { // Execute this action const resultA = await self.execute(ctx); if (!resultA.success) { // Propagate failure with accumulated effects return { success: false, error: resultA.error, effects: resultA.effects }; } // Apply f to get the next action const actionB = f(resultA.value); // Execute the next action const resultB = await actionB.execute(ctx); // Combine effects from both actions const combinedEffects = combineEffects(resultA.effects, resultB.effects); return { success: resultB.success, value: resultB.value, error: resultB.error, effects: combinedEffects }; }; return new Action(boundRun); } /** * Functor map. * * Apply a pure function to the action's result. * * @param f - Pure function from A to B * @returns A new action with the transformed result */ map(f) { return this.bind(a => Action.pure(f(a))); } /** * Sequence with another action, ignoring this result. * * Useful for side-effect-only actions. * * @param nextAction - The action to execute after this one * @returns The result of nextAction */ then(nextAction) { return this.bind(_ => nextAction); } // ============ Static Constructors ============ /** * Lift a pure value into Action (return/unit). * * This is the 'return' operation of the monad. * * Satisfies Left Identity: pure(a).bind(f) == f(a) * Satisfies Right Identity: m.bind(pure) == m * * @param value - The value to lift * @returns An action that immediately succeeds with the value */ static pure(value) { return new Action(async (_ctx) => pureResult(value)); } /** * Create a failing action. * * @param error - The error message * @returns An action that immediately fails */ static fail(error) { return new Action(async (_ctx) => failResult(error)); } /** * Reader monad operation: Get the current context. * * @returns An action that returns the context */ static ask() { return new Action(async (ctx) => pureResult(ctx)); } /** * Reader monad operation: Apply function to context. * * @param f - Function to apply to context * @returns An action that returns f(context) */ static asks(f) { return Action.ask().map(f); } /** * Writer monad operation: Emit an effect. * * @param effect - The effect to emit * @returns An action that emits the effect */ static tell(effect) { return new Action(async (_ctx) => ({ success: true, value: undefined, effects: effect })); } /** * Convenience: Log a message. * * @param message - The log message * @returns An action that logs the message */ static log(message) { return Action.tell({ ...emptyEffect(), logs: [message] }); } /** * Create an action from an async function. * * @param f - Async function (ctx -> A) * @returns An action wrapping the function */ static fromAsync(f) { return new Action(async (ctx) => { try { const result = await f(ctx); return pureResult(result); } catch (e) { return failResult(String(e)); } }); } /** * Create an action from a synchronous function. * * @param f - Sync function (ctx -> A) * @returns An action wrapping the function */ static fromSync(f) { return new Action(async (ctx) => { try { const result = f(ctx); return pureResult(result); } catch (e) { return failResult(String(e)); } }); } } // ============ Action Composition Utilities ============ /** * Sequence a list of actions, collecting results. * * @param actions - List of actions to sequence * @returns An action that produces a list of results */ export function sequence(actions) { if (actions.length === 0) { return Action.pure([]); } return new Action(async (ctx) => { const results = []; let combinedEffects = emptyEffect(); for (const action of actions) { const result = await action.execute(ctx); combinedEffects = combineEffects(combinedEffects, result.effects); if (!result.success) { return { success: false, error: result.error, effects: combinedEffects }; } results.push(result.value); } return { success: true, value: results, effects: combinedEffects }; }); } /** * Execute actions in parallel, collecting results. * * @param actions - List of actions to execute in parallel * @returns An action that produces a list of results */ export function parallel(actions) { if (actions.length === 0) { return Action.pure([]); } return new Action(async (ctx) => { // Execute all actions concurrently const resultPromises = actions.map(action => action.execute(ctx)); const results = await Promise.all(resultPromises); // Combine effects and check for failures let combinedEffects = emptyEffect(); const values = []; for (const result of results) { combinedEffects = combineEffects(combinedEffects, result.effects); if (!result.success) { return { success: false, error: result.error, effects: combinedEffects }; } values.push(result.value); } return { success: true, value: values, effects: combinedEffects }; }); } /** * Kleisli composition: Compose two monadic functions. * * (g <=< f)(a) = f(a).bind(g) * * This is the categorical composition in the Kleisli category. * * @param f - First Kleisli arrow (A -> Action<B>) * @param g - Second Kleisli arrow (B -> Action<C>) * @returns Composed Kleisli arrow (A -> Action<C>) */ export function kleisliCompose(f, g) { return (a) => f(a).bind(g); } /** * The identity Kleisli arrow. * * This is Action.pure, serving as the identity for Kleisli composition. * * @returns The identity function in the Kleisli category */ export function identityAction() { return Action.pure; } // ============ Monad Law Verification ============ /** * Verify Left Identity law: pure(a).bind(f) == f(a) * * @param a - A value * @param f - A Kleisli arrow * @param ctx - Execution context * @returns True if the law holds */ export async function verifyLeftIdentity(a, f, ctx) { const left = await Action.pure(a).bind(f).execute(ctx); const right = await f(a).execute(ctx); return (left.success === right.success && JSON.stringify(left.value) === JSON.stringify(right.value) && left.error === right.error); } /** * Verify Right Identity law: m.bind(pure) == m * * @param m - An action * @param ctx - Execution context * @returns True if the law holds */ export async function verifyRightIdentity(m, ctx) { const left = await m.bind(Action.pure).execute(ctx); const right = await m.execute(ctx); return (left.success === right.success && JSON.stringify(left.value) === JSON.stringify(right.value) && left.error === right.error); } /** * Verify Associativity law: (m.bind(f)).bind(g) == m.bind(x => f(x).bind(g)) * * @param m - An action * @param f - First Kleisli arrow * @param g - Second Kleisli arrow * @param ctx - Execution context * @returns True if the law holds */ export async function verifyAssociativity(m, f, g, ctx) { const left = await m.bind(f).bind(g).execute(ctx); const right = await m.bind(x => f(x).bind(g)).execute(ctx); return (left.success === right.success && JSON.stringify(left.value) === JSON.stringify(right.value) && left.error === right.error); } /** * Contract-aware Action that produces VCard pairs. * * Extends the base Action monad with Hoare Logic contracts: * - Verifies preconditions before execution * - Verifies postconditions after execution * - Produces VCard pair as verification evidence */ export class ContractAction extends Action { contract; constructor(run, contract) { super(run); this.contract = contract; } /** * Get the contract for this action. */ getContract() { return this.contract; } /** * Verify preconditions against the context. */ verifyPreconditions(ctx) { const conditions = this.contract.preconditions.map(cond => { try { const satisfied = cond.check(ctx); return { name: cond.name, expression: cond.expression, satisfied }; } catch (e) { return { name: cond.name, expression: cond.expression, satisfied: false, error: String(e) }; } }); return { phase: 'pre', conditions, allSatisfied: conditions.every(c => c.satisfied), timestamp: Date.now() }; } /** * Verify postconditions against the result. */ verifyPostconditions(result) { const conditions = this.contract.postconditions.map(cond => { try { const satisfied = cond.check(result); return { name: cond.name, expression: cond.expression, satisfied }; } catch (e) { return { name: cond.name, expression: cond.expression, satisfied: false, error: String(e) }; } }); return { phase: 'post', conditions, allSatisfied: conditions.every(c => c.satisfied), timestamp: Date.now() }; } /** * Create a PreCondition VCard from verification result. * Returns a hash representing the VCard (in production, would store in MCard collection). */ createPreConditionVCard(verification, actionHash) { const vcard = { type: 'PreConditionVCard', actionHash, timestamp: new Date(verification.timestamp).toISOString(), preconditions: verification.conditions, certification: { allSatisfied: verification.allSatisfied, checkedAt: new Date(verification.timestamp).toISOString(), certifier: 'ptr_engine_v0.2.0' } }; // In production: store as MCard and return hash // For now: return a deterministic hash representation return `vcard:pre:${actionHash}:${verification.timestamp}`; } /** * Create a PostCondition VCard from verification result. * Links back to the PreCondition VCard. */ createPostConditionVCard(verification, actionHash, preVCardHash, result) { const vcard = { type: 'PostConditionVCard', actionHash, timestamp: new Date(verification.timestamp).toISOString(), preConditionVCardHash: preVCardHash, postconditions: verification.conditions, executionSummary: { success: result.success, executionTimeMs: result.effects.executionTime, effectsAccumulated: { logCount: result.effects.logs.length, memoryUpdates: result.effects.memoryUpdates.length, toolCalls: result.effects.toolCalls.length } }, certification: { allSatisfied: verification.allSatisfied, checkedAt: new Date(verification.timestamp).toISOString(), certifier: 'ptr_engine_v0.2.0' } }; // In production: store as MCard and return hash return `vcard:post:${actionHash}:${verification.timestamp}`; } /** * Generate a hash for this action (based on contract). */ getActionHash() { const contractSummary = { preConditions: this.contract.preconditions.map(c => c.name), postConditions: this.contract.postconditions.map(c => c.name) }; return `action:${JSON.stringify(contractSummary)}`.substring(0, 64); } /** * Execute with full contract verification, producing VCard pair. * * This is the primary method for contract-aware execution: * 1. Verify preconditions → create PreCondition VCard * 2. Execute action (only if preconditions pass) * 3. Verify postconditions → create PostCondition VCard * 4. Return result with VCard pair */ async executeWithContract(ctx) { const actionHash = this.getActionHash(); // Phase 1: Verify preconditions const preVerification = this.verifyPreconditions(ctx); const preVCard = this.createPreConditionVCard(preVerification, actionHash); // If preconditions fail, return early with failure if (!preVerification.allSatisfied) { const failedConditions = preVerification.conditions .filter(c => !c.satisfied) .map(c => c.name) .join(', '); const failedResult = failResult(`Precondition failed: ${failedConditions}`); const postVerification = { phase: 'post', conditions: [], allSatisfied: false, timestamp: Date.now() }; return { result: failedResult, vCardPair: { preConditionVCard: preVCard, postConditionVCard: '', actionHash, linkedAt: Date.now() }, preVerification, postVerification }; } // Phase 2: Execute action const result = await this.execute(ctx); // Phase 3: Verify postconditions const postVerification = result.success ? this.verifyPostconditions(result) : { phase: 'post', conditions: [], allSatisfied: false, timestamp: Date.now() }; const postVCard = this.createPostConditionVCard(postVerification, actionHash, preVCard, result); return { result, vCardPair: { preConditionVCard: preVCard, postConditionVCard: postVCard, actionHash, linkedAt: Date.now() }, preVerification, postVerification }; } /** * Execute with full REPL cycle instrumentation. * * Implements the REPL paradigm with observability: * - prep: Load context, validate preconditions (V_pre) * - exec: Execute action logic * - post: Verify postconditions, generate VCard (V_post) * - await: Record effects, cache result * * @param ctx - The action context * @returns ContractExecutionResult with VCard pair and REPL metrics */ async executeWithREPL(ctx) { const sidecar = OpenTelemetrySidecar.getInstance(); const actionHash = this.getActionHash(); // ============================================================ // PHASE 1: PREP (Read) - Validate preconditions // ============================================================ const prepSpan = sidecar.startPhase(REPLPhase.PREP, { attributes: { actionHash } }); const preVerification = this.verifyPreconditions(ctx); const preVCard = this.createPreConditionVCard(preVerification, actionHash); sidecar.endPhase(prepSpan, preVerification.allSatisfied, { preconditionCount: this.contract.preconditions.length, allSatisfied: preVerification.allSatisfied }); // If preconditions fail, skip exec if (!preVerification.allSatisfied) { const failedConditions = preVerification.conditions .filter(c => !c.satisfied) .map(c => c.name) .join(', '); const failedResult = failResult(`Precondition failed: ${failedConditions}`); const postVerification = { phase: 'post', conditions: [], allSatisfied: false, timestamp: Date.now() }; return { result: failedResult, vCardPair: { preConditionVCard: preVCard, postConditionVCard: '', actionHash, linkedAt: Date.now() }, preVerification, postVerification }; } // ============================================================ // PHASE 2: EXEC (Evaluate) - Execute action // ============================================================ const execSpan = sidecar.startPhase(REPLPhase.EXEC, { attributes: { actionHash } }); const result = await this.execute(ctx); sidecar.endPhase(execSpan, result.success, { executionTimeMs: result.effects.executionTime }); // ============================================================ // PHASE 3: POST (Print) - Verify postconditions, generate VCard // ============================================================ const postSpan = sidecar.startPhase(REPLPhase.POST, { attributes: { actionHash } }); const postVerification = result.success ? this.verifyPostconditions(result) : { phase: 'post', conditions: [], allSatisfied: false, timestamp: Date.now() }; const postVCard = this.createPostConditionVCard(postVerification, actionHash, preVCard, result); sidecar.endPhase(postSpan, postVerification.allSatisfied, { postconditionCount: this.contract.postconditions.length, allSatisfied: postVerification.allSatisfied }); // ============================================================ // PHASE 4: AWAIT (Loop) - Record effects, return result // ============================================================ const awaitSpan = sidecar.startPhase(REPLPhase.AWAIT, { attributes: { actionHash } }); const executionResult = { result, vCardPair: { preConditionVCard: preVCard, postConditionVCard: postVCard, actionHash, linkedAt: Date.now() }, preVerification, postVerification }; sidecar.endPhase(awaitSpan, true, { memoryUpdates: result.effects.memoryUpdates.length, toolCalls: result.effects.toolCalls.length, logCount: result.effects.logs.length }); return executionResult; } /** * Compose contract-aware actions, chaining VCard pairs. */ bindWithContract(f) { const self = this; // Merged contract: preconditions from self, postconditions from f's result const mergedContract = { preconditions: this.contract.preconditions, postconditions: [] // Will be filled by the bound action }; const boundRun = async (ctx) => { const resultA = await self.execute(ctx); if (!resultA.success) { return { success: false, error: resultA.error, effects: resultA.effects }; } const actionB = f(resultA.value); const resultB = await actionB.execute(ctx); return { success: resultB.success, value: resultB.value, error: resultB.error, effects: combineEffects(resultA.effects, resultB.effects) }; }; return new ContractAction(boundRun, mergedContract); } // ============ Static Constructors ============ /** * Create a contract-aware action from a pure value. */ static pureWithContract(value, contract) { const defaultContract = contract ?? { preconditions: [], postconditions: [{ name: 'has_value', expression: 'result.value !== undefined', check: r => r.value !== undefined }] }; return new ContractAction(async (_ctx) => pureResult(value), defaultContract); } /** * Create a contract-aware action from an async function. */ static fromAsyncWithContract(f, contract) { return new ContractAction(async (ctx) => { try { const result = await f(ctx); return pureResult(result); } catch (e) { return failResult(String(e)); } }, contract); } } //# sourceMappingURL=Action.js.map