kweaver-dolphin 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DolphinLanguageSDK/__init__.py +58 -0
- dolphin/__init__.py +62 -0
- dolphin/cli/__init__.py +20 -0
- dolphin/cli/args/__init__.py +9 -0
- dolphin/cli/args/parser.py +567 -0
- dolphin/cli/builtin_agents/__init__.py +22 -0
- dolphin/cli/commands/__init__.py +4 -0
- dolphin/cli/interrupt/__init__.py +8 -0
- dolphin/cli/interrupt/handler.py +205 -0
- dolphin/cli/interrupt/keyboard.py +82 -0
- dolphin/cli/main.py +49 -0
- dolphin/cli/multimodal/__init__.py +34 -0
- dolphin/cli/multimodal/clipboard.py +327 -0
- dolphin/cli/multimodal/handler.py +249 -0
- dolphin/cli/multimodal/image_processor.py +214 -0
- dolphin/cli/multimodal/input_parser.py +149 -0
- dolphin/cli/runner/__init__.py +8 -0
- dolphin/cli/runner/runner.py +989 -0
- dolphin/cli/ui/__init__.py +10 -0
- dolphin/cli/ui/console.py +2795 -0
- dolphin/cli/ui/input.py +340 -0
- dolphin/cli/ui/layout.py +425 -0
- dolphin/cli/ui/stream_renderer.py +302 -0
- dolphin/cli/utils/__init__.py +8 -0
- dolphin/cli/utils/helpers.py +135 -0
- dolphin/cli/utils/version.py +49 -0
- dolphin/core/__init__.py +107 -0
- dolphin/core/agent/__init__.py +10 -0
- dolphin/core/agent/agent_state.py +69 -0
- dolphin/core/agent/base_agent.py +970 -0
- dolphin/core/code_block/__init__.py +0 -0
- dolphin/core/code_block/agent_init_block.py +0 -0
- dolphin/core/code_block/assign_block.py +98 -0
- dolphin/core/code_block/basic_code_block.py +1865 -0
- dolphin/core/code_block/explore_block.py +1327 -0
- dolphin/core/code_block/explore_block_v2.py +712 -0
- dolphin/core/code_block/explore_strategy.py +672 -0
- dolphin/core/code_block/judge_block.py +220 -0
- dolphin/core/code_block/prompt_block.py +32 -0
- dolphin/core/code_block/skill_call_deduplicator.py +291 -0
- dolphin/core/code_block/tool_block.py +129 -0
- dolphin/core/common/__init__.py +17 -0
- dolphin/core/common/constants.py +176 -0
- dolphin/core/common/enums.py +1173 -0
- dolphin/core/common/exceptions.py +133 -0
- dolphin/core/common/multimodal.py +539 -0
- dolphin/core/common/object_type.py +165 -0
- dolphin/core/common/output_format.py +432 -0
- dolphin/core/common/types.py +36 -0
- dolphin/core/config/__init__.py +16 -0
- dolphin/core/config/global_config.py +1289 -0
- dolphin/core/config/ontology_config.py +133 -0
- dolphin/core/context/__init__.py +12 -0
- dolphin/core/context/context.py +1580 -0
- dolphin/core/context/context_manager.py +161 -0
- dolphin/core/context/var_output.py +82 -0
- dolphin/core/context/variable_pool.py +356 -0
- dolphin/core/context_engineer/__init__.py +41 -0
- dolphin/core/context_engineer/config/__init__.py +5 -0
- dolphin/core/context_engineer/config/settings.py +402 -0
- dolphin/core/context_engineer/core/__init__.py +7 -0
- dolphin/core/context_engineer/core/budget_manager.py +327 -0
- dolphin/core/context_engineer/core/context_assembler.py +583 -0
- dolphin/core/context_engineer/core/context_manager.py +637 -0
- dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
- dolphin/core/context_engineer/example/incremental_example.py +267 -0
- dolphin/core/context_engineer/example/traditional_example.py +334 -0
- dolphin/core/context_engineer/services/__init__.py +5 -0
- dolphin/core/context_engineer/services/compressor.py +399 -0
- dolphin/core/context_engineer/utils/__init__.py +6 -0
- dolphin/core/context_engineer/utils/context_utils.py +441 -0
- dolphin/core/context_engineer/utils/message_formatter.py +270 -0
- dolphin/core/context_engineer/utils/token_utils.py +139 -0
- dolphin/core/coroutine/__init__.py +15 -0
- dolphin/core/coroutine/context_snapshot.py +154 -0
- dolphin/core/coroutine/context_snapshot_profile.py +922 -0
- dolphin/core/coroutine/context_snapshot_store.py +268 -0
- dolphin/core/coroutine/execution_frame.py +145 -0
- dolphin/core/coroutine/execution_state_registry.py +161 -0
- dolphin/core/coroutine/resume_handle.py +101 -0
- dolphin/core/coroutine/step_result.py +101 -0
- dolphin/core/executor/__init__.py +18 -0
- dolphin/core/executor/debug_controller.py +630 -0
- dolphin/core/executor/dolphin_executor.py +1063 -0
- dolphin/core/executor/executor.py +624 -0
- dolphin/core/flags/__init__.py +27 -0
- dolphin/core/flags/definitions.py +49 -0
- dolphin/core/flags/manager.py +113 -0
- dolphin/core/hook/__init__.py +95 -0
- dolphin/core/hook/expression_evaluator.py +499 -0
- dolphin/core/hook/hook_dispatcher.py +380 -0
- dolphin/core/hook/hook_types.py +248 -0
- dolphin/core/hook/isolated_variable_pool.py +284 -0
- dolphin/core/interfaces.py +53 -0
- dolphin/core/llm/__init__.py +0 -0
- dolphin/core/llm/llm.py +495 -0
- dolphin/core/llm/llm_call.py +100 -0
- dolphin/core/llm/llm_client.py +1285 -0
- dolphin/core/llm/message_sanitizer.py +120 -0
- dolphin/core/logging/__init__.py +20 -0
- dolphin/core/logging/logger.py +526 -0
- dolphin/core/message/__init__.py +8 -0
- dolphin/core/message/compressor.py +749 -0
- dolphin/core/parser/__init__.py +8 -0
- dolphin/core/parser/parser.py +405 -0
- dolphin/core/runtime/__init__.py +10 -0
- dolphin/core/runtime/runtime_graph.py +926 -0
- dolphin/core/runtime/runtime_instance.py +446 -0
- dolphin/core/skill/__init__.py +14 -0
- dolphin/core/skill/context_retention.py +157 -0
- dolphin/core/skill/skill_function.py +686 -0
- dolphin/core/skill/skill_matcher.py +282 -0
- dolphin/core/skill/skillkit.py +700 -0
- dolphin/core/skill/skillset.py +72 -0
- dolphin/core/trajectory/__init__.py +10 -0
- dolphin/core/trajectory/recorder.py +189 -0
- dolphin/core/trajectory/trajectory.py +522 -0
- dolphin/core/utils/__init__.py +9 -0
- dolphin/core/utils/cache_kv.py +212 -0
- dolphin/core/utils/tools.py +340 -0
- dolphin/lib/__init__.py +93 -0
- dolphin/lib/debug/__init__.py +8 -0
- dolphin/lib/debug/visualizer.py +409 -0
- dolphin/lib/memory/__init__.py +28 -0
- dolphin/lib/memory/async_processor.py +220 -0
- dolphin/lib/memory/llm_calls.py +195 -0
- dolphin/lib/memory/manager.py +78 -0
- dolphin/lib/memory/sandbox.py +46 -0
- dolphin/lib/memory/storage.py +245 -0
- dolphin/lib/memory/utils.py +51 -0
- dolphin/lib/ontology/__init__.py +12 -0
- dolphin/lib/ontology/basic/__init__.py +0 -0
- dolphin/lib/ontology/basic/base.py +102 -0
- dolphin/lib/ontology/basic/concept.py +130 -0
- dolphin/lib/ontology/basic/object.py +11 -0
- dolphin/lib/ontology/basic/relation.py +63 -0
- dolphin/lib/ontology/datasource/__init__.py +27 -0
- dolphin/lib/ontology/datasource/datasource.py +66 -0
- dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
- dolphin/lib/ontology/datasource/sql.py +845 -0
- dolphin/lib/ontology/mapping.py +177 -0
- dolphin/lib/ontology/ontology.py +733 -0
- dolphin/lib/ontology/ontology_context.py +16 -0
- dolphin/lib/ontology/ontology_manager.py +107 -0
- dolphin/lib/skill_results/__init__.py +31 -0
- dolphin/lib/skill_results/cache_backend.py +559 -0
- dolphin/lib/skill_results/result_processor.py +181 -0
- dolphin/lib/skill_results/result_reference.py +179 -0
- dolphin/lib/skill_results/skillkit_hook.py +324 -0
- dolphin/lib/skill_results/strategies.py +328 -0
- dolphin/lib/skill_results/strategy_registry.py +150 -0
- dolphin/lib/skillkits/__init__.py +44 -0
- dolphin/lib/skillkits/agent_skillkit.py +155 -0
- dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
- dolphin/lib/skillkits/env_skillkit.py +250 -0
- dolphin/lib/skillkits/mcp_adapter.py +616 -0
- dolphin/lib/skillkits/mcp_skillkit.py +771 -0
- dolphin/lib/skillkits/memory_skillkit.py +650 -0
- dolphin/lib/skillkits/noop_skillkit.py +31 -0
- dolphin/lib/skillkits/ontology_skillkit.py +89 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
- dolphin/lib/skillkits/resource/__init__.py +52 -0
- dolphin/lib/skillkits/resource/models/__init__.py +6 -0
- dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
- dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
- dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
- dolphin/lib/skillkits/resource/skill_cache.py +215 -0
- dolphin/lib/skillkits/resource/skill_loader.py +395 -0
- dolphin/lib/skillkits/resource/skill_validator.py +406 -0
- dolphin/lib/skillkits/resource_skillkit.py +11 -0
- dolphin/lib/skillkits/search_skillkit.py +163 -0
- dolphin/lib/skillkits/sql_skillkit.py +274 -0
- dolphin/lib/skillkits/system_skillkit.py +509 -0
- dolphin/lib/skillkits/vm_skillkit.py +65 -0
- dolphin/lib/utils/__init__.py +9 -0
- dolphin/lib/utils/data_process.py +207 -0
- dolphin/lib/utils/handle_progress.py +178 -0
- dolphin/lib/utils/security.py +139 -0
- dolphin/lib/utils/text_retrieval.py +462 -0
- dolphin/lib/vm/__init__.py +11 -0
- dolphin/lib/vm/env_executor.py +895 -0
- dolphin/lib/vm/python_session_manager.py +453 -0
- dolphin/lib/vm/vm.py +610 -0
- dolphin/sdk/__init__.py +60 -0
- dolphin/sdk/agent/__init__.py +12 -0
- dolphin/sdk/agent/agent_factory.py +236 -0
- dolphin/sdk/agent/dolphin_agent.py +1106 -0
- dolphin/sdk/api/__init__.py +4 -0
- dolphin/sdk/runtime/__init__.py +8 -0
- dolphin/sdk/runtime/env.py +363 -0
- dolphin/sdk/skill/__init__.py +10 -0
- dolphin/sdk/skill/global_skills.py +706 -0
- dolphin/sdk/skill/traditional_toolkit.py +260 -0
- kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
- kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
- kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
- kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
- kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
- kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from dolphin.core.logging.logger import get_logger
|
|
9
|
+
from .context_snapshot import ContextSnapshot
|
|
10
|
+
|
|
11
|
+
logger = get_logger()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ContextSnapshotStore(ABC):
|
|
15
|
+
"""Abstract interface for context snapshot storage"""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def save_snapshot(self, snapshot: ContextSnapshot) -> str:
|
|
19
|
+
"""Save snapshot, return snapshot ID"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def load_snapshot(self, snapshot_id: str) -> Optional[ContextSnapshot]:
|
|
24
|
+
"""Load snapshot"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def delete_snapshot(self, snapshot_id: str) -> bool:
|
|
29
|
+
"""Delete snapshot"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def list_snapshots(self, frame_id: Optional[str] = None) -> List[str]:
|
|
34
|
+
"""List snapshots"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def save_pending_snapshot(self, snapshot: ContextSnapshot) -> str:
|
|
39
|
+
"""Save pending snapshot (for transactional)"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def finalize_snapshot(self, snapshot_id: str) -> bool:
|
|
44
|
+
"""Confirm Snapshot (Atomic Rename)"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FileContextSnapshotStore(ContextSnapshotStore):
|
|
49
|
+
"""Filesystem-based context snapshot storage"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, base_path: str = "./data/snapshots"):
|
|
52
|
+
self.base_path = base_path
|
|
53
|
+
self._lock = threading.RLock()
|
|
54
|
+
self._ensure_directory()
|
|
55
|
+
|
|
56
|
+
def _ensure_directory(self):
|
|
57
|
+
"""Ensure the storage directory exists"""
|
|
58
|
+
os.makedirs(self.base_path, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
def _get_snapshot_path(self, snapshot_id: str) -> str:
|
|
61
|
+
"""Get snapshot file path"""
|
|
62
|
+
return os.path.join(self.base_path, f"{snapshot_id}.json")
|
|
63
|
+
|
|
64
|
+
def _get_pending_path(self, snapshot_id: str) -> str:
|
|
65
|
+
"""Get pending snapshot file path"""
|
|
66
|
+
return os.path.join(self.base_path, f"{snapshot_id}.pending.json")
|
|
67
|
+
|
|
68
|
+
def save_snapshot(self, snapshot: ContextSnapshot) -> str:
|
|
69
|
+
"""Save snapshot"""
|
|
70
|
+
with self._lock:
|
|
71
|
+
file_path = self._get_snapshot_path(snapshot.snapshot_id)
|
|
72
|
+
try:
|
|
73
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
74
|
+
json.dump(snapshot.encode(), f, ensure_ascii=False, indent=2)
|
|
75
|
+
return snapshot.snapshot_id
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
f"Failed to save snapshot {snapshot.snapshot_id}: {e}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def load_snapshot(self, snapshot_id: str) -> Optional[ContextSnapshot]:
|
|
82
|
+
"""Load snapshot"""
|
|
83
|
+
with self._lock:
|
|
84
|
+
file_path = self._get_snapshot_path(snapshot_id)
|
|
85
|
+
if not os.path.exists(file_path):
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
90
|
+
data = json.load(f)
|
|
91
|
+
return ContextSnapshot.decode(data)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise RuntimeError(f"Failed to load snapshot {snapshot_id}: {e}")
|
|
94
|
+
|
|
95
|
+
def delete_snapshot(self, snapshot_id: str) -> bool:
|
|
96
|
+
"""Delete snapshot"""
|
|
97
|
+
with self._lock:
|
|
98
|
+
file_path = self._get_snapshot_path(snapshot_id)
|
|
99
|
+
if os.path.exists(file_path):
|
|
100
|
+
try:
|
|
101
|
+
os.remove(file_path)
|
|
102
|
+
return True
|
|
103
|
+
except Exception:
|
|
104
|
+
return False
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def list_snapshots(self, frame_id: Optional[str] = None) -> List[str]:
|
|
108
|
+
"""List snapshots"""
|
|
109
|
+
with self._lock:
|
|
110
|
+
snapshots = []
|
|
111
|
+
if not os.path.exists(self.base_path):
|
|
112
|
+
return snapshots
|
|
113
|
+
|
|
114
|
+
for filename in os.listdir(self.base_path):
|
|
115
|
+
if filename.endswith(".json") and not filename.endswith(
|
|
116
|
+
".pending.json"
|
|
117
|
+
):
|
|
118
|
+
snapshot_id = filename[:-5] # Remove .json suffix
|
|
119
|
+
|
|
120
|
+
# If frame_id is specified, filter it.
|
|
121
|
+
if frame_id:
|
|
122
|
+
try:
|
|
123
|
+
snapshot = self.load_snapshot(snapshot_id)
|
|
124
|
+
if snapshot and snapshot.frame_id == frame_id:
|
|
125
|
+
snapshots.append(snapshot_id)
|
|
126
|
+
except Exception:
|
|
127
|
+
continue
|
|
128
|
+
else:
|
|
129
|
+
snapshots.append(snapshot_id)
|
|
130
|
+
|
|
131
|
+
return sorted(snapshots)
|
|
132
|
+
|
|
133
|
+
def save_pending_snapshot(self, snapshot: ContextSnapshot) -> str:
|
|
134
|
+
"""Pending snapshot"""
|
|
135
|
+
with self._lock:
|
|
136
|
+
pending_path = self._get_pending_path(snapshot.snapshot_id)
|
|
137
|
+
try:
|
|
138
|
+
with open(pending_path, "w", encoding="utf-8") as f:
|
|
139
|
+
json.dump(snapshot.encode(), f, ensure_ascii=False, indent=2)
|
|
140
|
+
return snapshot.snapshot_id
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"Failed to save pending snapshot {snapshot.snapshot_id}: {e}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def finalize_snapshot(self, snapshot_id: str) -> bool:
|
|
147
|
+
"""Confirm Snapshot (Atomic Rename)"""
|
|
148
|
+
with self._lock:
|
|
149
|
+
pending_path = self._get_pending_path(snapshot_id)
|
|
150
|
+
final_path = self._get_snapshot_path(snapshot_id)
|
|
151
|
+
|
|
152
|
+
if not os.path.exists(pending_path):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
os.rename(pending_path, final_path)
|
|
157
|
+
return True
|
|
158
|
+
except Exception:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
def cleanup_pending_snapshots(self, max_age_seconds: int = 3600) -> int:
|
|
162
|
+
"""Clean up expired pending snapshots"""
|
|
163
|
+
with self._lock:
|
|
164
|
+
cleaned = 0
|
|
165
|
+
current_time = time.time()
|
|
166
|
+
|
|
167
|
+
if not os.path.exists(self.base_path):
|
|
168
|
+
return cleaned
|
|
169
|
+
|
|
170
|
+
for filename in os.listdir(self.base_path):
|
|
171
|
+
if filename.endswith(".pending.json"):
|
|
172
|
+
file_path = os.path.join(self.base_path, filename)
|
|
173
|
+
try:
|
|
174
|
+
file_mtime = os.path.getmtime(file_path)
|
|
175
|
+
if current_time - file_mtime > max_age_seconds:
|
|
176
|
+
os.remove(file_path)
|
|
177
|
+
cleaned += 1
|
|
178
|
+
except Exception:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
return cleaned
|
|
182
|
+
|
|
183
|
+
def get_snapshot_stats(self) -> Dict:
|
|
184
|
+
"""Get snapshot storage statistics"""
|
|
185
|
+
with self._lock:
|
|
186
|
+
stats = {
|
|
187
|
+
"total_snapshots": 0,
|
|
188
|
+
"pending_snapshots": 0,
|
|
189
|
+
"total_size_bytes": 0,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if not os.path.exists(self.base_path):
|
|
193
|
+
return stats
|
|
194
|
+
|
|
195
|
+
for filename in os.listdir(self.base_path):
|
|
196
|
+
file_path = os.path.join(self.base_path, filename)
|
|
197
|
+
try:
|
|
198
|
+
file_size = os.path.getsize(file_path)
|
|
199
|
+
stats["total_size_bytes"] += file_size
|
|
200
|
+
|
|
201
|
+
if filename.endswith(".pending.json"):
|
|
202
|
+
stats["pending_snapshots"] += 1
|
|
203
|
+
elif filename.endswith(".json"):
|
|
204
|
+
stats["total_snapshots"] += 1
|
|
205
|
+
except Exception:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
return stats
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class MemoryContextSnapshotStore(ContextSnapshotStore):
|
|
212
|
+
"""In-memory context snapshot storage (for testing)"""
|
|
213
|
+
|
|
214
|
+
def __init__(self):
|
|
215
|
+
self._snapshots: Dict[str, ContextSnapshot] = {}
|
|
216
|
+
self._pending: Dict[str, ContextSnapshot] = {}
|
|
217
|
+
self._lock = threading.RLock()
|
|
218
|
+
|
|
219
|
+
def save_snapshot(self, snapshot: ContextSnapshot) -> str:
|
|
220
|
+
"""Save snapshot"""
|
|
221
|
+
with self._lock:
|
|
222
|
+
self._snapshots[snapshot.snapshot_id] = snapshot
|
|
223
|
+
return snapshot.snapshot_id
|
|
224
|
+
|
|
225
|
+
def load_snapshot(self, snapshot_id: str) -> Optional[ContextSnapshot]:
|
|
226
|
+
"""Load snapshot"""
|
|
227
|
+
with self._lock:
|
|
228
|
+
return self._snapshots.get(snapshot_id)
|
|
229
|
+
|
|
230
|
+
def delete_snapshot(self, snapshot_id: str) -> bool:
|
|
231
|
+
"""Delete snapshot"""
|
|
232
|
+
with self._lock:
|
|
233
|
+
if snapshot_id in self._snapshots:
|
|
234
|
+
del self._snapshots[snapshot_id]
|
|
235
|
+
return True
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def list_snapshots(self, frame_id: Optional[str] = None) -> List[str]:
|
|
239
|
+
"""List snapshots"""
|
|
240
|
+
with self._lock:
|
|
241
|
+
if frame_id:
|
|
242
|
+
return [
|
|
243
|
+
sid
|
|
244
|
+
for sid, snapshot in self._snapshots.items()
|
|
245
|
+
if snapshot.frame_id == frame_id
|
|
246
|
+
]
|
|
247
|
+
return list(self._snapshots.keys())
|
|
248
|
+
|
|
249
|
+
def save_pending_snapshot(self, snapshot: ContextSnapshot) -> str:
|
|
250
|
+
"""Pending snapshot"""
|
|
251
|
+
with self._lock:
|
|
252
|
+
self._pending[snapshot.snapshot_id] = snapshot
|
|
253
|
+
return snapshot.snapshot_id
|
|
254
|
+
|
|
255
|
+
def finalize_snapshot(self, snapshot_id: str) -> bool:
|
|
256
|
+
"""Confirm Snapshot"""
|
|
257
|
+
with self._lock:
|
|
258
|
+
if snapshot_id in self._pending:
|
|
259
|
+
snapshot = self._pending.pop(snapshot_id)
|
|
260
|
+
self._snapshots[snapshot_id] = snapshot
|
|
261
|
+
return True
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
def clear(self):
|
|
265
|
+
"""Clear all snapshots"""
|
|
266
|
+
with self._lock:
|
|
267
|
+
self._snapshots.clear()
|
|
268
|
+
self._pending.clear()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import uuid
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional, List, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FrameStatus(Enum):
|
|
9
|
+
RUNNING = "running"
|
|
10
|
+
PAUSED = "paused"
|
|
11
|
+
COMPLETED = "completed"
|
|
12
|
+
FAILED = "failed"
|
|
13
|
+
WAITING_FOR_INTERVENTION = "waiting_for_intervention"
|
|
14
|
+
TERMINATED = "terminated"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WaitReason(Enum):
|
|
18
|
+
"""Reason for WAITING status (entropy-reduction: reuse WAITING with discriminator).
|
|
19
|
+
|
|
20
|
+
This enum distinguishes different waiting scenarios without adding new frame statuses.
|
|
21
|
+
"""
|
|
22
|
+
TOOL_REQUEST = "tool_request" # Tool requests user input (ToolInterrupt)
|
|
23
|
+
USER_INTERRUPT = "user_interrupt" # User actively interrupted (UserInterrupt)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ExecutionFrame:
|
|
28
|
+
"""Execution frame - saves execution state and control flow information"""
|
|
29
|
+
|
|
30
|
+
frame_id: str
|
|
31
|
+
parent_id: Optional[str] = None
|
|
32
|
+
agent_id: Optional[str] = None
|
|
33
|
+
block_pointer: int = 0
|
|
34
|
+
block_stack: List[Dict] = None
|
|
35
|
+
status: FrameStatus = FrameStatus.RUNNING
|
|
36
|
+
context_snapshot_id: str = ""
|
|
37
|
+
children: List[str] = None
|
|
38
|
+
created_at: float = None
|
|
39
|
+
updated_at: float = None
|
|
40
|
+
original_content: str = "" # Added: Save original dolphin script content
|
|
41
|
+
error: Optional[Dict] = None # Added: Error Messages
|
|
42
|
+
wait_reason: Optional[WaitReason] = None # Reason when in WAITING status
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
if self.block_stack is None:
|
|
46
|
+
self.block_stack = []
|
|
47
|
+
if self.children is None:
|
|
48
|
+
self.children = []
|
|
49
|
+
if self.created_at is None:
|
|
50
|
+
self.created_at = time.time()
|
|
51
|
+
if self.updated_at is None:
|
|
52
|
+
self.updated_at = time.time()
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def create_root_frame(cls, agent_id: Optional[str] = None) -> "ExecutionFrame":
|
|
56
|
+
"""Create root execution frame"""
|
|
57
|
+
return cls(
|
|
58
|
+
frame_id=str(uuid.uuid4()),
|
|
59
|
+
parent_id=None,
|
|
60
|
+
agent_id=agent_id,
|
|
61
|
+
block_pointer=0,
|
|
62
|
+
block_stack=[],
|
|
63
|
+
status=FrameStatus.RUNNING,
|
|
64
|
+
children=[],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def create_child_frame(
|
|
69
|
+
cls, parent_frame: "ExecutionFrame", agent_id: Optional[str] = None
|
|
70
|
+
) -> "ExecutionFrame":
|
|
71
|
+
"""Create sub-execution frame"""
|
|
72
|
+
return cls(
|
|
73
|
+
frame_id=str(uuid.uuid4()),
|
|
74
|
+
parent_id=parent_frame.frame_id,
|
|
75
|
+
agent_id=agent_id,
|
|
76
|
+
block_pointer=0,
|
|
77
|
+
block_stack=[],
|
|
78
|
+
status=FrameStatus.RUNNING,
|
|
79
|
+
children=[],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def update_timestamp(self):
|
|
83
|
+
"""Update timestamp"""
|
|
84
|
+
self.updated_at = time.time()
|
|
85
|
+
|
|
86
|
+
def is_completed(self) -> bool:
|
|
87
|
+
"""Check if completed"""
|
|
88
|
+
return self.status == FrameStatus.COMPLETED
|
|
89
|
+
|
|
90
|
+
def is_failed(self) -> bool:
|
|
91
|
+
"""Check if failed"""
|
|
92
|
+
return self.status == FrameStatus.FAILED
|
|
93
|
+
|
|
94
|
+
def is_paused(self) -> bool:
|
|
95
|
+
"""Check if paused"""
|
|
96
|
+
return self.status == FrameStatus.PAUSED
|
|
97
|
+
|
|
98
|
+
def is_running(self) -> bool:
|
|
99
|
+
"""Check if running"""
|
|
100
|
+
return self.status == FrameStatus.RUNNING
|
|
101
|
+
|
|
102
|
+
def is_waiting_for_intervention(self) -> bool:
|
|
103
|
+
"""Check if waiting for user intervention"""
|
|
104
|
+
return self.status == FrameStatus.WAITING_FOR_INTERVENTION
|
|
105
|
+
|
|
106
|
+
def is_terminated(self) -> bool:
|
|
107
|
+
"""Check if already terminated"""
|
|
108
|
+
return self.status == FrameStatus.TERMINATED
|
|
109
|
+
|
|
110
|
+
def is_waiting_for_user_input(self) -> bool:
|
|
111
|
+
"""Check if waiting for user input (due to UserInterrupt)"""
|
|
112
|
+
return (
|
|
113
|
+
self.status == FrameStatus.WAITING_FOR_INTERVENTION
|
|
114
|
+
and self.wait_reason == WaitReason.USER_INTERRUPT
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict:
|
|
118
|
+
"""Convert to dictionary format"""
|
|
119
|
+
return {
|
|
120
|
+
"frame_id": self.frame_id,
|
|
121
|
+
"parent_id": self.parent_id,
|
|
122
|
+
"agent_id": self.agent_id,
|
|
123
|
+
"block_pointer": self.block_pointer,
|
|
124
|
+
"block_stack": self.block_stack,
|
|
125
|
+
"status": self.status.value,
|
|
126
|
+
"context_snapshot_id": self.context_snapshot_id,
|
|
127
|
+
"children": self.children,
|
|
128
|
+
"created_at": self.created_at,
|
|
129
|
+
"updated_at": self.updated_at,
|
|
130
|
+
"original_content": self.original_content,
|
|
131
|
+
"error": self.error,
|
|
132
|
+
"wait_reason": self.wait_reason.value if self.wait_reason else None,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_dict(cls, data: Dict) -> "ExecutionFrame":
|
|
137
|
+
"""Create execution frame from dictionary"""
|
|
138
|
+
data = data.copy()
|
|
139
|
+
data["status"] = FrameStatus(data["status"])
|
|
140
|
+
# Handle wait_reason if present
|
|
141
|
+
if data.get("wait_reason"):
|
|
142
|
+
data["wait_reason"] = WaitReason(data["wait_reason"])
|
|
143
|
+
else:
|
|
144
|
+
data["wait_reason"] = None
|
|
145
|
+
return cls(**data)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
from .execution_frame import ExecutionFrame, FrameStatus
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExecutionStateRegistry:
|
|
8
|
+
"""Execution Status Registry - Manage the lifecycle of all execution frames"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._frames: Dict[str, ExecutionFrame] = {}
|
|
12
|
+
self._lock = threading.RLock() # Using Reentrant Lock
|
|
13
|
+
|
|
14
|
+
def register_frame(self, frame: ExecutionFrame) -> bool:
|
|
15
|
+
"""Register Execution Frame"""
|
|
16
|
+
with self._lock:
|
|
17
|
+
if frame.frame_id in self._frames:
|
|
18
|
+
return False
|
|
19
|
+
self._frames[frame.frame_id] = frame
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
def get_frame(self, frame_id: str) -> Optional[ExecutionFrame]:
|
|
23
|
+
"""Get execution frame"""
|
|
24
|
+
with self._lock:
|
|
25
|
+
return self._frames.get(frame_id)
|
|
26
|
+
|
|
27
|
+
def update_frame(self, frame: ExecutionFrame) -> bool:
|
|
28
|
+
"""Update execution frame"""
|
|
29
|
+
with self._lock:
|
|
30
|
+
if frame.frame_id not in self._frames:
|
|
31
|
+
return False
|
|
32
|
+
frame.update_timestamp()
|
|
33
|
+
self._frames[frame.frame_id] = frame
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def compare_and_swap(
|
|
37
|
+
self, frame_id: str, expected_version: int, updates: Dict
|
|
38
|
+
) -> Tuple[bool, Optional[ExecutionFrame]]:
|
|
39
|
+
"""Optimistic Concurrency Control - Compare and Swap"""
|
|
40
|
+
with self._lock:
|
|
41
|
+
frame = self._frames.get(frame_id)
|
|
42
|
+
if not frame:
|
|
43
|
+
return False, None
|
|
44
|
+
|
|
45
|
+
# Simplified version control - using update timestamp as version
|
|
46
|
+
current_version = int(frame.updated_at)
|
|
47
|
+
if current_version != expected_version:
|
|
48
|
+
return False, frame
|
|
49
|
+
|
|
50
|
+
# Application Update
|
|
51
|
+
for key, value in updates.items():
|
|
52
|
+
if hasattr(frame, key):
|
|
53
|
+
setattr(frame, key, value)
|
|
54
|
+
|
|
55
|
+
frame.update_timestamp()
|
|
56
|
+
self._frames[frame_id] = frame
|
|
57
|
+
return True, frame
|
|
58
|
+
|
|
59
|
+
def remove_frame(self, frame_id: str) -> bool:
|
|
60
|
+
"""Remove execution frame"""
|
|
61
|
+
with self._lock:
|
|
62
|
+
if frame_id in self._frames:
|
|
63
|
+
del self._frames[frame_id]
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def get_child_frames(self, parent_id: str) -> List[ExecutionFrame]:
|
|
68
|
+
"""Get the list of child execution frames"""
|
|
69
|
+
with self._lock:
|
|
70
|
+
children = []
|
|
71
|
+
for frame in self._frames.values():
|
|
72
|
+
if frame.parent_id == parent_id:
|
|
73
|
+
children.append(frame)
|
|
74
|
+
return children
|
|
75
|
+
|
|
76
|
+
def get_frame_tree(self, root_id: str) -> Dict:
|
|
77
|
+
"""Get the frame tree with the specified frame as root"""
|
|
78
|
+
with self._lock:
|
|
79
|
+
root_frame = self._frames.get(root_id)
|
|
80
|
+
if not root_frame:
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
def build_tree(frame_id: str) -> Dict:
|
|
84
|
+
frame = self._frames.get(frame_id)
|
|
85
|
+
if not frame:
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
tree = {"frame": frame, "children": []}
|
|
89
|
+
|
|
90
|
+
for child_id in frame.children:
|
|
91
|
+
child_tree = build_tree(child_id)
|
|
92
|
+
if child_tree:
|
|
93
|
+
tree["children"].append(child_tree)
|
|
94
|
+
|
|
95
|
+
return tree
|
|
96
|
+
|
|
97
|
+
return build_tree(root_id)
|
|
98
|
+
|
|
99
|
+
def list_frames_by_status(self, status: FrameStatus) -> List[ExecutionFrame]:
|
|
100
|
+
"""List execution frames by status"""
|
|
101
|
+
with self._lock:
|
|
102
|
+
return [frame for frame in self._frames.values() if frame.status == status]
|
|
103
|
+
|
|
104
|
+
def list_all_frames(self) -> List[ExecutionFrame]:
|
|
105
|
+
"""List all executed frames"""
|
|
106
|
+
with self._lock:
|
|
107
|
+
return list(self._frames.values())
|
|
108
|
+
|
|
109
|
+
def get_frame_count(self) -> int:
|
|
110
|
+
"""Get the number of execution frames"""
|
|
111
|
+
with self._lock:
|
|
112
|
+
return len(self._frames)
|
|
113
|
+
|
|
114
|
+
def get_stats(self) -> Dict:
|
|
115
|
+
"""Get registry statistics"""
|
|
116
|
+
with self._lock:
|
|
117
|
+
stats = {
|
|
118
|
+
"total_frames": len(self._frames),
|
|
119
|
+
"running": 0,
|
|
120
|
+
"paused": 0,
|
|
121
|
+
"completed": 0,
|
|
122
|
+
"failed": 0,
|
|
123
|
+
"waiting_for_intervention": 0,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for frame in self._frames.values():
|
|
127
|
+
if frame.status == FrameStatus.RUNNING:
|
|
128
|
+
stats["running"] += 1
|
|
129
|
+
elif frame.status == FrameStatus.PAUSED:
|
|
130
|
+
stats["paused"] += 1
|
|
131
|
+
elif frame.status == FrameStatus.COMPLETED:
|
|
132
|
+
stats["completed"] += 1
|
|
133
|
+
elif frame.status == FrameStatus.FAILED:
|
|
134
|
+
stats["failed"] += 1
|
|
135
|
+
elif frame.status == FrameStatus.WAITING_FOR_INTERVENTION:
|
|
136
|
+
stats["waiting_for_intervention"] += 1
|
|
137
|
+
|
|
138
|
+
return stats
|
|
139
|
+
|
|
140
|
+
def cleanup_completed_frames(self, max_age_seconds: int = 3600) -> int:
|
|
141
|
+
"""Clean up completed old execution frames"""
|
|
142
|
+
with self._lock:
|
|
143
|
+
current_time = time.time()
|
|
144
|
+
to_remove = []
|
|
145
|
+
|
|
146
|
+
for frame_id, frame in self._frames.items():
|
|
147
|
+
if (
|
|
148
|
+
frame.status in [FrameStatus.COMPLETED, FrameStatus.FAILED]
|
|
149
|
+
and current_time - frame.updated_at > max_age_seconds
|
|
150
|
+
):
|
|
151
|
+
to_remove.append(frame_id)
|
|
152
|
+
|
|
153
|
+
for frame_id in to_remove:
|
|
154
|
+
del self._frames[frame_id]
|
|
155
|
+
|
|
156
|
+
return len(to_remove)
|
|
157
|
+
|
|
158
|
+
def clear(self):
|
|
159
|
+
"""Clear all execution frames"""
|
|
160
|
+
with self._lock:
|
|
161
|
+
self._frames.clear()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Dict, Optional, Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ResumeHandle:
|
|
8
|
+
"""Resume handle - used to restore paused execution.
|
|
9
|
+
|
|
10
|
+
Supports two interrupt types:
|
|
11
|
+
- "tool_interrupt": Tool requested user input, resume from breakpoint
|
|
12
|
+
- "user_interrupt": User actively interrupted, restart block with new context
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
frame_id: ID of the execution frame
|
|
16
|
+
snapshot_id: ID of the context snapshot
|
|
17
|
+
resume_token: Unique token for this resume operation
|
|
18
|
+
interrupt_type: Type of interrupt ("tool_interrupt" or "user_interrupt")
|
|
19
|
+
current_block: Block pointer at interrupt time (for user_interrupt)
|
|
20
|
+
restart_block: Whether to restart the block (True for user_interrupt)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
frame_id: str
|
|
24
|
+
snapshot_id: str
|
|
25
|
+
resume_token: str
|
|
26
|
+
interrupt_type: Literal["tool_interrupt", "user_interrupt"] = "tool_interrupt"
|
|
27
|
+
current_block: Optional[int] = None
|
|
28
|
+
restart_block: bool = False
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def create_handle(cls, frame_id: str, snapshot_id: str) -> "ResumeHandle":
|
|
32
|
+
"""Create recovery handle (for tool interrupt - backward compatible)"""
|
|
33
|
+
return cls(
|
|
34
|
+
frame_id=frame_id,
|
|
35
|
+
snapshot_id=snapshot_id,
|
|
36
|
+
resume_token=str(uuid.uuid4()),
|
|
37
|
+
interrupt_type="tool_interrupt",
|
|
38
|
+
restart_block=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create_user_interrupt_handle(
|
|
43
|
+
cls,
|
|
44
|
+
frame_id: str,
|
|
45
|
+
snapshot_id: str,
|
|
46
|
+
current_block: Optional[int] = None,
|
|
47
|
+
) -> "ResumeHandle":
|
|
48
|
+
"""Create resume handle for user interrupt.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
frame_id: ID of the execution frame
|
|
52
|
+
snapshot_id: ID of the context snapshot
|
|
53
|
+
current_block: Block pointer at interrupt time
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ResumeHandle configured for user interrupt (restart_block=True)
|
|
57
|
+
"""
|
|
58
|
+
return cls(
|
|
59
|
+
frame_id=frame_id,
|
|
60
|
+
snapshot_id=snapshot_id,
|
|
61
|
+
resume_token=str(uuid.uuid4()),
|
|
62
|
+
interrupt_type="user_interrupt",
|
|
63
|
+
current_block=current_block,
|
|
64
|
+
restart_block=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> Dict:
|
|
68
|
+
"""Convert to dictionary format"""
|
|
69
|
+
return {
|
|
70
|
+
"frame_id": self.frame_id,
|
|
71
|
+
"snapshot_id": self.snapshot_id,
|
|
72
|
+
"resume_token": self.resume_token,
|
|
73
|
+
"interrupt_type": self.interrupt_type,
|
|
74
|
+
"current_block": self.current_block,
|
|
75
|
+
"restart_block": self.restart_block,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, data: Dict) -> "ResumeHandle":
|
|
80
|
+
"""Create a restore handle from a dictionary"""
|
|
81
|
+
# Handle backward compatibility for old data without new fields
|
|
82
|
+
return cls(
|
|
83
|
+
frame_id=data["frame_id"],
|
|
84
|
+
snapshot_id=data["snapshot_id"],
|
|
85
|
+
resume_token=data["resume_token"],
|
|
86
|
+
interrupt_type=data.get("interrupt_type", "tool_interrupt"),
|
|
87
|
+
current_block=data.get("current_block"),
|
|
88
|
+
restart_block=data.get("restart_block", False),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def is_valid(self) -> bool:
|
|
92
|
+
"""Check if the handle is valid"""
|
|
93
|
+
return bool(self.frame_id and self.snapshot_id and self.resume_token)
|
|
94
|
+
|
|
95
|
+
def is_user_interrupt(self) -> bool:
|
|
96
|
+
"""Check if this handle is for a user interrupt"""
|
|
97
|
+
return self.interrupt_type == "user_interrupt"
|
|
98
|
+
|
|
99
|
+
def is_tool_interrupt(self) -> bool:
|
|
100
|
+
"""Check if this handle is for a tool interrupt"""
|
|
101
|
+
return self.interrupt_type == "tool_interrupt"
|