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,1580 @@
1
+ import asyncio
2
+ import os
3
+ import json
4
+ from datetime import datetime
5
+ from typing import Dict, List, Optional, Any, Union, TYPE_CHECKING
6
+
7
+ from dolphin.core.common.enums import MessageRole, SkillInfo, Messages, SkillType
8
+ from dolphin.core.config.global_config import GlobalConfig
9
+ from dolphin.core.common.constants import (
10
+ KEY_MAX_ANSWER_CONTENT_LENGTH,
11
+ KEY_SESSION_ID,
12
+ KEY_USER_ID,
13
+ MAX_ANSWER_CONTENT_LENGTH,
14
+ MAX_LOG_LENGTH,
15
+ )
16
+ from dolphin.core.context_engineer.core.context_manager import (
17
+ ContextManager,
18
+ )
19
+ from dolphin.core.runtime.runtime_graph import RuntimeGraph
20
+ from dolphin.core.skill.skill_function import SkillFunction
21
+ from dolphin.core.skill.skillkit import Skillkit
22
+ from dolphin.core.skill.skillset import Skillset
23
+ from dolphin.core.skill.skill_matcher import SkillMatcher
24
+
25
+ from dolphin.core.common.types import Var
26
+ from dolphin.core.context.var_output import VarOutput, SourceType
27
+ from dolphin.core.context.variable_pool import VariablePool
28
+ from dolphin.core.logging.logger import get_logger
29
+ from dolphin.core.trajectory.trajectory import Trajectory
30
+
31
+ # Import sdk/lib modules under TYPE_CHECKING to avoid circular imports
32
+ if TYPE_CHECKING:
33
+ from dolphin.sdk.skill.global_skills import GlobalSkills
34
+ from dolphin.lib.memory.manager import MemoryManager
35
+ from dolphin.lib.skill_results.skillkit_hook import SkillkitHook
36
+
37
+
38
+ logger = get_logger("context")
39
+
40
+
41
+ class Context:
42
+ def __init__(
43
+ self,
44
+ config: Optional[GlobalConfig] = None,
45
+ global_skills: Optional["GlobalSkills"] = None,
46
+ memory_manager: Optional["MemoryManager"] = None,
47
+ global_types=None,
48
+ skillkit_hook: Optional["SkillkitHook"] = None,
49
+ context_manager: Optional[ContextManager] = None,
50
+ verbose: bool = False,
51
+ is_cli: bool = False,
52
+ ):
53
+ self.config = config
54
+ self.global_skills = global_skills
55
+ self.memory_manager = memory_manager
56
+ self.global_types = global_types
57
+ self.variable_pool = VariablePool()
58
+ self.skillkit = Skillkit()
59
+ self.messages: Dict[str, Messages] = (
60
+ dict()
61
+ ) # Use Messages class instead of list
62
+ self.messages_dirty: bool = True # Initially dirty, ensure synchronization on first use
63
+ self.user_id = None
64
+ self.session_id = None
65
+ self.cur_agent = None
66
+ self.max_answer_len = MAX_ANSWER_CONTENT_LENGTH
67
+ self.verbose = verbose # Control log detail level
68
+ self.is_cli = is_cli # Control CLI rendering (Rich/Live Markdown)
69
+
70
+ self.time_extracted_knowledge = None
71
+
72
+ self.runtime_graph = RuntimeGraph()
73
+
74
+ # Initialize all_skills to avoid AttributeError
75
+ self.all_skills = Skillset()
76
+
77
+ # Initialize skillkit_hook
78
+ if skillkit_hook is not None:
79
+ self.skillkit_hook = skillkit_hook
80
+ else:
81
+ self.skillkit_hook = None
82
+
83
+ # Initialize context_manager
84
+ self.context_manager = context_manager or ContextManager()
85
+
86
+ # Initialize trajectory (disabled by default, enabled via init_trajectory)
87
+ self.trajectory: Optional[Trajectory] = None
88
+
89
+ # Historical injection idempotency flag: avoid re-injecting variable history within the same context lifecycle
90
+ self.history_injected: bool = False
91
+
92
+ # The name of the model used last (to maintain model consistency in multi-turn conversations)
93
+ self._last_model_name: Optional[str] = None
94
+
95
+ # The final skills configuration used (inherited skill filtering configuration for multi-turn conversations)
96
+ self._last_skills: Optional[List[str]] = None
97
+
98
+ # The final exploration mode used (when conducting multi-turn conversations, inherits the mode configuration)
99
+ self._last_explore_mode: Optional[str] = None
100
+
101
+ # The last system prompt used (to maintain consistent system bucket across multi-turn conversations)
102
+ self._last_system_prompt: Optional[str] = None
103
+
104
+ # User interrupt event (injected by Agent layer for cooperative cancellation)
105
+ self._interrupt_event: Optional[asyncio.Event] = None
106
+
107
+ def set_skillkit_hook(self, skillkit_hook: "SkillkitHook"):
108
+ """Set skillkit_hook"""
109
+ self.skillkit_hook = skillkit_hook
110
+ logger.debug("skillkit_hook has been set")
111
+
112
+ def get_skillkit_hook(self) -> Optional["SkillkitHook"]:
113
+ """Get skillkit_hook"""
114
+ return self.skillkit_hook
115
+
116
+ def has_skillkit_hook(self) -> bool:
117
+ """Check if skillkit_hook exists"""
118
+ return self.skillkit_hook is not None
119
+
120
+ def init_trajectory(self, trajectory_path: str, overwrite: bool = True):
121
+ """Initialize trajectory recording
122
+
123
+ Args:
124
+ trajectory_path: Path to save the trajectory file
125
+ overwrite: Whether to overwrite existing trajectory files (default is True)
126
+ """
127
+ self.trajectory = Trajectory(trajectory_path, overwrite=overwrite)
128
+ logger.debug(f"Trajectory initialized: {trajectory_path} (overwrite={overwrite})")
129
+
130
+ def copy(self):
131
+ copied = Context(
132
+ config=self.config,
133
+ global_skills=self.global_skills,
134
+ memory_manager=self.memory_manager,
135
+ global_types=self.global_types,
136
+ verbose=self.verbose,
137
+ is_cli=self.is_cli,
138
+ )
139
+ copied.variable_pool = self.variable_pool.copy()
140
+ copied.skillkit = self.skillkit
141
+ # Copy Messages object
142
+ copied.messages = {}
143
+ for agent_name, messages in self.messages.items():
144
+ copied.messages[agent_name] = messages.copy()
145
+ # copied.compressed_messages = self.compressed_messages # Commented out this line because the attribute does not exist
146
+ copied.cur_agent = self.cur_agent
147
+ copied.user_id = self.user_id
148
+ copied.session_id = self.session_id
149
+ copied.cur_agent = self.cur_agent
150
+ copied.max_answer_len = self.max_answer_len
151
+ copied.verbose = self.verbose
152
+ copied.is_cli = self.is_cli
153
+ copied.runtime_graph = self.runtime_graph.copy()
154
+ return copied
155
+
156
+ def set_cur_agent(self, agent):
157
+ self.cur_agent = agent
158
+ self.runtime_graph.set_agent(agent)
159
+
160
+ def get_cur_agent(self):
161
+ return self.cur_agent
162
+
163
+ def get_cur_agent_name(self):
164
+ return self.cur_agent.get_name() if self.cur_agent else None
165
+
166
+ def set_user_id(self, user_id: str):
167
+ self.user_id = user_id
168
+
169
+ def get_user_id(self):
170
+ return self.user_id
171
+
172
+ def set_session_id(self, session_id: str):
173
+ self.session_id = session_id
174
+
175
+ def get_session_id(self):
176
+ return self.session_id
177
+
178
+ def set_max_answer_len(self, max_answer_len: int):
179
+ self.max_answer_len = max_answer_len
180
+
181
+ def get_max_answer_len(self):
182
+ return self.max_answer_len
183
+
184
+ def set_verbose(self, verbose: bool):
185
+ """Set verbose mode"""
186
+ self.verbose = verbose
187
+
188
+ def get_verbose(self) -> bool:
189
+ """Get verbose mode status"""
190
+ return self.verbose
191
+
192
+ def is_verbose(self) -> bool:
193
+ """Check if verbose mode is enabled (detailed logging)"""
194
+ return self.verbose
195
+
196
+ def set_cli_mode(self, is_cli: bool):
197
+ """Set CLI mode (controls Rich/terminal beautification)"""
198
+ self.is_cli = is_cli
199
+
200
+ def is_cli_mode(self) -> bool:
201
+ """Check if running in CLI mode (controls Rich/terminal beautification)
202
+
203
+ This is separate from verbose:
204
+ - verbose: controls log detail level
205
+ - is_cli: controls terminal beautification (Rich Live Markdown, colors, etc.)
206
+
207
+ Use cases:
208
+ - verbose=True, is_cli=True: CLI with detailed beautiful output
209
+ - verbose=True, is_cli=False: API/script with detailed plain text logs
210
+ - verbose=False, is_cli=True: CLI with quiet but beautiful output
211
+ - verbose=False, is_cli=False: API/script in silent mode
212
+ """
213
+ return self.is_cli
214
+
215
+ def get_config(self):
216
+ return self.config
217
+
218
+ def get_memory_manager(self):
219
+ return self.memory_manager
220
+
221
+ def get_global_types(self):
222
+ return self.global_types
223
+
224
+ def get_history_messages(self, normalize: bool = True):
225
+ """Get history, default normalized to Messages without modifying the original storage structure of the variable pool.
226
+
227
+ Args:
228
+ normalize: Whether to convert history into a Messages object
229
+
230
+ Returns:
231
+ Messages or the original stored history
232
+ """
233
+ history_raw = self.get_var_value("history")
234
+ if not normalize:
235
+ return history_raw
236
+
237
+ if isinstance(history_raw, Messages):
238
+ return history_raw.copy()
239
+
240
+ normalized = Messages()
241
+ if history_raw is None:
242
+ return normalized
243
+ if isinstance(history_raw, list):
244
+ normalized.extend_plain_messages(history_raw)
245
+ else:
246
+ raise ValueError(f"Invalid history format: {type(history_raw)}, expected list or Messages object.")
247
+ return normalized
248
+
249
+ def set_variable(self, name, value):
250
+ if name == KEY_USER_ID:
251
+ self.set_user_id(value)
252
+ elif name == KEY_SESSION_ID:
253
+ self.set_session_id(value)
254
+ elif name == KEY_MAX_ANSWER_CONTENT_LENGTH:
255
+ self.set_max_answer_len(int(value))
256
+
257
+ self.variable_pool.set_var(name, value)
258
+
259
+ def set_var_output(
260
+ self,
261
+ name,
262
+ value,
263
+ source_type=SourceType.OTHER,
264
+ skill_info: Optional[SkillInfo] = None,
265
+ ):
266
+ """Set variable
267
+ :param name: variable name
268
+ :param value: variable value
269
+ :param source_type: variable source type
270
+ :param skill_info: skill information
271
+ """
272
+ self.variable_pool.set_var_output(name, value, source_type, skill_info)
273
+
274
+ def init_variables(self, variables):
275
+ """Initialize variable pool
276
+ :param variables: variable dictionary
277
+ """
278
+ self.variable_pool.init_variables(variables)
279
+
280
+ def init_skillkit(self, skillkit: Skillkit):
281
+ self.set_skills(skillkit)
282
+
283
+ def init_agents(self, agents):
284
+ self.agents = agents
285
+
286
+ def add_agents(self, agents):
287
+ for key, value in agents.items():
288
+ self.agents[key] = value
289
+
290
+ def get_agent(self, agent_name):
291
+ return self.agents[agent_name]
292
+
293
+ def get_agents(self):
294
+ return self.agents
295
+
296
+ def has_agent(self, agent_name):
297
+ """Check if the specified agent skill exists
298
+ :param agent_name: agent name
299
+ :return: whether it exists
300
+ """
301
+ # Support direct use of agent_name and skill names with prefixes
302
+ return (
303
+ self.skillkit.hasSkill(agent_name)
304
+ or self.skillkit.hasSkill(f"run_{agent_name}")
305
+ or self.skillkit.hasSkill(f"arun_{agent_name}")
306
+ )
307
+
308
+ def exec_agent(self, agent_name, **kwargs):
309
+ """Synchronous execution of agent (via skill system)
310
+ :param agent_name: agent name
311
+ :param kwargs: parameters passed to the agent
312
+ :return: execution result
313
+ """
314
+ # First try directly using agent_name, then try the run_ prefix
315
+ skill_name = agent_name
316
+ if not self.skillkit.hasSkill(skill_name):
317
+ skill_name = f"run_{agent_name}"
318
+
319
+ if not self.skillkit.hasSkill(skill_name):
320
+ raise ValueError(
321
+ f"Agent skill not found: {agent_name} (tried: {agent_name}, run_{agent_name})"
322
+ )
323
+
324
+ result = self.skillkit.exec(skill_name, **kwargs)
325
+ return result.result if hasattr(result, "result") else result
326
+
327
+ async def aexec_agent(self, agent_name, **kwargs):
328
+ """Asynchronously execute agent (via skill system)
329
+ :param agent_name: agent name
330
+ :param kwargs: parameters to pass to the agent
331
+ :return: execution result
332
+ """
333
+ # First try directly using agent_name, then try the arun_ prefix
334
+ skill_name = agent_name
335
+ if not self.skillkit.hasSkill(skill_name):
336
+ skill_name = f"arun_{agent_name}"
337
+
338
+ if not self.skillkit.hasSkill(skill_name):
339
+ raise ValueError(
340
+ f"Agent skill not found: {agent_name} (tried: {agent_name}, arun_{agent_name})"
341
+ )
342
+
343
+ result = await self.skillkit.aexec(skill_name, **kwargs)
344
+ return result.result if hasattr(result, "result") else result
345
+
346
+ def get_var_value(self, name, default_value=None):
347
+ """Get variable
348
+ :param name: variable name
349
+ :return: variable value, or None if it does not exist
350
+ """
351
+ # Compatibility layer: If it's a known flag, redirect to the new system
352
+ # Return string ("true"/"false") or boolean (True/False) based on the caller's default value type
353
+ # - If default_value is None or str: return the string "true"/"false" (compatible with the old string comparison style)
354
+ # - Otherwise: return boolean True/False (compatible with scenarios where it's used directly as a boolean)
355
+ # Lazy import to avoid circular dependencies; use definitions.DEFAULT_VALUES as the true source
356
+ try:
357
+ from dolphin.core.flags.definitions import DEFAULT_VALUES
358
+
359
+ if name in DEFAULT_VALUES:
360
+ from dolphin.core import flags
361
+
362
+ enabled = bool(flags.is_enabled(name))
363
+ if default_value is None or isinstance(default_value, str):
364
+ return "true" if enabled else "false"
365
+ return enabled
366
+ except Exception:
367
+ pass
368
+ return self.variable_pool.get_var_value(name, default_value)
369
+
370
+ def get_var_path_value(self, varpath, default_value=None):
371
+ """Get variable path value
372
+ :param varpath: variable path
373
+ :return: variable path value, returns None if it does not exist
374
+ """
375
+ return self.variable_pool.get_var_path_value(varpath, default_value)
376
+
377
+ def get_var_obj(self, name):
378
+ """Get the variable object
379
+ :param name: variable name
380
+ :return: Variable object, or None if it does not exist
381
+ """
382
+ return self.variable_pool.get_var(name)
383
+
384
+ def get_all_variables(self):
385
+ """Get all variables
386
+ :return: Dictionary mapping variable names to variable values
387
+ """
388
+ return self.variable_pool.get_all_variables()
389
+
390
+ def get_user_variables(self, include_system_context_vars=False):
391
+ """Get user-defined variables, excluding internal variables.
392
+
393
+ Parameters:
394
+ include_system_context_vars: Whether to include system context variables (e.g., _user_id, _session_id, etc.)
395
+ Default is False for backward compatibility
396
+
397
+ :return: Dictionary mapping user variable names to their values
398
+ """
399
+ return self.variable_pool.get_user_variables(include_system_context_vars)
400
+
401
+ def get_all_variables_values(self):
402
+ """Get all variable values
403
+ :return: List of variable values
404
+ """
405
+ return self.variable_pool.get_all_variables_values()
406
+
407
+ def get_variables_values(self, variable_names: list[str]):
408
+ """Get the variable dictionary for the specified variable names to improve performance and avoid retrieving all variables.
409
+ :param variable_names: List of variable names to retrieve
410
+ :return: Dictionary containing the specified variables, in the format {variable name: variable value}
411
+ """
412
+ if not variable_names:
413
+ return {}
414
+
415
+ result = {}
416
+ for var_name in variable_names:
417
+ if self.variable_pool.contain_var(var_name):
418
+ var = self.variable_pool.get_var(var_name)
419
+ result[var_name] = (
420
+ var.value if var is not None and hasattr(var, "value") else None
421
+ )
422
+ return result
423
+
424
+ def get_skillkit(self, skillNames: Optional[List[str]] = None):
425
+ """Get the skill set, supporting wildcard (glob), exact matching, and optional skillkit namespace.
426
+
427
+ Args:
428
+ skillNames: List of skill patterns.
429
+ - Plain tool name/pattern: "_python", "*_resource*"
430
+ - Namespaced pattern: "<skillkit>.<pattern>", e.g. "resource_skillkit.*"
431
+ If None, return the merged skill set;
432
+ If empty list [], indicates no skills are enabled
433
+
434
+ Returns:
435
+ Skillset: The matched skill set, or the merged skill set if no skills match
436
+ """
437
+
438
+ # If no matching pattern is provided, return the merged skill set
439
+ if skillNames is None:
440
+ return self.all_skills
441
+
442
+ # When an explicit empty list is passed in, it indicates that the caller does not wish to expose any tools.
443
+ # For example, the scenario where tools=[] is configured in DPH
444
+ if isinstance(skillNames, list) and len(skillNames) == 0:
445
+ return Skillset()
446
+
447
+ skills = self.all_skills.getSkills()
448
+ owner_names = SkillMatcher.get_owner_skillkits(skills)
449
+
450
+ # Use optimized batch matching (pre-parses patterns, deduplicates results)
451
+ matched_skills, any_namespaced_pattern = SkillMatcher.match_skills_batch(
452
+ skills, skillNames, owner_names
453
+ )
454
+
455
+ # If no skills match:
456
+ # - if namespaced patterns were used, return an empty set (safer default)
457
+ # - otherwise keep the historical behavior and return the merged skill set
458
+ if not matched_skills:
459
+ if any_namespaced_pattern:
460
+ return Skillset()
461
+ return self.all_skills
462
+
463
+ # Return the matching skill set
464
+ result_skillset = Skillset()
465
+ for skill in matched_skills:
466
+ result_skillset.addSkill(skill)
467
+
468
+ # Auto-inject _get_result_detail if needed
469
+ self._inject_detail_skill_if_needed(result_skillset)
470
+
471
+ return result_skillset
472
+
473
+ def _inject_detail_skill_if_needed(self, skillset: Skillset):
474
+ """Auto-inject _get_result_detail if any skill uses omitting modes (SUMMARY/REFERENCE)"""
475
+ try:
476
+ from dolphin.core.skill.context_retention import ContextRetentionMode
477
+ from dolphin.lib.skillkits.system_skillkit import SystemFunctions
478
+ except ImportError:
479
+ return
480
+
481
+ skills = skillset.getSkills()
482
+ should_inject = False
483
+
484
+ for skill in skills:
485
+ # Check for configured retention strategy
486
+ config = getattr(skill.func, '_context_retention', None)
487
+ if config and config.mode in (
488
+ ContextRetentionMode.SUMMARY,
489
+ ContextRetentionMode.REFERENCE,
490
+ ):
491
+ should_inject = True
492
+ break
493
+
494
+ if should_inject:
495
+ # Check if already exists in skillset
496
+ has_detail_skill = any(
497
+ "_get_result_detail" in s.get_function_name()
498
+ for s in skills
499
+ )
500
+
501
+ if not has_detail_skill:
502
+ # Try to get the skill from SystemFunctions
503
+ # We try different name variations just in case
504
+ detail_skill = SystemFunctions.getSkill("_get_result_detail")
505
+ if not detail_skill:
506
+ detail_skill = SystemFunctions.getSkill("system_functions._get_result_detail")
507
+
508
+ # If still not found (e.g. SystemFunctions not initialized with it), we can pick it manually if possible
509
+ # But typically SystemFunctions singleton has it.
510
+ if detail_skill:
511
+ skillset.addSkill(detail_skill)
512
+ else:
513
+ # Fallback: look through SystemFunctions.getSkills() manually
514
+ for s in SystemFunctions.getSkills():
515
+ if "_get_result_detail" in s.get_function_name():
516
+ skillset.addSkill(s)
517
+ break
518
+
519
+ def get_skill(self, name):
520
+ return self.skillkit.getSkill(name)
521
+
522
+ def get_skill_type(self, skill_name: str) -> SkillType:
523
+ """Get the type of a skill
524
+
525
+ Args:
526
+ skill_name: The name of the skill
527
+
528
+ Returns:
529
+ SkillType: The type of the skill
530
+ """
531
+ skill = self.get_skill(skill_name)
532
+ if skill:
533
+ # Try to get the tool type, return the default type if not available
534
+ return getattr(skill, "tool_type", SkillType.TOOL)
535
+ else:
536
+ # Default returns TOOL type
537
+ return SkillType.TOOL
538
+
539
+ def is_skillkit_empty(self):
540
+ return self.skillkit is None or self.skillkit.isEmpty()
541
+
542
+ def exec_skill(self, name, **kwargs):
543
+ return self.skillkit.exec(name, **kwargs)
544
+
545
+ async def aexec_skill(self, name, **kwargs):
546
+ return self.skillkit.aexec(name, **kwargs)
547
+
548
+ def get_agent_skill(self, skill_function: SkillFunction):
549
+ if self.global_skills is None:
550
+ return None
551
+ return self.global_skills.getAgent(skill_function.get_function_name())
552
+
553
+ def sync_variables(self, context: "Context"):
554
+ self.variable_pool.sync_variables(context.variable_pool)
555
+
556
+ def delete_variable(self, name):
557
+ """Delete variable
558
+ :param name: variable name
559
+ """
560
+ self.variable_pool.delete_var(name)
561
+
562
+ def clear_variables(self):
563
+ """Clear all variables"""
564
+ self.variable_pool.clear()
565
+
566
+ def list_variables(self):
567
+ """List all variables
568
+ :return: A list containing all variable names
569
+ """
570
+ return self.variable_pool.keys()
571
+
572
+ def set_skills(self, skillkit):
573
+ self.skillkit = skillkit
574
+ self._calc_all_skills()
575
+
576
+ def append_var_output(
577
+ self, name, value, source_type=SourceType.OTHER, skill_info=None
578
+ ):
579
+ var = self.variable_pool.get_var(name)
580
+ if not var:
581
+ var = VarOutput(
582
+ name=name, value=[], source_type=SourceType.LIST, skill_info=skill_info
583
+ )
584
+
585
+ if not isinstance(var, VarOutput):
586
+ if isinstance(var, Var):
587
+ if var.value:
588
+ if isinstance(var.value, list):
589
+ init_var = VarOutput(
590
+ name=name,
591
+ value=[],
592
+ source_type=SourceType.LIST,
593
+ skill_info=skill_info,
594
+ )
595
+ for item in var.value:
596
+ init_var.add(
597
+ VarOutput(
598
+ name=name,
599
+ value=item,
600
+ source_type=source_type,
601
+ skill_info=skill_info,
602
+ )
603
+ )
604
+ var = init_var
605
+ else:
606
+ var = VarOutput(
607
+ name=name,
608
+ value=[var],
609
+ source_type=SourceType.LIST,
610
+ skill_info=skill_info,
611
+ )
612
+ else:
613
+ var = VarOutput(
614
+ name=name,
615
+ value=[],
616
+ source_type=SourceType.LIST,
617
+ skill_info=skill_info,
618
+ )
619
+ else:
620
+ var = VarOutput(
621
+ name=name,
622
+ value=[var],
623
+ source_type=SourceType.LIST,
624
+ skill_info=skill_info,
625
+ )
626
+
627
+ new_var = var.add(
628
+ VarOutput(
629
+ name=name, value=value, source_type=source_type, skill_info=skill_info
630
+ )
631
+ )
632
+ self.variable_pool.set_var(name, new_var)
633
+
634
+ def set_last_var_output(
635
+ self, name, value, source_type=SourceType.OTHER, skill_info=None
636
+ ):
637
+ var = self.variable_pool.get_var(name)
638
+ if var:
639
+ var.set_last(
640
+ VarOutput(
641
+ name=name,
642
+ value=value,
643
+ source_type=source_type,
644
+ skill_info=skill_info,
645
+ )
646
+ )
647
+ self.variable_pool.set_var(name, var)
648
+
649
+ def update_var_output(
650
+ self, name, value, source_type=SourceType.OTHER, skill_info=None
651
+ ):
652
+ """Update variable
653
+ :param name: Variable name
654
+ :param value: Variable value
655
+ :param source_type: Variable source type
656
+ :param skill_info: Skill information
657
+ """
658
+ var = self.variable_pool.get_var(name)
659
+ if var:
660
+ # Check if the variable has the corresponding attribute and attempt to update it
661
+ if hasattr(var, "value"):
662
+ if isinstance(getattr(var, "value", None), list):
663
+ var.value[-1] = value # type: ignore
664
+ else:
665
+ setattr(var, "value", value)
666
+ # Try to update source_type and skill_info
667
+ if hasattr(var, "source_type"):
668
+ setattr(var, "source_type", source_type)
669
+ if hasattr(var, "skill_info"):
670
+ setattr(var, "skill_info", skill_info)
671
+ else:
672
+ self.set_var_output(name, value, source_type, skill_info)
673
+
674
+ def recognize_variable(self, dolphin_str):
675
+ # Identify the positions of all variables in a string - Simple variables: `$variableName` - Array indices: `$variableName[index]` - Nested properties: `$variableName.key1.key2`
676
+ """Identify the positions of all variables in a string
677
+ :param dolphin_str: The string to be identified
678
+ :return: A list of tuples containing variable names and their positions [('variable name', (start position, end position)), ...]
679
+ """
680
+ return self.variable_pool.recognize_variable(dolphin_str)
681
+
682
+ def get_variable_type(self, variable_str):
683
+ return self.variable_pool.get_variable_type(variable_str)
684
+
685
+ def reset_messages(self):
686
+ agent_name = self.get_cur_agent_name()
687
+ if agent_name is None:
688
+ agent_name = "default"
689
+ self.messages[agent_name] = Messages()
690
+ # Note: This only clears the image, messages in context_manager still exist
691
+ # If you need to clear the context_manager, you should call context_manager.clear_bucket()
692
+ # Here maintain the original behavior, only clear the image
693
+ self.messages_dirty = False # The mirror has already been explicitly set, no synchronization is needed.
694
+
695
+ # Reset the historical injection flag to allow new sessions to re-inject history.
696
+ self.history_injected = False
697
+
698
+ def reset_for_block(self):
699
+ """Reset context state for new code blocks.
700
+
701
+ This method uniformly handles the reset logic before executing a code block, including:
702
+ 1. Mark trajectory stage baseline (needs to be done before clearing, as current state is required)
703
+ 2. Reset message mirroring
704
+ 3. Clean up temporary buckets (SCRATCHPAD, SYSTEM, QUERY)
705
+
706
+ Note: This method is specifically designed for resetting before executing code blocks and should not be used in other scenarios.
707
+ """
708
+ # Step 1: Mark stage baseline BEFORE clearing (trajectory needs current state)
709
+ # The baseline captures the message count before this block starts executing
710
+ if getattr(self, "trajectory", None):
711
+ try:
712
+ self.trajectory.begin_stage(self.context_manager)
713
+ logger.debug("Trajectory stage baseline marked")
714
+ except (AttributeError, TypeError) as e:
715
+ logger.debug(f"Failed to mark trajectory baseline: {e}")
716
+
717
+ # Step 2: Reset message mirror for fresh block execution
718
+ # This prevents message accumulation across blocks and ensures clean state
719
+ self.reset_messages()
720
+
721
+ # Step 3: Clear transient buckets that should not persist across blocks:
722
+ # - SCRATCHPAD: temporary working memory for current block only
723
+ # - SYSTEM: system prompt may be different per block
724
+ # - QUERY: user query changes per block
725
+ # Note: These buckets will be re-populated by specific blocks (e.g., llm_chat)
726
+ try:
727
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
728
+
729
+ cm = self.context_manager
730
+ removed_buckets = []
731
+ for bucket in [BuildInBucket.SCRATCHPAD.value,
732
+ BuildInBucket.SYSTEM.value,
733
+ BuildInBucket.QUERY.value]:
734
+ try:
735
+ cm.remove_bucket(bucket)
736
+ removed_buckets.append(bucket)
737
+ except (AttributeError, KeyError):
738
+ pass # Bucket doesn't exist, which is fine
739
+
740
+ if removed_buckets:
741
+ logger.debug(f"Cleared buckets for fresh block state: {removed_buckets}")
742
+ except Exception as e:
743
+ # Log unexpected errors but don't fail block execution
744
+ logger.warning(f"Unexpected error during bucket cleanup: {e}")
745
+
746
+ def clear_messages(self):
747
+ """Clear message history - alias of reset_messages"""
748
+ self.reset_messages()
749
+
750
+ def set_messages(self, messages: Messages):
751
+ self.get_messages().set_messages(messages) # Use Messages.set_messages
752
+
753
+ def sync_messages_from_llm_context(self, force: bool = False) -> Messages:
754
+ """Core synchronization method: synchronize messages from the context_manager single data source to the current Agent's mirror.
755
+
756
+ Args:
757
+ force: Whether to force synchronization (ignore dirty flag optimization)
758
+
759
+ Returns:
760
+ Messages: The updated Messages object
761
+
762
+ Raises:
763
+ SyncError: An error occurred during synchronization
764
+
765
+ Note:
766
+ 1. Uses a lazy loading strategy: synchronization is performed only when the dirty flag is True or force=True
767
+ 2. Synchronization scope: Only updates the current Agent's mirror (self.messages[self.agent_name])
768
+ 3. Thread safety: Ensured by the locking mechanism inside context_manager
769
+ """
770
+ from dolphin.core.common.exceptions import SyncError
771
+
772
+ agent_name = self.get_cur_agent_name() or "default"
773
+
774
+ # Performance optimization: Check dirty flags
775
+ if not force and not self.messages_dirty:
776
+ return self.messages.get(agent_name, Messages())
777
+
778
+ try:
779
+ # 1. Obtain the authoritative, deduplicated, and policy-sorted final message list from context_manager
780
+ llm_messages = self.context_manager.to_dph_messages()
781
+
782
+ # 2. Get the current agent's image container (create one if it does not exist)
783
+ if agent_name not in self.messages:
784
+ self.messages[agent_name] = Messages()
785
+ target_mirror = self.messages[agent_name]
786
+
787
+ # 3. Update the image content in place, keeping references unchanged
788
+ # Messages.set_messages() already exists (common.py line 366-368)
789
+ target_mirror.set_messages(llm_messages)
790
+
791
+ # 4. Clear dirty marks
792
+ self.messages_dirty = False
793
+
794
+ logger.debug(f"Synced messages for agent '{agent_name}': {len(llm_messages.get_messages())} messages")
795
+
796
+ return target_mirror
797
+
798
+ except Exception as e:
799
+ logger.error(f"Failed to sync messages from context_manager: {e}")
800
+ raise SyncError(f"Message synchronization failed: {e}") from e
801
+
802
+ def get_messages(self):
803
+ """Get the message mirror of the current Agent (auto-sync)"""
804
+ agent_name = self.get_cur_agent_name()
805
+ if agent_name is None:
806
+ agent_name = "default"
807
+
808
+ # Auto Sync
809
+ self.sync_messages_from_llm_context()
810
+
811
+ if agent_name not in self.messages:
812
+ self.messages[agent_name] = Messages()
813
+
814
+ return self.messages[agent_name]
815
+
816
+ def get_messages_with_tool_calls(self):
817
+ """Get all messages that have tool calls"""
818
+ return self.get_messages().get_messages_with_tool_calls()
819
+
820
+ def get_tool_response_messages(self):
821
+ """Get all tool response messages"""
822
+ return self.get_messages().get_tool_response_messages()
823
+
824
+ # ============ Added: Unified message management convenience methods ============
825
+ def add_user_message(self, content, bucket: str = None):
826
+ """Add user message (unified interface)
827
+
828
+ Args:
829
+ content: Message content, can be:
830
+ - str: Plain text message
831
+ - List[Dict]: Multimodal content (e.g., [{"type": "text", "text": "..."}, {"type": "image_url", ...}])
832
+ bucket: Bucket name, default is SCRATCHPAD
833
+
834
+ Note:
835
+ Only written to context_manager (single data source),
836
+ Mirrors are synchronized on-demand via sync_messages_from_llm_context().
837
+ """
838
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
839
+
840
+ if bucket is None:
841
+ bucket = BuildInBucket.SCRATCHPAD.value
842
+
843
+ # Unique write path: added to context_manager
844
+ messages = Messages()
845
+ messages.add_message(content, MessageRole.USER)
846
+ self.context_manager.add_bucket(bucket, messages)
847
+
848
+ # Mark as dirty (remove double writing)
849
+ self.messages_dirty = True
850
+
851
+ def add_assistant_message(self, content: str, bucket: str = None):
852
+ """Add assistant message (unified interface)
853
+
854
+ Args:
855
+ content: Message content
856
+ bucket: Bucket name, default is SCRATCHPAD
857
+
858
+ Note:
859
+ Only written to context_manager (single data source),
860
+ Mirrors are synchronized on-demand via sync_messages_from_llm_context().
861
+ """
862
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
863
+
864
+ if bucket is None:
865
+ bucket = BuildInBucket.SCRATCHPAD.value
866
+
867
+ # Unique write path: added to context_manager
868
+ messages = Messages()
869
+ messages.add_message(content, MessageRole.ASSISTANT)
870
+ self.context_manager.add_bucket(bucket, messages)
871
+
872
+ # Mark as dirty (remove double writing)
873
+ self.messages_dirty = True
874
+
875
+ def add_system_message(self, content: str, bucket: str = None):
876
+ """Add system message (unified interface)
877
+
878
+ Args:
879
+ content: Message content
880
+ bucket: Bucket name, default is SYSTEM
881
+
882
+ Note:
883
+ Only written to context_manager (single data source),
884
+ Mirrors are synchronized on-demand via sync_messages_from_llm_context().
885
+ """
886
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
887
+
888
+ if bucket is None:
889
+ bucket = BuildInBucket.SYSTEM.value
890
+
891
+ messages = Messages()
892
+ messages.add_message(content, MessageRole.SYSTEM)
893
+ # If the corresponding bucket already exists, replace its content directly; otherwise, create a new bucket.
894
+ if self.context_manager is not None:
895
+ if bucket in self.context_manager.state.buckets:
896
+ # Directly replace the content, avoid merging Messages again
897
+ self.context_manager.replace_bucket_content(bucket, messages)
898
+ # Mark the message mirror as dirty to ensure subsequent synchronization.
899
+ self.messages_dirty = True
900
+ else:
901
+ # The initial creation still uses context_manager.add_bucket, with message_role set to SYSTEM
902
+ self.context_manager.add_bucket(
903
+ bucket_name=bucket,
904
+ content=messages,
905
+ message_role=MessageRole.SYSTEM,
906
+ )
907
+ self.messages_dirty = True
908
+
909
+ def add_tool_call_message_v2(self, content: str, tool_calls: list, bucket: str = None):
910
+ """Add tool call messages (unified interface)
911
+
912
+ Args:
913
+ content: Message content
914
+ tool_calls: List of tool calls
915
+ bucket: Bucket name, default is SCRATCHPAD
916
+
917
+ Note:
918
+ Only write to context_manager (single data source),
919
+ Mirrors are synchronized on-demand via sync_messages_from_llm_context().
920
+ """
921
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
922
+
923
+ if bucket is None:
924
+ bucket = BuildInBucket.SCRATCHPAD.value
925
+
926
+ # Unique write path: added to context_manager
927
+ messages = Messages()
928
+ messages.add_tool_call_message(content=content, tool_calls=tool_calls)
929
+ self.context_manager.add_bucket(bucket, messages)
930
+
931
+ # Mark as dirty (remove double writing)
932
+ self.messages_dirty = True
933
+
934
+ def add_tool_response_message_v2(self, content: str, tool_call_id: str, bucket: str = None):
935
+ """Add tool response message (unified interface)
936
+
937
+ Args:
938
+ content: Message content
939
+ tool_call_id: Tool call ID
940
+ bucket: Bucket name, default is SCRATCHPAD
941
+
942
+ Note:
943
+ Only write to context_manager (single data source),
944
+ Mirrors are synchronized on-demand via sync_messages_from_llm_context().
945
+ """
946
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
947
+
948
+ if bucket is None:
949
+ bucket = BuildInBucket.SCRATCHPAD.value
950
+
951
+ # Unique write path: added to context_manager
952
+ messages = Messages()
953
+ messages.add_tool_response_message(content=content, tool_call_id=tool_call_id)
954
+ self.context_manager.add_bucket(bucket, messages)
955
+
956
+ # Mark as dirty (remove double writing)
957
+ self.messages_dirty = True
958
+
959
+ def set_messages_batch(self, messages: Messages, bucket: str = None):
960
+ """Batch set messages (uniform interface)
961
+ Used to restore the previous messages state
962
+
963
+ Args:
964
+ messages: Messages object
965
+ bucket: bucket name, default is SCRATCHPAD
966
+ """
967
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
968
+
969
+ if bucket is None:
970
+ bucket = BuildInBucket.SCRATCHPAD.value
971
+
972
+ # Empty the current bucket
973
+ self.context_manager.clear_bucket(bucket)
974
+ # Add new messages
975
+ self.context_manager.add_bucket(bucket, messages)
976
+
977
+ # Mark as dirty
978
+ self.messages_dirty = True
979
+
980
+ def set_history_bucket(self, messages: Messages):
981
+ """Set or override the content of the history bucket to always remain consistent with the history snapshot in the variable pool.
982
+
983
+ Args:
984
+ messages: Normalized historical messages (Messages)
985
+ """
986
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
987
+
988
+ if not self.context_manager:
989
+ return
990
+
991
+ bucket_name = BuildInBucket.HISTORY.value
992
+
993
+ # If the bucket already exists, directly replace the content to avoid duplicate merge at the Messages level
994
+ if bucket_name in self.context_manager.state.buckets:
995
+ self.context_manager.replace_bucket_content(bucket_name, messages)
996
+ self.messages_dirty = True
997
+ else:
998
+ # First created, unified through add_bucket to maintain consistent configuration
999
+ self.add_bucket(
1000
+ bucket_name=bucket_name,
1001
+ content=messages,
1002
+ message_role=MessageRole.USER,
1003
+ )
1004
+
1005
+ def add_bucket(
1006
+ self,
1007
+ bucket_name: str,
1008
+ content: Union[str, Messages],
1009
+ priority: float = 1.0,
1010
+ allocated_tokens: Optional[int] = None,
1011
+ message_role: Optional[MessageRole] = None,
1012
+ ) -> None:
1013
+ """Unified bucket addition interface (wraps context_manager.add_bucket)
1014
+
1015
+ This is the recommended way to add a bucket, automatically handling the messages_dirty flag.
1016
+ External direct calls to self.context_manager.add_bucket() are prohibited.
1017
+
1018
+ Args:
1019
+ bucket_name: Bucket name
1020
+ content: Content, supports string or Messages type
1021
+ priority: Priority
1022
+ allocated_tokens: Number of allocated tokens
1023
+ message_role: Message role
1024
+
1025
+ Note:
1026
+ This method ensures the messages_dirty flag is correctly set,
1027
+ guaranteeing the message synchronization mechanism works properly.
1028
+ """
1029
+ self.context_manager.add_bucket(
1030
+ bucket_name=bucket_name,
1031
+ content=content,
1032
+ priority=priority,
1033
+ allocated_tokens=allocated_tokens,
1034
+ message_role=message_role,
1035
+ )
1036
+ # Automatically mark as dirty to ensure message synchronization
1037
+ self.messages_dirty = True
1038
+
1039
+ # ============ End: Unified Message Management Convenience Methods ============
1040
+
1041
+ async def update_usage(self, final_chunk):
1042
+ if not self.get_var_value("usage"):
1043
+ default_uasge = {
1044
+ "prompt_tokens": 0,
1045
+ "total_tokens": 0,
1046
+ "completion_tokens": 0,
1047
+ }
1048
+ self.set_variable("usage", default_uasge)
1049
+
1050
+ if not hasattr(final_chunk, "usage") or (
1051
+ hasattr(final_chunk, "usage") and not final_chunk.usage
1052
+ ):
1053
+ return
1054
+
1055
+ try:
1056
+ usage = (
1057
+ final_chunk.usage if final_chunk.usage
1058
+ else (final_chunk.choices[0].usage if final_chunk.choices else None)
1059
+ ) # SDK after AD packaging and DeepSeek native interface
1060
+ except:
1061
+ usage = (
1062
+ final_chunk["usage"]
1063
+ if final_chunk.get("usage", 0)
1064
+ else (final_chunk["choices"][0]["usage"] if final_chunk.get("choices") and len(final_chunk["choices"]) > 0 else None)
1065
+ ) # Current API request method for obtaining usage
1066
+ # If usage information cannot be obtained, return directly.
1067
+ if usage is None:
1068
+ return
1069
+
1070
+ llm_tokens = self.get_var_value("usage")
1071
+ if llm_tokens is None:
1072
+ llm_tokens = {
1073
+ "prompt_tokens": 0,
1074
+ "total_tokens": 0,
1075
+ "completion_tokens": 0,
1076
+ }
1077
+
1078
+ if isinstance(usage, dict):
1079
+ llm_tokens["prompt_tokens"] += usage.get("prompt_tokens", 0)
1080
+ llm_tokens["total_tokens"] += usage.get("total_tokens", 0)
1081
+ llm_tokens["completion_tokens"] += usage.get("completion_tokens", 0)
1082
+ else:
1083
+ llm_tokens["prompt_tokens"] += usage.prompt_tokens
1084
+ llm_tokens["total_tokens"] += usage.total_tokens
1085
+ llm_tokens["completion_tokens"] += usage.completion_tokens
1086
+ self.set_variable("usage", llm_tokens)
1087
+
1088
+ def get_runtime_graph(self):
1089
+ return self.runtime_graph
1090
+
1091
+ def set_last_model_name(self, model_name: str):
1092
+ """
1093
+ Set the last used model name.
1094
+ This should be called when making LLM calls to maintain model consistency across multiple rounds.
1095
+
1096
+ Args:
1097
+ model_name: The model name to store
1098
+ """
1099
+ if model_name:
1100
+ self._last_model_name = model_name
1101
+
1102
+ def get_last_model_name(self) -> Optional[str]:
1103
+ """
1104
+ Get the last used model name.
1105
+ This is useful for maintaining model consistency across multiple rounds of conversation.
1106
+
1107
+ Returns:
1108
+ Optional[str]: The model name if found, None otherwise
1109
+ """
1110
+ return self._last_model_name
1111
+
1112
+ def set_last_skills(self, skills: Optional[List[str]]):
1113
+ """
1114
+ Set the last used skills configuration.
1115
+ This should be called when executing explore blocks to maintain skills consistency across multiple rounds.
1116
+
1117
+ Args:
1118
+ skills: The skills list to store (can be None to clear)
1119
+ """
1120
+ self._last_skills = skills
1121
+
1122
+ def get_last_skills(self) -> Optional[List[str]]:
1123
+ """
1124
+ Get the last used skills configuration.
1125
+ This is useful for maintaining skills consistency across multiple rounds of conversation.
1126
+
1127
+ Returns:
1128
+ Optional[List[str]]: The skills list if found, None otherwise
1129
+ """
1130
+ return self._last_skills
1131
+
1132
+ def set_last_explore_mode(self, mode: Optional[str]):
1133
+ """
1134
+ Set the last used explore mode.
1135
+ This should be called when executing explore blocks to maintain mode consistency across multiple rounds.
1136
+
1137
+ Args:
1138
+ mode: The explore mode to store ('prompt' or 'tool_call', can be None to clear)
1139
+ """
1140
+ self._last_explore_mode = mode
1141
+
1142
+ def get_last_explore_mode(self) -> Optional[str]:
1143
+ """
1144
+ Get the last used explore mode.
1145
+ This is useful for maintaining mode consistency across multiple rounds of conversation.
1146
+
1147
+ Returns:
1148
+ Optional[str]: The explore mode if found ('prompt' or 'tool_call'), None otherwise
1149
+ """
1150
+ return self._last_explore_mode
1151
+
1152
+ def set_last_system_prompt(self, system_prompt: str):
1153
+ """
1154
+ Set the last used system prompt.
1155
+ This is useful for restoring the _system bucket during multi-turn conversations (e.g., continue_exploration).
1156
+
1157
+ Args:
1158
+ system_prompt: The system prompt to store
1159
+ """
1160
+ if system_prompt and str(system_prompt).strip():
1161
+ self._last_system_prompt = str(system_prompt)
1162
+
1163
+ def get_last_system_prompt(self) -> Optional[str]:
1164
+ """
1165
+ Get the last used system prompt.
1166
+
1167
+ Returns:
1168
+ Optional[str]: The system prompt if found, otherwise None
1169
+ """
1170
+ return self._last_system_prompt
1171
+
1172
+ def get_execution_trace(self, title=None) -> Dict[str, Any]:
1173
+ """Generate and return runtime execution trace information (Execution Trace)
1174
+
1175
+ The execution trace records the complete execution flow of the Agent, including:
1176
+ - Execution order and duration of each code block
1177
+ - LLM call details (input/output tokens, model, etc.)
1178
+ - Variable changes and state transitions
1179
+
1180
+ Args:
1181
+ title (str, optional): Trace title. If not provided, a default title is used.
1182
+
1183
+ Returns:
1184
+ Dict[str, Any]: Execution trace information containing call_chain and LLM details
1185
+ """
1186
+ return self.runtime_graph.profile(title or "")
1187
+
1188
+ # Backward compatibility: retain old method names
1189
+ def get_profile(self, title=None):
1190
+ """[Deprecated] Please use get_execution_trace() instead"""
1191
+ import warnings
1192
+ warnings.warn("get_profile() 已废弃,请使用 get_execution_trace()", DeprecationWarning, stacklevel=2)
1193
+ return self.get_execution_trace(title)
1194
+
1195
+ def get_snapshot_analysis(self, title=None, format='markdown', options=None):
1196
+ """Generate and return a visualization analysis report for ContextSnapshot (Snapshot Analysis)
1197
+
1198
+ Snapshot analysis creates a snapshot of the current context and generates a detailed analysis report, including:
1199
+ - Message statistics (bucketed by role, size, type)
1200
+ - Variable statistics (bucketed by type, size, namespace)
1201
+ - Memory usage analysis (original size, compressed size, compression ratio)
1202
+ - Optimization suggestions
1203
+
1204
+ Args:
1205
+ title (str, optional): Title of the analysis report
1206
+ format (str): Output format, either 'markdown' or 'json'
1207
+ options (dict, optional): Configuration options, including thresholds, rendering options, etc.
1208
+
1209
+ Returns:
1210
+ str or dict: Markdown-formatted report (if format='markdown') or JSON-formatted data (if format='json')
1211
+
1212
+ Examples:
1213
+ # Get Markdown report
1214
+ analysis = context.get_snapshot_analysis(title="Step 5 Analysis")
1215
+ print(analysis)
1216
+
1217
+ # Get JSON data
1218
+ analysis_data = context.get_snapshot_analysis(format='json')
1219
+ print(f"Compression: {analysis_data['compression_ratio']:.1%}")
1220
+ """
1221
+ # Create Snapshot
1222
+ snapshot = self.export_runtime_state(frame_id="analysis_snapshot")
1223
+
1224
+ # Generate analysis report
1225
+ return snapshot.profile(format=format, title=title, options=options)
1226
+
1227
+ def save_trajectory(
1228
+ self,
1229
+ agent_name: str = "main",
1230
+ trajectory_path: Optional[str] = None,
1231
+ force_save: bool = False,
1232
+ pretty_format: bool = False,
1233
+ stage: Optional[str] = None,
1234
+ ):
1235
+ """
1236
+ Save dialog messages to file (legacy/simple mode).
1237
+
1238
+ Note: Stage-based trajectory saving is now handled by the Trajectory class.
1239
+ This method delegates to Trajectory.save_simple() for consistency.
1240
+
1241
+ Args:
1242
+ agent_name: The agent name, defaults to "main"
1243
+ trajectory_path: Custom trajectory path
1244
+ force_save: Force save even if memory is not enabled
1245
+ pretty_format: Save in pretty formatted text instead of JSON
1246
+ stage: [Deprecated] Use context.trajectory.finalize_stage()
1247
+ """
1248
+ # Stage-based saving should use Trajectory class
1249
+ if stage is not None:
1250
+ logger.warning(
1251
+ "Stage-based trajectory saving via save_trajectory() is deprecated. "
1252
+ "Use context.trajectory.finalize_stage() instead."
1253
+ )
1254
+ return
1255
+
1256
+ if not (force_save or (self.config and self.config.memory_config and self.config.memory_config.enabled)):
1257
+ return
1258
+
1259
+ # Determine trajectory file path
1260
+ if not trajectory_path:
1261
+ current_date = datetime.now().strftime("%Y%m%d%H%M")
1262
+ dialog_base_path = (
1263
+ getattr(self.config.memory_config, "dialog_path", "data/dialog/")
1264
+ if self.config and self.config.memory_config
1265
+ else "data/dialog/"
1266
+ )
1267
+ user_id = self.user_id or "_default_user_"
1268
+ dialog_dir = f"{dialog_base_path}{agent_name}/user_{user_id}"
1269
+ trajectory_path = f"{dialog_dir}/dialog_{current_date}.json"
1270
+ os.makedirs(dialog_dir, exist_ok=True)
1271
+
1272
+ # Delegate to Trajectory class for actual saving
1273
+ Trajectory.save_simple(
1274
+ messages=self.get_messages().get_messages(),
1275
+ tools=self.skillkit.getSkillsSchema(),
1276
+ file_path=trajectory_path,
1277
+ pretty_format=pretty_format,
1278
+ user_id=self.user_id
1279
+ )
1280
+
1281
+ def info(self, log_str):
1282
+ logger.info(self._make_log(log_str))
1283
+
1284
+ def debug(self, log_str):
1285
+ logger.debug(self._make_log(log_str))
1286
+
1287
+ def warn(self, log_str):
1288
+ logger.warning(self._make_log(log_str))
1289
+
1290
+ def error(self, log_str):
1291
+ logger.error(self._make_log(log_str))
1292
+
1293
+ def _calc_all_skills(self):
1294
+ """
1295
+ Calculate all skills
1296
+ """
1297
+ self.all_skills = Skillset()
1298
+
1299
+ # Add skills from self.skillkit
1300
+ if self.skillkit and not self.skillkit.isEmpty():
1301
+ self.all_skills.addSkillkit(self.skillkit)
1302
+
1303
+ # Add skills from global_skills
1304
+ if self.global_skills is not None:
1305
+ self.all_skills.addSkillkit(self.global_skills.getAllSkills())
1306
+
1307
+ def _make_log(self, log_str):
1308
+ if len(log_str) < MAX_LOG_LENGTH:
1309
+ return "session[{}] {}".format(
1310
+ self.session_id, log_str.replace("\n", "\\n")
1311
+ )
1312
+ else:
1313
+ log_str = (
1314
+ log_str[: int(MAX_LOG_LENGTH * 1 / 3)]
1315
+ + "..."
1316
+ + log_str[-int(MAX_LOG_LENGTH * 2 / 3) :]
1317
+ )
1318
+ return "session[{}] {}".format(
1319
+ self.session_id, log_str.replace("\n", "\\n")
1320
+ )
1321
+
1322
+ def _export_context_manager_state(self) -> Dict[str, Any]:
1323
+ """Export the complete state of context_manager
1324
+
1325
+ Returns:
1326
+ A dictionary containing information about all buckets
1327
+ """
1328
+ buckets_state = []
1329
+
1330
+ for bucket_name, bucket in self.context_manager.state.buckets.items():
1331
+ # Serializing ContextBucket
1332
+ bucket_data = {
1333
+ "name": bucket_name,
1334
+ "priority": bucket.priority,
1335
+ "allocated_tokens": bucket.allocated_tokens,
1336
+ "message_role": bucket.message_role.value,
1337
+ "is_compressed": bucket.is_compressed,
1338
+ "messages": []
1339
+ }
1340
+
1341
+ # Extract message content
1342
+ if isinstance(bucket.content, Messages):
1343
+ for msg in bucket.content.get_messages():
1344
+ bucket_data["messages"].append({
1345
+ "role": msg.role.value,
1346
+ "content": msg.content,
1347
+ "timestamp": msg.timestamp,
1348
+ "user_id": msg.user_id,
1349
+ "tool_calls": msg.tool_calls,
1350
+ "tool_call_id": msg.tool_call_id,
1351
+ "metadata": msg.metadata,
1352
+ })
1353
+ elif isinstance(bucket.content, str):
1354
+ # If it is a string type, save as a single message
1355
+ bucket_data["messages"].append({
1356
+ "role": bucket.message_role.value,
1357
+ "content": bucket.content,
1358
+ "timestamp": None,
1359
+ "user_id": "",
1360
+ "tool_calls": None,
1361
+ "tool_call_id": None,
1362
+ "metadata": {},
1363
+ })
1364
+
1365
+ buckets_state.append(bucket_data)
1366
+
1367
+ return {
1368
+ "buckets": buckets_state,
1369
+ "layout_policy": self.context_manager.state.layout_policy,
1370
+ "bucket_order": self.context_manager.state.bucket_order,
1371
+ "total_tokens": self.context_manager.state.total_tokens,
1372
+ }
1373
+
1374
+ def export_runtime_state(self, frame_id: str) -> "ContextSnapshot":
1375
+ """Export runtime state as a snapshot"""
1376
+ from dolphin.core.coroutine.context_snapshot import ContextSnapshot
1377
+
1378
+ # Ensure the image is up to date
1379
+ self.sync_messages_from_llm_context(force=True)
1380
+
1381
+ # Export variable state
1382
+ variables = {}
1383
+ for name, var_obj in self.variable_pool.get_all_variables().items():
1384
+ if isinstance(var_obj, VarOutput):
1385
+ # VarOutput knows how to serialize itself into a dict
1386
+ variables[name] = var_obj.to_dict()
1387
+ elif isinstance(var_obj, Var):
1388
+ # It's a simple Var wrapper. We only want to store the value.
1389
+ value = var_obj.value
1390
+ if isinstance(value, Messages):
1391
+ # This is the problematic type. Serialize it.
1392
+ variables[name] = value.to_dict()
1393
+ else:
1394
+ # Assume other values are primitives and store them directly.
1395
+ variables[name] = value
1396
+ # This case is for values stored in the pool without a Var wrapper.
1397
+ elif isinstance(var_obj, Messages):
1398
+ variables[name] = var_obj.get_messages_as_dict()
1399
+ else:
1400
+ variables[name] = var_obj
1401
+
1402
+ # Export message history (export from the synchronized mirror)
1403
+ messages = []
1404
+ for agent_name, agent_messages in self.messages.items():
1405
+ for msg in agent_messages.get_messages():
1406
+ messages.append(
1407
+ {
1408
+ "agent": agent_name,
1409
+ "role": msg.role.value,
1410
+ "content": msg.content,
1411
+ "timestamp": msg.timestamp,
1412
+ "user_id": msg.user_id,
1413
+ "tool_calls": msg.tool_calls,
1414
+ "tool_call_id": msg.tool_call_id,
1415
+ "metadata": msg.metadata,
1416
+ }
1417
+ )
1418
+
1419
+ # Export runtime state
1420
+ runtime_state = {
1421
+ "user_id": self.user_id,
1422
+ "session_id": self.session_id,
1423
+ "cur_agent": self.cur_agent.getName() if self.cur_agent else None,
1424
+ "max_answer_len": self.max_answer_len,
1425
+ }
1426
+
1427
+ # Export skill set status
1428
+ skillkit_state = {}
1429
+ if self.skillkit and not self.skillkit.isEmpty():
1430
+ try:
1431
+ skillkit_state = {
1432
+ "skills_schema": self.skillkit.getSkillsSchema(),
1433
+ "skill_count": len(self.skillkit.getSkills()),
1434
+ }
1435
+ except Exception as e:
1436
+ logger.warning(f"Failed to export skillkit state: {e}")
1437
+
1438
+ # Export the complete state of context_manager (including bucket structure)
1439
+ context_manager_state = self._export_context_manager_state()
1440
+ return ContextSnapshot.create_snapshot(
1441
+ frame_id=frame_id,
1442
+ variables=variables,
1443
+ messages=messages,
1444
+ runtime_state=runtime_state,
1445
+ skillkit_state=skillkit_state,
1446
+ context_manager_state=context_manager_state,
1447
+ )
1448
+
1449
+ def apply_runtime_state(self, snapshot: "ContextSnapshot"):
1450
+ """Restore runtime state from snapshot"""
1451
+ from dolphin.core.context_engineer.config.settings import BuildInBucket
1452
+
1453
+ # Restore variable state
1454
+ self.variable_pool.clear()
1455
+ for name, value in snapshot.variables.items():
1456
+ # Support restoring VarOutput structure from snapshot
1457
+ try:
1458
+ from dolphin.core.context.var_output import VarOutput
1459
+
1460
+ if VarOutput.is_serialized_dict(value):
1461
+ # This is the serialized structure of VarOutput
1462
+ self.variable_pool.set_var(name, VarOutput.from_dict(value))
1463
+ else:
1464
+ # Other simple types are restored by value
1465
+ self.variable_pool.set_var(name, value)
1466
+ except Exception:
1467
+ # Fallback: Set to original value to avoid execution interruption due to restore failure
1468
+ self.variable_pool.set_var(name, value)
1469
+
1470
+ # Restore message history to context_manager (single data source)
1471
+ self.context_manager.state.buckets.clear()
1472
+
1473
+ # Check if there is a complete context_manager_state (new version snapshot)
1474
+ # Note: Even if buckets is an empty list, use the new version restore logic as long as context_manager_state exists
1475
+ # Full restoration: Reconstruct the complete bucket structure from context_manager_state
1476
+ bucket_count = len(snapshot.context_manager_state.get('buckets', []))
1477
+ logger.info(f"Restoring {bucket_count} buckets from context_manager_state")
1478
+
1479
+ for bucket_data in snapshot.context_manager_state['buckets']:
1480
+ # Reconstruct Messages object
1481
+ messages = Messages()
1482
+ for msg_data in bucket_data['messages']:
1483
+ messages.append_message(
1484
+ role=MessageRole(msg_data['role']),
1485
+ content=msg_data['content'],
1486
+ user_id=msg_data.get('user_id', ''),
1487
+ tool_calls=msg_data.get('tool_calls'),
1488
+ tool_call_id=msg_data.get('tool_call_id'),
1489
+ metadata=msg_data.get('metadata', {}),
1490
+ )
1491
+
1492
+ # Reconstruct ContextBucket (preserving original priority and other attributes)
1493
+ if messages.get_messages(): # Only add non-empty messages
1494
+ self.context_manager.add_bucket(
1495
+ bucket_name=bucket_data['name'],
1496
+ content=messages,
1497
+ priority=bucket_data['priority'],
1498
+ allocated_tokens=bucket_data['allocated_tokens'],
1499
+ message_role=MessageRole(bucket_data['message_role']),
1500
+ )
1501
+
1502
+ # Restore bucket_order and layout_policy
1503
+ self.context_manager.state.bucket_order = snapshot.context_manager_state.get('bucket_order', [])
1504
+ self.context_manager.state.layout_policy = snapshot.context_manager_state.get('layout_policy', 'default')
1505
+
1506
+ logger.info("Successfully restored complete bucket structure from snapshot")
1507
+
1508
+ # Synchronize from context_manager to mirror (ensure symmetry)
1509
+ self.messages.clear()
1510
+ self.messages_dirty = True # Force Synchronization
1511
+ self.sync_messages_from_llm_context(force=True)
1512
+
1513
+ # Resume runtime state
1514
+ runtime_state = snapshot.runtime_state
1515
+ if runtime_state:
1516
+ self.user_id = runtime_state.get("user_id")
1517
+ self.session_id = runtime_state.get("session_id")
1518
+ self.max_answer_len = runtime_state.get(
1519
+ "max_answer_len", MAX_ANSWER_CONTENT_LENGTH
1520
+ )
1521
+
1522
+ # Resume the current agent (to be handled in the calling function above)
1523
+ # The restoration of self.cur_agent requires external coordination
1524
+
1525
+ # Verify recovery results
1526
+ bucket_count = len(self.context_manager.state.buckets)
1527
+ total_messages = sum(
1528
+ len(bucket.content.get_messages()) if isinstance(bucket.content, Messages) else 1
1529
+ for bucket in self.context_manager.state.buckets.values()
1530
+ )
1531
+
1532
+ logger.info(
1533
+ f"Restored context state from snapshot {snapshot.snapshot_id}: "
1534
+ f"{bucket_count} buckets, {total_messages} total messages, "
1535
+ f"schema_version={snapshot.schema_version}"
1536
+ )
1537
+
1538
+ # === User Interrupt API ===
1539
+
1540
+ def set_interrupt_event(self, interrupt_event: asyncio.Event) -> None:
1541
+ """Set the user interrupt event (injected by Agent layer).
1542
+
1543
+ Args:
1544
+ interrupt_event: asyncio.Event that will be set when user requests interrupt
1545
+ """
1546
+ self._interrupt_event = interrupt_event
1547
+
1548
+ def get_interrupt_event(self) -> Optional[asyncio.Event]:
1549
+ """Get the user interrupt event.
1550
+
1551
+ Returns:
1552
+ The interrupt event, or None if not set
1553
+ """
1554
+ return self._interrupt_event
1555
+
1556
+ def is_interrupted(self) -> bool:
1557
+ """Check if user has requested an interrupt.
1558
+
1559
+ Returns:
1560
+ True if interrupt event is set, False otherwise
1561
+ """
1562
+ return self._interrupt_event is not None and self._interrupt_event.is_set()
1563
+
1564
+ def check_user_interrupt(self) -> None:
1565
+ """Check user interrupt status and raise exception if interrupted.
1566
+
1567
+ This is the primary checkpoint method to be called at strategic locations
1568
+ during execution (e.g., LLM streaming loop, before skill execution).
1569
+
1570
+ Raises:
1571
+ UserInterrupt: If user has requested interrupt
1572
+ """
1573
+ if self.is_interrupted():
1574
+ from dolphin.core.common.exceptions import UserInterrupt
1575
+ raise UserInterrupt("User interrupted execution")
1576
+
1577
+ def clear_interrupt(self) -> None:
1578
+ """Clear the interrupt status (called when resuming execution)."""
1579
+ if self._interrupt_event is not None:
1580
+ self._interrupt_event.clear()