Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions dare_framework/checkpoint/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
"""Supported default checkpoint implementations and contributors.

This module keeps legacy checkpoint symbols importable after checkpoint internals
were consolidated into ``dare_framework.checkpoint.kernel``.
"""

from __future__ import annotations

from copy import deepcopy
from dataclasses import asdict
from typing import Any
from uuid import uuid4

from dare_framework.context.types import Message

STM = "stm"
SESSION_STATE = "session_state"
SESSION_CONTEXT = "session_context"
WORKSPACE_FILES = "workspace_files"


class MemoryCheckpointStore:
"""In-memory checkpoint payload store kept for facade compatibility."""

def __init__(self) -> None:
self._store: dict[str, dict[str, Any]] = {}

def put(self, checkpoint_id: str, payload: dict[str, Any]) -> None:
self._store[checkpoint_id] = _clone_payload(payload)

def get(self, checkpoint_id: str) -> dict[str, Any] | None:
if checkpoint_id not in self._store:
return None
return _clone_payload(self._store[checkpoint_id])

def delete(self, checkpoint_id: str) -> bool:
if checkpoint_id in self._store:
del self._store[checkpoint_id]
return True
return False


def _scope_keys(scope: Any, method_name: str) -> list[str]:
method = getattr(scope, method_name, None)
if callable(method):
values = method()
if isinstance(values, (list, tuple, set)):
return [str(v) for v in values]
if isinstance(scope, (list, tuple, set)):
return [str(v) for v in scope]
return []


def _clone_payload(value: Any) -> Any:
"""Clone nested checkpoint payloads so save/restore stays side-effect free."""
return deepcopy(value)


class DefaultCheckpointSaveRestore:
"""Legacy save/restore coordinator over contributor payload components."""

def __init__(self, store: MemoryCheckpointStore, contributors: list[Any]) -> None:
self._store = store
self._contributors = {
str(c.component_key): c
for c in contributors
if getattr(c, "component_key", None) is not None
}

def save(self, scope: Any, ctx: Any) -> str:
payload: dict[str, Any] = {}
for key in _scope_keys(scope, "keys_for_save"):
contributor = self._contributors.get(key)
if contributor is None:
continue
payload[key] = contributor.serialize(ctx)
checkpoint_id = uuid4().hex[:16]
self._store.put(checkpoint_id, payload)
return checkpoint_id

def restore(self, checkpoint_id: str, scope: Any, ctx: Any) -> None:
payload = self._store.get(checkpoint_id)
if payload is None:
raise LookupError(f"Checkpoint not found: {checkpoint_id!r}")
for key in _scope_keys(scope, "keys_for_restore"):
if key not in payload:
continue
contributor = self._contributors.get(key)
if contributor is None:
continue
contributor.deserialize_and_apply(payload[key], ctx)


class StmContributor:
"""Serialize/restore short-term memory messages."""

component_key = STM

def serialize(self, ctx: Any) -> list[dict[str, Any]]:
context = getattr(ctx, "context", None)
if context is None:
return []
messages = context.stm_get()
return [
{
"role": m.role,
"kind": m.kind,
"text": m.text,
"attachments": [
{
"kind": attachment.kind,
"uri": attachment.uri,
"mime_type": attachment.mime_type,
"filename": attachment.filename,
"metadata": _clone_payload(getattr(attachment, "metadata", {}) or {}),
}
for attachment in m.attachments
],
"data": _clone_payload(m.data),
"name": m.name,
"metadata": _clone_payload(getattr(m, "metadata", {}) or {}),
"mark": getattr(m, "mark", None),
"id": getattr(m, "id", None),
}
for m in messages
]

def deserialize_and_apply(self, payload: list[Any], ctx: Any) -> None:
context = getattr(ctx, "context", None)
if context is None:
return
context.stm_clear()
for item in payload or []:
if not isinstance(item, dict):
continue
restored_text = item["text"] if "text" in item else item.get("content", "")
context.stm_add(
Message(
role=item.get("role", "user"),
kind=item.get("kind", "chat"),
text=restored_text,
attachments=_clone_payload(item.get("attachments")) or [],
data=_clone_payload(item.get("data")),
name=item.get("name"),
metadata=dict(item.get("metadata") or {}),
mark=item.get("mark", "temporary"),
id=item.get("id"),
)
)


class SessionStateContributor:
"""Serialize/restore minimal session-state fields."""

component_key = SESSION_STATE

def serialize(self, ctx: Any) -> dict[str, Any] | None:
state = getattr(ctx, "session_state", None)
if state is None:
return None
try:
return asdict(state)
except Exception:
return {
"current_milestone_idx": getattr(state, "current_milestone_idx", None),
"task_id": getattr(state, "task_id", None),
"run_id": getattr(state, "run_id", None),
}

def deserialize_and_apply(self, payload: Any, ctx: Any) -> None:
state = getattr(ctx, "session_state", None)
if state is None or not isinstance(payload, dict):
return
if "current_milestone_idx" in payload and hasattr(state, "current_milestone_idx"):
state.current_milestone_idx = payload["current_milestone_idx"]
Comment on lines +174 to +175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore task/run identifiers from session-state payload

SessionStateContributor.serialize() includes task_id and run_id in the checkpoint payload, but deserialize_and_apply() only writes back current_milestone_idx. When a checkpoint is restored into a new or reset session object, the execution resumes with stale identifiers, which can break continuity for metadata keyed by task/run IDs even though those IDs were captured in the snapshot.

Useful? React with 👍 / 👎.

if "task_id" in payload and hasattr(state, "task_id"):
state.task_id = payload["task_id"]
if "run_id" in payload and hasattr(state, "run_id"):
state.run_id = payload["run_id"]


class SessionContextContributor:
"""Serialize session context for audit trails (restore intentionally no-op)."""

component_key = SESSION_CONTEXT

def serialize(self, ctx: Any) -> dict[str, Any] | None:
session_context = getattr(ctx, "session_context", None)
if session_context is None:
return None
try:
serialized = asdict(session_context)
except Exception:
return {
"session_id": getattr(session_context, "session_id", None),
"task_id": getattr(session_context, "task_id", None),
}
config_value = serialized.get("config")
if config_value is not None and not isinstance(config_value, dict):
try:
serialized["config"] = asdict(config_value)
except Exception:
serialized["config"] = None
return serialized

def deserialize_and_apply(self, payload: Any, ctx: Any) -> None:
_ = (payload, ctx)
# SessionContext is construction-time state; legacy restore path keeps this as no-op.


class WorkspaceGitContributor:
"""Compatibility no-op contributor after workspace-git checkpoint removal."""

component_key = WORKSPACE_FILES

def serialize(self, ctx: Any) -> dict[str, Any]:
_ = ctx
return {}

def deserialize_and_apply(self, payload: Any, ctx: Any) -> None:
_ = (payload, ctx)

__all__ = [
"MemoryCheckpointStore",
"DefaultCheckpointSaveRestore",
"StmContributor",
"WorkspaceGitContributor",
"SessionStateContributor",
"SessionContextContributor",
]
12 changes: 6 additions & 6 deletions dare_framework/embedding/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""embedding domain facade."""

from dare_framework.embedding.interfaces import IEmbeddingAdapter
from dare_framework.embedding.types import EmbeddingOptions, EmbeddingResult
from dare_framework.embedding._internal import OpenAIEmbeddingAdapter
__all__ = [
"IEmbeddingAdapter",
from dare_framework.embedding.interfaces import IEmbeddingAdapter
from dare_framework.embedding.types import EmbeddingOptions, EmbeddingResult
from dare_framework.embedding.defaults import OpenAIEmbeddingAdapter

__all__ = [
"IEmbeddingAdapter",
"EmbeddingOptions",
"EmbeddingResult",
"OpenAIEmbeddingAdapter",
Expand Down
5 changes: 5 additions & 0 deletions dare_framework/embedding/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Supported default embedding implementations."""

from dare_framework.embedding._internal.openai_embedding import OpenAIEmbeddingAdapter

__all__ = ["OpenAIEmbeddingAdapter"]
2 changes: 1 addition & 1 deletion dare_framework/event/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""event domain facade."""

from dare_framework.event._internal import DefaultEventLog, SQLiteEventLog
from dare_framework.event.defaults import DefaultEventLog, SQLiteEventLog
from dare_framework.event.kernel import IEventLog
from dare_framework.event.types import Event, RuntimeSnapshot

Expand Down
5 changes: 5 additions & 0 deletions dare_framework/event/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Supported default event-log implementations."""

from dare_framework.event._internal.sqlite_event_log import DefaultEventLog, SQLiteEventLog

__all__ = ["SQLiteEventLog", "DefaultEventLog"]
2 changes: 1 addition & 1 deletion dare_framework/hook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from dare_framework.hook.interfaces import IHookManager
from dare_framework.hook.kernel import IExtensionPoint, IHook, HookFn
from dare_framework.hook._internal import HookExtensionPoint
from dare_framework.hook.defaults import HookExtensionPoint
from dare_framework.hook.types import HookDecision, HookEnvelope, HookPhase, HookResult

__all__ = [
Expand Down
5 changes: 5 additions & 0 deletions dare_framework/hook/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Supported default extension-point implementation."""

from dare_framework.hook._internal.hook_extension_point import HookExtensionPoint

__all__ = ["HookExtensionPoint"]
2 changes: 1 addition & 1 deletion dare_framework/plan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
ValidatedStep,
VerifyResult,
)
from dare_framework.plan._internal import DefaultPlanner, DefaultRemediator
from dare_framework.plan.defaults import DefaultPlanner, DefaultRemediator

__all__ = [
# Interfaces
Expand Down
6 changes: 6 additions & 0 deletions dare_framework/plan/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Supported default planner/remediator implementations."""

from dare_framework.plan._internal.default_planner import DefaultPlanner
from dare_framework.plan._internal.default_remediator import DefaultRemediator

__all__ = ["DefaultPlanner", "DefaultRemediator"]
7 changes: 5 additions & 2 deletions dare_framework/security/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Security domain facade."""

from dare_framework.security._internal import DefaultSecurityBoundary
from dare_framework.security.defaults import DefaultSecurityBoundary
from dare_framework.security.errors import (
SECURITY_APPROVAL_MANAGER_MISSING,
SECURITY_POLICY_CHECK_FAILED,
SECURITY_POLICY_DENIED,
SECURITY_TRUST_DERIVATION_FAILED,
SecurityBoundaryError,
)
from dare_framework.security.impl import NoOpSecurityBoundary, PolicySecurityBoundary
from dare_framework.security.impl import (
NoOpSecurityBoundary,
PolicySecurityBoundary,
)
from dare_framework.security.kernel import ISecurityBoundary
from dare_framework.security.types import PolicyDecision, RiskLevel, SandboxSpec, TrustedInput

Expand Down
5 changes: 5 additions & 0 deletions dare_framework/security/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Public default security boundary exports."""

from dare_framework.security._internal.default_security_boundary import DefaultSecurityBoundary

__all__ = ["DefaultSecurityBoundary"]
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ async def execute_safe(
return await _execute_callable(fn)


# Backward-compatible alias for legacy references.
# Backward-compatible alias for legacy impl references.
DefaultSecurityBoundary = PolicySecurityBoundary


Expand Down
14 changes: 7 additions & 7 deletions dare_framework/transport/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@
from dare_framework.transport.serialization import jsonify_transport_value
from dare_framework.transport.types import (
ActionPayload,
AttachmentRef,
ControlPayload,
EnvelopePayload,
EnvelopeKind,
MessageRole,
EnvelopePayload,
MessageKind,
MessagePayload,
AttachmentRef,
MessageRole,
Receiver,
SelectDomain,
SelectKind,
SelectPayload,
Sender,
TransportEnvelope,
new_envelope_id,
Receiver,
Sender,
)
from dare_framework.transport.interaction import (
AgentControl,
ActionHandlerDispatcher,
AgentControl,
ResourceAction,
)
from dare_framework.transport._internal import (
from dare_framework.transport.adapters import (
DefaultAgentChannel,
DirectClientChannel,
StdioClientChannel,
Expand Down
15 changes: 15 additions & 0 deletions dare_framework/transport/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Supported default transport channel adapters."""

from dare_framework.transport._internal import (
DefaultAgentChannel,
DirectClientChannel,
StdioClientChannel,
WebSocketClientChannel,
)

__all__ = [
"DefaultAgentChannel",
"DirectClientChannel",
"StdioClientChannel",
"WebSocketClientChannel",
]
5 changes: 5 additions & 0 deletions docs/design/Framework_MinSurface_Review.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,8 @@ MCP adapters, etc.
- Tight coupling to concrete classes prevents safe refactors.
- External users rely on internal DTOs that must then be supported forever.
- Increased likelihood of breaking changes with each internal improvement.

## 5. Implementation Note (2026-03-04)
- T0-4 facade compliance batch moved public-domain `__init__.py` imports away from direct `._internal` references into explicit public export layers (`defaults.py`, `impl`, `adapters`).
- The following facades now import only public modules directly: `checkpoint`, `embedding`, `event`, `hook`, `plan`, `security`, `transport`.
- A regression rule was added in `tests/unit/test_package_initializers_facade_pattern.py` to block future direct `._internal` imports from public facades.
Loading
Loading