-
Notifications
You must be signed in to change notification settings - Fork 2
Extract runtime facade compatibility layer from PR 188 #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
3bb4706
130b6cc
22cb1da
7c78e1f
f32e736
35a8670
5362ee5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| """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] = dict(payload) | ||
|
|
||
| def get(self, checkpoint_id: str) -> dict[str, Any] | None: | ||
| if checkpoint_id not in self._store: | ||
| return None | ||
| return dict(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": dict(getattr(m, "metadata", {}) or {}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in
|
||
| "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 | ||
| context.stm_add( | ||
| Message( | ||
| role=item.get("role", "user"), | ||
| kind=item.get("kind", "chat"), | ||
| text=item.get("text") or item.get("content", ""), | ||
| attachments=_clone_payload(item.get("attachments")) or [], | ||
| data=_clone_payload(item.get("data")), | ||
| name=item.get("name"), | ||
| metadata=dict(item.get("metadata") or {}), | ||
|
Comment on lines
+137
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in
|
||
| 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
+173
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
| 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", | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,25 @@ | |
| _UNCHANGED = object() | ||
|
|
||
|
|
||
| def _freeze_value(value: Any) -> Any: | ||
| """Build a hashable structural key for nested message payloads.""" | ||
| if isinstance(value, dict): | ||
| return tuple(sorted((str(key), _freeze_value(item)) for key, item in value.items())) | ||
| if isinstance(value, list): | ||
| return tuple(_freeze_value(item) for item in value) | ||
| if isinstance(value, tuple): | ||
| return tuple(_freeze_value(item) for item in value) | ||
| if hasattr(value, "kind") and hasattr(value, "uri"): | ||
| return ( | ||
| getattr(value.kind, "value", value.kind), | ||
| value.uri, | ||
| value.mime_type, | ||
| value.filename, | ||
| _freeze_value(getattr(value, "metadata", {})), | ||
| ) | ||
| return value | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in
|
||
|
|
||
|
|
||
| def _copy_message( | ||
| message: Message, | ||
| *, | ||
|
|
@@ -41,18 +60,24 @@ def _copy_message( | |
|
|
||
|
|
||
| def _dedup_messages(messages: List[Message]) -> Tuple[List[Message], int]: | ||
| """Lightweight de-duplication on (role, text).""" | ||
| seen: set[int] = set() | ||
| """De-duplicate only when the full public message payload matches.""" | ||
| seen: set[Any] = set() | ||
| result: List[Message] = [] | ||
| removed = 0 | ||
|
|
||
| for msg in messages: | ||
| key = (msg.role, msg.text) | ||
| digest = hash(key) | ||
| if digest in seen: | ||
| key = ( | ||
| msg.role, | ||
| msg.kind, | ||
| msg.text, | ||
| msg.name, | ||
| _freeze_value(msg.attachments), | ||
| _freeze_value(msg.data), | ||
| ) | ||
| if key in seen: | ||
| removed += 1 | ||
| continue | ||
| seen.add(digest) | ||
| seen.add(key) | ||
| result.append(msg) | ||
|
|
||
| return result, removed | ||
|
|
||
| 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"] |
| 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"] |
| 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"] |
| 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"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MemoryCheckpointStore.get()returns only a shallowdict(...)copy, so nested structures remain shared with_store. In practice this corrupts checkpoints: afterrestore(), mutating nested message metadata in the restored context can mutate the stored snapshot, and a second restore returns the mutated state instead of the original checkpoint. Checkpoint snapshots should be immutable once saved, so this path needs a deep clone on read (and ideally on write as well).Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handled in
5362ee5.MemoryCheckpointStorenow deep-copies payloads on bothput()andget(), so nested checkpoint state cannot be mutated through restored objects or repeated reads. Added a regression intests/unit/test_checkpoint_defaults.pythat restores the same checkpoint twice and verifies mutating the first restored context does not affect the second restore.