generated from coulomb/repo-seed
feat(memory): add graph runtime import store
This commit is contained in:
42
docs/memory-graph-runtime.md
Normal file
42
docs/memory-graph-runtime.md
Normal 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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
126
src/kontextual_engine/adapters/memory/graph_store.py
Normal file
126
src/kontextual_engine/adapters/memory/graph_store.py
Normal 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))
|
||||
@@ -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",
|
||||
|
||||
556
src/kontextual_engine/core/memory.py
Normal file
556
src/kontextual_engine/core/memory.py
Normal 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)
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
47
src/kontextual_engine/ports/memory.py
Normal file
47
src/kontextual_engine/ports/memory.py
Normal 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]: ...
|
||||
@@ -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",
|
||||
|
||||
100
src/kontextual_engine/services/memory_service.py
Normal file
100
src/kontextual_engine/services/memory_service.py
Normal 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,
|
||||
},
|
||||
)
|
||||
153
tests/test_memory_graph_runtime.py
Normal file
153
tests/test_memory_graph_runtime.py
Normal 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"}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user