feat(memory): add graph runtime import store

This commit is contained in:
2026-05-15 01:56:51 +02:00
parent 5c450fcaa5
commit 8daab687b2
12 changed files with 1103 additions and 4 deletions

View File

@@ -0,0 +1,42 @@
# Memory Graph Runtime
`kontextual-engine` owns the operational side of the Markitect memory graph
contracts. The engine imports Markitect-compatible graph/profile envelopes,
maps source contract ids to stable runtime ids, and stores graph nodes, edges,
and append-only events for later governed retrieval and lifecycle operations.
This repository does not redefine the Markitect syntax vocabulary. The first
runtime slice checks only schema compatibility, required ids, edge endpoint
integrity, and append-only event identity. Node and edge kind validation remains
with `markitect-tool`.
## Implemented Slice
- `MemoryGraphImportResult` maps `markitect.memory.graph.v1` contracts into
runtime records.
- `MemoryProfileRecord` maps `markitect.memory.profile.v1` profiles without
enforcing runtime policy, latency, retention, or compaction declarations yet.
- `MemoryGraphRepository` defines the storage port for profile, node, edge, and
event records.
- `InMemoryMemoryGraphRepository` provides deterministic local storage for
tests and future service wiring.
- `MemoryRuntimeService.import_markitect_graph()` persists an imported graph and
can attach an audit event when an `OperationContext` is supplied.
## Boundary
`markitect-tool` remains responsible for:
- profile and graph syntax validation
- graph selection compilation into context packages
- deterministic package summaries and source-span preservation
`kontextual-engine` is responsible for:
- runtime ids and persistence
- append-only event storage
- permission-aware retrieval and context assembly
- retention, refresh, compaction, review gates, and audit behavior
`infospace-bench` should consume these records and Markitect fixtures to measure
retrieval quality, latency, budget pressure, and regression behavior.

View File

@@ -13,6 +13,7 @@ from .artifacts import (
)
from .adapters.memory import InMemoryAssetRegistryRepository
from .adapters.memory import InMemoryBlobStorage
from .adapters.memory import InMemoryMemoryGraphRepository
from .adapters.local_files import LocalBlobStorage
from .adapters.s3 import S3BlobStorage
from .adapters.sqlite import SQLiteAssetRegistryRepository
@@ -49,12 +50,20 @@ from .core import (
IngestionJobStatus,
KnowledgeAsset,
LifecycleState,
MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION,
MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION,
MetadataFieldDefinition,
MetadataRecord,
MetadataSchema,
MetadataSchemaAssignment,
MetadataValidationIssue,
MetadataValueType,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryGraphImportResult,
MemoryNodeRecord,
MemoryProfileRecord,
MemorySourceSpan,
NormalizedDocument,
OperationContext,
PolicyDecision,
@@ -107,6 +116,7 @@ from .ports import (
BlobWriteResult,
DirectorySourceConnector,
FormatExtractor,
MemoryGraphRepository,
PolicyGateway,
SourceConnector,
)
@@ -125,6 +135,8 @@ from .services import (
ContextEntityQueryRequest,
ContextEntityQueryResult,
LexicalIndexRefreshResult,
MemoryGraphImportSummary,
MemoryRuntimeService,
RelationshipChangeResult,
RelationshipQueryItem,
RelationshipQueryRequest,
@@ -217,6 +229,7 @@ __all__ = [
"FormatExtractor",
"InMemoryAssetRegistryRepository",
"InMemoryBlobStorage",
"InMemoryMemoryGraphRepository",
"InMemoryKnowledgeRepository",
"IngestionRequest",
"IngestionResult",
@@ -233,12 +246,23 @@ __all__ = [
"LexicalIndexRefreshResult",
"LifecycleState",
"LocalBlobStorage",
"MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION",
"MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION",
"MetadataFieldDefinition",
"MetadataRecord",
"MetadataSchema",
"MetadataSchemaAssignment",
"MetadataValidationIssue",
"MetadataValueType",
"MemoryEdgeRecord",
"MemoryEventRecord",
"MemoryGraphImportResult",
"MemoryGraphImportSummary",
"MemoryGraphRepository",
"MemoryNodeRecord",
"MemoryProfileRecord",
"MemoryRuntimeService",
"MemorySourceSpan",
"NormalizedDocument",
"NotFoundError",
"OperationFailure",

View File

@@ -2,5 +2,10 @@
from .asset_registry import InMemoryAssetRegistryRepository
from .blob_storage import InMemoryBlobStorage
from .graph_store import InMemoryMemoryGraphRepository
__all__ = ["InMemoryAssetRegistryRepository", "InMemoryBlobStorage"]
__all__ = [
"InMemoryAssetRegistryRepository",
"InMemoryBlobStorage",
"InMemoryMemoryGraphRepository",
]

View File

@@ -0,0 +1,126 @@
"""In-memory operational memory graph repository."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterable
from kontextual_engine.core import (
LifecycleState,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryNodeRecord,
MemoryProfileRecord,
)
from kontextual_engine.errors import DuplicateResourceError, NotFoundError, ValidationError
@dataclass
class InMemoryMemoryGraphRepository:
profiles: dict[str, MemoryProfileRecord] = field(default_factory=dict)
nodes: dict[str, MemoryNodeRecord] = field(default_factory=dict)
edges: dict[str, MemoryEdgeRecord] = field(default_factory=dict)
events: dict[str, MemoryEventRecord] = field(default_factory=dict)
def save_memory_profile(self, profile: MemoryProfileRecord) -> MemoryProfileRecord:
self.profiles[str(profile.profile_id)] = profile
return profile
def get_memory_profile(self, profile_id: str) -> MemoryProfileRecord:
try:
return self.profiles[profile_id]
except KeyError as exc:
raise NotFoundError("Memory profile not found", details={"profile_id": profile_id}) from exc
def save_memory_node(self, node: MemoryNodeRecord) -> MemoryNodeRecord:
self.nodes[str(node.node_id)] = node
return node
def get_memory_node(self, node_id: str) -> MemoryNodeRecord:
try:
return self.nodes[node_id]
except KeyError as exc:
raise NotFoundError("Memory node not found", details={"node_id": node_id}) from exc
def list_memory_nodes(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
lifecycle: LifecycleState | str | None = None,
) -> list[MemoryNodeRecord]:
nodes: Iterable[MemoryNodeRecord] = self.nodes.values()
if graph_id is not None:
nodes = [node for node in nodes if node.graph_id == graph_id]
if kind is not None:
nodes = [node for node in nodes if node.kind == kind]
if lifecycle is not None:
lifecycle = LifecycleState(lifecycle)
nodes = [node for node in nodes if node.lifecycle == lifecycle]
return sorted(nodes, key=lambda node: (node.graph_id, node.kind, node.contract_node_id))
def save_memory_edge(self, edge: MemoryEdgeRecord) -> MemoryEdgeRecord:
source = self.get_memory_node(edge.source_node_id)
target = self.get_memory_node(edge.target_node_id)
if source.graph_id != edge.graph_id or target.graph_id != edge.graph_id:
raise ValidationError(
"Memory edge endpoints must belong to the edge graph.",
details={
"edge_id": edge.edge_id,
"graph_id": edge.graph_id,
"source_graph_id": source.graph_id,
"target_graph_id": target.graph_id,
},
)
self.edges[str(edge.edge_id)] = edge
return edge
def get_memory_edge(self, edge_id: str) -> MemoryEdgeRecord:
try:
return self.edges[edge_id]
except KeyError as exc:
raise NotFoundError("Memory edge not found", details={"edge_id": edge_id}) from exc
def list_memory_edges(
self,
*,
graph_id: str | None = None,
source_node_id: str | None = None,
target_node_id: str | None = None,
) -> list[MemoryEdgeRecord]:
edges: Iterable[MemoryEdgeRecord] = self.edges.values()
if graph_id is not None:
edges = [edge for edge in edges if edge.graph_id == graph_id]
if source_node_id is not None:
edges = [edge for edge in edges if edge.source_node_id == source_node_id]
if target_node_id is not None:
edges = [edge for edge in edges if edge.target_node_id == target_node_id]
return sorted(edges, key=lambda edge: (edge.graph_id, edge.kind, edge.contract_edge_id))
def append_memory_event(self, event: MemoryEventRecord) -> MemoryEventRecord:
if event.event_id in self.events:
raise DuplicateResourceError(
"Memory event already exists; event storage is append-only.",
details={"event_id": event.event_id, "contract_event_id": event.contract_event_id},
)
self.events[str(event.event_id)] = event
return event
def get_memory_event(self, event_id: str) -> MemoryEventRecord:
try:
return self.events[event_id]
except KeyError as exc:
raise NotFoundError("Memory event not found", details={"event_id": event_id}) from exc
def list_memory_events(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
) -> list[MemoryEventRecord]:
events: Iterable[MemoryEventRecord] = self.events.values()
if graph_id is not None:
events = [event for event in events if event.graph_id == graph_id]
if kind is not None:
events = [event for event in events if event.kind == kind]
return sorted(events, key=lambda event: (event.graph_id, event.timestamp, event.contract_event_id))

View File

@@ -36,6 +36,17 @@ from .metadata import (
MetadataValueType,
Sensitivity,
)
from .memory import (
MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION,
MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryGraphImportResult,
MemoryNodeRecord,
MemoryProfileRecord,
MemorySourceSpan,
runtime_id,
)
from .policy import PolicyDecision, PolicyEffect
from .primitives import content_digest, mapping_digest, new_id, stable_json_dumps, utc_now
from .provenance import (
@@ -100,12 +111,20 @@ __all__ = [
"IngestionJobStatus",
"KnowledgeAsset",
"LifecycleState",
"MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION",
"MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION",
"MetadataFieldDefinition",
"MetadataRecord",
"MetadataSchema",
"MetadataSchemaAssignment",
"MetadataValidationIssue",
"MetadataValueType",
"MemoryEdgeRecord",
"MemoryEventRecord",
"MemoryGraphImportResult",
"MemoryNodeRecord",
"MemoryProfileRecord",
"MemorySourceSpan",
"NormalizedDocument",
"OperationContext",
"PolicyDecision",
@@ -114,6 +133,7 @@ __all__ = [
"RepresentationKind",
"RetrievalFeedbackLabel",
"RetrievalFeedbackRecord",
"runtime_id",
"Sensitivity",
"SourceReference",
"SourcePayload",

View File

@@ -0,0 +1,556 @@
"""Runtime memory graph records mapped from Markitect contracts."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from .metadata import LifecycleState
from .primitives import compact_dict, mapping_digest, utc_now
MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION = "markitect.memory.graph.v1"
MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION = "markitect.memory.profile.v1"
@dataclass(frozen=True)
class MemorySourceSpan:
path: str
snapshot_id: str | None = None
unit_kind: str | None = None
unit_index: int | None = None
line_start: int | None = None
line_end: int | None = None
selector: str | None = None
engine: str | None = None
@classmethod
def from_contract(cls, data: dict[str, Any]) -> "MemorySourceSpan":
return cls(
path=str(data["path"]),
snapshot_id=_optional_str(data.get("snapshot_id")),
unit_kind=_optional_str(data.get("unit_kind")),
unit_index=_optional_int(data.get("unit_index")),
line_start=_optional_int(data.get("line_start")),
line_end=_optional_int(data.get("line_end")),
selector=_optional_str(data.get("selector")),
engine=_optional_str(data.get("engine")),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"path": self.path,
"snapshot_id": self.snapshot_id,
"unit_kind": self.unit_kind,
"unit_index": self.unit_index,
"line_start": self.line_start,
"line_end": self.line_end,
"selector": self.selector,
"engine": self.engine,
}
)
@dataclass(frozen=True)
class MemoryProfileRecord:
contract_profile_id: str
schema_version: str
title: str = ""
intent: str = ""
memory_kinds: tuple[str, ...] = ()
stores: dict[str, Any] = field(default_factory=dict)
limits: dict[str, Any] = field(default_factory=dict)
latency: dict[str, Any] = field(default_factory=dict)
retention: dict[str, Any] = field(default_factory=dict)
refresh: dict[str, Any] = field(default_factory=dict)
compaction: dict[str, Any] = field(default_factory=dict)
activation: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict)
observability: dict[str, Any] = field(default_factory=dict)
failure: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
profile_id: str | None = None
imported_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.profile_id is None:
object.__setattr__(
self,
"profile_id",
runtime_id("memprofile", self.schema_version, self.contract_profile_id),
)
object.__setattr__(self, "memory_kinds", tuple(self.memory_kinds))
@classmethod
def from_markitect_contract(cls, data: dict[str, Any]) -> "MemoryProfileRecord":
schema_version = str(data.get("schema_version") or data.get("schema") or "")
if schema_version != MARKITECT_MEMORY_PROFILE_SCHEMA_VERSION:
raise ValueError(f"Unsupported memory profile schema `{schema_version}`.")
contract_id = str(data.get("id") or "")
if not contract_id:
raise ValueError("Memory profile contract must declare an id.")
return cls(
contract_profile_id=contract_id,
schema_version=schema_version,
title=str(data.get("title") or data.get("name") or ""),
intent=str(data.get("intent") or data.get("description") or ""),
memory_kinds=tuple(str(item) for item in data.get("memory_kinds", ())),
stores=dict(data.get("stores") or {}),
limits=dict(data.get("limits") or {}),
latency=dict(data.get("latency") or data.get("latency_targets") or {}),
retention=dict(data.get("retention") or data.get("retention_policy") or {}),
refresh=dict(data.get("refresh") or {}),
compaction=dict(data.get("compaction") or {}),
activation=dict(data.get("activation") or data.get("context_budget") or {}),
policy=dict(data.get("policy") or {}),
observability=dict(data.get("observability") or {}),
failure=dict(data.get("failure") or {}),
metadata=dict(data.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"profile_id": self.profile_id,
"contract_profile_id": self.contract_profile_id,
"schema_version": self.schema_version,
"title": self.title,
"intent": self.intent,
"memory_kinds": list(self.memory_kinds),
"stores": dict(self.stores),
"limits": dict(self.limits),
"latency": dict(self.latency),
"retention": dict(self.retention),
"refresh": dict(self.refresh),
"compaction": dict(self.compaction),
"activation": dict(self.activation),
"policy": dict(self.policy),
"observability": dict(self.observability),
"failure": dict(self.failure),
"metadata": dict(self.metadata),
"imported_at": self.imported_at,
}
)
@dataclass(frozen=True)
class MemoryNodeRecord:
graph_id: str
contract_graph_id: str
contract_node_id: str
kind: str
text: str = ""
namespace: dict[str, Any] = field(default_factory=dict)
source_spans: tuple[MemorySourceSpan, ...] = ()
provenance: tuple[dict[str, Any], ...] = ()
freshness: dict[str, Any] = field(default_factory=dict)
confidence: float | None = None
policy: dict[str, Any] = field(default_factory=dict)
lifecycle: LifecycleState = LifecycleState.ACTIVE
metadata: dict[str, Any] = field(default_factory=dict)
node_id: str | None = None
created_at: str = field(default_factory=lambda: utc_now().isoformat())
updated_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.node_id is None:
object.__setattr__(
self,
"node_id",
runtime_id("memnode", self.contract_graph_id, self.contract_node_id),
)
object.__setattr__(self, "source_spans", tuple(self.source_spans))
object.__setattr__(self, "provenance", tuple(dict(item) for item in self.provenance))
object.__setattr__(self, "lifecycle", LifecycleState(self.lifecycle))
@classmethod
def from_markitect_contract(
cls,
*,
graph_id: str,
contract_graph_id: str,
data: dict[str, Any],
graph_namespace: dict[str, Any] | None = None,
) -> "MemoryNodeRecord":
contract_node_id = str(data.get("id") or "")
if not contract_node_id:
raise ValueError("Memory graph node must declare an id.")
spans = data.get("source_spans") or data.get("spans") or []
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
contract_node_id=contract_node_id,
kind=str(data.get("kind") or data.get("type") or ""),
text=str(data.get("text") or data.get("content") or data.get("summary") or ""),
namespace=dict(data.get("namespace") or graph_namespace or {}),
source_spans=tuple(MemorySourceSpan.from_contract(span) for span in _mapping_list(spans)),
provenance=tuple(_mapping_list(data.get("provenance"))),
freshness=dict(data.get("freshness") or {}),
confidence=_optional_float(data.get("confidence")),
policy=dict(data.get("policy") or {}),
metadata=dict(data.get("metadata") or {}),
)
@property
def resource_id(self) -> str:
return f"memory-node:{self.node_id}"
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"node_id": self.node_id,
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"contract_node_id": self.contract_node_id,
"kind": self.kind,
"text": self.text,
"namespace": dict(self.namespace),
"source_spans": [span.to_dict() for span in self.source_spans],
"provenance": [dict(item) for item in self.provenance],
"freshness": dict(self.freshness),
"confidence": self.confidence,
"policy": dict(self.policy),
"lifecycle": self.lifecycle.value,
"metadata": dict(self.metadata),
"created_at": self.created_at,
"updated_at": self.updated_at,
}
)
@dataclass(frozen=True)
class MemoryEdgeRecord:
graph_id: str
contract_graph_id: str
contract_edge_id: str
kind: str
source_node_id: str
target_node_id: str
source_contract_node_id: str
target_contract_node_id: str
provenance: tuple[dict[str, Any], ...] = ()
freshness: dict[str, Any] = field(default_factory=dict)
confidence: float | None = None
policy: dict[str, Any] = field(default_factory=dict)
lifecycle: LifecycleState = LifecycleState.ACTIVE
metadata: dict[str, Any] = field(default_factory=dict)
edge_id: str | None = None
created_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.edge_id is None:
object.__setattr__(
self,
"edge_id",
runtime_id("memedge", self.contract_graph_id, self.contract_edge_id),
)
object.__setattr__(self, "provenance", tuple(dict(item) for item in self.provenance))
object.__setattr__(self, "lifecycle", LifecycleState(self.lifecycle))
@classmethod
def from_markitect_contract(
cls,
*,
graph_id: str,
contract_graph_id: str,
data: dict[str, Any],
node_id_by_contract_id: dict[str, str],
) -> "MemoryEdgeRecord":
kind = str(data.get("kind") or data.get("type") or "")
source_contract_id = str(data.get("source") or data.get("from") or "")
target_contract_id = str(data.get("target") or data.get("to") or "")
contract_edge_id = str(
data.get("id")
or f"edge:{stable_suffix(kind, source_contract_id, target_contract_id)}"
)
try:
source_node_id = node_id_by_contract_id[source_contract_id]
target_node_id = node_id_by_contract_id[target_contract_id]
except KeyError as exc:
missing = source_contract_id if source_contract_id not in node_id_by_contract_id else target_contract_id
raise ValueError(f"Memory edge `{contract_edge_id}` references unknown node `{missing}`.") from exc
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
contract_edge_id=contract_edge_id,
kind=kind,
source_node_id=source_node_id,
target_node_id=target_node_id,
source_contract_node_id=source_contract_id,
target_contract_node_id=target_contract_id,
provenance=tuple(_mapping_list(data.get("provenance"))),
freshness=dict(data.get("freshness") or {}),
confidence=_optional_float(data.get("confidence")),
policy=dict(data.get("policy") or {}),
metadata=dict(data.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"edge_id": self.edge_id,
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"contract_edge_id": self.contract_edge_id,
"kind": self.kind,
"source_node_id": self.source_node_id,
"target_node_id": self.target_node_id,
"source_contract_node_id": self.source_contract_node_id,
"target_contract_node_id": self.target_contract_node_id,
"provenance": [dict(item) for item in self.provenance],
"freshness": dict(self.freshness),
"confidence": self.confidence,
"policy": dict(self.policy),
"lifecycle": self.lifecycle.value,
"metadata": dict(self.metadata),
"created_at": self.created_at,
}
)
@dataclass(frozen=True)
class MemoryEventRecord:
graph_id: str
contract_graph_id: str
contract_event_id: str
kind: str
timestamp: str
namespace: dict[str, Any] = field(default_factory=dict)
actor_id: str | None = None
thread: str | None = None
task: str | None = None
node_updates: tuple[dict[str, Any], ...] = ()
edge_updates: tuple[dict[str, Any], ...] = ()
package_refs: tuple[str, ...] = ()
activation_refs: tuple[str, ...] = ()
branch: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
event_id: str | None = None
appended_at: str = field(default_factory=lambda: utc_now().isoformat())
def __post_init__(self) -> None:
if self.event_id is None:
object.__setattr__(
self,
"event_id",
runtime_id("memevent", self.contract_graph_id, self.contract_event_id),
)
object.__setattr__(self, "node_updates", tuple(dict(item) for item in self.node_updates))
object.__setattr__(self, "edge_updates", tuple(dict(item) for item in self.edge_updates))
object.__setattr__(self, "package_refs", tuple(self.package_refs))
object.__setattr__(self, "activation_refs", tuple(self.activation_refs))
@classmethod
def from_markitect_contract(
cls,
*,
graph_id: str,
contract_graph_id: str,
data: dict[str, Any],
graph_namespace: dict[str, Any] | None = None,
) -> "MemoryEventRecord":
kind = str(data.get("kind") or data.get("type") or "recorded")
timestamp = str(data.get("timestamp") or data.get("at") or utc_now().isoformat())
contract_event_id = str(data.get("id") or f"event:{stable_suffix(kind, timestamp, data)}")
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
contract_event_id=contract_event_id,
kind=kind,
timestamp=timestamp,
namespace=dict(data.get("namespace") or graph_namespace or {}),
actor_id=_optional_str(data.get("actor") or data.get("actor_id")),
thread=_optional_str(data.get("thread")),
task=_optional_str(data.get("task")),
node_updates=tuple(_mapping_list(data.get("node_updates") or data.get("nodes"))),
edge_updates=tuple(_mapping_list(data.get("edge_updates") or data.get("edges"))),
package_refs=tuple(_string_list(data.get("package_refs") or data.get("packages"))),
activation_refs=tuple(_string_list(data.get("activation_refs") or data.get("activations"))),
branch=dict(data.get("branch") or {}),
policy=dict(data.get("policy") or {}),
metadata=dict(data.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"event_id": self.event_id,
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"contract_event_id": self.contract_event_id,
"kind": self.kind,
"timestamp": self.timestamp,
"namespace": dict(self.namespace),
"actor_id": self.actor_id,
"thread": self.thread,
"task": self.task,
"node_updates": [dict(item) for item in self.node_updates],
"edge_updates": [dict(item) for item in self.edge_updates],
"package_refs": list(self.package_refs),
"activation_refs": list(self.activation_refs),
"branch": dict(self.branch),
"policy": dict(self.policy),
"metadata": dict(self.metadata),
"appended_at": self.appended_at,
}
)
@dataclass(frozen=True)
class MemoryGraphImportResult:
graph_id: str
contract_graph_id: str
schema_version: str
title: str = ""
intent: str = ""
namespace: dict[str, Any] = field(default_factory=dict)
profile: MemoryProfileRecord | None = None
nodes: tuple[MemoryNodeRecord, ...] = ()
edges: tuple[MemoryEdgeRecord, ...] = ()
events: tuple[MemoryEventRecord, ...] = ()
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
object.__setattr__(self, "nodes", tuple(self.nodes))
object.__setattr__(self, "edges", tuple(self.edges))
object.__setattr__(self, "events", tuple(self.events))
@classmethod
def from_markitect_contract(
cls,
graph_contract: dict[str, Any],
*,
profile_contract: dict[str, Any] | None = None,
) -> "MemoryGraphImportResult":
schema_version = str(graph_contract.get("schema_version") or graph_contract.get("schema") or "")
if schema_version != MARKITECT_MEMORY_GRAPH_SCHEMA_VERSION:
raise ValueError(f"Unsupported memory graph schema `{schema_version}`.")
contract_graph_id = str(graph_contract.get("id") or "")
if not contract_graph_id:
raise ValueError("Memory graph contract must declare an id.")
graph_id = runtime_id("memgraph", schema_version, contract_graph_id)
namespace = dict(graph_contract.get("namespace") or {})
nodes = tuple(
MemoryNodeRecord.from_markitect_contract(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
data=node,
graph_namespace=namespace,
)
for node in _mapping_values(graph_contract.get("nodes"))
)
node_id_by_contract_id = {node.contract_node_id: str(node.node_id) for node in nodes}
edges = tuple(
MemoryEdgeRecord.from_markitect_contract(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
data=edge,
node_id_by_contract_id=node_id_by_contract_id,
)
for edge in _mapping_values(graph_contract.get("edges"))
)
events = tuple(
MemoryEventRecord.from_markitect_contract(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
data=event,
graph_namespace=namespace,
)
for event in _mapping_values(graph_contract.get("events"))
)
profile = MemoryProfileRecord.from_markitect_contract(profile_contract) if profile_contract else None
return cls(
graph_id=graph_id,
contract_graph_id=contract_graph_id,
schema_version=schema_version,
title=str(graph_contract.get("title") or graph_contract.get("name") or ""),
intent=str(graph_contract.get("intent") or graph_contract.get("description") or ""),
namespace=namespace,
profile=profile,
nodes=nodes,
edges=edges,
events=events,
metadata=dict(graph_contract.get("metadata") or {}),
)
def to_dict(self) -> dict[str, Any]:
return compact_dict(
{
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"schema_version": self.schema_version,
"title": self.title,
"intent": self.intent,
"namespace": dict(self.namespace),
"profile": self.profile.to_dict() if self.profile else None,
"nodes": [node.to_dict() for node in self.nodes],
"edges": [edge.to_dict() for edge in self.edges],
"events": [event.to_dict() for event in self.events],
"metadata": dict(self.metadata),
}
)
def runtime_id(prefix: str, *parts: Any) -> str:
return f"{prefix}_{stable_suffix(*parts)}"
def stable_suffix(*parts: Any) -> str:
digest = mapping_digest(parts)
return digest.removeprefix("sha256:")[:24]
def _mapping_values(value: Any) -> list[dict[str, Any]]:
if value is None:
return []
if isinstance(value, dict):
items: list[dict[str, Any]] = []
for key, raw in value.items():
if not isinstance(raw, dict):
continue
item = dict(raw)
item.setdefault("id", str(key))
items.append(item)
return items
return _mapping_list(value)
def _mapping_list(value: Any) -> list[dict[str, Any]]:
if value is None:
return []
if isinstance(value, dict):
return [dict(value)]
if isinstance(value, list):
return [dict(item) for item in value if isinstance(item, dict)]
if isinstance(value, tuple):
return [dict(item) for item in value if isinstance(item, dict)]
return []
def _string_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, (list, tuple)):
return [str(item) for item in value if item is not None]
return [str(value)]
def _optional_str(value: Any) -> str | None:
if value is None:
return None
text = str(value)
return text if text else None
def _optional_int(value: Any) -> int | None:
if value is None or value == "":
return None
return int(value)
def _optional_float(value: Any) -> float | None:
if value is None or value == "":
return None
return float(value)

View File

@@ -9,6 +9,7 @@ from .blob_storage import (
digest_storage_key,
)
from .ingestion import DirectorySourceConnector, FormatExtractor, SourceConnector
from .memory import MemoryGraphRepository
from .policy import AllowAllPolicyGateway, PolicyGateway
from .repositories import AssetRegistryRepository
@@ -23,6 +24,7 @@ __all__ = [
"digest_storage_key",
"DirectorySourceConnector",
"FormatExtractor",
"MemoryGraphRepository",
"PolicyGateway",
"SourceConnector",
]

View File

@@ -0,0 +1,47 @@
"""Ports for operational memory graph state."""
from __future__ import annotations
from typing import Protocol
from kontextual_engine.core import (
LifecycleState,
MemoryEdgeRecord,
MemoryEventRecord,
MemoryNodeRecord,
MemoryProfileRecord,
)
class MemoryGraphRepository(Protocol):
def save_memory_profile(self, profile: MemoryProfileRecord) -> MemoryProfileRecord: ...
def get_memory_profile(self, profile_id: str) -> MemoryProfileRecord: ...
def save_memory_node(self, node: MemoryNodeRecord) -> MemoryNodeRecord: ...
def get_memory_node(self, node_id: str) -> MemoryNodeRecord: ...
def list_memory_nodes(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
lifecycle: LifecycleState | str | None = None,
) -> list[MemoryNodeRecord]: ...
def save_memory_edge(self, edge: MemoryEdgeRecord) -> MemoryEdgeRecord: ...
def get_memory_edge(self, edge_id: str) -> MemoryEdgeRecord: ...
def list_memory_edges(
self,
*,
graph_id: str | None = None,
source_node_id: str | None = None,
target_node_id: str | None = None,
) -> list[MemoryEdgeRecord]: ...
def append_memory_event(self, event: MemoryEventRecord) -> MemoryEventRecord: ...
def get_memory_event(self, event_id: str) -> MemoryEventRecord: ...
def list_memory_events(
self,
*,
graph_id: str | None = None,
kind: str | None = None,
) -> list[MemoryEventRecord]: ...

View File

@@ -7,6 +7,7 @@ from .asset_service import (
)
from .content_service import RepresentationContentResult, RepresentationContentStream, RepresentationContentService
from .ingestion_service import AssetIngestionResult, AssetIngestionService
from .memory_service import MemoryGraphImportSummary, MemoryRuntimeService
from .retrieval_service import (
AssetQueryItem,
AssetQueryRequest,
@@ -53,6 +54,8 @@ __all__ = [
"ContextEntityQueryRequest",
"ContextEntityQueryResult",
"LexicalIndexRefreshResult",
"MemoryGraphImportSummary",
"MemoryRuntimeService",
"RelationshipChangeResult",
"RepresentationContentResult",
"RepresentationContentStream",

View File

@@ -0,0 +1,100 @@
"""Application service for operational memory graph imports."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from kontextual_engine.core import (
AuditEvent,
AuditOutcome,
MemoryGraphImportResult,
OperationContext,
)
from kontextual_engine.errors import ValidationError
from kontextual_engine.ports import MemoryGraphRepository
@dataclass(frozen=True)
class MemoryGraphImportSummary:
graph_id: str
contract_graph_id: str
imported_nodes: int
imported_edges: int
appended_events: int
profile_id: str | None = None
audit_event: AuditEvent | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
data = {
"graph_id": self.graph_id,
"contract_graph_id": self.contract_graph_id,
"profile_id": self.profile_id,
"imported_nodes": self.imported_nodes,
"imported_edges": self.imported_edges,
"appended_events": self.appended_events,
"metadata": dict(self.metadata),
}
if self.audit_event:
data["audit_event"] = self.audit_event.to_dict()
return data
class MemoryRuntimeService:
def __init__(self, repository: MemoryGraphRepository) -> None:
self.repository = repository
def import_markitect_graph(
self,
graph_contract: dict[str, Any],
*,
profile_contract: dict[str, Any] | None = None,
context: OperationContext | None = None,
) -> MemoryGraphImportSummary:
try:
imported = MemoryGraphImportResult.from_markitect_contract(
graph_contract,
profile_contract=profile_contract,
)
except ValueError as exc:
raise ValidationError(str(exc), details={"operation": "memory.import_markitect_graph"}) from exc
if imported.profile:
self.repository.save_memory_profile(imported.profile)
for node in imported.nodes:
self.repository.save_memory_node(node)
for edge in imported.edges:
self.repository.save_memory_edge(edge)
for event in imported.events:
self.repository.append_memory_event(event)
audit_event = None
if context:
audit_event = AuditEvent.from_context(
"memory.import_markitect_graph",
f"memory-graph:{imported.graph_id}",
AuditOutcome.SUCCESS,
context,
details={
"contract_graph_id": imported.contract_graph_id,
"profile_id": imported.profile.profile_id if imported.profile else None,
"nodes": len(imported.nodes),
"edges": len(imported.edges),
"events": len(imported.events),
},
)
return MemoryGraphImportSummary(
graph_id=imported.graph_id,
contract_graph_id=imported.contract_graph_id,
profile_id=imported.profile.profile_id if imported.profile else None,
imported_nodes=len(imported.nodes),
imported_edges=len(imported.edges),
appended_events=len(imported.events),
audit_event=audit_event,
metadata={
"schema_version": imported.schema_version,
"title": imported.title,
"intent": imported.intent,
},
)

View File

@@ -0,0 +1,153 @@
import pytest
from kontextual_engine import (
Actor,
ActorType,
DuplicateResourceError,
InMemoryMemoryGraphRepository,
MemoryGraphImportResult,
MemoryRuntimeService,
OperationContext,
ValidationError,
)
def test_markitect_memory_graph_import_preserves_contract_identity_and_spans() -> None:
imported = MemoryGraphImportResult.from_markitect_contract(
_graph_contract(),
profile_contract=_profile_contract(),
)
second_import = MemoryGraphImportResult.from_markitect_contract(_graph_contract())
assert imported.graph_id == second_import.graph_id
assert imported.contract_graph_id == "markitect-memory-decisions"
assert imported.profile is not None
assert imported.profile.contract_profile_id == "local-agent-memory"
assert imported.nodes[0].contract_node_id == "decision.contract-boundary"
assert imported.nodes[0].node_id == second_import.nodes[0].node_id
assert imported.nodes[0].source_spans[0].path == "workplans/MKTT-WP-0016.md"
assert imported.edges[0].source_node_id == imported.nodes[1].node_id
assert imported.events[0].package_refs == ("memory:package:example",)
def test_in_memory_memory_repository_persists_graph_and_append_only_events() -> None:
repo = InMemoryMemoryGraphRepository()
imported = MemoryGraphImportResult.from_markitect_contract(_graph_contract())
for node in imported.nodes:
repo.save_memory_node(node)
repo.save_memory_edge(imported.edges[0])
repo.append_memory_event(imported.events[0])
assert repo.list_memory_nodes(graph_id=imported.graph_id, kind="decision") == [imported.nodes[0]]
assert repo.list_memory_edges(source_node_id=imported.nodes[1].node_id) == [imported.edges[0]]
assert repo.list_memory_events(graph_id=imported.graph_id, kind="recorded") == [imported.events[0]]
with pytest.raises(DuplicateResourceError):
repo.append_memory_event(imported.events[0])
def test_memory_runtime_service_imports_contracts_and_reports_audit_context() -> None:
repo = InMemoryMemoryGraphRepository()
service = MemoryRuntimeService(repo)
actor = Actor.create(ActorType.AI_AGENT, actor_id="agent-codex", display_name="Codex")
context = OperationContext.create(actor, correlation_id="corr-memory")
summary = service.import_markitect_graph(
_graph_contract(),
profile_contract=_profile_contract(),
context=context,
)
assert summary.imported_nodes == 2
assert summary.imported_edges == 1
assert summary.appended_events == 1
assert summary.audit_event is not None
assert summary.audit_event.actor_id == "agent-codex"
assert summary.audit_event.correlation_id == "corr-memory"
assert repo.get_memory_profile(summary.profile_id).memory_kinds == (
"reasoning",
"knowledge",
"package",
)
def test_memory_runtime_service_rejects_invalid_edge_contracts() -> None:
repo = InMemoryMemoryGraphRepository()
service = MemoryRuntimeService(repo)
graph = _graph_contract()
graph["edges"][0]["target"] = "missing-node"
with pytest.raises(ValidationError) as exc:
service.import_markitect_graph(graph)
assert "unknown node" in str(exc.value)
def _profile_contract() -> dict:
return {
"schema_version": "markitect.memory.profile.v1",
"id": "local-agent-memory",
"title": "Local Agent Memory",
"intent": "Runtime import fixture.",
"memory_kinds": ["reasoning", "knowledge", "package"],
"stores": {
"reasoning": "local-event-log",
"knowledge": "local-graph-store",
"package": "markitect-context-registry",
},
"activation": {"max_items": 6, "max_tokens": 1800},
"policy": {"required_labels": ["public"]},
}
def _graph_contract() -> dict:
return {
"schema_version": "markitect.memory.graph.v1",
"id": "markitect-memory-decisions",
"title": "Markitect Memory Decisions",
"intent": "Preserve memory boundary decisions for runtime import.",
"namespace": {"project": "markitect-tool", "task": "MKTT-WP-0016"},
"nodes": [
{
"id": "decision.contract-boundary",
"kind": "decision",
"text": "Markitect compiles memory graph selections into context packages.",
"source_spans": [
{
"path": "workplans/MKTT-WP-0016.md",
"unit_kind": "section",
"selector": "tasks[id=P16.5]",
"engine": "selector",
}
],
"metadata": {"title": "Contract boundary decision"},
},
{
"id": "constraint.no-runtime-services",
"kind": "constraint",
"text": "Runtime persistence belongs in kontextual-engine.",
"policy": {"labels": ["public"]},
},
],
"edges": [
{
"id": "edge.boundary-support",
"kind": "supports",
"source": "constraint.no-runtime-services",
"target": "decision.contract-boundary",
}
],
"events": [
{
"id": "event.initial-contract",
"kind": "recorded",
"timestamp": "2026-05-15T00:00:00Z",
"actor": "agent-codex",
"task": "KONT-WP-0017",
"package_refs": ["memory:package:example"],
"node_updates": [
{"node_id": "decision.contract-boundary", "operation": "import"}
],
}
],
}

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Agentic Memory Graph Runtime And Operational State"
domain: markitect
repo: kontextual-engine
status: todo
status: active
owner: codex
topic_slug: markitect
planning_priority: medium
@@ -59,11 +59,32 @@ Disallowed responsibilities:
- Do not require any one graph database, vector store, LLM provider, or
enterprise policy system in the core implementation.
## Implementation Update - 2026-05-15
The first runtime-contract slice is implemented. `kontextual-engine` can now
map `markitect.memory.graph.v1` and `markitect.memory.profile.v1` envelopes
into stable runtime records, persist them through a `MemoryGraphRepository`
port, and use `InMemoryMemoryGraphRepository` as the deterministic local
implementation for tests.
The implementation deliberately does not validate Markitect's node/edge
vocabulary. It checks runtime compatibility concerns only: schema version,
required ids, edge endpoint integrity, stable runtime id mapping, source-span
preservation, and append-only event identity.
Primary files:
- `src/kontextual_engine/core/memory.py`
- `src/kontextual_engine/ports/memory.py`
- `src/kontextual_engine/adapters/memory/graph_store.py`
- `src/kontextual_engine/services/memory_service.py`
- `docs/memory-graph-runtime.md`
## P17.1 - Import and map Markitect memory contracts
```task
id: KONT-WP-0017-T001
status: todo
status: done
priority: high
state_hub_task_id: "1b1f8904-3a3b-4d23-9d00-176633578801"
```
@@ -86,7 +107,7 @@ Output: contract import design, mapping tests, and compatibility fixtures.
```task
id: KONT-WP-0017-T002
status: todo
status: done
priority: high
state_hub_task_id: "dc3557e3-d425-46b0-8acc-9ac0230a385c"
```