"""FunctionRuntime — governed invocation with Chronicle emission.""" from __future__ import annotations import time import uuid from typing import Any from .base import FunctionContext, FunctionResult from .registry import FunctionRegistry class FunctionRuntime: def __init__( self, registry: FunctionRegistry, chronicle_client: Any = None ) -> None: self.registry = registry self.chronicle_client = chronicle_client async def invoke( self, function_id: str, event: dict[str, Any], context: FunctionContext, ) -> FunctionResult: """Direct invocation of a function by ID.""" function = self.registry.get(function_id) if function is None: return FunctionResult(success=False, error=f"Unknown function: {function_id}") if function.gsap_required and not context.gsap_context_id: return FunctionResult(success=False, error="GSAP context required but not provided") if not context.invocation_id: context.invocation_id = str(uuid.uuid4()) # Emit FUNCTION_INVOKED (0x2A01) before execution await self._emit_chronicle( "FUNCTION_INVOKED", { "event_code": "0x2A01", "function_id": function_id, "trigger_event_kind": context.trigger_event_kind, "trigger_event_cid": context.trigger_event_cid, "chronicle_session_id": context.chronicle_session_id, "gsap_context_id": context.gsap_context_id, "invocation_id": context.invocation_id, "pipeline_run_id": context.pipeline_run_id, }, function, ) start = time.monotonic() result = await function.handle(event, context) result.duration_ms = (time.monotonic() - start) * 1000 # Emit FUNCTION_COMPLETED (0x2A02) after execution await self._emit_chronicle( "FUNCTION_COMPLETED", { "event_code": "0x2A02", "function_id": function_id, "invocation_id": context.invocation_id, "success": result.success, "duration_ms": result.duration_ms, "output_cid": result.output_cid() if result.data is not None else "", "error": result.error, }, function, ) return result async def dispatch( self, event_kind: str, event_data: dict[str, Any], trigger_cid: str = "", ) -> list[dict[str, Any]]: """Route an event to all functions registered for this event kind.""" functions = self.registry.by_trigger(event_kind) results = [] for function in functions: context = FunctionContext( trigger_event_kind=event_kind, trigger_event_cid=trigger_cid, invocation_id=str(uuid.uuid4()), ) result = await self.invoke(function.function_id, event_data, context) results.append({ "function_id": function.function_id, "success": result.success, "data": result.data, "error": result.error, "duration_ms": result.duration_ms, "lineage_cid": result.lineage_cid, }) return results async def _emit_chronicle( self, event_type: str, payload: dict[str, Any], function: Any ) -> None: if function.chronicle_enabled and self.chronicle_client is not None: try: await self.chronicle_client.emit(event_type, payload) except Exception: pass # Chronicle failure must not break invocation