feat(memory): define external adapter boundaries

This commit is contained in:
2026-05-15 11:23:50 +02:00
parent 0008b67b21
commit c94efeb83d
6 changed files with 250 additions and 4 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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",
]

View File

@@ -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]: ...

View File

@@ -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]

View File

@@ -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"
```