GCAP-SPEC-FUNCTION-DESCRIPTOR-0001 implementation. Mirrors connector runtime pattern exactly. FunctionPlugin — trigger_events, handle(), descriptor(), knative_manifest() FunctionRegistry — trigger_index for event-driven routing FunctionRuntime — invoke() + dispatch() with Chronicle lineage governed_function decorator — SDK surface for function authors BillingProcessor — GSAP_CR_RECEIVED → billable event with Chronicle CID EchoFunction — dev/test API: /functions/ catalogue, invoke, dispatch, manifest, health 8 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""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
|