kweaver-dolphin 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DolphinLanguageSDK/__init__.py +58 -0
- dolphin/__init__.py +62 -0
- dolphin/cli/__init__.py +20 -0
- dolphin/cli/args/__init__.py +9 -0
- dolphin/cli/args/parser.py +567 -0
- dolphin/cli/builtin_agents/__init__.py +22 -0
- dolphin/cli/commands/__init__.py +4 -0
- dolphin/cli/interrupt/__init__.py +8 -0
- dolphin/cli/interrupt/handler.py +205 -0
- dolphin/cli/interrupt/keyboard.py +82 -0
- dolphin/cli/main.py +49 -0
- dolphin/cli/multimodal/__init__.py +34 -0
- dolphin/cli/multimodal/clipboard.py +327 -0
- dolphin/cli/multimodal/handler.py +249 -0
- dolphin/cli/multimodal/image_processor.py +214 -0
- dolphin/cli/multimodal/input_parser.py +149 -0
- dolphin/cli/runner/__init__.py +8 -0
- dolphin/cli/runner/runner.py +989 -0
- dolphin/cli/ui/__init__.py +10 -0
- dolphin/cli/ui/console.py +2795 -0
- dolphin/cli/ui/input.py +340 -0
- dolphin/cli/ui/layout.py +425 -0
- dolphin/cli/ui/stream_renderer.py +302 -0
- dolphin/cli/utils/__init__.py +8 -0
- dolphin/cli/utils/helpers.py +135 -0
- dolphin/cli/utils/version.py +49 -0
- dolphin/core/__init__.py +107 -0
- dolphin/core/agent/__init__.py +10 -0
- dolphin/core/agent/agent_state.py +69 -0
- dolphin/core/agent/base_agent.py +970 -0
- dolphin/core/code_block/__init__.py +0 -0
- dolphin/core/code_block/agent_init_block.py +0 -0
- dolphin/core/code_block/assign_block.py +98 -0
- dolphin/core/code_block/basic_code_block.py +1865 -0
- dolphin/core/code_block/explore_block.py +1327 -0
- dolphin/core/code_block/explore_block_v2.py +712 -0
- dolphin/core/code_block/explore_strategy.py +672 -0
- dolphin/core/code_block/judge_block.py +220 -0
- dolphin/core/code_block/prompt_block.py +32 -0
- dolphin/core/code_block/skill_call_deduplicator.py +291 -0
- dolphin/core/code_block/tool_block.py +129 -0
- dolphin/core/common/__init__.py +17 -0
- dolphin/core/common/constants.py +176 -0
- dolphin/core/common/enums.py +1173 -0
- dolphin/core/common/exceptions.py +133 -0
- dolphin/core/common/multimodal.py +539 -0
- dolphin/core/common/object_type.py +165 -0
- dolphin/core/common/output_format.py +432 -0
- dolphin/core/common/types.py +36 -0
- dolphin/core/config/__init__.py +16 -0
- dolphin/core/config/global_config.py +1289 -0
- dolphin/core/config/ontology_config.py +133 -0
- dolphin/core/context/__init__.py +12 -0
- dolphin/core/context/context.py +1580 -0
- dolphin/core/context/context_manager.py +161 -0
- dolphin/core/context/var_output.py +82 -0
- dolphin/core/context/variable_pool.py +356 -0
- dolphin/core/context_engineer/__init__.py +41 -0
- dolphin/core/context_engineer/config/__init__.py +5 -0
- dolphin/core/context_engineer/config/settings.py +402 -0
- dolphin/core/context_engineer/core/__init__.py +7 -0
- dolphin/core/context_engineer/core/budget_manager.py +327 -0
- dolphin/core/context_engineer/core/context_assembler.py +583 -0
- dolphin/core/context_engineer/core/context_manager.py +637 -0
- dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
- dolphin/core/context_engineer/example/incremental_example.py +267 -0
- dolphin/core/context_engineer/example/traditional_example.py +334 -0
- dolphin/core/context_engineer/services/__init__.py +5 -0
- dolphin/core/context_engineer/services/compressor.py +399 -0
- dolphin/core/context_engineer/utils/__init__.py +6 -0
- dolphin/core/context_engineer/utils/context_utils.py +441 -0
- dolphin/core/context_engineer/utils/message_formatter.py +270 -0
- dolphin/core/context_engineer/utils/token_utils.py +139 -0
- dolphin/core/coroutine/__init__.py +15 -0
- dolphin/core/coroutine/context_snapshot.py +154 -0
- dolphin/core/coroutine/context_snapshot_profile.py +922 -0
- dolphin/core/coroutine/context_snapshot_store.py +268 -0
- dolphin/core/coroutine/execution_frame.py +145 -0
- dolphin/core/coroutine/execution_state_registry.py +161 -0
- dolphin/core/coroutine/resume_handle.py +101 -0
- dolphin/core/coroutine/step_result.py +101 -0
- dolphin/core/executor/__init__.py +18 -0
- dolphin/core/executor/debug_controller.py +630 -0
- dolphin/core/executor/dolphin_executor.py +1063 -0
- dolphin/core/executor/executor.py +624 -0
- dolphin/core/flags/__init__.py +27 -0
- dolphin/core/flags/definitions.py +49 -0
- dolphin/core/flags/manager.py +113 -0
- dolphin/core/hook/__init__.py +95 -0
- dolphin/core/hook/expression_evaluator.py +499 -0
- dolphin/core/hook/hook_dispatcher.py +380 -0
- dolphin/core/hook/hook_types.py +248 -0
- dolphin/core/hook/isolated_variable_pool.py +284 -0
- dolphin/core/interfaces.py +53 -0
- dolphin/core/llm/__init__.py +0 -0
- dolphin/core/llm/llm.py +495 -0
- dolphin/core/llm/llm_call.py +100 -0
- dolphin/core/llm/llm_client.py +1285 -0
- dolphin/core/llm/message_sanitizer.py +120 -0
- dolphin/core/logging/__init__.py +20 -0
- dolphin/core/logging/logger.py +526 -0
- dolphin/core/message/__init__.py +8 -0
- dolphin/core/message/compressor.py +749 -0
- dolphin/core/parser/__init__.py +8 -0
- dolphin/core/parser/parser.py +405 -0
- dolphin/core/runtime/__init__.py +10 -0
- dolphin/core/runtime/runtime_graph.py +926 -0
- dolphin/core/runtime/runtime_instance.py +446 -0
- dolphin/core/skill/__init__.py +14 -0
- dolphin/core/skill/context_retention.py +157 -0
- dolphin/core/skill/skill_function.py +686 -0
- dolphin/core/skill/skill_matcher.py +282 -0
- dolphin/core/skill/skillkit.py +700 -0
- dolphin/core/skill/skillset.py +72 -0
- dolphin/core/trajectory/__init__.py +10 -0
- dolphin/core/trajectory/recorder.py +189 -0
- dolphin/core/trajectory/trajectory.py +522 -0
- dolphin/core/utils/__init__.py +9 -0
- dolphin/core/utils/cache_kv.py +212 -0
- dolphin/core/utils/tools.py +340 -0
- dolphin/lib/__init__.py +93 -0
- dolphin/lib/debug/__init__.py +8 -0
- dolphin/lib/debug/visualizer.py +409 -0
- dolphin/lib/memory/__init__.py +28 -0
- dolphin/lib/memory/async_processor.py +220 -0
- dolphin/lib/memory/llm_calls.py +195 -0
- dolphin/lib/memory/manager.py +78 -0
- dolphin/lib/memory/sandbox.py +46 -0
- dolphin/lib/memory/storage.py +245 -0
- dolphin/lib/memory/utils.py +51 -0
- dolphin/lib/ontology/__init__.py +12 -0
- dolphin/lib/ontology/basic/__init__.py +0 -0
- dolphin/lib/ontology/basic/base.py +102 -0
- dolphin/lib/ontology/basic/concept.py +130 -0
- dolphin/lib/ontology/basic/object.py +11 -0
- dolphin/lib/ontology/basic/relation.py +63 -0
- dolphin/lib/ontology/datasource/__init__.py +27 -0
- dolphin/lib/ontology/datasource/datasource.py +66 -0
- dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
- dolphin/lib/ontology/datasource/sql.py +845 -0
- dolphin/lib/ontology/mapping.py +177 -0
- dolphin/lib/ontology/ontology.py +733 -0
- dolphin/lib/ontology/ontology_context.py +16 -0
- dolphin/lib/ontology/ontology_manager.py +107 -0
- dolphin/lib/skill_results/__init__.py +31 -0
- dolphin/lib/skill_results/cache_backend.py +559 -0
- dolphin/lib/skill_results/result_processor.py +181 -0
- dolphin/lib/skill_results/result_reference.py +179 -0
- dolphin/lib/skill_results/skillkit_hook.py +324 -0
- dolphin/lib/skill_results/strategies.py +328 -0
- dolphin/lib/skill_results/strategy_registry.py +150 -0
- dolphin/lib/skillkits/__init__.py +44 -0
- dolphin/lib/skillkits/agent_skillkit.py +155 -0
- dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
- dolphin/lib/skillkits/env_skillkit.py +250 -0
- dolphin/lib/skillkits/mcp_adapter.py +616 -0
- dolphin/lib/skillkits/mcp_skillkit.py +771 -0
- dolphin/lib/skillkits/memory_skillkit.py +650 -0
- dolphin/lib/skillkits/noop_skillkit.py +31 -0
- dolphin/lib/skillkits/ontology_skillkit.py +89 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
- dolphin/lib/skillkits/resource/__init__.py +52 -0
- dolphin/lib/skillkits/resource/models/__init__.py +6 -0
- dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
- dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
- dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
- dolphin/lib/skillkits/resource/skill_cache.py +215 -0
- dolphin/lib/skillkits/resource/skill_loader.py +395 -0
- dolphin/lib/skillkits/resource/skill_validator.py +406 -0
- dolphin/lib/skillkits/resource_skillkit.py +11 -0
- dolphin/lib/skillkits/search_skillkit.py +163 -0
- dolphin/lib/skillkits/sql_skillkit.py +274 -0
- dolphin/lib/skillkits/system_skillkit.py +509 -0
- dolphin/lib/skillkits/vm_skillkit.py +65 -0
- dolphin/lib/utils/__init__.py +9 -0
- dolphin/lib/utils/data_process.py +207 -0
- dolphin/lib/utils/handle_progress.py +178 -0
- dolphin/lib/utils/security.py +139 -0
- dolphin/lib/utils/text_retrieval.py +462 -0
- dolphin/lib/vm/__init__.py +11 -0
- dolphin/lib/vm/env_executor.py +895 -0
- dolphin/lib/vm/python_session_manager.py +453 -0
- dolphin/lib/vm/vm.py +610 -0
- dolphin/sdk/__init__.py +60 -0
- dolphin/sdk/agent/__init__.py +12 -0
- dolphin/sdk/agent/agent_factory.py +236 -0
- dolphin/sdk/agent/dolphin_agent.py +1106 -0
- dolphin/sdk/api/__init__.py +4 -0
- dolphin/sdk/runtime/__init__.py +8 -0
- dolphin/sdk/runtime/env.py +363 -0
- dolphin/sdk/skill/__init__.py +10 -0
- dolphin/sdk/skill/global_skills.py +706 -0
- dolphin/sdk/skill/traditional_toolkit.py +260 -0
- kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
- kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
- kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
- kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
- kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
- kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,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})"
|