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.
Files changed (199) hide show
  1. DolphinLanguageSDK/__init__.py +58 -0
  2. dolphin/__init__.py +62 -0
  3. dolphin/cli/__init__.py +20 -0
  4. dolphin/cli/args/__init__.py +9 -0
  5. dolphin/cli/args/parser.py +567 -0
  6. dolphin/cli/builtin_agents/__init__.py +22 -0
  7. dolphin/cli/commands/__init__.py +4 -0
  8. dolphin/cli/interrupt/__init__.py +8 -0
  9. dolphin/cli/interrupt/handler.py +205 -0
  10. dolphin/cli/interrupt/keyboard.py +82 -0
  11. dolphin/cli/main.py +49 -0
  12. dolphin/cli/multimodal/__init__.py +34 -0
  13. dolphin/cli/multimodal/clipboard.py +327 -0
  14. dolphin/cli/multimodal/handler.py +249 -0
  15. dolphin/cli/multimodal/image_processor.py +214 -0
  16. dolphin/cli/multimodal/input_parser.py +149 -0
  17. dolphin/cli/runner/__init__.py +8 -0
  18. dolphin/cli/runner/runner.py +989 -0
  19. dolphin/cli/ui/__init__.py +10 -0
  20. dolphin/cli/ui/console.py +2795 -0
  21. dolphin/cli/ui/input.py +340 -0
  22. dolphin/cli/ui/layout.py +425 -0
  23. dolphin/cli/ui/stream_renderer.py +302 -0
  24. dolphin/cli/utils/__init__.py +8 -0
  25. dolphin/cli/utils/helpers.py +135 -0
  26. dolphin/cli/utils/version.py +49 -0
  27. dolphin/core/__init__.py +107 -0
  28. dolphin/core/agent/__init__.py +10 -0
  29. dolphin/core/agent/agent_state.py +69 -0
  30. dolphin/core/agent/base_agent.py +970 -0
  31. dolphin/core/code_block/__init__.py +0 -0
  32. dolphin/core/code_block/agent_init_block.py +0 -0
  33. dolphin/core/code_block/assign_block.py +98 -0
  34. dolphin/core/code_block/basic_code_block.py +1865 -0
  35. dolphin/core/code_block/explore_block.py +1327 -0
  36. dolphin/core/code_block/explore_block_v2.py +712 -0
  37. dolphin/core/code_block/explore_strategy.py +672 -0
  38. dolphin/core/code_block/judge_block.py +220 -0
  39. dolphin/core/code_block/prompt_block.py +32 -0
  40. dolphin/core/code_block/skill_call_deduplicator.py +291 -0
  41. dolphin/core/code_block/tool_block.py +129 -0
  42. dolphin/core/common/__init__.py +17 -0
  43. dolphin/core/common/constants.py +176 -0
  44. dolphin/core/common/enums.py +1173 -0
  45. dolphin/core/common/exceptions.py +133 -0
  46. dolphin/core/common/multimodal.py +539 -0
  47. dolphin/core/common/object_type.py +165 -0
  48. dolphin/core/common/output_format.py +432 -0
  49. dolphin/core/common/types.py +36 -0
  50. dolphin/core/config/__init__.py +16 -0
  51. dolphin/core/config/global_config.py +1289 -0
  52. dolphin/core/config/ontology_config.py +133 -0
  53. dolphin/core/context/__init__.py +12 -0
  54. dolphin/core/context/context.py +1580 -0
  55. dolphin/core/context/context_manager.py +161 -0
  56. dolphin/core/context/var_output.py +82 -0
  57. dolphin/core/context/variable_pool.py +356 -0
  58. dolphin/core/context_engineer/__init__.py +41 -0
  59. dolphin/core/context_engineer/config/__init__.py +5 -0
  60. dolphin/core/context_engineer/config/settings.py +402 -0
  61. dolphin/core/context_engineer/core/__init__.py +7 -0
  62. dolphin/core/context_engineer/core/budget_manager.py +327 -0
  63. dolphin/core/context_engineer/core/context_assembler.py +583 -0
  64. dolphin/core/context_engineer/core/context_manager.py +637 -0
  65. dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
  66. dolphin/core/context_engineer/example/incremental_example.py +267 -0
  67. dolphin/core/context_engineer/example/traditional_example.py +334 -0
  68. dolphin/core/context_engineer/services/__init__.py +5 -0
  69. dolphin/core/context_engineer/services/compressor.py +399 -0
  70. dolphin/core/context_engineer/utils/__init__.py +6 -0
  71. dolphin/core/context_engineer/utils/context_utils.py +441 -0
  72. dolphin/core/context_engineer/utils/message_formatter.py +270 -0
  73. dolphin/core/context_engineer/utils/token_utils.py +139 -0
  74. dolphin/core/coroutine/__init__.py +15 -0
  75. dolphin/core/coroutine/context_snapshot.py +154 -0
  76. dolphin/core/coroutine/context_snapshot_profile.py +922 -0
  77. dolphin/core/coroutine/context_snapshot_store.py +268 -0
  78. dolphin/core/coroutine/execution_frame.py +145 -0
  79. dolphin/core/coroutine/execution_state_registry.py +161 -0
  80. dolphin/core/coroutine/resume_handle.py +101 -0
  81. dolphin/core/coroutine/step_result.py +101 -0
  82. dolphin/core/executor/__init__.py +18 -0
  83. dolphin/core/executor/debug_controller.py +630 -0
  84. dolphin/core/executor/dolphin_executor.py +1063 -0
  85. dolphin/core/executor/executor.py +624 -0
  86. dolphin/core/flags/__init__.py +27 -0
  87. dolphin/core/flags/definitions.py +49 -0
  88. dolphin/core/flags/manager.py +113 -0
  89. dolphin/core/hook/__init__.py +95 -0
  90. dolphin/core/hook/expression_evaluator.py +499 -0
  91. dolphin/core/hook/hook_dispatcher.py +380 -0
  92. dolphin/core/hook/hook_types.py +248 -0
  93. dolphin/core/hook/isolated_variable_pool.py +284 -0
  94. dolphin/core/interfaces.py +53 -0
  95. dolphin/core/llm/__init__.py +0 -0
  96. dolphin/core/llm/llm.py +495 -0
  97. dolphin/core/llm/llm_call.py +100 -0
  98. dolphin/core/llm/llm_client.py +1285 -0
  99. dolphin/core/llm/message_sanitizer.py +120 -0
  100. dolphin/core/logging/__init__.py +20 -0
  101. dolphin/core/logging/logger.py +526 -0
  102. dolphin/core/message/__init__.py +8 -0
  103. dolphin/core/message/compressor.py +749 -0
  104. dolphin/core/parser/__init__.py +8 -0
  105. dolphin/core/parser/parser.py +405 -0
  106. dolphin/core/runtime/__init__.py +10 -0
  107. dolphin/core/runtime/runtime_graph.py +926 -0
  108. dolphin/core/runtime/runtime_instance.py +446 -0
  109. dolphin/core/skill/__init__.py +14 -0
  110. dolphin/core/skill/context_retention.py +157 -0
  111. dolphin/core/skill/skill_function.py +686 -0
  112. dolphin/core/skill/skill_matcher.py +282 -0
  113. dolphin/core/skill/skillkit.py +700 -0
  114. dolphin/core/skill/skillset.py +72 -0
  115. dolphin/core/trajectory/__init__.py +10 -0
  116. dolphin/core/trajectory/recorder.py +189 -0
  117. dolphin/core/trajectory/trajectory.py +522 -0
  118. dolphin/core/utils/__init__.py +9 -0
  119. dolphin/core/utils/cache_kv.py +212 -0
  120. dolphin/core/utils/tools.py +340 -0
  121. dolphin/lib/__init__.py +93 -0
  122. dolphin/lib/debug/__init__.py +8 -0
  123. dolphin/lib/debug/visualizer.py +409 -0
  124. dolphin/lib/memory/__init__.py +28 -0
  125. dolphin/lib/memory/async_processor.py +220 -0
  126. dolphin/lib/memory/llm_calls.py +195 -0
  127. dolphin/lib/memory/manager.py +78 -0
  128. dolphin/lib/memory/sandbox.py +46 -0
  129. dolphin/lib/memory/storage.py +245 -0
  130. dolphin/lib/memory/utils.py +51 -0
  131. dolphin/lib/ontology/__init__.py +12 -0
  132. dolphin/lib/ontology/basic/__init__.py +0 -0
  133. dolphin/lib/ontology/basic/base.py +102 -0
  134. dolphin/lib/ontology/basic/concept.py +130 -0
  135. dolphin/lib/ontology/basic/object.py +11 -0
  136. dolphin/lib/ontology/basic/relation.py +63 -0
  137. dolphin/lib/ontology/datasource/__init__.py +27 -0
  138. dolphin/lib/ontology/datasource/datasource.py +66 -0
  139. dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
  140. dolphin/lib/ontology/datasource/sql.py +845 -0
  141. dolphin/lib/ontology/mapping.py +177 -0
  142. dolphin/lib/ontology/ontology.py +733 -0
  143. dolphin/lib/ontology/ontology_context.py +16 -0
  144. dolphin/lib/ontology/ontology_manager.py +107 -0
  145. dolphin/lib/skill_results/__init__.py +31 -0
  146. dolphin/lib/skill_results/cache_backend.py +559 -0
  147. dolphin/lib/skill_results/result_processor.py +181 -0
  148. dolphin/lib/skill_results/result_reference.py +179 -0
  149. dolphin/lib/skill_results/skillkit_hook.py +324 -0
  150. dolphin/lib/skill_results/strategies.py +328 -0
  151. dolphin/lib/skill_results/strategy_registry.py +150 -0
  152. dolphin/lib/skillkits/__init__.py +44 -0
  153. dolphin/lib/skillkits/agent_skillkit.py +155 -0
  154. dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
  155. dolphin/lib/skillkits/env_skillkit.py +250 -0
  156. dolphin/lib/skillkits/mcp_adapter.py +616 -0
  157. dolphin/lib/skillkits/mcp_skillkit.py +771 -0
  158. dolphin/lib/skillkits/memory_skillkit.py +650 -0
  159. dolphin/lib/skillkits/noop_skillkit.py +31 -0
  160. dolphin/lib/skillkits/ontology_skillkit.py +89 -0
  161. dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
  162. dolphin/lib/skillkits/resource/__init__.py +52 -0
  163. dolphin/lib/skillkits/resource/models/__init__.py +6 -0
  164. dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
  165. dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
  166. dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
  167. dolphin/lib/skillkits/resource/skill_cache.py +215 -0
  168. dolphin/lib/skillkits/resource/skill_loader.py +395 -0
  169. dolphin/lib/skillkits/resource/skill_validator.py +406 -0
  170. dolphin/lib/skillkits/resource_skillkit.py +11 -0
  171. dolphin/lib/skillkits/search_skillkit.py +163 -0
  172. dolphin/lib/skillkits/sql_skillkit.py +274 -0
  173. dolphin/lib/skillkits/system_skillkit.py +509 -0
  174. dolphin/lib/skillkits/vm_skillkit.py +65 -0
  175. dolphin/lib/utils/__init__.py +9 -0
  176. dolphin/lib/utils/data_process.py +207 -0
  177. dolphin/lib/utils/handle_progress.py +178 -0
  178. dolphin/lib/utils/security.py +139 -0
  179. dolphin/lib/utils/text_retrieval.py +462 -0
  180. dolphin/lib/vm/__init__.py +11 -0
  181. dolphin/lib/vm/env_executor.py +895 -0
  182. dolphin/lib/vm/python_session_manager.py +453 -0
  183. dolphin/lib/vm/vm.py +610 -0
  184. dolphin/sdk/__init__.py +60 -0
  185. dolphin/sdk/agent/__init__.py +12 -0
  186. dolphin/sdk/agent/agent_factory.py +236 -0
  187. dolphin/sdk/agent/dolphin_agent.py +1106 -0
  188. dolphin/sdk/api/__init__.py +4 -0
  189. dolphin/sdk/runtime/__init__.py +8 -0
  190. dolphin/sdk/runtime/env.py +363 -0
  191. dolphin/sdk/skill/__init__.py +10 -0
  192. dolphin/sdk/skill/global_skills.py +706 -0
  193. dolphin/sdk/skill/traditional_toolkit.py +260 -0
  194. kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
  195. kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
  196. kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
  197. kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
  198. kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
  199. kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1063 @@
1
+ import os
2
+
3
+ import time
4
+ import glob
5
+ from typing import Optional, TYPE_CHECKING
6
+ from dolphin.core import flags
7
+ from dolphin.core.config.global_config import GlobalConfig
8
+ from dolphin.core.context.context import Context
9
+ from dolphin.core.common.constants import (
10
+ KEY_MAX_ANSWER_CONTENT_LENGTH,
11
+ KEY_SESSION_ID,
12
+ KEY_USER_ID,
13
+ )
14
+ from dolphin.core.context_engineer.core.context_manager import (
15
+ ContextManager,
16
+ )
17
+ from dolphin.core.executor.executor import Executor
18
+ from dolphin.core.common.object_type import ObjectTypeFactory
19
+ from dolphin.core.context.var_output import VarOutput
20
+ from dolphin.core.logging.logger import get_logger
21
+ from dolphin.core.common.enums import MessageRole
22
+
23
+ # Import sdk/lib modules under TYPE_CHECKING to avoid circular imports
24
+ if TYPE_CHECKING:
25
+ from dolphin.lib.memory.manager import MemoryManager
26
+ from dolphin.sdk.skill.global_skills import GlobalSkills
27
+ from dolphin.sdk.skill.traditional_toolkit import TriditionalToolkit
28
+ from dolphin.lib.vm.vm import VM, VMFactory
29
+
30
+ logger = get_logger()
31
+
32
+
33
+ class DolphinExecutor:
34
+ def __init__(
35
+ self,
36
+ global_config: GlobalConfig | None = None,
37
+ global_configpath="./config/global.yaml",
38
+ global_skills=None,
39
+ global_types=None,
40
+ type_folders=None,
41
+ context_manager: Optional[ContextManager] = None,
42
+ verbose: bool = False,
43
+ is_cli: bool = False,
44
+ ):
45
+ # Lazy imports to avoid circular dependencies
46
+ from dolphin.sdk.skill.global_skills import GlobalSkills
47
+ from dolphin.lib.memory.manager import MemoryManager
48
+ from dolphin.lib.vm.vm import VM, VMFactory
49
+
50
+ # Initialize configuration with fallback logic
51
+ if global_config is not None:
52
+ self.config = global_config
53
+ elif os.path.exists(global_configpath):
54
+ self.config = GlobalConfig.from_yaml(global_configpath)
55
+ else:
56
+ self.config = GlobalConfig()
57
+ self.global_skills = (
58
+ global_skills if global_skills is not None else GlobalSkills(self.config)
59
+ )
60
+ self.global_types = (
61
+ global_types if global_types is not None else ObjectTypeFactory()
62
+ )
63
+
64
+ # Auto-load type definitions if not provided and type_folders is not specified
65
+ if global_types is None and type_folders is None:
66
+ self._loadDefaultTypeFiles()
67
+ elif type_folders is not None:
68
+ self._loadTypeFilesFromFolders(type_folders)
69
+
70
+ self.memory_manager = MemoryManager(global_config=self.config)
71
+ self.context_manager = context_manager
72
+ if self.context_manager is None:
73
+ if self.config.context_engineer_config is not None and \
74
+ self.config.context_engineer_config.context_config is not None:
75
+ self.context_manager = ContextManager(context_config=self.config.context_engineer_config.context_config)
76
+ else:
77
+ self.context_manager = ContextManager()
78
+
79
+ self.context = Context(
80
+ config=self.config,
81
+ global_skills=self.global_skills,
82
+ memory_manager=self.memory_manager,
83
+ global_types=self.global_types,
84
+ context_manager=self.context_manager,
85
+ verbose=verbose,
86
+ is_cli=is_cli,
87
+ )
88
+
89
+ self.vm: VM = None
90
+ if self.config.vm_config is not None:
91
+ self.vm = VMFactory.createVM(self.config.vm_config)
92
+
93
+ # Coroutine execution components
94
+ self.state_registry = None
95
+ self.snapshot_store = None
96
+ self._init_coroutine_components()
97
+ # Debug controller reference (for CLI post-mortem access)
98
+ self._debug_controller = None
99
+
100
+ def _init_coroutine_components(self):
101
+ """Initialize coroutine execution component"""
102
+ from dolphin.core.coroutine import (
103
+ ExecutionStateRegistry,
104
+ )
105
+ from dolphin.core.coroutine.context_snapshot_store import (
106
+ MemoryContextSnapshotStore,
107
+ )
108
+
109
+ self.state_registry = ExecutionStateRegistry()
110
+ self.snapshot_store = MemoryContextSnapshotStore()
111
+
112
+ def _loadDefaultTypeFiles(self):
113
+ """
114
+ Auto-load type definitions from common directories
115
+ """
116
+ # Common directories to search for .type files
117
+ default_type_paths = [
118
+ "./examples/types", # Project examples types
119
+ "./types", # Root types directory
120
+ "./src/types", # Source types directory
121
+ ]
122
+
123
+ for type_path in default_type_paths:
124
+ if os.path.exists(type_path):
125
+ self._loadTypeFilesFromFolder(type_path)
126
+
127
+ def _loadTypeFilesFromFolders(self, type_folders):
128
+ """
129
+ Load type definitions from specified folders
130
+
131
+ Args:
132
+ type_folders: List of folder paths or single folder path
133
+ """
134
+ if isinstance(type_folders, str):
135
+ type_folders = [type_folders]
136
+
137
+ for folder in type_folders:
138
+ if os.path.exists(folder):
139
+ self._loadTypeFilesFromFolder(folder)
140
+ else:
141
+ logger.warning(f"Type folder not found: {folder}")
142
+
143
+ def _loadTypeFilesFromFolder(self, folder_path):
144
+ """
145
+ Scan for .type files in the specified folder and load them into global_types
146
+
147
+ Args:
148
+ folder_path: Path to the folder containing .type files
149
+ """
150
+ # Get all .type files recursively
151
+ search_pattern = os.path.join(folder_path, "**", "*.type")
152
+ type_files = glob.glob(search_pattern, recursive=True)
153
+
154
+ for file_path in type_files:
155
+ try:
156
+ self.global_types.load(file_path)
157
+ logger.debug(f"Loaded type definition: {file_path}")
158
+ except Exception as e:
159
+ logger.warning(f"Failed to load type file {file_path}: {e}")
160
+ continue
161
+
162
+ def load_type(self, type_json: dict):
163
+ """
164
+ Load a type definition from JSON data into global_types
165
+
166
+ Args:
167
+ type_json (dict): JSON type definition
168
+ """
169
+ self.global_types.load_from_json(type_json)
170
+
171
+ def load_type_from_file(self, file_path: str):
172
+ """
173
+ Load a type definition from a .type file
174
+
175
+ Args:
176
+ file_path (str): Path to the .type file
177
+ """
178
+ self.global_types.load(file_path)
179
+
180
+ async def executor_init(self, infos):
181
+ for key, value in infos.items():
182
+ if key == "config":
183
+ self.config_initialize(value)
184
+ if key == "variables":
185
+ await self.context_initialize(param_dict=value)
186
+ elif key == "tools":
187
+ await self.context_initialize(tool_dict=value)
188
+ elif key == "skillkit":
189
+ await self.context_initialize(skillkit=value)
190
+ elif key == "skillkit_hook":
191
+ await self.context_initialize(skillkit_hook=value)
192
+ else:
193
+ pass
194
+
195
+ def config_initialize(self, configs):
196
+ # Lazy imports to avoid circular dependencies
197
+ from dolphin.lib.memory.manager import MemoryManager
198
+ from dolphin.lib.vm.vm import VM, VMFactory
199
+
200
+ self.config = GlobalConfig.from_dict(configs)
201
+ self.memory_manager = MemoryManager(global_config=self.config)
202
+ # Recreate Context while keeping the original verbose and is_cli settings
203
+ current_verbose = (
204
+ getattr(self.context, "verbose", False)
205
+ if hasattr(self, "context")
206
+ else False
207
+ )
208
+ current_is_cli = (
209
+ getattr(self.context, "is_cli", False)
210
+ if hasattr(self, "context")
211
+ else False
212
+ )
213
+ self.context = Context(
214
+ config=self.config,
215
+ global_skills=self.global_skills,
216
+ memory_manager=self.memory_manager,
217
+ global_types=self.global_types,
218
+ verbose=current_verbose,
219
+ is_cli=current_is_cli,
220
+ ) # Initialize context here with preserved verbose/is_cli settings
221
+
222
+ self.vm: VM = None
223
+ if self.config.vm_config is not None:
224
+ self.vm = VMFactory.createVM(self.config.vm_config)
225
+
226
+ async def context_initialize(
227
+ self,
228
+ param_dict=None,
229
+ tool_dict=None,
230
+ skillkit=None,
231
+ skillkit_hook=None,
232
+ ):
233
+ if param_dict is not None:
234
+ for name, value in param_dict.items():
235
+ if VarOutput.is_serialized_dict(value):
236
+ value = VarOutput.from_dict(value)
237
+ self.context.set_variable(name=name, value=value)
238
+
239
+ if tool_dict is not None:
240
+ from dolphin.sdk.skill.traditional_toolkit import TriditionalToolkit
241
+ triditional_toolkit = TriditionalToolkit.buildFromTooldict(tool_dict)
242
+ self.context.set_skills(triditional_toolkit)
243
+ elif skillkit is not None:
244
+ self.context.set_skills(skillkit)
245
+ elif skillkit_hook is not None:
246
+ self.context.set_skillkit_hook(skillkit_hook)
247
+
248
+ def set_context(self, context: Context):
249
+ self.context = context
250
+
251
+ async def continue_exploration(self, **kwargs):
252
+ """Continue exploring based on the existing context (multi-turn dialogue scenario)
253
+
254
+ This method reuses the message history, variable pool, and other states from the current context,
255
+ and executes a new explore session to process the user's subsequent input.
256
+
257
+ Args:
258
+ model: Model name; if empty, the model used in the previous session from context will be used
259
+ use_history: Whether to use historical messages, default is True
260
+ mode: Exploration mode ("prompt" or "tool_call"); if empty, the mode used in the previous session from context will be used
261
+ skills: List of skills; if empty, the skill configuration used in the previous session from context will be used
262
+ tools: Alias for skills
263
+ content: User input content (optional)
264
+ output_var: Name of the output variable, default is "result"
265
+ **kwargs: Additional parameters
266
+
267
+ Note:
268
+ The mode and skills parameters will automatically inherit the configuration from the previous round in context.
269
+ To override, explicitly pass them via kwargs.
270
+
271
+ Yields:
272
+ Execution results
273
+ """
274
+ if flags.is_enabled(flags.EXPLORE_BLOCK_V2):
275
+ raise NotImplementedError(
276
+ "continue_exploration 暂不支持 EXPLORE_BLOCK_V2 模式,"
277
+ "请使用 flags.set_flag(flags.EXPLORE_BLOCK_V2, False) 禁用后再调用"
278
+ )
279
+
280
+ # Ensure interrupt state is cleared for the new exploration round
281
+ if hasattr(self.context, "clear_interrupt"):
282
+ self.context.clear_interrupt()
283
+
284
+ from dolphin.core.code_block.explore_block import ExploreBlock
285
+ from dolphin.core.trajectory.recorder import Recorder
286
+ from dolphin.core.runtime.runtime_instance import ProgressInstance
287
+
288
+ # Create exploration block (default prompt mode)
289
+ explore_block = ExploreBlock(
290
+ context=self.context,
291
+ debug_infos=None,
292
+ tools_format=kwargs.get("tools_format", "medium"),
293
+ )
294
+
295
+ # Manually register block and progress to runtime_graph
296
+ if self.context.runtime_graph:
297
+ self.context.runtime_graph.set_block(explore_block)
298
+
299
+ progress = ProgressInstance(self.context)
300
+ if self.context.runtime_graph:
301
+ self.context.runtime_graph.set_progress(progress)
302
+
303
+ explore_block.recorder = Recorder(
304
+ self.context,
305
+ progress=progress,
306
+ )
307
+
308
+ # Extract parameters from kwargs
309
+ model_name = kwargs.get("model") or self.context.get_last_model_name() or ""
310
+ use_history = kwargs.get("use_history", True)
311
+
312
+ # Sync the model name to ExploreBlock
313
+ if model_name:
314
+ explore_block.model = model_name
315
+
316
+ # Use the continue_exploration method of ExploreBlock
317
+ async for result in explore_block.continue_exploration(
318
+ model=model_name,
319
+ use_history=use_history,
320
+ **kwargs,
321
+ ):
322
+ yield result
323
+
324
+ async def run(self, content, output_variables: Optional[list] = None, **kwargs):
325
+ start_time = time.perf_counter()
326
+
327
+ # Pass the debug mode parameter to Executor
328
+ debug_mode = kwargs.get("debug_mode", False)
329
+ break_on_start = kwargs.get("break_on_start", False)
330
+ break_at = kwargs.get("break_at", None)
331
+ self.executor = Executor(
332
+ context=self.context,
333
+ debug_mode=debug_mode,
334
+ break_on_start=break_on_start,
335
+ break_at=break_at,
336
+ )
337
+ # expose debug controller for post-mortem if present
338
+ try:
339
+ self._debug_controller = getattr(self.executor, "debug_controller", None)
340
+ except Exception:
341
+ self._debug_controller = None
342
+
343
+ async for result in self.executor.run(
344
+ content, output_variables=output_variables, **kwargs
345
+ ):
346
+ yield result
347
+ end_time = time.perf_counter()
348
+ self.context.debug(f"Time taken: {end_time - start_time} seconds")
349
+
350
+ async def run_and_get_result(
351
+ self, content, output_variables: Optional[list] = None, **kwargs
352
+ ):
353
+ start_time = time.perf_counter()
354
+
355
+ start_time = time.perf_counter()
356
+ # Pass the debug mode parameter to Executor
357
+ debug_mode = kwargs.get("debug_mode", False)
358
+ break_on_start = kwargs.get("break_on_start", False)
359
+ break_at = kwargs.get("break_at", None)
360
+ self.executor = Executor(
361
+ context=self.context,
362
+ debug_mode=debug_mode,
363
+ break_on_start=break_on_start,
364
+ break_at=break_at,
365
+ )
366
+ # expose debug controller for post-mortem if present
367
+ try:
368
+ self._debug_controller = getattr(self.executor, "debug_controller", None)
369
+ except Exception:
370
+ self._debug_controller = None
371
+
372
+ async for result in self.executor.run_and_get_result(
373
+ content, output_variables=output_variables, **kwargs
374
+ ):
375
+ yield result
376
+ end_time = time.perf_counter()
377
+ self.context.debug(f"Time taken: {end_time - start_time} seconds")
378
+
379
+ def _prepare_for_run(self, **kwargs):
380
+ # Only set skills if not already set (to preserve agent skills set by Environment)
381
+ if self.context.is_skillkit_empty():
382
+ # Try to get all skills including custom skills from global skills
383
+ # If global_skills has getAllSkills method, use it; otherwise fall back to installed skills
384
+ if hasattr(self.global_skills, "getAllSkills") and callable(
385
+ getattr(self.global_skills, "getAllSkills")
386
+ ):
387
+ all_skills = self.global_skills.getAllSkills()
388
+ self.context.set_skills(all_skills)
389
+ else:
390
+ installed_skills = self.global_skills.getInstalledSkills()
391
+ self.context.set_skills(installed_skills)
392
+
393
+ if KEY_USER_ID in kwargs:
394
+ self.context.set_variable(name=KEY_USER_ID, value=kwargs.get(KEY_USER_ID))
395
+
396
+ if KEY_SESSION_ID in kwargs:
397
+ self.context.set_variable(
398
+ name=KEY_SESSION_ID, value=kwargs.get(KEY_SESSION_ID)
399
+ )
400
+
401
+ if KEY_MAX_ANSWER_CONTENT_LENGTH in kwargs:
402
+ self.context.set_variable(
403
+ name=KEY_MAX_ANSWER_CONTENT_LENGTH,
404
+ value=kwargs.get(KEY_MAX_ANSWER_CONTENT_LENGTH),
405
+ )
406
+
407
+ def shutdown(self):
408
+ """
409
+ Gracefully shutdown the DolphinExecutor and its components.
410
+ """
411
+ pass
412
+
413
+ def get_execution_trace(self, title=None):
414
+ """Generate and return execution trace information for the current execution context.
415
+
416
+ Args:
417
+ title (str, optional): Trace title. If not provided, use the default title.
418
+
419
+ Returns:
420
+ str: Execution trace information (text/JSON string) containing call_chain and LLM details.
421
+ """
422
+ trace_data = self.context.get_execution_trace(title)
423
+
424
+ # If the lower layer has already returned a string, pass it through directly.
425
+ if isinstance(trace_data, str):
426
+ return trace_data
427
+
428
+ # Default conversion to JSON text, ensuring that the upper layer writing files is str
429
+ try:
430
+ import json
431
+
432
+ return json.dumps(trace_data, ensure_ascii=False, indent=2)
433
+ except Exception:
434
+ # Fallback: use repr when not serializable to avoid throwing errors again
435
+ return repr(trace_data)
436
+
437
+ @property
438
+ def debug_controller(self):
439
+ """Expose a debug controller instance for post-mortem.
440
+
441
+ Priority:
442
+ - Controller captured from the last execution step/run (coroutine path)
443
+ - Controller from the currently bound low-level Executor (run path)
444
+ """
445
+ if self._debug_controller is not None:
446
+ return self._debug_controller
447
+ try:
448
+ return getattr(self.executor, "debug_controller", None)
449
+ except Exception:
450
+ return None
451
+
452
+ # Backward compatibility: retain old method names
453
+ def get_profile(self, title=None):
454
+ """[Deprecated] Please use get_execution_trace() instead"""
455
+ import warnings
456
+ warnings.warn("get_profile() 已废弃,请使用 get_execution_trace()", DeprecationWarning, stacklevel=2)
457
+ return self.get_execution_trace(title)
458
+
459
+ # === Exception handling helper ===
460
+ def _handle_execution_exception(
461
+ self,
462
+ e: Exception,
463
+ frame,
464
+ block_pointer: int,
465
+ ):
466
+ """Unified handler for execution exceptions (UserInterrupt, ToolInterrupt, others).
467
+
468
+ Args:
469
+ e: The exception to handle
470
+ frame: Current execution frame
471
+ block_pointer: Current block pointer position
472
+
473
+ Returns:
474
+ StepResult if interrupt was handled, None if exception should be re-raised
475
+ """
476
+ import traceback
477
+ from dolphin.core.utils.tools import ToolInterrupt
478
+ from dolphin.core.common.exceptions import UserInterrupt
479
+ from dolphin.core.coroutine.execution_frame import FrameStatus, WaitReason
480
+ from dolphin.core.coroutine.step_result import StepResult
481
+ from dolphin.core.coroutine import ResumeHandle
482
+
483
+ # Handle user interruption (user actively interrupted execution)
484
+ if isinstance(e, UserInterrupt):
485
+ frame.status = FrameStatus.WAITING_FOR_INTERVENTION
486
+ frame.wait_reason = WaitReason.USER_INTERRUPT
487
+ intervention_snapshot_id = self._save_frame_snapshot(frame)
488
+ frame.error = {
489
+ "error_type": "UserInterrupt",
490
+ "message": str(e),
491
+ "at_block": block_pointer,
492
+ "intervention_snapshot_id": intervention_snapshot_id,
493
+ }
494
+ return StepResult.user_interrupted(
495
+ resume_handle=ResumeHandle.create_user_interrupt_handle(
496
+ frame_id=frame.frame_id,
497
+ snapshot_id=intervention_snapshot_id,
498
+ current_block=block_pointer,
499
+ ),
500
+ result={"partial_output": self.context.get_user_variables()},
501
+ )
502
+
503
+ # Handle tool interruption (tool requested user input)
504
+ if isinstance(e, ToolInterrupt):
505
+ frame.status = FrameStatus.WAITING_FOR_INTERVENTION
506
+ frame.wait_reason = WaitReason.TOOL_REQUEST
507
+ intervention_snapshot_id = self._save_frame_snapshot(frame)
508
+ frame.error = {
509
+ "error_type": "ToolInterrupt",
510
+ "message": str(e),
511
+ "tool_name": getattr(e, "tool_name", ""),
512
+ "tool_args": getattr(e, "tool_args", []),
513
+ "at_block": block_pointer,
514
+ "intervention_snapshot_id": intervention_snapshot_id,
515
+ }
516
+ return StepResult.interrupted(
517
+ resume_handle=ResumeHandle.create_handle(
518
+ frame_id=frame.frame_id,
519
+ snapshot_id=intervention_snapshot_id,
520
+ )
521
+ )
522
+
523
+ # Other errors: mark failed and return None to signal re-raise
524
+ frame.status = FrameStatus.FAILED
525
+ error_snapshot_id = self._save_frame_snapshot(frame)
526
+ frame.error = {
527
+ "error_type": type(e).__name__,
528
+ "message": str(e),
529
+ "stack": traceback.format_exc(),
530
+ "at_block": block_pointer,
531
+ "error_snapshot_id": error_snapshot_id,
532
+ }
533
+ return None # Caller should re-raise the exception
534
+
535
+ # === Added coroutine methods ===
536
+ async def start_coroutine(self, content, **kwargs):
537
+ """Starts a resumable execution"""
538
+ from dolphin.core.coroutine import ExecutionFrame
539
+
540
+ self._prepare_for_run(**kwargs)
541
+
542
+ # 1. Create an execution frame and save the original content
543
+ frame = ExecutionFrame.create_root_frame()
544
+ frame.original_content = content
545
+
546
+ # Save debug-related parameters to the execution frame (as part of the execution context)
547
+ frame.debug_mode = kwargs.get("debug_mode", False)
548
+ frame.break_on_start = kwargs.get("break_on_start", False)
549
+ frame.break_at = kwargs.get("break_at", None)
550
+
551
+ # 2. Initialize context and create snapshot
552
+ snapshot = self._create_snapshot(frame.frame_id)
553
+ frame.context_snapshot_id = snapshot.snapshot_id
554
+
555
+ # 3. Register to the state manager
556
+ self.state_registry.register_frame(frame)
557
+ self.snapshot_store.save_snapshot(snapshot)
558
+
559
+ # 4. Record the currently active coroutine
560
+ self._current_frame_id = frame.frame_id
561
+
562
+ return frame
563
+
564
+ async def step_coroutine(self, frame_id: str):
565
+ """Execute one step and return a unified StepResult.
566
+
567
+ Returns:
568
+ StepResult: Unified execution result, containing status and optional result data/restore handle.
569
+ - StepResult.running(): Still executing.
570
+ - StepResult.completed(result): Execution completed, contains final result.
571
+ - StepResult.interrupted(handle): Tool interrupted, contains restore handle.
572
+ """
573
+ import traceback
574
+ from dolphin.core.coroutine.execution_frame import FrameStatus
575
+ from dolphin.core.coroutine.step_result import StepResult
576
+
577
+ # 1. Get frame and restore context
578
+ frame = self.state_registry.get_frame(frame_id)
579
+ if not frame:
580
+ raise ValueError(f"Frame not found: {frame_id}")
581
+
582
+ snapshot = self.snapshot_store.load_snapshot(frame.context_snapshot_id)
583
+ if not snapshot:
584
+ raise ValueError(f"Snapshot not found: {frame.context_snapshot_id}")
585
+
586
+ self._restore_context(snapshot)
587
+
588
+ try:
589
+ # 2. Get the parsed blocks (cached in the frame or re-parsed)
590
+ blocks = self._get_or_parse_blocks(frame)
591
+
592
+ # 3. Execute single step (read debug configuration from frame)
593
+ executor = Executor(
594
+ context=self.context,
595
+ step_mode=True,
596
+ debug_mode=frame.debug_mode,
597
+ break_on_start=frame.break_on_start,
598
+ break_at=frame.break_at,
599
+ )
600
+ # capture debug controller for post-mortem access
601
+ try:
602
+ self._debug_controller = getattr(executor, "debug_controller", None)
603
+ except Exception:
604
+ self._debug_controller = None
605
+
606
+ step_info = None
607
+
608
+ async for result in executor.run_step(blocks, frame.block_pointer):
609
+ if isinstance(result, tuple) and len(result) == 2:
610
+ # This is the step completion information
611
+ step_info = result
612
+
613
+ new_pointer, is_complete = (
614
+ step_info if step_info else (frame.block_pointer, True)
615
+ )
616
+
617
+ # 4. Update Status
618
+ frame.block_pointer = new_pointer
619
+
620
+ if is_complete:
621
+ frame.status = FrameStatus.COMPLETED
622
+
623
+ # 5. Save State
624
+ self._save_frame_snapshot(frame)
625
+
626
+ # 6. Return the completion status and result variables
627
+ if is_complete:
628
+ # Return user-defined variables as results (excluding internal variables)
629
+ return StepResult.completed(result=self.context.get_user_variables())
630
+ else:
631
+ return StepResult.running()
632
+
633
+ except Exception as e:
634
+ result = self._handle_execution_exception(e, frame, frame.block_pointer)
635
+ if result is not None:
636
+ return result
637
+ raise e
638
+
639
+ async def run_coroutine(self, frame_id: str, progress_callback=None):
640
+ """Execute continuously until interrupted or completed
641
+
642
+ Returns:
643
+ StepResult: Unified execution result
644
+ - StepResult.completed(result): Execution completed, containing final result
645
+ - StepResult.interrupted(handle): Tool interrupted, containing recovery handle
646
+
647
+ Semantics:
648
+ - Execute subsequent blocks sequentially starting from the current frame's block_pointer
649
+ - If ToolInterrupt is triggered during execution, save an interrupt snapshot and return interrupt status
650
+ - If all blocks are executed, save a completion snapshot and return completion status
651
+
652
+ Note:
653
+ - Unlike step_coroutine, this method does not save snapshots at the end of each block; it saves only once at "interrupt/completion"
654
+ """
655
+ import traceback
656
+ from dolphin.core.coroutine.execution_frame import FrameStatus
657
+ from dolphin.core.coroutine.step_result import StepResult
658
+
659
+ # 1. Get frame and restore context
660
+ frame = self.state_registry.get_frame(frame_id)
661
+ if not frame:
662
+ raise ValueError(f"Frame not found: {frame_id}")
663
+
664
+ snapshot = self.snapshot_store.load_snapshot(frame.context_snapshot_id)
665
+ if not snapshot:
666
+ raise ValueError(f"Snapshot not found: {frame.context_snapshot_id}")
667
+
668
+ self._restore_context(snapshot)
669
+
670
+ try:
671
+ # 2. Get the parsed blocks (cached in the frame or re-parsed)
672
+ blocks = self._get_or_parse_blocks(frame)
673
+
674
+ # 3. Execute continuously until interrupted or completed (read debug configuration from frame)
675
+ executor = Executor(
676
+ context=self.context,
677
+ step_mode=False,
678
+ debug_mode=frame.debug_mode,
679
+ break_on_start=frame.break_on_start,
680
+ break_at=frame.break_at,
681
+ )
682
+ # capture debug controller for post-mortem access
683
+ try:
684
+ self._debug_controller = getattr(executor, "debug_controller", None)
685
+ except Exception:
686
+ self._debug_controller = None
687
+
688
+ pointer = frame.block_pointer
689
+ while pointer < len(blocks):
690
+ current_block = blocks[pointer]
691
+ try:
692
+ # Pass through and consume the progress items (dict/[]/strings, etc.) generated by block
693
+ # If progress_callback is provided, it will be called once for each progress item generated.
694
+ async for resp in executor.blocks_act([current_block]):
695
+ if progress_callback is not None:
696
+ try:
697
+ progress_callback(resp)
698
+ except Exception:
699
+ # Callbacks should not affect the main execution flow
700
+ pass
701
+ except Exception as e:
702
+ result = self._handle_execution_exception(e, frame, pointer)
703
+ if result is not None:
704
+ return result
705
+ raise e
706
+
707
+ # The current block has completed normally, advance the pointer
708
+ pointer += 1
709
+
710
+ # 4. Completed: Update pointers and state, save snapshot only here
711
+ frame.block_pointer = pointer
712
+ frame.status = FrameStatus.COMPLETED
713
+ self._save_frame_snapshot(frame)
714
+
715
+ # Returns the completion status
716
+ return StepResult.completed(result=self.context.get_user_variables())
717
+
718
+ except Exception as e:
719
+ # Fallback exception handling (theoretically handled in inner loop)
720
+ result = self._handle_execution_exception(e, frame, frame.block_pointer)
721
+ if result is not None:
722
+ return result
723
+ raise e
724
+
725
+ async def pause_coroutine(self, frame_id: str):
726
+ """Pause execution"""
727
+ from dolphin.core.coroutine import ResumeHandle
728
+ from dolphin.core.coroutine.execution_frame import FrameStatus
729
+
730
+ frame = self.state_registry.get_frame(frame_id)
731
+ if not frame:
732
+ raise ValueError(f"Frame not found: {frame_id}")
733
+
734
+ frame.status = FrameStatus.PAUSED
735
+ self.state_registry.update_frame(frame)
736
+
737
+ return ResumeHandle.create_handle(
738
+ frame_id=frame_id, snapshot_id=frame.context_snapshot_id
739
+ )
740
+
741
+ async def resume_coroutine(self, handle, updates=None):
742
+ """Resume execution, and optionally inject updated data into the context"""
743
+ from dolphin.core.coroutine.execution_frame import FrameStatus
744
+
745
+ # 1. Get frame
746
+ frame = self.state_registry.get_frame(handle.frame_id)
747
+ if not frame:
748
+ raise ValueError(f"Frame not found: {handle.frame_id}")
749
+
750
+ # Check if recovery is from tool interruption
751
+ is_intervention_resume = frame.status == FrameStatus.WAITING_FOR_INTERVENTION
752
+
753
+ # 2. Restore context from snapshot
754
+ snapshot = self.snapshot_store.load_snapshot(handle.snapshot_id)
755
+ if not snapshot:
756
+ raise ValueError(f"Snapshot not found: {handle.snapshot_id}")
757
+
758
+ self._restore_context(snapshot)
759
+
760
+ # 3. Processing Data Injection
761
+ if updates:
762
+ # Apply updates data
763
+ for key, value in updates.items():
764
+ self.context.set_variable(key, value)
765
+
766
+ # 4. Special handling for interrupt recovery
767
+ if is_intervention_resume:
768
+ from dolphin.core.coroutine.execution_frame import WaitReason
769
+
770
+ # Handle user interruption (UserInterrupt)
771
+ if frame.wait_reason == WaitReason.USER_INTERRUPT:
772
+ # If there is new user input provided via updates, inject it as a message
773
+ if updates and "__user_interrupt_input__" in updates:
774
+ user_input = updates.pop("__user_interrupt_input__")
775
+ # Use the convenient method to add user message
776
+ self.context.add_user_message(user_input, bucket="conversation_history")
777
+ logger.info(f"UserInterrupt resume: injected user input of length {len(user_input)}")
778
+
779
+ # Clean up status once handled
780
+ frame.wait_reason = None
781
+ frame.error = None
782
+
783
+ # Handle tool interruption (ToolInterrupt)
784
+ elif frame.error and "tool_name" in frame.error:
785
+ tool_name = frame.error["tool_name"]
786
+ tool_args = frame.error.get("tool_args", [])
787
+
788
+ # If there are tools to recover data, set them to the context
789
+ if updates and "tool_result" in updates:
790
+ self.context.set_variable("tool_result", updates["tool_result"])
791
+
792
+ # Clean up error messages, as the interruption has been resolved
793
+ frame.error = None
794
+
795
+ # 5. Create new snapshot (if updated)
796
+ if updates or is_intervention_resume:
797
+ self._save_frame_snapshot(frame)
798
+
799
+ # 6. Set the frame status to RUNNING and save
800
+ frame.status = FrameStatus.RUNNING
801
+ self.state_registry.update_frame(frame)
802
+
803
+ return frame
804
+
805
+ async def terminate_coroutine(self, frame_id: str, terminate_children: bool = True):
806
+ """Terminate execution frame tree
807
+
808
+ Args:
809
+ frame_id: The ID of the frame to terminate
810
+ terminate_children: Whether to terminate all child frames as well, default is True
811
+
812
+ Returns:
813
+ The terminated frame
814
+
815
+ Raises:
816
+ ValueError: If the frame does not exist
817
+ """
818
+ from dolphin.core.coroutine.execution_frame import FrameStatus
819
+
820
+ # 1. Get frame
821
+ frame = self.state_registry.get_frame(frame_id)
822
+ if not frame:
823
+ raise ValueError(f"Frame not found: {frame_id}")
824
+
825
+ # 2. Terminate all subframes (if needed)
826
+ if terminate_children and frame.children:
827
+ for child_id in frame.children:
828
+ try:
829
+ await self.terminate_coroutine(child_id, terminate_children=True)
830
+ except ValueError:
831
+ # The subframe may no longer exist, ignore the error
832
+ pass
833
+
834
+ # 3. Set frame status to TERMINATED
835
+ frame.status = FrameStatus.TERMINATED
836
+ frame.update_timestamp()
837
+
838
+ # 4. Save frame state
839
+ self.state_registry.update_frame(frame)
840
+
841
+ return frame
842
+
843
+ def _create_snapshot(self, frame_id: str):
844
+ """Create context snapshot"""
845
+ return self.context.export_runtime_state(frame_id)
846
+
847
+ def _restore_context(self, snapshot):
848
+ """Restore context from snapshot"""
849
+ self.context.apply_runtime_state(snapshot)
850
+
851
+ def _save_frame_snapshot(self, frame):
852
+ """Create and save a context snapshot of the execution frame
853
+
854
+ Args:
855
+ frame: The execution frame to save a snapshot of
856
+
857
+ Returns:
858
+ str: The ID of the newly created snapshot
859
+ """
860
+ new_snapshot = self._create_snapshot(frame.frame_id)
861
+ self.snapshot_store.save_snapshot(new_snapshot)
862
+ frame.context_snapshot_id = new_snapshot.snapshot_id
863
+ self.state_registry.update_frame(frame)
864
+ return new_snapshot.snapshot_id
865
+
866
+ def _get_or_parse_blocks(self, frame):
867
+ """Get or parse blocks"""
868
+ content = frame.original_content
869
+ if not content:
870
+ return []
871
+
872
+ executor = Executor(context=self.context)
873
+ return executor.get_parsed_blocks(content)
874
+
875
+ # === New High-Level API (for Multi-turn Dialogue Scenarios) ===
876
+
877
+ async def append_incremental_message(self, payload: dict):
878
+ """Append an incremental message to the current coroutine (utilizing prefix cache)
879
+
880
+ Args:
881
+ payload: State or event information (arbitrary structure), for example {"event_type": "...", "content": "...", "metadata": {...}}
882
+
883
+ Design philosophy:
884
+ - Messages history accumulates like a log, facilitating prefix cache
885
+ - Each call appends only an incremental message
886
+ - Synchronously updates the history variable in context, compatible with /explore/(history=true)
887
+ """
888
+ # Format as user message
889
+ message_content = self._format_round_message(payload)
890
+
891
+ # Append to messages (normal path, keep compatibility)
892
+ self.context.add_user_message(message_content, bucket="conversation_history")
893
+
894
+ # Update the history variable at the same time (for the old path /explore/(history=true))
895
+ from dolphin.core.common.enums import Messages
896
+ history_raw = self.context.get_history_messages(normalize=False)
897
+
898
+ # Convert to list format if needed (handle different return types)
899
+ if history_raw is None:
900
+ history_list = []
901
+ elif isinstance(history_raw, Messages):
902
+ # Convert Messages object to list of dicts
903
+ history_list = history_raw.get_messages_as_dict()
904
+ elif isinstance(history_raw, list):
905
+ history_list = history_raw
906
+ else:
907
+ logger.warning(f"Unexpected history type: {type(history_raw)}, initializing as empty list")
908
+ history_list = []
909
+
910
+ history_list.append({"role": MessageRole.USER.value, "content": message_content})
911
+ self.context.set_variable("history", history_list)
912
+
913
+ # Update the snapshot of the current coroutine
914
+ if hasattr(self, "_current_frame_id") and self._current_frame_id:
915
+ frame = self.state_registry.get_frame(self._current_frame_id)
916
+ if frame:
917
+ self._save_frame_snapshot(frame)
918
+
919
+ def _format_round_message(self, round_info: dict) -> str:
920
+ """Format round information into message content"""
921
+ import json
922
+
923
+ # If the message field already exists, use it directly.
924
+ if "message" in round_info:
925
+ return round_info["message"]
926
+
927
+ # Otherwise, convert the entire dict to JSON format
928
+ return json.dumps(round_info, ensure_ascii=False, indent=2)
929
+
930
+ async def inject_context(self, updates: dict):
931
+ """Inject context variables (variable replacement mode) into the currently active coroutine.
932
+
933
+ ⚠️ Note: This method creates a new snapshot and cannot utilize prefix cache
934
+ ⚠️ For multi-turn conversation scenarios, append_round_message() is recommended
935
+
936
+ Args:
937
+ updates: Dictionary of variables to inject
938
+ """
939
+ if not hasattr(self, "_current_frame_id") or not self._current_frame_id:
940
+ raise ValueError("没有活跃的协程")
941
+
942
+ frame = self.state_registry.get_frame(self._current_frame_id)
943
+ from dolphin.core.coroutine import ResumeHandle
944
+
945
+ handle = ResumeHandle(frame.frame_id, frame.context_snapshot_id, "")
946
+ await self.resume_coroutine(handle, updates=updates)
947
+
948
+ async def step_current_coroutine(self):
949
+ """Execute the next step of the currently active coroutine.
950
+
951
+ Returns:
952
+ Execution result (bool indicating whether completion is achieved, or ResumeHandle indicating interruption)
953
+
954
+ Internal implementation:
955
+ Calls step_coroutine(self._current_frame_id)
956
+ """
957
+ if not hasattr(self, "_current_frame_id") or not self._current_frame_id:
958
+ raise ValueError("没有活跃的协程")
959
+
960
+ return await self.step_coroutine(self._current_frame_id)
961
+
962
+ def is_waiting_for_intervention(self) -> bool:
963
+ """Check whether the current coroutine is waiting for an interrupt handler.
964
+
965
+ Returns:
966
+ bool: Whether it is waiting for an interrupt
967
+ """
968
+ if not hasattr(self, "_current_frame_id") or not self._current_frame_id:
969
+ return False
970
+
971
+ frame = self.state_registry.get_frame(self._current_frame_id)
972
+ from dolphin.core.coroutine.execution_frame import FrameStatus
973
+
974
+ return frame.status == FrameStatus.WAITING_FOR_INTERVENTION
975
+
976
+ def get_intervention_data(self) -> dict:
977
+ """Get interrupt data (such as tool call parameters)
978
+
979
+ Returns:
980
+ dict: Interrupt-related data
981
+ """
982
+ if not hasattr(self, "_current_frame_id") or not self._current_frame_id:
983
+ raise ValueError("没有活跃的协程")
984
+
985
+ frame = self.state_registry.get_frame(self._current_frame_id)
986
+ if frame.error and frame.error.get("error_type") == "ToolInterrupt":
987
+ return {
988
+ "tool_name": frame.error.get("tool_name", ""),
989
+ "tool_args": frame.error.get("tool_args", []),
990
+ "at_block": frame.error.get("at_block", 0),
991
+ }
992
+
993
+ return {}
994
+
995
+ def get_messages_range(self, start: int, end: int):
996
+ """Read messages within the specified range (left-closed, right-open), indexed sequentially from the start of the session.
997
+
998
+ Args:
999
+ start: Starting index (inclusive)
1000
+ end: Ending index (exclusive)
1001
+
1002
+ Returns:
1003
+ PlainMessages (list[dict]), each containing fields such as role/content
1004
+ """
1005
+ messages = self.context.get_messages()
1006
+ all_messages_dict = messages.get_messages_as_dict() # Return PlainMessages
1007
+
1008
+ # Ensure the index is within the valid range
1009
+ start = max(0, start)
1010
+ end = min(len(all_messages_dict), end)
1011
+
1012
+ return all_messages_dict[start:end]
1013
+
1014
+ def replace_messages_range(self, start: int, end: int, replacement):
1015
+ """Replace the [start, end) interval with replacement (a string or a single message).
1016
+
1017
+ Note: The replacement will invalidate the prefix cache for this interval, and subsequent accumulation will continue from the new summary.
1018
+ When replacement is a str, it will be automatically wrapped as a user message; when it's a dict, it should include role/content.
1019
+
1020
+ Args:
1021
+ start: starting index (inclusive)
1022
+ end: ending index (exclusive)
1023
+ replacement: string or message dictionary
1024
+ """
1025
+ from dolphin.core.common.enums import SingleMessage, MessageRole
1026
+ from datetime import datetime
1027
+
1028
+ messages = self.context.get_messages()
1029
+ all_messages = messages.messages if hasattr(messages, "messages") else []
1030
+
1031
+ # Ensure the index is within the valid range
1032
+ start = max(0, start)
1033
+ end = min(len(all_messages), end)
1034
+
1035
+ # Handle replacement, convert to SingleMessage
1036
+ if isinstance(replacement, str):
1037
+ replacement_msg = SingleMessage(
1038
+ role=MessageRole.USER,
1039
+ content=replacement,
1040
+ timestamp=datetime.now().isoformat(),
1041
+ )
1042
+ elif isinstance(replacement, dict):
1043
+ role = replacement.get("role", MessageRole.USER)
1044
+ if isinstance(role, str):
1045
+ role = MessageRole(role)
1046
+ replacement_msg = SingleMessage(
1047
+ role=role,
1048
+ content=replacement.get("content", ""),
1049
+ timestamp=datetime.now().isoformat(),
1050
+ )
1051
+ elif isinstance(replacement, SingleMessage):
1052
+ replacement_msg = replacement
1053
+ else:
1054
+ raise ValueError(f"Invalid replacement type: {type(replacement)}")
1055
+
1056
+ # Perform replacement
1057
+ all_messages[start:end] = [replacement_msg]
1058
+
1059
+ # Update the snapshot of the current coroutine
1060
+ if hasattr(self, "_current_frame_id") and self._current_frame_id:
1061
+ frame = self.state_registry.get_frame(self._current_frame_id)
1062
+ if frame:
1063
+ self._save_frame_snapshot(frame)