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,970 @@
1
+ """Base Agent Class Definition
2
+
3
+ Contains the BaseAgent abstract base class and AgentEventListener event listener
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import AsyncGenerator, Any, Dict, Optional, Callable
8
+ from asyncio import Event, Lock
9
+ import asyncio
10
+ from datetime import datetime
11
+
12
+ from dolphin.core.config.global_config import GlobalConfig
13
+ from dolphin.core.logging.logger import get_logger
14
+
15
+ from dolphin.core.agent.agent_state import AgentState, AgentEvent, AgentStatus, PauseType
16
+ from dolphin.core.coroutine.step_result import StepResult
17
+ from dolphin.core.common.exceptions import AgentLifecycleException
18
+
19
+
20
+ class AgentEventListener:
21
+ """Agent Event Listener"""
22
+
23
+ def __init__(self):
24
+ self.listeners: Dict[AgentEvent, list] = {}
25
+ for event in AgentEvent:
26
+ self.listeners[event] = []
27
+ self._logger = get_logger("agent.event_listener")
28
+
29
+ def add_listener(self, event: AgentEvent, callback: Callable):
30
+ """Add event listener"""
31
+ self.listeners[event].append(callback)
32
+
33
+ def remove_listener(self, event: AgentEvent, callback: Callable):
34
+ """Remove event listener"""
35
+ if callback in self.listeners[event]:
36
+ self.listeners[event].remove(callback)
37
+
38
+ async def emit(self, event: AgentEvent, agent: "BaseAgent", data: Any = None):
39
+ """Trigger event (take a snapshot of the listener list to avoid being modified during traversal)"""
40
+ # Use snapshots to avoid affecting the current traversal due to adding or removing listeners in callbacks
41
+ for callback in list(self.listeners[event]):
42
+ try:
43
+ if asyncio.iscoroutinefunction(callback):
44
+ await callback(agent, event, data)
45
+ else:
46
+ callback(agent, event, data)
47
+ except Exception as e:
48
+ self._logger.error(f"Error in event listener for {event}: {e}")
49
+
50
+
51
+ class BaseAgent(ABC):
52
+ """Agent abstract base class, defining lifecycle management interface"""
53
+
54
+ def __init__(
55
+ self,
56
+ name: str,
57
+ description: Optional[str] = None,
58
+ global_config: Optional[GlobalConfig] = None,
59
+ ):
60
+ # ========================================
61
+ # 1. Basic Properties
62
+ # ========================================
63
+ self.name: str = name # Agent name
64
+ self.description: str = description or "" # Agent description
65
+ self.global_config: Optional[GlobalConfig] = global_config # Global configuration
66
+ self.status = AgentStatus(state=AgentState.CREATED) # Agent status object
67
+ self.event_listener = AgentEventListener() # Event listener
68
+ self._logger = get_logger("agent") # Logger instance
69
+
70
+ # ========================================
71
+ # 2. Synchronization Primitives
72
+ # ========================================
73
+ # Locks
74
+ self._state_lock = Lock() # State change lock, ensures atomic state transitions
75
+ self._arun_active_lock = Lock() # arun() reentrancy protection lock
76
+
77
+ # Events - for coroutine communication and cooperative cancellation
78
+ self._pause_event = Event() # Pause signal: clear=paused, set=running
79
+ self._pause_event.set() # Default: not paused
80
+ self._terminate_event = Event() # Terminate signal: set=termination requested
81
+ self._interrupt_event = Event() # User interrupt signal: set=user requests interrupt (new input)
82
+
83
+ # ========================================
84
+ # 3. Coroutine Execution State
85
+ # ========================================
86
+ self._current_frame = None # Current execution frame (ExecutionFrame)
87
+ self._resume_handle = None # Resume handle (ResumeHandle), for resuming from pause point
88
+ self._pause_type: Optional[PauseType] = None # Pause type: MANUAL/TOOL_INTERRUPT/USER_INTERRUPT
89
+ self._arun_active = False # Whether arun() is active, prevents concurrent calls
90
+
91
+ # ========================================
92
+ # 4. User Interrupt Related
93
+ # ========================================
94
+ self._pending_user_input: Optional[str] = None # Pending user input for resume after user interrupt
95
+
96
+ # ========================================
97
+ # 5. State Transition Mapping
98
+ # ========================================
99
+ # Defines valid state transition paths for the Agent finite state machine
100
+ self._valid_transitions = {
101
+ AgentState.CREATED: [
102
+ AgentState.INITIALIZED,
103
+ AgentState.TERMINATED,
104
+ AgentState.ERROR,
105
+ ],
106
+ AgentState.INITIALIZED: [
107
+ AgentState.RUNNING,
108
+ AgentState.TERMINATED,
109
+ AgentState.ERROR,
110
+ ],
111
+ AgentState.RUNNING: [
112
+ AgentState.PAUSED,
113
+ AgentState.COMPLETED,
114
+ AgentState.ERROR,
115
+ AgentState.TERMINATED,
116
+ ],
117
+ AgentState.PAUSED: [AgentState.RUNNING, AgentState.TERMINATED],
118
+ AgentState.COMPLETED: [AgentState.TERMINATED, AgentState.INITIALIZED],
119
+ AgentState.ERROR: [AgentState.TERMINATED, AgentState.INITIALIZED],
120
+ AgentState.TERMINATED: [], # Terminal state, no transitions allowed
121
+ }
122
+
123
+ @property
124
+ def state(self) -> AgentState:
125
+ """Get current status"""
126
+ return self.status.state
127
+
128
+ async def _change_state(
129
+ self, new_state: AgentState, message: str = "", data: Any = None
130
+ ):
131
+ """State change
132
+
133
+ Note: Avoid executing callbacks while holding the lock to prevent potential deadlocks.
134
+ """
135
+ # First calculate the state transition and event type, then trigger the event outside the lock
136
+ async with self._state_lock:
137
+ if new_state not in self._valid_transitions[self.state]:
138
+ raise AgentLifecycleException(
139
+ "INVALID_STATE_TRANSITION",
140
+ f"Cannot transition from {self.state.value} to {new_state.value}",
141
+ )
142
+
143
+ old_state = self.state
144
+ self.status.state = new_state
145
+ self.status.message = message
146
+ self.status.data = data
147
+ self.status.timestamp = datetime.now()
148
+
149
+ self._logger.debug(f"State changed: {old_state.value} -> {new_state.value}")
150
+
151
+ # Select event type (RUNNING: distinguish START from RESUME)
152
+ event_type = None
153
+ if new_state == AgentState.INITIALIZED:
154
+ event_type = AgentEvent.INIT
155
+ elif new_state == AgentState.RUNNING:
156
+ event_type = (
157
+ AgentEvent.RESUME
158
+ if old_state == AgentState.PAUSED
159
+ else AgentEvent.START
160
+ )
161
+ elif new_state == AgentState.PAUSED:
162
+ event_type = AgentEvent.PAUSE
163
+ elif new_state == AgentState.COMPLETED:
164
+ event_type = AgentEvent.COMPLETE
165
+ elif new_state == AgentState.TERMINATED:
166
+ event_type = AgentEvent.TERMINATE
167
+ elif new_state == AgentState.ERROR:
168
+ event_type = AgentEvent.ERROR
169
+
170
+ event_payload = {
171
+ "old_state": old_state,
172
+ "new_state": new_state,
173
+ "message": message,
174
+ "data": data,
175
+ }
176
+
177
+ # Lock external trigger events to avoid deadlock caused by re-acquiring the lock inside callbacks.
178
+ if event_type is not None:
179
+ await self.event_listener.emit(event_type, self, event_payload)
180
+
181
+ async def initialize(self) -> bool:
182
+ """Initialize Agent"""
183
+ try:
184
+ await self._on_initialize()
185
+ await self._change_state(AgentState.INITIALIZED, "Agent initialized")
186
+ return True
187
+ except Exception as e:
188
+ await self._change_state(
189
+ AgentState.ERROR, f"Initialization failed: {str(e)}"
190
+ )
191
+ raise AgentLifecycleException("INIT_FAILED", str(e)) from e
192
+
193
+ @abstractmethod
194
+ async def _on_initialize(self):
195
+ """Initialization logic implemented by subclasses"""
196
+ pass
197
+
198
+ def run(self, **kwargs) -> Any:
199
+ """Run Agent synchronously
200
+
201
+ Note: Do not call within an asynchronous context that already has an event loop.
202
+
203
+ Args:
204
+ agent (Agent): The agent to run.
205
+ messages (List[Message]): The messages to process.
206
+ tools (Optional[List[Tool]]): The tools to use.
207
+ max_turns (int): The maximum number of turns to run.
208
+ stream (bool): Whether to stream the response.
209
+ tool_choice (Optional[str]): The tool choice to use.
210
+
211
+ Returns:
212
+ Message: The final message.
213
+ """
214
+ try:
215
+ # If a running event loop currently exists, an exception will be raised to prompt the user to use the asynchronous interface instead.
216
+ asyncio.get_running_loop()
217
+ raise AgentLifecycleException(
218
+ "SYNC_RUN_IN_ASYNC",
219
+ "run() cannot be called from an async context; use 'async for ... in arun(...)' or 'await _run_sync(...)'",
220
+ )
221
+ except RuntimeError:
222
+ # No running event loop, safe to use asyncio.run
223
+ return asyncio.run(self._run_sync(**kwargs))
224
+
225
+ async def _run_sync(self, **kwargs) -> Any:
226
+ """Synchronous wrapper"""
227
+ last_result = None
228
+ async for result in self.arun(**kwargs):
229
+ last_result = result
230
+ return last_result
231
+
232
+ async def arun(self, run_mode: bool = True, **kwargs) -> AsyncGenerator[Any, None]:
233
+ """Run the Agent asynchronously (implemented using coroutine series methods)
234
+
235
+ This method executes step-by-step using coroutine series methods, supporting:
236
+ - Pause/resume
237
+ - Automatic handling of ToolInterrupt
238
+ - State synchronization
239
+
240
+ Args:
241
+ run_mode (bool):
242
+ True (default) uses "fast mode", running all the way to a tool interrupt or completion in one go, saving snapshots only at these two points.
243
+ False uses "step mode", executing step by step, advancing one block per step.
244
+ """
245
+ # 0. Reentrant protection: Prevent multiple concurrent arun() calls
246
+ async with self._arun_active_lock:
247
+ if self._arun_active:
248
+ raise AgentLifecycleException(
249
+ "ALREADY_RUNNING",
250
+ "Cannot call arun() while agent is already running",
251
+ )
252
+ self._arun_active = True
253
+
254
+ try:
255
+ # 1. Check and initialize
256
+ if self.state not in [
257
+ AgentState.INITIALIZED,
258
+ AgentState.PAUSED,
259
+ AgentState.RUNNING,
260
+ ]:
261
+ if self.state == AgentState.CREATED:
262
+ await self.initialize()
263
+ elif self.state == AgentState.COMPLETED:
264
+ # Allow re-running completed agents: clean up execution-related states before state transitions.
265
+ try:
266
+ if self._current_frame is not None:
267
+ await self._on_terminate_coroutine()
268
+ except Exception:
269
+ # Executing frame termination failure should not block rerunning
270
+ pass
271
+ # Clean up old execution contexts
272
+ self._current_frame = None
273
+ self._resume_handle = None
274
+ self._pause_type = None
275
+ await self._change_state(
276
+ AgentState.INITIALIZED, "Agent reinitialized for new run"
277
+ )
278
+ else:
279
+ # ERROR or TERMINATED states do not allow execution
280
+ raise AgentLifecycleException(
281
+ "INVALID_STATE",
282
+ f"Agent cannot run from state {self.state.value}",
283
+ )
284
+
285
+ # 2. Restore/Continue Logic
286
+ if self.state == AgentState.PAUSED and self._current_frame is not None:
287
+ # For scenarios with separate pause handling for resume handles: tool interruption vs manual pause
288
+ if self._resume_handle is not None:
289
+ if self._pause_type == PauseType.TOOL_INTERRUPT:
290
+ raise AgentLifecycleException(
291
+ "NEED_RESUME",
292
+ "Agent paused due to tool interrupt; call resume() with updates before arun()",
293
+ )
294
+ else:
295
+ # Manual pause or User Interrupt: auto-resume handler and continue (no external explicit resume required)
296
+ self._logger.debug(
297
+ f"Manual pause/interrupt detected in arun() (type={self._pause_type.value if self._pause_type else 'None'}); auto-resuming"
298
+ )
299
+ # Prepare updates if it was a user interrupt with pending input
300
+ updates = None
301
+ if self._pause_type == PauseType.USER_INTERRUPT and self._pending_user_input:
302
+ updates = {"__user_interrupt_input__": self._pending_user_input}
303
+ self._pending_user_input = None # Consume it
304
+
305
+ self._current_frame = await self._on_resume_coroutine(updates)
306
+ self._resume_handle = None
307
+ self._pause_type = None
308
+ self._pause_event.set()
309
+ await self._change_state(
310
+ AgentState.RUNNING, "Agent auto-resumed from manual pause"
311
+ )
312
+ await self._on_resume()
313
+ else:
314
+ # When paused but no resume handle has been generated (very early pause), it can be continued directly.
315
+ self._pause_event.set()
316
+ await self._change_state(
317
+ AgentState.RUNNING, "Agent resumed from pause"
318
+ )
319
+ elif self.state == AgentState.RUNNING and self._current_frame is not None:
320
+ # Already running, ensure not paused
321
+ self._pause_event.set()
322
+ elif self.state == AgentState.RUNNING and self._current_frame is None:
323
+ # RUNNING but no executing frame: state unsynchronized, performing self-healing restart (START event will not be triggered again)
324
+ self._logger.error(
325
+ "Agent in RUNNING state but no frame; restarting coroutine without state change"
326
+ )
327
+ self._terminate_event.clear()
328
+ self._pause_event.set()
329
+ self._current_frame = await self._on_start_coroutine(**kwargs)
330
+ else:
331
+ # 3. Start coroutine execution
332
+ self._terminate_event.clear()
333
+ self._interrupt_event.clear()
334
+ self._pause_event.set()
335
+
336
+ await self._change_state(AgentState.RUNNING, "Agent started execution")
337
+
338
+ # Call the subclass-implemented start method
339
+ self._current_frame = await self._on_start_coroutine(**kwargs)
340
+
341
+ # 4. Execution
342
+
343
+ if run_mode:
344
+ # Fast mode: Run once until interrupt/completion
345
+ # Prefer the subclass-implemented _on_run_coroutine, otherwise fall back to step-by-step advancement
346
+ if hasattr(self, "_on_run_coroutine") and callable(
347
+ getattr(self, "_on_run_coroutine")
348
+ ):
349
+ # Respect pause/terminate signals before entering execution
350
+ await self._pause_event.wait()
351
+ if self._terminate_event.is_set():
352
+ await self._on_terminate_coroutine()
353
+ return
354
+
355
+ # Use wrappers to improve responsiveness to terminate
356
+ run_result = await self._run_with_terminate_checks()
357
+ # A termination signal may be triggered during execution: perform a quick backoff once more here
358
+ if self._terminate_event.is_set():
359
+ await self._on_terminate_coroutine()
360
+ return
361
+ else:
362
+ # Back off: Step forward using step mode until an interrupt or completion is encountered (increase protection upper limit to avoid accidental infinite loops)
363
+ run_result = None
364
+ step_count = 0
365
+ max_steps = 1000
366
+ while True:
367
+ step_count += 1
368
+ if step_count > max_steps:
369
+ self._logger.error(
370
+ f"Exceeded max steps ({max_steps}) in fallback run mode"
371
+ )
372
+ raise AgentLifecycleException(
373
+ "MAX_STEPS_EXCEEDED",
374
+ f"Agent exceeded {max_steps} steps without completion or interrupt",
375
+ )
376
+ await self._pause_event.wait()
377
+ if self._terminate_event.is_set():
378
+ await self._on_terminate_coroutine()
379
+ return
380
+ step_result = await self._on_step_coroutine()
381
+ # New API using StepResult
382
+ if step_result.is_interrupted or step_result.is_completed:
383
+ run_result = step_result
384
+ break
385
+
386
+ # Unified Processing Results
387
+ if run_result is None:
388
+ self._logger.warning(
389
+ "run_result is None, likely due to termination during execution"
390
+ )
391
+ # Termination has already been handled in _run_with_terminate_checks or the upper layer, return directly
392
+ return
393
+
394
+ if run_result.is_interrupted:
395
+ self._resume_handle = run_result.resume_handle
396
+
397
+ # 统一使用 "interrupted" 状态,通过 interrupt_type 区分类型
398
+ if run_result.is_user_interrupted:
399
+ self._pause_type = PauseType.USER_INTERRUPT
400
+ await self._change_state(
401
+ AgentState.PAUSED, "Agent paused due to user interrupt"
402
+ )
403
+ else:
404
+ self._pause_type = PauseType.TOOL_INTERRUPT
405
+ await self._change_state(
406
+ AgentState.PAUSED, "Agent paused due to tool interrupt"
407
+ )
408
+
409
+ # 统一输出格式:status 固定为 "interrupted",通过 interrupt_type 区分
410
+ yield {
411
+ "status": "interrupted",
412
+ "handle": run_result.resume_handle,
413
+ "interrupt_type": run_result.resume_handle.interrupt_type if run_result.resume_handle else self._pause_type.value,
414
+ }
415
+ return
416
+
417
+ elif run_result.is_completed:
418
+ await self._change_state(
419
+ AgentState.COMPLETED, "Agent completed execution"
420
+ )
421
+ yield run_result.result or {"status": "completed"}
422
+ return
423
+
424
+ else:
425
+ # True exceptional cases: return value type does not match expectations
426
+ self._logger.error(
427
+ f"Unexpected run_result type: {type(run_result)}, value: {run_result}"
428
+ )
429
+ raise AgentLifecycleException(
430
+ "UNEXPECTED_STATE",
431
+ f"Unexpected run_result type: {type(run_result)}",
432
+ )
433
+
434
+ else:
435
+ # Stepping mode: Maintain original fine-grained progression and step-by-step output
436
+ while True:
437
+ # Check whether pause is needed
438
+ await self._pause_event.wait()
439
+
440
+ # Check whether termination is needed
441
+ if self._terminate_event.is_set():
442
+ await self._on_terminate_coroutine()
443
+ break
444
+
445
+ # Execute one step
446
+ step_result = await self._on_step_coroutine()
447
+
448
+ # 5. Processing Step Results
449
+ if step_result.is_interrupted:
450
+ self._resume_handle = step_result.resume_handle
451
+
452
+ # 统一使用 "interrupted" 状态,通过 interrupt_type 区分类型
453
+ if step_result.is_user_interrupted:
454
+ self._pause_type = PauseType.USER_INTERRUPT
455
+ await self._change_state(
456
+ AgentState.PAUSED, "Agent paused due to user interrupt"
457
+ )
458
+ else:
459
+ # ToolInterrupt: Automatically pause and save resume handle
460
+ self._pause_type = PauseType.TOOL_INTERRUPT
461
+ await self._change_state(
462
+ AgentState.PAUSED, "Agent paused due to tool interrupt"
463
+ )
464
+
465
+ # 统一输出格式
466
+ yield {
467
+ "status": "interrupted",
468
+ "handle": step_result.resume_handle,
469
+ "interrupt_type": step_result.resume_handle.interrupt_type if step_result.resume_handle else self._pause_type.value,
470
+ }
471
+ break
472
+
473
+ elif step_result.is_completed:
474
+ # Execution completed, including actual results
475
+ await self._change_state(
476
+ AgentState.COMPLETED, "Agent completed execution"
477
+ )
478
+ # Generate final result - return the actual execution result
479
+ yield step_result.result or {"status": "completed"}
480
+ break
481
+
482
+ else:
483
+ # Continue execution, producing intermediate results
484
+ yield {"status": "running", "step_result": step_result}
485
+
486
+ except Exception as e:
487
+ # If the exception is UserInterrupt, it might have been raised during state transition or context check
488
+ # We should not transition to ERROR if it's a controlled interrupt
489
+ if isinstance(e, AgentLifecycleException) and e.code == "NEED_RESUME":
490
+ raise
491
+
492
+ # If agent is already terminated, do not wipe state with ERROR
493
+ if self.state == AgentState.TERMINATED:
494
+ self._logger.debug(f"Exception during termination (ignored): {e}")
495
+ raise
496
+
497
+ # If agent is paused (e.g. interrupt happened), do not transition to ERROR
498
+ if self.state == AgentState.PAUSED:
499
+ self._logger.debug(f"Exception while paused (ignored for ERROR state): {e}")
500
+ # Re-raise to let the caller handle it (e.g. runner loop catching UserInterrupt)
501
+ raise
502
+
503
+ await self._change_state(AgentState.ERROR, f"Execution failed: {str(e)}")
504
+ if isinstance(e, AgentLifecycleException):
505
+ raise
506
+ raise AgentLifecycleException("EXECUTION_FAILED", str(e)) from e
507
+ finally:
508
+ # Clean up arun reentrancy flag
509
+ async with self._arun_active_lock:
510
+ self._arun_active = False
511
+
512
+ async def _run_with_terminate_checks(self):
513
+ """wrap _on_run_coroutine(): improve responsiveness to terminate in fast mode.
514
+
515
+ 注意:
516
+ - 目前实践表明,额外创建任务并轮询 terminate_event 容易引入复杂竞态,
517
+ 尤其是在执行器已经正常完成时,可能导致外层感知不到完成信号。
518
+ - 因此这里简化为:若未收到 terminate 信号,直接 await _on_run_coroutine()。
519
+ 仍保留对外部 CancelledError 的处理,以兼容 Ctrl+C 等外部中断。
520
+ """
521
+ from dolphin.core.logging.logger import console
522
+
523
+ try:
524
+ # 若在进入前就已收到 terminate 信号,直接返回 None 交由上层处理
525
+ if self._terminate_event.is_set():
526
+ return None
527
+
528
+ run_coro = getattr(self, "_on_run_coroutine")
529
+ return await run_coro()
530
+ except asyncio.CancelledError:
531
+ # External cancellation: return None for upper layer to handle as termination
532
+ self._logger.debug("_run_with_terminate_checks cancelled externally")
533
+ return None
534
+
535
+ async def _on_execute(self, **kwargs) -> AsyncGenerator[Any, None]:
536
+ """[Deprecated] Old execution interface.
537
+
538
+ Please use coroutine interfaces: `arun()`/`step()`/`start_coroutine()`.
539
+ This method is no longer called by BaseAgent, retained only for a few legacy call chains.
540
+ """
541
+ raise AgentLifecycleException(
542
+ "DEPRECATED_API",
543
+ "_on_execute() is deprecated; use coroutine APIs (arun/step/start_coroutine).",
544
+ )
545
+
546
+ async def pause(self) -> bool:
547
+ """Pause Agent (based on coroutine)"""
548
+ if self.state != AgentState.RUNNING:
549
+ raise AgentLifecycleException(
550
+ "INVALID_STATE", f"Cannot pause agent from state {self.state.value}"
551
+ )
552
+
553
+ try:
554
+ # Pause coroutine execution
555
+ if self._current_frame is not None:
556
+ self._resume_handle = await self._on_pause_coroutine()
557
+ self._pause_type = PauseType.MANUAL
558
+
559
+ self._pause_event.clear()
560
+ await self._change_state(AgentState.PAUSED, "Agent paused")
561
+ await self._on_pause()
562
+ return True
563
+ except Exception as e:
564
+ raise AgentLifecycleException("PAUSE_FAILED", str(e)) from e
565
+
566
+ @abstractmethod
567
+ async def _on_pause(self):
568
+ """Pause logic implemented by subclasses"""
569
+ pass
570
+
571
+ async def resume(self, updates: Optional[Dict[str, Any]] = None) -> bool:
572
+ """Resume Agent (based on coroutine)
573
+
574
+ Args:
575
+ updates: Variable updates to inject (used to resume from tool interruption)
576
+ """
577
+ if self.state != AgentState.PAUSED:
578
+ raise AgentLifecycleException(
579
+ "INVALID_STATE", f"Cannot resume agent from state {self.state.value}"
580
+ )
581
+
582
+ try:
583
+ # Resume coroutine execution
584
+ if self._resume_handle is not None:
585
+ self._current_frame = await self._on_resume_coroutine(updates)
586
+ self._resume_handle = None
587
+ self._pause_type = None
588
+
589
+ self._pause_event.set()
590
+ await self._change_state(AgentState.RUNNING, "Agent resumed")
591
+ await self._on_resume()
592
+ return True
593
+ except Exception as e:
594
+ raise AgentLifecycleException("RESUME_FAILED", str(e)) from e
595
+
596
+ @abstractmethod
597
+ async def _on_resume(self):
598
+ """Recovery logic implemented by subclasses"""
599
+ pass
600
+
601
+ async def terminate(self) -> bool:
602
+ """Terminate Agent"""
603
+ if self.state == AgentState.TERMINATED:
604
+ return True
605
+
606
+ try:
607
+ # Set termination flag
608
+ self._terminate_event.set()
609
+ self._pause_event.set() # Ensure that pausing will not cause blocking
610
+ # Forcefully terminate coroutine execution frame (even outside an arun loop)
611
+ try:
612
+ await self._on_terminate_coroutine()
613
+ except Exception:
614
+ # Termination frame failure does not affect the overall termination process
615
+ pass
616
+
617
+ await self._change_state(AgentState.TERMINATED, "Agent terminated")
618
+ await self._on_terminate()
619
+ # Clean up the executing state to avoid external references holding expired references.
620
+ self._current_frame = None
621
+ self._resume_handle = None
622
+ self._pause_type = None
623
+ return True
624
+ except Exception as e:
625
+ raise AgentLifecycleException("TERMINATE_FAILED", str(e)) from e
626
+
627
+ async def interrupt(self) -> bool:
628
+ """User-initiated interrupt to provide new input.
629
+
630
+ Unlike pause() which expects resumption from breakpoint, interrupt() signals
631
+ that the user wants to provide new instructions and expects the agent to
632
+ re-reason with the new context.
633
+
634
+ Returns:
635
+ True if interrupt was successfully initiated
636
+
637
+ Note:
638
+ This method now works in any state (not just RUNNING) to support
639
+ interrupt signals arriving during state transitions. The interrupt
640
+ event will be set regardless, allowing the next checkpoint to catch it.
641
+ """
642
+ if self.state != AgentState.RUNNING:
643
+ self._logger.warning(
644
+ f"Interrupt requested for agent {self.name} in {self.state.value} state "
645
+ f"(expected RUNNING). Setting interrupt event anyway."
646
+ )
647
+
648
+ self._logger.info(f"User interrupt requested for agent {self.name}")
649
+ self._interrupt_event.set()
650
+ return True
651
+
652
+ async def resume_with_input(self, user_input: Optional[str] = None) -> bool:
653
+ """Resume execution after user interrupt, optionally with new input.
654
+
655
+ This method is called after interrupt() to resume execution. If user_input
656
+ is provided, it will be added to the context before resuming, triggering
657
+ re-reasoning. If None, execution continues from the breakpoint.
658
+
659
+ Args:
660
+ user_input: New user instruction/message to add to context.
661
+ None means continue without new input.
662
+
663
+ Returns:
664
+ True if resume was successful
665
+
666
+ Raises:
667
+ AgentLifecycleException: If agent is not in PAUSED state or pause type
668
+ is not 'user_interrupt'
669
+ """
670
+ if self.state != AgentState.PAUSED:
671
+ raise AgentLifecycleException(
672
+ "INVALID_STATE",
673
+ f"Cannot resume agent in {self.state.value} state, must be PAUSED",
674
+ )
675
+
676
+ if self._pause_type != PauseType.USER_INTERRUPT:
677
+ raise AgentLifecycleException(
678
+ "INVALID_PAUSE_TYPE",
679
+ f"resume_with_input() requires pause_type=USER_INTERRUPT, "
680
+ f"got '{self._pause_type}'. Use resume() for tool interrupts.",
681
+ )
682
+
683
+ self._pending_user_input = user_input
684
+ self._logger.info(
685
+ f"Resume with input prepared, input={'provided' if user_input else 'none'}"
686
+ )
687
+
688
+ # Clear interrupt event to allow continued execution
689
+ self._interrupt_event.clear()
690
+
691
+ # NOTE: We do not change state to RUNNING here.
692
+ # arun() will detect PAUSED state and the presence of _pending_user_input
693
+ # to correctly resume the coroutine frame with updates.
694
+
695
+ await self._on_resume()
696
+ return True
697
+
698
+ def get_interrupt_event(self) -> Event:
699
+ """Get the interrupt event for injection into context.
700
+
701
+ This allows the Context layer to check for interrupts during execution.
702
+
703
+ Returns:
704
+ The asyncio.Event used for interrupt signaling
705
+ """
706
+ return self._interrupt_event
707
+
708
+ def clear_interrupt(self) -> None:
709
+ """Clear the user interrupt state."""
710
+ self._interrupt_event.clear()
711
+ if hasattr(self, "executor") and self.executor and self.executor.context:
712
+ self.executor.context.clear_interrupt()
713
+ self._logger.debug(f"Interrupt state cleared for agent {self.name}")
714
+ async def reset_for_retry(self) -> bool:
715
+ """Reset the Agent state to retry, avoiding ALREADY_RUNNING errors.
716
+
717
+ This method clears the execution state without changing the agent's basic configuration,
718
+ allowing the agent to restart execution without concurrent call errors.
719
+ """
720
+ try:
721
+ # If the agent is running, terminate it first.
722
+ if self.state == AgentState.RUNNING:
723
+ await self.terminate()
724
+
725
+ # If the agent is in an error or terminated state, clean up the state and reinitialize.
726
+ if self.state in [AgentState.ERROR, AgentState.TERMINATED]:
727
+ # Clean up all execution-related states
728
+ self._current_frame = None
729
+ self._resume_handle = None
730
+ self._pause_type = None
731
+
732
+ # Reset arun activity flag
733
+ async with self._arun_active_lock:
734
+ self._arun_active = False
735
+
736
+ # Reset event status
737
+ self._terminate_event.clear()
738
+ self._pause_event.set()
739
+
740
+ # Reset to initialization state
741
+ await self._change_state(
742
+ AgentState.INITIALIZED,
743
+ "Agent reset for retry"
744
+ )
745
+
746
+ return True
747
+ except Exception as e:
748
+ # If the reset fails, at least attempt to reset the arun activity flag
749
+ try:
750
+ async with self._arun_active_lock:
751
+ self._arun_active = False
752
+ except:
753
+ pass
754
+ raise AgentLifecycleException("RESET_FAILED", str(e)) from e
755
+
756
+ @abstractmethod
757
+ async def _on_terminate(self):
758
+ """Termination logic implemented by subclasses"""
759
+ pass
760
+
761
+ # === Coroutine Series Abstract Methods ===
762
+ @abstractmethod
763
+ async def _on_start_coroutine(self, **kwargs):
764
+ """Subclass implementation: Start coroutine execution
765
+
766
+ Returns:
767
+ ExecutionFrame: Execution frame object
768
+ """
769
+ pass
770
+
771
+ @abstractmethod
772
+ async def _on_step_coroutine(self) -> StepResult:
773
+ """Subclass implementation: Execute one step
774
+
775
+ Returns:
776
+ StepResult: Step execution result
777
+ """
778
+ pass
779
+
780
+ @abstractmethod
781
+ async def _on_pause_coroutine(self):
782
+ """Subclass implementation: pause coroutine
783
+
784
+ Returns:
785
+ ResumeHandle: resume handle
786
+ """
787
+ pass
788
+
789
+ @abstractmethod
790
+ async def _on_resume_coroutine(self, updates: Optional[Dict[str, Any]] = None):
791
+ """Subclass implementation: resume coroutine
792
+
793
+ Args:
794
+ updates: variable updates to inject
795
+
796
+ Returns:
797
+ ExecutionFrame: the resumed execution frame
798
+ """
799
+ pass
800
+
801
+ @abstractmethod
802
+ async def _on_terminate_coroutine(self):
803
+ """Subclass implementation: Terminating coroutines"""
804
+ pass
805
+
806
+ def get_status(self) -> AgentStatus:
807
+ """Get Agent status"""
808
+ return self.status
809
+
810
+ def get_name(self) -> str:
811
+ return self.name
812
+
813
+ # Backward-compatible legacy API aliases
814
+ def getName(self) -> str:
815
+ return self.get_name()
816
+
817
+ def get_description(self) -> str:
818
+ """Get Agent Description"""
819
+ return self.description
820
+
821
+ def set_description(self, description: str):
822
+ """Set Agent Description"""
823
+ self.description = description
824
+
825
+ def add_event_listener(self, event: AgentEvent, callback: Callable):
826
+ """Add event listener"""
827
+ self.event_listener.add_listener(event, callback)
828
+
829
+ def remove_event_listener(self, event: AgentEvent, callback: Callable):
830
+ """Remove event listener"""
831
+ self.event_listener.remove_listener(event, callback)
832
+
833
+ def is_running(self) -> bool:
834
+ """Check if running"""
835
+ return self.state == AgentState.RUNNING
836
+
837
+ def is_paused(self) -> bool:
838
+ """Check if paused"""
839
+ return self.state == AgentState.PAUSED
840
+
841
+ def is_completed(self) -> bool:
842
+ """Check if completed"""
843
+ return self.state == AgentState.COMPLETED
844
+
845
+ def is_terminated(self) -> bool:
846
+ """Check if terminated"""
847
+ return self.state == AgentState.TERMINATED
848
+
849
+ def get_resume_handle(self):
850
+ """Get recovery handle (for resuming after tool interruption)"""
851
+ return self._resume_handle
852
+
853
+ def get_current_frame(self):
854
+ """Get the current executing frame"""
855
+ return self._current_frame
856
+
857
+ async def step(self):
858
+ """Execute one step (single-step execution)
859
+
860
+ This is a convenient method for single-step execution in coroutine mode.
861
+ The first call will automatically start the coroutine, and subsequent calls will advance the execution.
862
+
863
+ Returns:
864
+ bool or ResumeHandle or dict:
865
+ - ResumeHandle: encountered tool interruption
866
+ - dict: execution result (containing completed and result)
867
+ - bool: True indicates completion, False indicates continuation
868
+ """
869
+ # Concurrency protection: prevents calling step() while arun() is running.
870
+ if self._arun_active:
871
+ raise AgentLifecycleException(
872
+ "CONCURRENT_EXECUTION", "Cannot call step() while arun() is active"
873
+ )
874
+
875
+ # If not yet initialized, initialize first
876
+ if self.state == AgentState.CREATED:
877
+ await self.initialize()
878
+
879
+ # If it is in a paused state, handle the resume semantics
880
+ if self.state == AgentState.PAUSED:
881
+ if self._resume_handle is not None:
882
+ if self._pause_type == PauseType.TOOL_INTERRUPT:
883
+ raise AgentLifecycleException(
884
+ "NEED_RESUME",
885
+ "Agent paused due to tool interrupt; call resume() with updates before step()",
886
+ )
887
+ else:
888
+ # Manual pause: auto-resume handler and continue (no external explicit resume required)
889
+ self._logger.debug("Manual pause detected in step(); auto-resuming")
890
+ self._current_frame = await self._on_resume_coroutine(None)
891
+ self._resume_handle = None
892
+ self._pause_type = None
893
+ self._pause_event.set()
894
+ await self._change_state(
895
+ AgentState.RUNNING,
896
+ "Agent auto-resumed from manual pause via step()",
897
+ )
898
+ await self._on_resume()
899
+ else:
900
+ # Only when no resume handle is generated during suspension (early suspension) is it allowed to continue execution directly via step()
901
+ self._pause_event.set()
902
+ await self._change_state(AgentState.RUNNING, "Agent resumed via step()")
903
+
904
+ # If the coroutine has not been started yet, start it first.
905
+ if self._current_frame is None and self.state == AgentState.INITIALIZED:
906
+ self._current_frame = await self._on_start_coroutine()
907
+ await self._change_state(AgentState.RUNNING, "Agent started via step()")
908
+
909
+ # If the agent has completed but needs to be re-executed, clean up the state and restart.
910
+ if self.state == AgentState.COMPLETED:
911
+ # First terminate the current coroutine and clean up the state
912
+ try:
913
+ if self._current_frame is not None:
914
+ await self._on_terminate_coroutine()
915
+ except Exception:
916
+ # Termination failure does not affect restart
917
+ pass
918
+ # Clean up the execution context
919
+ self._current_frame = None
920
+ self._resume_handle = None
921
+ self._pause_type = None
922
+ # Transition the state back to INITIALIZED and restart it.
923
+ await self._change_state(
924
+ AgentState.INITIALIZED, "Agent reinitialized for new run"
925
+ )
926
+ # Restart coroutine
927
+ self._current_frame = await self._on_start_coroutine()
928
+ await self._change_state(AgentState.RUNNING, "Agent restarted via step()")
929
+
930
+ # If in RUNNING state but no executing frame, it indicates a state mismatch — log error and self-healing restart
931
+ if self.state == AgentState.RUNNING and self._current_frame is None:
932
+ self._logger.error(
933
+ "Agent in RUNNING state but no frame; restarting coroutine via step()"
934
+ )
935
+ self._terminate_event.clear()
936
+ self._pause_event.set()
937
+ self._current_frame = await self._on_start_coroutine()
938
+
939
+ # Execute one step and synchronize states
940
+ if self.state == AgentState.RUNNING:
941
+ step_result = await self._on_step_coroutine()
942
+
943
+ # Process step results and synchronize status
944
+ if step_result.is_interrupted:
945
+ # Tool interruption: record handle and switch to pause
946
+ self._resume_handle = step_result.resume_handle
947
+ self._pause_type = PauseType.TOOL_INTERRUPT
948
+ await self._change_state(
949
+ AgentState.PAUSED, "Agent paused due to tool interrupt"
950
+ )
951
+ return step_result
952
+
953
+ if step_result.is_completed:
954
+ await self._change_state(
955
+ AgentState.COMPLETED, "Agent completed execution"
956
+ )
957
+ return step_result
958
+
959
+ # Not completed, continue running
960
+ return step_result
961
+
962
+ # Other illegal states (such as ERROR/TERMINATED) do not allow step-by-step execution
963
+ raise AgentLifecycleException(
964
+ "INVALID_STATE", f"Cannot step agent from state {self.state.value}"
965
+ )
966
+
967
+ def __str__(self) -> str:
968
+ if self.description:
969
+ return f"BaseAgent(name={self.name}, description='{self.description}', state={self.state.value})"
970
+ return f"BaseAgent(name={self.name}, state={self.state.value})"