Add profile-driven runtime adapter resolution

This commit is contained in:
2026-05-18 20:53:16 +02:00
parent 850979ccf1
commit b36a935f48
6 changed files with 618 additions and 19 deletions

View File

@@ -45,7 +45,7 @@ from .retrieval import (
retrieve_graph_neighborhood,
select_event_path,
)
from .service import RuntimeConfig, health_report, service_contracts
from .service import RuntimeAdapterBundle, RuntimeConfig, health_report, resolve_runtime_adapters, runtime_from_config, service_contracts
from .planner import plan_profile_execution
from .runtime import PhaseMemoryRuntime
@@ -98,7 +98,10 @@ __all__ = [
"retrieve_graph_neighborhood",
"select_event_path",
"RuntimeConfig",
"RuntimeAdapterBundle",
"health_report",
"resolve_runtime_adapters",
"runtime_from_config",
"service_contracts",
]

View File

@@ -3,14 +3,18 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from .adapters import (
AllowAllPolicyGateway,
FileBackedMemoryGraphStore,
InMemoryMemoryEventLog,
InMemoryMemoryGraphStore,
InMemoryRuntimeRegistry,
InMemorySemanticIndex,
JsonlAuditSink,
JsonlMemoryEventLog,
NoopContextPackageCompiler,
RecordingAuditSink,
)
@@ -37,11 +41,22 @@ SERVICE_OPERATIONS = {
@dataclass(frozen=True)
class RuntimeConfig:
local_store_path: str = ".phase-memory-local"
adapter_registry: dict[str, str] = field(default_factory=lambda: {"graph_store": "memory", "event_log": "memory"})
adapter_registry: dict[str, str] = field(
default_factory=lambda: {
"graph_store": "memory",
"event_log": "memory",
"policy_gateway": "allow-all",
"audit_sink": "recording",
"package_compiler": "noop",
"semantic_index": "disabled",
"runtime_registry": "memory",
}
)
policy_mode: str = "allow-all"
audit_sink_mode: str = "recording"
package_compiler_mode: str = "noop"
semantic_index_mode: str = "disabled"
runtime_registry_mode: str = "memory"
dry_run_default: bool = True
trust_zone_labels: tuple[str, ...] = ("local",)
@@ -49,14 +64,108 @@ class RuntimeConfig:
def local_default(cls) -> "RuntimeConfig":
return cls()
@classmethod
def from_profile(cls, profile: ProfileIntent | dict[str, Any], *, local_store_path: str | None = None) -> "RuntimeConfig":
profile_intent = profile if isinstance(profile, ProfileIntent) else ProfileIntent.from_mapping(profile)
base = cls.local_default()
runtime_metadata = _runtime_metadata(profile_intent)
stores = dict(profile_intent.stores)
policy = dict(profile_intent.policy)
activation = dict(profile_intent.activation)
observability = dict(profile_intent.observability)
store_path = str(
local_store_path
or runtime_metadata.get("local_store_path")
or profile_intent.metadata.get("local_store_path")
or stores.get("local_store_path")
or base.local_store_path
)
policy_mode = _mode_from(policy, ("mode", "policy_mode", "gateway"), default=base.policy_mode)
audit_sink_mode = _mode_from(observability, ("audit_sink", "audit_sink_mode"), default=_store_mode(stores, ("audit_sink", "audit"), base.audit_sink_mode))
package_compiler_mode = _mode_from(
runtime_metadata,
("package_compiler", "package_compiler_mode"),
default=_store_mode(stores, ("package_compiler", "context_package", "package"), base.package_compiler_mode),
)
semantic_index_mode = _mode_from(
activation,
("semantic_index", "semantic_index_mode"),
default=_store_mode(stores, ("semantic_index", "semantic"), base.semantic_index_mode),
)
runtime_registry_mode = _mode_from(
observability,
("runtime_registry", "runtime_registry_mode"),
default=_mode_from(runtime_metadata, ("runtime_registry", "runtime_registry_mode"), default=base.runtime_registry_mode),
)
graph_store_mode = _infer_graph_store_mode(stores, default=base.adapter_registry["graph_store"])
event_log_mode = _infer_event_log_mode(stores, default=base.adapter_registry["event_log"])
dry_run_default = _bool_from(policy, "dry_run_default", default=base.dry_run_default)
trust_zone_labels = _string_tuple(
policy.get("trust_zone_labels")
or policy.get("trust_zones")
or policy.get("required_labels")
or base.trust_zone_labels
)
return cls(
local_store_path=store_path,
adapter_registry={
"graph_store": graph_store_mode,
"event_log": event_log_mode,
"policy_gateway": policy_mode,
"audit_sink": audit_sink_mode,
"package_compiler": package_compiler_mode,
"semantic_index": semantic_index_mode,
"runtime_registry": runtime_registry_mode,
},
policy_mode=policy_mode,
audit_sink_mode=audit_sink_mode,
package_compiler_mode=package_compiler_mode,
semantic_index_mode=semantic_index_mode,
runtime_registry_mode=runtime_registry_mode,
dry_run_default=dry_run_default,
trust_zone_labels=trust_zone_labels,
)
def adapter_mode(self, adapter: str) -> str:
legacy = {
"graph_store": "memory",
"event_log": "memory",
"policy_gateway": self.policy_mode,
"audit_sink": self.audit_sink_mode,
"package_compiler": self.package_compiler_mode,
"semantic_index": self.semantic_index_mode,
"runtime_registry": self.runtime_registry_mode,
}
return _normalize_mode(self.adapter_registry.get(adapter) or legacy.get(adapter) or "")
def diagnostics(self) -> tuple[Diagnostic, ...]:
diagnostics: list[Diagnostic] = []
if not self.local_store_path:
diagnostics.append(Diagnostic("error", "missing_store_path", "Runtime config requires a local store path.", "local_store_path"))
if self.policy_mode not in {"allow-all", "external"}:
diagnostics.append(Diagnostic("error", "unsupported_policy_mode", "Unsupported policy mode.", "policy_mode", {"policy_mode": self.policy_mode}))
if self.semantic_index_mode not in {"disabled", "external"}:
diagnostics.append(Diagnostic("error", "unsupported_semantic_index_mode", "Unsupported semantic index mode.", "semantic_index_mode", {"semantic_index_mode": self.semantic_index_mode}))
for adapter, allowed_modes in _SUPPORTED_ADAPTER_MODES.items():
mode = self.adapter_mode(adapter)
if mode not in allowed_modes:
diagnostics.append(
Diagnostic(
"error",
"unsupported_adapter_mode",
"Unsupported adapter mode.",
f"adapter_registry.{adapter}",
{"adapter": adapter, "mode": mode, "allowed_modes": sorted(allowed_modes)},
)
)
elif mode == "external":
diagnostics.append(
Diagnostic(
"warn",
"external_adapter_declared",
"External adapter mode is declared and must be supplied by the caller.",
f"adapter_registry.{adapter}",
{"adapter": adapter},
)
)
return tuple(diagnostics)
def to_dict(self) -> dict[str, Any]:
@@ -67,20 +176,134 @@ class RuntimeConfig:
"audit_sink_mode": self.audit_sink_mode,
"package_compiler_mode": self.package_compiler_mode,
"semantic_index_mode": self.semantic_index_mode,
"runtime_registry_mode": self.runtime_registry_mode,
"dry_run_default": self.dry_run_default,
"trust_zone_labels": list(self.trust_zone_labels),
}
@dataclass(frozen=True)
class RuntimeAdapterBundle:
graph_store: Any
event_log: Any
package_compiler: Any
policy_gateway: Any
audit_sink: Any
semantic_index: Any | None = None
runtime_registry: Any | None = None
diagnostics: tuple[Diagnostic, ...] = ()
def to_runtime(self) -> PhaseMemoryRuntime:
errors = [diagnostic for diagnostic in self.diagnostics if diagnostic.severity == "error"]
if errors:
codes = ", ".join(diagnostic.code for diagnostic in errors)
raise ValueError(f"Runtime adapters are not ready: {codes}")
return PhaseMemoryRuntime(
graph_store=self.graph_store,
event_log=self.event_log,
package_compiler=self.package_compiler,
policy_gateway=self.policy_gateway,
audit_sink=self.audit_sink,
)
def to_dict(self) -> dict[str, Any]:
return {
"graph_store": self.graph_store.__class__.__name__,
"event_log": self.event_log.__class__.__name__,
"package_compiler": self.package_compiler.__class__.__name__,
"policy_gateway": self.policy_gateway.__class__.__name__,
"audit_sink": self.audit_sink.__class__.__name__,
"semantic_index": self.semantic_index.__class__.__name__ if self.semantic_index is not None else "disabled",
"runtime_registry": self.runtime_registry.__class__.__name__ if self.runtime_registry is not None else "disabled",
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
}
def service_contracts() -> dict[str, Any]:
return {"schema_version": SERVICE_CONTRACT_SCHEMA, "operations": SERVICE_OPERATIONS}
def runtime_from_config(config: RuntimeConfig | None = None) -> PhaseMemoryRuntime:
def resolve_runtime_adapters(config: RuntimeConfig | None = None, *, external_adapters: dict[str, Any] | None = None) -> RuntimeAdapterBundle:
config = config or RuntimeConfig.local_default()
# First service-ready slice keeps adapters dependency-light. External
# adapter resolution belongs behind the registry in later deployments.
return PhaseMemoryRuntime()
external_adapters = dict(external_adapters or {})
diagnostics = list(config.diagnostics())
root = Path(config.local_store_path)
graph_store = _resolve_adapter(
"graph_store",
config.adapter_mode("graph_store"),
external_adapters,
diagnostics,
local_factories={
"memory": InMemoryMemoryGraphStore,
"file": lambda: FileBackedMemoryGraphStore(root),
},
)
event_log = _resolve_adapter(
"event_log",
config.adapter_mode("event_log"),
external_adapters,
diagnostics,
local_factories={
"memory": InMemoryMemoryEventLog,
"jsonl": lambda: JsonlMemoryEventLog(root / "events.jsonl"),
},
)
package_compiler = _resolve_adapter(
"package_compiler",
config.adapter_mode("package_compiler"),
external_adapters,
diagnostics,
local_factories={"noop": NoopContextPackageCompiler},
)
policy_gateway = _resolve_adapter(
"policy_gateway",
config.adapter_mode("policy_gateway"),
external_adapters,
diagnostics,
local_factories={"allow-all": AllowAllPolicyGateway},
)
audit_sink = _resolve_adapter(
"audit_sink",
config.adapter_mode("audit_sink"),
external_adapters,
diagnostics,
local_factories={
"recording": RecordingAuditSink,
"jsonl": lambda: JsonlAuditSink(root / "audit.jsonl"),
},
)
semantic_index = _resolve_adapter(
"semantic_index",
config.adapter_mode("semantic_index"),
external_adapters,
diagnostics,
local_factories={
"disabled": lambda: None,
"memory": InMemorySemanticIndex,
},
)
runtime_registry = _resolve_adapter(
"runtime_registry",
config.adapter_mode("runtime_registry"),
external_adapters,
diagnostics,
local_factories={"memory": InMemoryRuntimeRegistry},
)
return RuntimeAdapterBundle(
graph_store=graph_store,
event_log=event_log,
package_compiler=package_compiler,
policy_gateway=policy_gateway,
audit_sink=audit_sink,
semantic_index=semantic_index,
runtime_registry=runtime_registry,
diagnostics=tuple(diagnostics),
)
def runtime_from_config(config: RuntimeConfig | None = None, *, external_adapters: dict[str, Any] | None = None) -> PhaseMemoryRuntime:
return resolve_runtime_adapters(config, external_adapters=external_adapters).to_runtime()
def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None = None) -> dict[str, Any]:
@@ -112,9 +335,15 @@ def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None =
class LocalServiceRunner:
"""Minimal optional service runner shape without web framework dependency."""
def __init__(self, runtime: PhaseMemoryRuntime | None = None, config: RuntimeConfig | None = None) -> None:
def __init__(
self,
runtime: PhaseMemoryRuntime | None = None,
config: RuntimeConfig | None = None,
*,
external_adapters: dict[str, Any] | None = None,
) -> None:
self.config = config or RuntimeConfig.local_default()
self.runtime = runtime or runtime_from_config(self.config)
self.runtime = runtime or runtime_from_config(self.config, external_adapters=external_adapters)
def handle(self, operation: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
payload = payload or {}
@@ -213,3 +442,135 @@ def default_conformance_adapters() -> dict[str, Any]:
"semantic_index": InMemorySemanticIndex(),
"runtime_registry": InMemoryRuntimeRegistry(),
}
_SUPPORTED_ADAPTER_MODES = {
"graph_store": {"memory", "file", "external"},
"event_log": {"memory", "jsonl", "external"},
"policy_gateway": {"allow-all", "external"},
"audit_sink": {"recording", "jsonl", "external"},
"package_compiler": {"noop", "external"},
"semantic_index": {"disabled", "memory", "external"},
"runtime_registry": {"memory", "external"},
}
_MODE_ALIASES = {
"allow_all": "allow-all",
"allowall": "allow-all",
"local-allow-all": "allow-all",
"file-backed": "file",
"file-backed-graph-store": "file",
"local-graph-store": "file",
"local-json": "file",
"json-lines": "jsonl",
"local-event-log": "jsonl",
"jsonl-event-log": "jsonl",
"recording-audit": "recording",
"jsonl-audit": "jsonl",
"noop-compiler": "noop",
"markitect-context-package": "noop",
"in-memory": "memory",
"local-memory": "memory",
"off": "disabled",
"none": "disabled",
"false": "disabled",
"true": "memory",
}
def _resolve_adapter(
adapter: str,
mode: str,
external_adapters: dict[str, Any],
diagnostics: list[Diagnostic],
*,
local_factories: dict[str, Any],
) -> Any:
if mode == "external":
if adapter in external_adapters:
return external_adapters[adapter]
diagnostics.append(
Diagnostic(
"error",
"missing_external_adapter",
"External adapter mode was requested but no adapter instance was supplied.",
f"adapter_registry.{adapter}",
{"adapter": adapter},
)
)
return None
factory = local_factories.get(mode)
if factory is None:
diagnostics.append(
Diagnostic(
"error",
"unresolved_adapter_mode",
"Adapter mode cannot be resolved by the local runtime.",
f"adapter_registry.{adapter}",
{"adapter": adapter, "mode": mode},
)
)
return None
return factory()
def _runtime_metadata(profile: ProfileIntent) -> dict[str, Any]:
runtime = profile.metadata.get("runtime")
return dict(runtime) if isinstance(runtime, dict) else {}
def _mode_from(mapping: dict[str, Any], keys: tuple[str, ...], *, default: str) -> str:
for key in keys:
if key in mapping:
return _normalize_mode(mapping[key])
return _normalize_mode(default)
def _store_mode(stores: dict[str, str], keys: tuple[str, ...], default: str) -> str:
for key in keys:
if key in stores:
return _normalize_mode(stores[key])
return _normalize_mode(default)
def _infer_graph_store_mode(stores: dict[str, str], *, default: str) -> str:
specific = _store_mode(stores, ("graph_store", "memory_graph", "graph"), "")
if specific:
return specific
values = {_normalize_mode(value) for value in stores.values()}
if "file" in values:
return "file"
return _normalize_mode(default)
def _infer_event_log_mode(stores: dict[str, str], *, default: str) -> str:
specific = _store_mode(stores, ("event_log", "events", "conversation"), "")
if specific:
return specific
values = {_normalize_mode(value) for value in stores.values()}
if "jsonl" in values:
return "jsonl"
return _normalize_mode(default)
def _normalize_mode(value: Any) -> str:
text = str(value).strip().lower().replace("_", "-")
return _MODE_ALIASES.get(text, text)
def _bool_from(mapping: dict[str, Any], key: str, *, default: bool) -> bool:
if key not in mapping:
return default
value = mapping[key]
if isinstance(value, bool):
return value
return str(value).strip().lower() in {"1", "true", "yes", "on"}
def _string_tuple(value: Any) -> tuple[str, ...]:
if value is None:
return ()
if isinstance(value, str):
return (value,)
return tuple(str(item) for item in value)