diff --git a/docs/memory-graph-runtime.md b/docs/memory-graph-runtime.md index 10f7d51..d1dd451 100644 --- a/docs/memory-graph-runtime.md +++ b/docs/memory-graph-runtime.md @@ -46,6 +46,10 @@ with `markitect-tool`. - `MemoryRuntimeService.export_runtime_envelope()` emits a portable runtime envelope containing graph nodes, edges, memory events, and audit traces with operation id, actor, policy decision, and filter metadata. +- `MemoryRuntimeAdapterCapability`, `MemorySemanticIndex`, + `MemoryAuditPublisher`, and `MemoryRuntimeRegistry` define optional adapter + boundaries for vector/semantic indexes, durable audit sinks, remote registries, + and sibling memory runtimes. ## Boundary @@ -64,5 +68,24 @@ with `markitect-tool`. - retention, refresh, compaction, review gates, and audit behavior - agent-safe update plans and Markitect-compatible export envelopes +## External Adapter Boundaries + +External graph databases should implement `MemoryGraphRepository`; they are +storage adapters, not alternate memory domain models. Enterprise policy systems +should implement `PolicyGateway`; policy decisions remain explicit inputs to +runtime operations. + +Optional advanced adapters use small dedicated ports: + +- `MemorySemanticIndex` for vector or hybrid retrieval indexes. +- `MemoryAuditPublisher` for durable event sinks, webhooks, or SIEM pipelines. +- `MemoryRuntimeRegistry` for remote memory registries and sibling runtimes + such as future phase-memory services. + +Adapters advertise `MemoryRuntimeAdapterCapability` with adapter kind, +supported operations, optional dependencies, and whether they are deterministic +local adapters. Adapter payloads must be serializable dictionaries or core +runtime records, so core tests can stay deterministic and dependency-free. + `infospace-bench` should consume these records and Markitect fixtures to measure retrieval quality, latency, budget pressure, and regression behavior. diff --git a/src/kontextual_engine/__init__.py b/src/kontextual_engine/__init__.py index 7ce2fea..cfeb146 100644 --- a/src/kontextual_engine/__init__.py +++ b/src/kontextual_engine/__init__.py @@ -116,7 +116,11 @@ from .ports import ( BlobWriteResult, DirectorySourceConnector, FormatExtractor, + MemoryAuditPublisher, MemoryGraphRepository, + MemoryRuntimeAdapterCapability, + MemoryRuntimeRegistry, + MemorySemanticIndex, PolicyGateway, SourceConnector, ) @@ -271,6 +275,7 @@ __all__ = [ "MetadataSchemaAssignment", "MetadataValidationIssue", "MetadataValueType", + "MemoryAuditPublisher", "MemoryCompactionRequest", "MemoryEdgeRecord", "MemoryEventRecord", @@ -292,7 +297,10 @@ __all__ = [ "MemoryRetentionRequest", "MemoryRuntimeExportRequest", "MemoryRuntimeExportResult", + "MemoryRuntimeAdapterCapability", + "MemoryRuntimeRegistry", "MemoryRuntimeService", + "MemorySemanticIndex", "MemorySourceSpan", "MemoryUpdatePlan", "MemoryUpdateRequest", diff --git a/src/kontextual_engine/ports/__init__.py b/src/kontextual_engine/ports/__init__.py index 0013e66..bcfe9e8 100644 --- a/src/kontextual_engine/ports/__init__.py +++ b/src/kontextual_engine/ports/__init__.py @@ -9,7 +9,13 @@ from .blob_storage import ( digest_storage_key, ) from .ingestion import DirectorySourceConnector, FormatExtractor, SourceConnector -from .memory import MemoryGraphRepository +from .memory import ( + MemoryAuditPublisher, + MemoryGraphRepository, + MemoryRuntimeAdapterCapability, + MemoryRuntimeRegistry, + MemorySemanticIndex, +) from .policy import AllowAllPolicyGateway, PolicyGateway from .repositories import AssetRegistryRepository @@ -24,7 +30,11 @@ __all__ = [ "digest_storage_key", "DirectorySourceConnector", "FormatExtractor", + "MemoryAuditPublisher", "MemoryGraphRepository", + "MemoryRuntimeAdapterCapability", + "MemoryRuntimeRegistry", + "MemorySemanticIndex", "PolicyGateway", "SourceConnector", ] diff --git a/src/kontextual_engine/ports/memory.py b/src/kontextual_engine/ports/memory.py index 3292eb0..ef995ee 100644 --- a/src/kontextual_engine/ports/memory.py +++ b/src/kontextual_engine/ports/memory.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import Protocol +from dataclasses import dataclass, field +from typing import Any, Protocol from kontextual_engine.core import ( AuditEvent, @@ -14,6 +15,30 @@ from kontextual_engine.core import ( ) +@dataclass(frozen=True) +class MemoryRuntimeAdapterCapability: + adapter_name: str + adapter_kind: str + supported_operations: tuple[str, ...] = () + required_dependencies: tuple[str, ...] = () + deterministic_local: bool = False + metadata: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__(self, "supported_operations", tuple(self.supported_operations)) + object.__setattr__(self, "required_dependencies", tuple(self.required_dependencies)) + + def to_dict(self) -> dict[str, Any]: + return { + "adapter_name": self.adapter_name, + "adapter_kind": self.adapter_kind, + "supported_operations": list(self.supported_operations), + "required_dependencies": list(self.required_dependencies), + "deterministic_local": self.deterministic_local, + "metadata": dict(self.metadata), + } + + class MemoryGraphRepository(Protocol): def save_memory_profile(self, profile: MemoryProfileRecord) -> MemoryProfileRecord: ... def get_memory_profile(self, profile_id: str) -> MemoryProfileRecord: ... @@ -56,3 +81,38 @@ class MemoryGraphRepository(Protocol): correlation_id: str | None = None, operation: str | None = None, ) -> list[AuditEvent]: ... + + +class MemorySemanticIndex(Protocol): + name: str + + def capabilities(self) -> MemoryRuntimeAdapterCapability: ... + + def upsert_memory_nodes(self, nodes: list[MemoryNodeRecord]) -> dict[str, Any]: ... + + def query_memory_nodes( + self, + *, + graph_id: str, + query: str, + limit: int = 10, + filters: dict[str, Any] | None = None, + ) -> list[dict[str, Any]]: ... + + +class MemoryAuditPublisher(Protocol): + name: str + + def capabilities(self) -> MemoryRuntimeAdapterCapability: ... + + def publish_memory_audit_event(self, event: AuditEvent) -> dict[str, Any]: ... + + +class MemoryRuntimeRegistry(Protocol): + name: str + + def capabilities(self) -> MemoryRuntimeAdapterCapability: ... + + def publish_runtime_envelope(self, envelope: dict[str, Any]) -> dict[str, Any]: ... + + def fetch_runtime_envelope(self, reference: str) -> dict[str, Any]: ... diff --git a/tests/test_memory_runtime_adapter_boundaries.py b/tests/test_memory_runtime_adapter_boundaries.py new file mode 100644 index 0000000..44b4cf7 --- /dev/null +++ b/tests/test_memory_runtime_adapter_boundaries.py @@ -0,0 +1,140 @@ +from kontextual_engine import ( + Actor, + ActorType, + AuditEvent, + AuditOutcome, + MemoryAuditPublisher, + MemoryRuntimeAdapterCapability, + MemoryRuntimeRegistry, + MemorySemanticIndex, + OperationContext, +) + + +def test_memory_runtime_adapter_capability_serializes_dependency_boundary() -> None: + capability = MemoryRuntimeAdapterCapability( + adapter_name="local-semantic-index", + adapter_kind="semantic_index", + supported_operations=("upsert_memory_nodes", "query_memory_nodes"), + required_dependencies=("numpy",), + deterministic_local=True, + metadata={"ownership": "optional-adapter"}, + ) + + assert capability.to_dict() == { + "adapter_name": "local-semantic-index", + "adapter_kind": "semantic_index", + "supported_operations": ["upsert_memory_nodes", "query_memory_nodes"], + "required_dependencies": ["numpy"], + "deterministic_local": True, + "metadata": {"ownership": "optional-adapter"}, + } + + +def test_optional_memory_adapter_ports_exchange_serializable_payloads() -> None: + semantic_index: MemorySemanticIndex = FakeSemanticIndex() + audit_publisher: MemoryAuditPublisher = CapturingAuditPublisher() + registry: MemoryRuntimeRegistry = InMemoryRuntimeRegistry() + context = OperationContext.create( + Actor.create(ActorType.AI_AGENT, actor_id="agent-adapter-test"), + correlation_id="corr-adapter-boundary", + ) + audit_event = AuditEvent.from_context( + "memory.runtime.export", + "memory-graph:memgraph-test", + AuditOutcome.SUCCESS, + context, + details={"operation_id": "memexport-test"}, + ) + + upsert_result = semantic_index.upsert_memory_nodes([]) + query_result = semantic_index.query_memory_nodes(graph_id="memgraph-test", query="contract boundary") + audit_result = audit_publisher.publish_memory_audit_event(audit_event) + registry_result = registry.publish_runtime_envelope( + { + "schema_version": "kontextual.memory.runtime-export.v1", + "graph_id": "memgraph-test", + "nodes": [], + "audit_events": [audit_event.to_dict()], + } + ) + fetched = registry.fetch_runtime_envelope(registry_result["reference"]) + + assert semantic_index.capabilities().adapter_kind == "semantic_index" + assert upsert_result == {"upserted": 0} + assert query_result[0]["graph_id"] == "memgraph-test" + assert audit_result["event_id"] == audit_event.event_id + assert registry_result["reference"] == "memory-registry://memgraph-test" + assert fetched["audit_events"][0]["operation"] == "memory.runtime.export" + + +class FakeSemanticIndex: + name = "fake-semantic-index" + + def capabilities(self) -> MemoryRuntimeAdapterCapability: + return MemoryRuntimeAdapterCapability( + adapter_name=self.name, + adapter_kind="semantic_index", + supported_operations=("upsert_memory_nodes", "query_memory_nodes"), + deterministic_local=True, + ) + + def upsert_memory_nodes(self, nodes: list) -> dict: + return {"upserted": len(nodes)} + + def query_memory_nodes( + self, + *, + graph_id: str, + query: str, + limit: int = 10, + filters: dict | None = None, + ) -> list[dict]: + return [ + { + "graph_id": graph_id, + "node_id": "memnode-test", + "score": 1.0, + "query": query, + "filters": filters or {}, + "limit": limit, + } + ] + + +class CapturingAuditPublisher: + name = "capturing-audit-publisher" + + def capabilities(self) -> MemoryRuntimeAdapterCapability: + return MemoryRuntimeAdapterCapability( + adapter_name=self.name, + adapter_kind="audit_sink", + supported_operations=("publish_memory_audit_event",), + deterministic_local=True, + ) + + def publish_memory_audit_event(self, event: AuditEvent) -> dict: + return {"published": True, "event_id": event.event_id, "operation": event.operation} + + +class InMemoryRuntimeRegistry: + name = "in-memory-runtime-registry" + + def __init__(self) -> None: + self.envelopes: dict[str, dict] = {} + + def capabilities(self) -> MemoryRuntimeAdapterCapability: + return MemoryRuntimeAdapterCapability( + adapter_name=self.name, + adapter_kind="runtime_registry", + supported_operations=("publish_runtime_envelope", "fetch_runtime_envelope"), + deterministic_local=True, + ) + + def publish_runtime_envelope(self, envelope: dict) -> dict: + reference = f"memory-registry://{envelope['graph_id']}" + self.envelopes[reference] = dict(envelope) + return {"reference": reference, "graph_id": envelope["graph_id"]} + + def fetch_runtime_envelope(self, reference: str) -> dict: + return self.envelopes[reference] diff --git a/workplans/KONT-WP-0017-agentic-memory-graph-runtime.md b/workplans/KONT-WP-0017-agentic-memory-graph-runtime.md index 0a0df7c..62bdc90 100644 --- a/workplans/KONT-WP-0017-agentic-memory-graph-runtime.md +++ b/workplans/KONT-WP-0017-agentic-memory-graph-runtime.md @@ -4,7 +4,7 @@ type: workplan title: "Agentic Memory Graph Runtime And Operational State" domain: markitect repo: kontextual-engine -status: active +status: completed owner: codex topic_slug: markitect planning_priority: medium @@ -104,6 +104,11 @@ and operation. Runtime export envelopes include graph nodes, edges, memory events, audit traces, operation id, actor metadata, policy decisions, and filter metadata for portable inspection or handoff. +The external adapter boundary slice is implemented. Optional semantic indexes, +audit sinks, and runtime registries are defined as ports with capability +metadata, while graph databases continue to implement `MemoryGraphRepository` +and enterprise policy systems continue to implement `PolicyGateway`. + ## P17.1 - Import and map Markitect memory contracts ```task @@ -242,7 +247,7 @@ Output: audit model, observability events, export fixtures, and tests. ```task id: KONT-WP-0017-T007 -status: todo +status: done priority: medium state_hub_task_id: "c3cf79d8-389c-4583-a02d-303d0ce75cc1" ```