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,989 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dolphin Agent Runner
|
|
3
|
+
|
|
4
|
+
This module contains the execution logic for running Dolphin agents.
|
|
5
|
+
The main runDolphinAgent function has been refactored into smaller,
|
|
6
|
+
single-responsibility functions for better maintainability.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Fixed bottom input layout with scrollable content
|
|
10
|
+
- ESC key interrupt support for agent execution
|
|
11
|
+
- Status bar with spinner animation during processing
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import datetime
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import random
|
|
20
|
+
import sys
|
|
21
|
+
import traceback
|
|
22
|
+
import uuid
|
|
23
|
+
from typing import Any, Dict, Optional
|
|
24
|
+
|
|
25
|
+
from rich.console import Console as RichConsole
|
|
26
|
+
|
|
27
|
+
from dolphin.core import flags
|
|
28
|
+
from dolphin.core.common.exceptions import DebuggerQuitException, UserInterrupt
|
|
29
|
+
from dolphin.core.agent.agent_state import AgentState, PauseType
|
|
30
|
+
from dolphin.core.logging.logger import console
|
|
31
|
+
from dolphin.cli.ui.console import console_session_start, console_display_session_info
|
|
32
|
+
|
|
33
|
+
from dolphin.cli.args.parser import Args
|
|
34
|
+
from dolphin.cli.utils.helpers import buildVariables, outputVariablesToJson
|
|
35
|
+
from dolphin.cli.interrupt.handler import InterruptToken
|
|
36
|
+
from dolphin.cli.ui.layout import LayoutManager
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def initializeEnvironment(args: Args):
|
|
40
|
+
"""Initialize Dolphin environment
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
args: Parsed CLI arguments
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Tuple of (env, globalConfig)
|
|
47
|
+
"""
|
|
48
|
+
from dolphin.sdk.runtime.env import Env
|
|
49
|
+
from dolphin.core.config.global_config import GlobalConfig
|
|
50
|
+
|
|
51
|
+
globalConfigPath = args.config if args.config else "./config/global.yaml"
|
|
52
|
+
globalConfig = GlobalConfig.from_yaml(globalConfigPath)
|
|
53
|
+
|
|
54
|
+
env = Env(
|
|
55
|
+
globalConfig=globalConfig,
|
|
56
|
+
agentFolderPath=args.folder,
|
|
57
|
+
skillkitFolderPath=args.skillFolder,
|
|
58
|
+
output_variables=[],
|
|
59
|
+
verbose=args.saveHistory,
|
|
60
|
+
is_cli=True, # CLI mode: enable Rich/terminal beautification
|
|
61
|
+
log_level=(
|
|
62
|
+
logging.DEBUG if flags.is_enabled(flags.DEBUG_MODE) else logging.INFO
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return env, globalConfig
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def loadAndPrepareAgent(env, args: Args, initialVariables: Dict[str, Any]):
|
|
70
|
+
"""Load and prepare agent for execution
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
env: Dolphin environment
|
|
74
|
+
args: Parsed CLI arguments
|
|
75
|
+
initialVariables: Initial variables to pass to agent
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Prepared agent instance
|
|
79
|
+
"""
|
|
80
|
+
from dolphin.sdk.agent.dolphin_agent import DolphinAgent
|
|
81
|
+
|
|
82
|
+
availableAgents = env.getAgentNames()
|
|
83
|
+
|
|
84
|
+
if args.agent not in availableAgents:
|
|
85
|
+
console(f"Error: Agent '{args.agent}' not found in folder '{args.folder}'")
|
|
86
|
+
console(f"Available agents: {availableAgents}")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
agent = env.getAgent(args.agent)
|
|
90
|
+
|
|
91
|
+
# Check if agent is in ERROR state and reset if needed
|
|
92
|
+
if (agent is not None and hasattr(agent, "state") and agent.state.name == "ERROR"):
|
|
93
|
+
agent = await _recoverAgentFromError(env, args, agent)
|
|
94
|
+
|
|
95
|
+
await agent.initialize()
|
|
96
|
+
|
|
97
|
+
if agent is None:
|
|
98
|
+
console(f"Error: Failed to get agent instance for '{args.agent}'")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
# Configure trajectory
|
|
102
|
+
if args.trajectoryPath:
|
|
103
|
+
agent.set_trajectorypath(args.trajectoryPath)
|
|
104
|
+
agent.set_agent_name(args.agent)
|
|
105
|
+
|
|
106
|
+
# Setup context
|
|
107
|
+
env._setupAgentContext(agent)
|
|
108
|
+
|
|
109
|
+
# Initialize with variables
|
|
110
|
+
if initialVariables:
|
|
111
|
+
initParams = {"variables": initialVariables}
|
|
112
|
+
await agent.executor.executor_init(initParams)
|
|
113
|
+
|
|
114
|
+
return agent
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def _recoverAgentFromError(env, args: Args, agent):
|
|
118
|
+
"""Recover agent from ERROR state
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
env: Dolphin environment
|
|
122
|
+
args: Parsed CLI arguments
|
|
123
|
+
agent: Agent in error state
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Recovered agent instance
|
|
127
|
+
"""
|
|
128
|
+
from dolphin.sdk.agent.dolphin_agent import DolphinAgent
|
|
129
|
+
|
|
130
|
+
agentFilePath = None
|
|
131
|
+
for filePath in env._scanDolphinFiles(args.folder):
|
|
132
|
+
try:
|
|
133
|
+
tempAgent = DolphinAgent(
|
|
134
|
+
file_path=filePath,
|
|
135
|
+
global_config=env.globalConfig,
|
|
136
|
+
global_skills=env.globalSkills,
|
|
137
|
+
global_types=env.global_types,
|
|
138
|
+
)
|
|
139
|
+
if tempAgent.get_name() == args.agent:
|
|
140
|
+
agentFilePath = filePath
|
|
141
|
+
break
|
|
142
|
+
except Exception:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if agentFilePath:
|
|
146
|
+
freshAgent = DolphinAgent(
|
|
147
|
+
file_path=agentFilePath,
|
|
148
|
+
global_config=env.globalConfig,
|
|
149
|
+
global_skills=env.globalSkills,
|
|
150
|
+
global_types=env.global_types,
|
|
151
|
+
)
|
|
152
|
+
env.agents[args.agent] = freshAgent
|
|
153
|
+
return freshAgent
|
|
154
|
+
else:
|
|
155
|
+
console(
|
|
156
|
+
f"Warning: Could not find file path for agent '{args.agent}', "
|
|
157
|
+
"proceeding with existing instance"
|
|
158
|
+
)
|
|
159
|
+
return agent
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _get_skillkit_info(agent) -> Optional[Dict[str, int]]:
|
|
163
|
+
"""Extract skillkit information from agent for display.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
agent: The DolphinAgent instance
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dict mapping skillkit name to tool count, or None if unavailable
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
context = agent.get_context()
|
|
173
|
+
if context is None:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
all_skills = context.all_skills.getSkills() if context.all_skills else []
|
|
177
|
+
if not all_skills:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
# Group skills by owner skillkit name
|
|
181
|
+
skillkit_counts: Dict[str, int] = {}
|
|
182
|
+
for skill in all_skills:
|
|
183
|
+
owner_name = getattr(skill, 'owner_name', None)
|
|
184
|
+
if owner_name:
|
|
185
|
+
skillkit_counts[owner_name] = skillkit_counts.get(owner_name, 0) + 1
|
|
186
|
+
else:
|
|
187
|
+
# Skills without owner go to "builtin"
|
|
188
|
+
skillkit_counts["builtin"] = skillkit_counts.get("builtin", 0) + 1
|
|
189
|
+
|
|
190
|
+
return skillkit_counts if skillkit_counts else None
|
|
191
|
+
except Exception:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _handle_user_interrupt(agent, layout, source: str) -> None:
|
|
196
|
+
"""Handle user interrupt (ESC or Ctrl-C) by setting up agent state for resumption.
|
|
197
|
+
|
|
198
|
+
This is a shared handler for both UserInterrupt and asyncio.CancelledError,
|
|
199
|
+
as they have identical semantics: user wants to provide new input.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
agent: The DolphinAgent instance
|
|
203
|
+
layout: The LayoutManager instance
|
|
204
|
+
source: String identifying the interrupt source for logging ("UserInterrupt" or "CancelledError")
|
|
205
|
+
"""
|
|
206
|
+
from dolphin.core.agent.agent_state import AgentState, PauseType
|
|
207
|
+
from dolphin.cli.ui.console import StatusBar
|
|
208
|
+
|
|
209
|
+
layout.hide_status()
|
|
210
|
+
|
|
211
|
+
# Set agent state for proper resumption with context preservation
|
|
212
|
+
agent._state = AgentState.PAUSED
|
|
213
|
+
agent._pause_type = PauseType.USER_INTERRUPT
|
|
214
|
+
|
|
215
|
+
# Clear the interrupt event so future calls don't immediately re-interrupt
|
|
216
|
+
if hasattr(agent, 'clear_interrupt'):
|
|
217
|
+
agent.clear_interrupt()
|
|
218
|
+
elif hasattr(agent, 'get_interrupt_event'):
|
|
219
|
+
event = agent.get_interrupt_event()
|
|
220
|
+
if event:
|
|
221
|
+
event.clear()
|
|
222
|
+
|
|
223
|
+
StatusBar._debug_log(f"_handle_user_interrupt: handled {source}, agent state set to PAUSED/USER_INTERRUPT")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def runConversationLoop(agent, args: Args, initialVariables: Dict[str, Any]) -> bool:
|
|
227
|
+
"""Run the main conversation loop with fixed layout and interrupt support.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
agent: Agent instance
|
|
231
|
+
args: Parsed CLI arguments
|
|
232
|
+
initialVariables: Initial variables
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if should enter post-mortem after interactive mode ends
|
|
236
|
+
"""
|
|
237
|
+
# Initialize layout manager and interrupt token
|
|
238
|
+
layout = LayoutManager(enabled=args.interactive)
|
|
239
|
+
interrupt_token = InterruptToken()
|
|
240
|
+
|
|
241
|
+
currentQuery = args.query
|
|
242
|
+
|
|
243
|
+
if currentQuery:
|
|
244
|
+
agent.add_bucket(bucket_name="_query", content=currentQuery)
|
|
245
|
+
|
|
246
|
+
if args.interactive:
|
|
247
|
+
mode = "Interactive"
|
|
248
|
+
# Setup scroll region FIRST, then print banner inside the scrollable area
|
|
249
|
+
layout.start_session(mode, args.agent)
|
|
250
|
+
console_session_start(mode, args.agent)
|
|
251
|
+
|
|
252
|
+
# Display available skillkits and command hints
|
|
253
|
+
skillkit_info = _get_skillkit_info(agent)
|
|
254
|
+
console_display_session_info(skillkit_info, show_commands=True)
|
|
255
|
+
|
|
256
|
+
if flags.is_enabled(flags.DEBUG_MODE):
|
|
257
|
+
console("💡 输入 /debug 进入实时调试,/trace /snapshot /vars 快速查看", verbose=args.saveHistory)
|
|
258
|
+
else:
|
|
259
|
+
mode = "Execution"
|
|
260
|
+
# No layout for non-interactive mode, just print banner
|
|
261
|
+
console_session_start(mode, args.agent)
|
|
262
|
+
|
|
263
|
+
# Display available skillkits (no command hints in non-interactive mode)
|
|
264
|
+
skillkit_info = _get_skillkit_info(agent)
|
|
265
|
+
console_display_session_info(skillkit_info, show_commands=False)
|
|
266
|
+
|
|
267
|
+
isFirstExecution = True
|
|
268
|
+
enterPostmortemAfterInteractive = False
|
|
269
|
+
|
|
270
|
+
# DEBUG: Import StatusBar for logging
|
|
271
|
+
from dolphin.cli.ui.console import StatusBar
|
|
272
|
+
StatusBar._debug_log(f"runConversationLoop: starting, interactive={args.interactive}, currentQuery={currentQuery!r}")
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
# Bind interrupt token to agent and event loop
|
|
276
|
+
interrupt_token.bind(agent, asyncio.get_running_loop())
|
|
277
|
+
|
|
278
|
+
while True:
|
|
279
|
+
StatusBar._debug_log(f"runConversationLoop: loop iteration, isFirstExecution={isFirstExecution}, currentQuery={currentQuery!r}")
|
|
280
|
+
|
|
281
|
+
# Prompt for input if not first execution and interactive mode
|
|
282
|
+
if not currentQuery and args.interactive and not isFirstExecution:
|
|
283
|
+
StatusBar._debug_log(f"runConversationLoop: calling _promptUserInput")
|
|
284
|
+
currentQuery, shouldBreak, debugCommand = await _promptUserInput(
|
|
285
|
+
args, interrupt_token
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Handle live debug command
|
|
289
|
+
if debugCommand is not None:
|
|
290
|
+
await _handleLiveDebugCommand(agent, debugCommand)
|
|
291
|
+
currentQuery = None
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
if shouldBreak:
|
|
295
|
+
if flags.is_enabled(flags.DEBUG_MODE) and args.interactive:
|
|
296
|
+
enterPostmortemAfterInteractive = True
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
# Clear interrupt state before execution
|
|
301
|
+
interrupt_token.clear()
|
|
302
|
+
|
|
303
|
+
# Show inline status bar (simplified - no fixed positioning)
|
|
304
|
+
if args.interactive:
|
|
305
|
+
layout.show_status("Processing your request", "esc to interrupt")
|
|
306
|
+
|
|
307
|
+
# Start keyboard monitor for ESC interrupt
|
|
308
|
+
monitor_stop = None
|
|
309
|
+
monitor_task = None
|
|
310
|
+
if args.interactive:
|
|
311
|
+
import threading
|
|
312
|
+
from dolphin.cli.interrupt.keyboard import _monitor_interrupt
|
|
313
|
+
monitor_stop = threading.Event()
|
|
314
|
+
monitor_task = asyncio.create_task(_monitor_interrupt(interrupt_token, monitor_stop))
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
if isFirstExecution:
|
|
318
|
+
StatusBar._debug_log(f"runConversationLoop: running first execution")
|
|
319
|
+
await _runFirstExecution(agent, args, initialVariables)
|
|
320
|
+
isFirstExecution = False
|
|
321
|
+
StatusBar._debug_log(f"runConversationLoop: first execution done")
|
|
322
|
+
else:
|
|
323
|
+
await _runSubsequentExecution(agent, args, currentQuery)
|
|
324
|
+
finally:
|
|
325
|
+
# Stop keyboard monitor
|
|
326
|
+
if monitor_stop:
|
|
327
|
+
monitor_stop.set()
|
|
328
|
+
if monitor_task:
|
|
329
|
+
try:
|
|
330
|
+
await monitor_task
|
|
331
|
+
except:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# Hide status bar after completion
|
|
335
|
+
if args.interactive:
|
|
336
|
+
layout.hide_status()
|
|
337
|
+
StatusBar._debug_log(f"runConversationLoop: status bar hidden")
|
|
338
|
+
|
|
339
|
+
except DebuggerQuitException:
|
|
340
|
+
layout.hide_status()
|
|
341
|
+
console("✅ 调试会话已结束。")
|
|
342
|
+
break
|
|
343
|
+
except UserInterrupt:
|
|
344
|
+
# UserInterrupt: user pressed ESC, interrupt() was called
|
|
345
|
+
StatusBar._debug_log(f"runConversationLoop: UserInterrupt caught, continuing loop")
|
|
346
|
+
if args.interactive:
|
|
347
|
+
_handle_user_interrupt(agent, layout, "UserInterrupt")
|
|
348
|
+
isFirstExecution = False
|
|
349
|
+
else:
|
|
350
|
+
raise
|
|
351
|
+
except asyncio.CancelledError:
|
|
352
|
+
# CancelledError: Ctrl-C SIGINT or asyncio task cancellation
|
|
353
|
+
StatusBar._debug_log(f"runConversationLoop: CancelledError caught, continuing loop")
|
|
354
|
+
if args.interactive:
|
|
355
|
+
_handle_user_interrupt(agent, layout, "CancelledError")
|
|
356
|
+
isFirstExecution = False
|
|
357
|
+
else:
|
|
358
|
+
raise
|
|
359
|
+
except Exception as e:
|
|
360
|
+
StatusBar._debug_log(f"runConversationLoop: Exception caught: {type(e).__name__}: {e}")
|
|
361
|
+
raise
|
|
362
|
+
|
|
363
|
+
currentQuery = None
|
|
364
|
+
StatusBar._debug_log(f"runConversationLoop: after execution, about to check interactive={args.interactive}")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if not args.interactive:
|
|
368
|
+
StatusBar._debug_log(f"runConversationLoop: not interactive, breaking")
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
# Final output for interactive mode
|
|
372
|
+
if args.interactive and args.outputVariables:
|
|
373
|
+
outputVariablesToJson(agent.get_context(), args.outputVariables)
|
|
374
|
+
|
|
375
|
+
finally:
|
|
376
|
+
StatusBar._debug_log(f"runConversationLoop: finally block executing")
|
|
377
|
+
# Cleanup
|
|
378
|
+
interrupt_token.unbind()
|
|
379
|
+
if args.interactive:
|
|
380
|
+
layout.end_session()
|
|
381
|
+
|
|
382
|
+
return enterPostmortemAfterInteractive
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async def _promptUserInput(
|
|
386
|
+
args: Args,
|
|
387
|
+
interrupt_token: Optional[InterruptToken] = None
|
|
388
|
+
) -> tuple:
|
|
389
|
+
"""Prompt user for input in interactive mode with ESC interrupt support.
|
|
390
|
+
|
|
391
|
+
Simplified version: input follows content naturally, no scroll region management.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
args: Parsed CLI arguments
|
|
395
|
+
interrupt_token: Optional InterruptToken for ESC handling
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of (query, shouldBreak, debugCommand)
|
|
399
|
+
- query: User query string or None
|
|
400
|
+
- shouldBreak: Whether to break the conversation loop
|
|
401
|
+
- debugCommand: Debug command if user requested live debug, else None
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
from dolphin.cli.ui.input import (
|
|
405
|
+
prompt_conversation_with_multimodal,
|
|
406
|
+
EscapeInterrupt
|
|
407
|
+
)
|
|
408
|
+
from dolphin.cli.ui.console import StatusBar
|
|
409
|
+
import sys
|
|
410
|
+
|
|
411
|
+
StatusBar._debug_log(f"_promptUserInput: starting (simplified)")
|
|
412
|
+
|
|
413
|
+
# Ensure cursor is visible before prompting
|
|
414
|
+
sys.stdout.write("\033[?25h")
|
|
415
|
+
sys.stdout.flush()
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
# Get any real-time input buffered while the agent was running
|
|
419
|
+
default_text = ""
|
|
420
|
+
if interrupt_token:
|
|
421
|
+
default_text = interrupt_token.get_realtime_input(consume=True)
|
|
422
|
+
if default_text:
|
|
423
|
+
StatusBar._debug_log(f"_promptUserInput: found realtime buffer: {default_text!r}")
|
|
424
|
+
|
|
425
|
+
# Use multimodal-aware prompt that processes @paste, @image:, @url: syntax
|
|
426
|
+
# Returns: str for plain text, List[Dict] for multimodal content
|
|
427
|
+
currentQuery = await prompt_conversation_with_multimodal(
|
|
428
|
+
prompt_text="> ",
|
|
429
|
+
interrupt_token=interrupt_token,
|
|
430
|
+
verbose=True
|
|
431
|
+
)
|
|
432
|
+
StatusBar._debug_log(f"_promptUserInput: got input: {currentQuery!r}")
|
|
433
|
+
|
|
434
|
+
except EscapeInterrupt:
|
|
435
|
+
# ESC pressed during input - treat as empty input
|
|
436
|
+
StatusBar._debug_log(f"_promptUserInput: EscapeInterrupt")
|
|
437
|
+
return None, False, None
|
|
438
|
+
|
|
439
|
+
except (EOFError, KeyboardInterrupt):
|
|
440
|
+
from dolphin.cli.ui.console import console_conversation_end
|
|
441
|
+
console_conversation_end()
|
|
442
|
+
return None, True, None
|
|
443
|
+
|
|
444
|
+
# Handle multimodal content (List[Dict]) - return directly without string checks
|
|
445
|
+
if isinstance(currentQuery, list):
|
|
446
|
+
# This is multimodal content, return as-is
|
|
447
|
+
return currentQuery, False, None
|
|
448
|
+
|
|
449
|
+
# For string input, check exit commands and debug prefixes
|
|
450
|
+
if not currentQuery or currentQuery.lower().strip() in ["exit", "quit", "q", ""]:
|
|
451
|
+
console("Conversation ended", verbose=args.saveHistory)
|
|
452
|
+
return None, True, None
|
|
453
|
+
|
|
454
|
+
# Check for debug command prefixes (live debug mode)
|
|
455
|
+
# /debug enters REPL, others execute once and return to conversation
|
|
456
|
+
debugPrefixes = {
|
|
457
|
+
"/debug": None, # /debug or /debug <cmd> -> enters REPL or executes single cmd
|
|
458
|
+
"/trace": "trace",
|
|
459
|
+
"/snapshot": "snapshot",
|
|
460
|
+
"/vars": "vars",
|
|
461
|
+
"/var": "var",
|
|
462
|
+
"/progress": "progress",
|
|
463
|
+
"/help": "help",
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
queryLower = currentQuery.lower()
|
|
467
|
+
for prefix, defaultCmd in debugPrefixes.items():
|
|
468
|
+
if queryLower.startswith(prefix):
|
|
469
|
+
# Extract the debug command
|
|
470
|
+
remainder = currentQuery[len(prefix):].strip()
|
|
471
|
+
if defaultCmd:
|
|
472
|
+
# For /trace, /snapshot, etc., the command is the prefix itself
|
|
473
|
+
debugCmd = f"{defaultCmd} {remainder}".strip() if remainder else defaultCmd
|
|
474
|
+
else:
|
|
475
|
+
# For /debug, remainder is the full command (or 'help' if empty)
|
|
476
|
+
debugCmd = remainder if remainder else "help"
|
|
477
|
+
return None, False, debugCmd
|
|
478
|
+
|
|
479
|
+
return currentQuery, False, None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
async def _handleLiveDebugCommand(agent, debugCommand: str) -> None:
|
|
483
|
+
"""Handle live debug command during conversation
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
agent: Agent instance
|
|
487
|
+
debugCommand: Debug command string (e.g., "trace", "vars", "snapshot json")
|
|
488
|
+
"""
|
|
489
|
+
debugCtrl = getattr(agent.executor, 'debug_controller', None)
|
|
490
|
+
|
|
491
|
+
if debugCtrl is None:
|
|
492
|
+
# No debug controller yet, create a temporary one for inspection
|
|
493
|
+
from dolphin.core.executor.debug_controller import DebugController
|
|
494
|
+
context = agent.get_context()
|
|
495
|
+
if context is None:
|
|
496
|
+
console("⚠️ Agent 尚未初始化,无法进入调试模式")
|
|
497
|
+
return
|
|
498
|
+
debugCtrl = DebugController(context)
|
|
499
|
+
|
|
500
|
+
await debugCtrl.enter_live_debug(debugCommand)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
async def _runFirstExecution(agent, args: Args, initialVariables: Dict[str, Any]) -> None:
|
|
504
|
+
"""Run first execution of agent"""
|
|
505
|
+
from dolphin.cli.ui.console import StatusBar
|
|
506
|
+
StatusBar._debug_log(f"_runFirstExecution: starting")
|
|
507
|
+
|
|
508
|
+
debugKwargs = {"debug_mode": flags.is_enabled(flags.DEBUG_MODE)}
|
|
509
|
+
if flags.is_enabled(flags.DEBUG_MODE):
|
|
510
|
+
if args.breakOnStart:
|
|
511
|
+
debugKwargs["break_on_start"] = True
|
|
512
|
+
if args.breakAt:
|
|
513
|
+
debugKwargs["break_at"] = args.breakAt
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
async for result in agent.arun(**debugKwargs, **initialVariables):
|
|
517
|
+
pass
|
|
518
|
+
StatusBar._debug_log(f"_runFirstExecution: arun completed")
|
|
519
|
+
except Exception as e:
|
|
520
|
+
StatusBar._debug_log(f"_runFirstExecution: exception during arun: {e}")
|
|
521
|
+
raise
|
|
522
|
+
|
|
523
|
+
if args.outputVariables and not args.interactive:
|
|
524
|
+
outputVariablesToJson(agent.get_context(), args.outputVariables)
|
|
525
|
+
|
|
526
|
+
StatusBar._debug_log(f"_runFirstExecution: done")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def _runSubsequentExecution(agent, args, query) -> None:
|
|
530
|
+
"""Run subsequent execution (chat mode or resume)
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
query: User input - can be str for text or List[Dict] for multimodal content
|
|
534
|
+
"""
|
|
535
|
+
from dolphin.core.agent.agent_state import AgentState, PauseType
|
|
536
|
+
|
|
537
|
+
# Check if the agent is paused due to user interrupt
|
|
538
|
+
# We use getattr/direct access for performance in CLI
|
|
539
|
+
is_user_interrupted = (
|
|
540
|
+
agent.state == AgentState.PAUSED and
|
|
541
|
+
getattr(agent, '_pause_type', None) == PauseType.USER_INTERRUPT
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
if is_user_interrupted:
|
|
545
|
+
# For UserInterrupt, we treat it as a multi-turn conversation continuation
|
|
546
|
+
# rather than a block restart. This allows LLM to see the partial output
|
|
547
|
+
# it was generating and continue from there with the new user input.
|
|
548
|
+
#
|
|
549
|
+
# Key insight: UserInterrupt is semantically "user wants to provide new input",
|
|
550
|
+
# which is the same as achat's purpose - continue the conversation.
|
|
551
|
+
|
|
552
|
+
# Clear interrupt state before continuing
|
|
553
|
+
if hasattr(agent, 'clear_interrupt'):
|
|
554
|
+
agent.clear_interrupt()
|
|
555
|
+
|
|
556
|
+
# Reset pause state for the agent so it can accept new work
|
|
557
|
+
agent._pause_type = None
|
|
558
|
+
if hasattr(agent, '_resume_handle'):
|
|
559
|
+
agent._resume_handle = None
|
|
560
|
+
|
|
561
|
+
# Reset agent state to RUNNING so interrupt() can work during execution
|
|
562
|
+
agent._state = AgentState.RUNNING
|
|
563
|
+
|
|
564
|
+
# Use achat with preserve_context=True to keep the scratchpad content
|
|
565
|
+
# This ensures LLM can see its partial output from before the interrupt
|
|
566
|
+
async for result in agent.achat(message=query, preserve_context=True):
|
|
567
|
+
pass
|
|
568
|
+
else:
|
|
569
|
+
# Also set state to RUNNING for normal achat path
|
|
570
|
+
agent._state = AgentState.RUNNING
|
|
571
|
+
async for result in agent.achat(message=query):
|
|
572
|
+
pass
|
|
573
|
+
|
|
574
|
+
if args.outputVariables:
|
|
575
|
+
outputVariablesToJson(agent.get_context(), args.outputVariables)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
async def saveExecutionArtifacts(agent, args: Args) -> None:
|
|
579
|
+
"""Save execution trace and snapshots
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
agent: Agent instance
|
|
583
|
+
args: Parsed CLI arguments
|
|
584
|
+
"""
|
|
585
|
+
if not args.saveHistory:
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# Save trajectory if not already saved via trajectoryPath
|
|
589
|
+
if not args.trajectoryPath:
|
|
590
|
+
agent.save_trajectory(agent_name=args.agent, force_save=True)
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
currentTime = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
594
|
+
randomSuffix = f"{random.randint(10000, 99999)}"
|
|
595
|
+
|
|
596
|
+
# Save execution trace
|
|
597
|
+
await _saveExecutionTrace(agent, args, currentTime, randomSuffix)
|
|
598
|
+
|
|
599
|
+
# Save snapshot analysis in debug mode
|
|
600
|
+
if flags.is_enabled(flags.DEBUG_MODE):
|
|
601
|
+
await _saveSnapshotAnalysis(agent, args, currentTime, randomSuffix)
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
console(f"Warning: Failed to save execution trace: {e}")
|
|
605
|
+
if args.saveHistory:
|
|
606
|
+
traceback.print_exc()
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
async def _saveExecutionTrace(agent, args: Args, currentTime: str, randomSuffix: str) -> None:
|
|
610
|
+
"""Save execution trace to file"""
|
|
611
|
+
traceContent = agent.get_execution_trace()
|
|
612
|
+
|
|
613
|
+
if args.tracePath:
|
|
614
|
+
tracePath = args.tracePath
|
|
615
|
+
traceDir = os.path.dirname(tracePath)
|
|
616
|
+
if traceDir:
|
|
617
|
+
os.makedirs(traceDir, exist_ok=True)
|
|
618
|
+
else:
|
|
619
|
+
traceDir = "data/execution_trace"
|
|
620
|
+
os.makedirs(traceDir, exist_ok=True)
|
|
621
|
+
traceFilename = f"execution_trace_{currentTime}_{randomSuffix}.txt"
|
|
622
|
+
tracePath = os.path.join(traceDir, traceFilename)
|
|
623
|
+
|
|
624
|
+
with open(tracePath, "w", encoding="utf-8") as f:
|
|
625
|
+
f.write(traceContent)
|
|
626
|
+
|
|
627
|
+
console(f"Execution trace saved to: {tracePath}", verbose=args.saveHistory)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
async def _saveSnapshotAnalysis(agent, args: Args, currentTime: str, randomSuffix: str) -> None:
|
|
631
|
+
"""Save snapshot analysis in debug mode"""
|
|
632
|
+
try:
|
|
633
|
+
snapshotDir = "data/snapshot_analysis"
|
|
634
|
+
os.makedirs(snapshotDir, exist_ok=True)
|
|
635
|
+
|
|
636
|
+
# Save Markdown format
|
|
637
|
+
snapshotAnalysis = agent.get_snapshot_analysis(
|
|
638
|
+
title=f"Debug Snapshot Analysis - {args.agent}"
|
|
639
|
+
)
|
|
640
|
+
snapshotFilename = f"snapshot_analysis_{currentTime}_{randomSuffix}.md"
|
|
641
|
+
snapshotPath = os.path.join(snapshotDir, snapshotFilename)
|
|
642
|
+
|
|
643
|
+
with open(snapshotPath, "w", encoding="utf-8") as f:
|
|
644
|
+
f.write(snapshotAnalysis)
|
|
645
|
+
|
|
646
|
+
console(f"Snapshot analysis saved to: {snapshotPath}", verbose=args.saveHistory)
|
|
647
|
+
|
|
648
|
+
# Save JSON format
|
|
649
|
+
snapshotJson = agent.get_snapshot_analysis(format='json')
|
|
650
|
+
snapshotJsonFilename = f"snapshot_analysis_{currentTime}_{randomSuffix}.json"
|
|
651
|
+
snapshotJsonPath = os.path.join(snapshotDir, snapshotJsonFilename)
|
|
652
|
+
|
|
653
|
+
with open(snapshotJsonPath, "w", encoding="utf-8") as f:
|
|
654
|
+
json.dump(snapshotJson, f, ensure_ascii=False, indent=2)
|
|
655
|
+
|
|
656
|
+
console(f"Snapshot analysis JSON saved to: {snapshotJsonPath}", verbose=args.saveHistory)
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
console(f"Warning: Failed to save snapshot analysis: {e}")
|
|
660
|
+
if args.saveHistory:
|
|
661
|
+
traceback.print_exc()
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
async def enterPostmortemIfNeeded(agent, args: Args, enterPostmortem: bool) -> None:
|
|
665
|
+
"""Enter post-mortem debug mode if conditions are met
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
agent: Agent instance
|
|
669
|
+
args: Parsed CLI arguments
|
|
670
|
+
enterPostmortem: Whether to enter post-mortem
|
|
671
|
+
"""
|
|
672
|
+
try:
|
|
673
|
+
shouldEnter = (
|
|
674
|
+
flags.is_enabled(flags.DEBUG_MODE)
|
|
675
|
+
and not getattr(args, 'autoContinue', False)
|
|
676
|
+
and (not args.interactive or enterPostmortem)
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if shouldEnter:
|
|
680
|
+
debugCtrl = getattr(agent.executor, 'debug_controller', None)
|
|
681
|
+
if debugCtrl is not None:
|
|
682
|
+
console("\n✅ 程序执行完毕,已进入 Post-Mortem 调试模式。输入 'help' 查看命令,'quit' 退出。")
|
|
683
|
+
await debugCtrl.post_mortem_loop()
|
|
684
|
+
else:
|
|
685
|
+
console("⚠️ 未找到调试控制器,无法进入 Post-Mortem 模式。")
|
|
686
|
+
except Exception as e:
|
|
687
|
+
console(f"Warning: Post-Mortem 调试模式发生错误: {e}")
|
|
688
|
+
if args.saveHistory:
|
|
689
|
+
traceback.print_exc()
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
async def runDolphinAgent(args: Args) -> None:
|
|
693
|
+
"""Run Dolphin Language agent
|
|
694
|
+
|
|
695
|
+
This is the main orchestrator function that coordinates:
|
|
696
|
+
1. Environment initialization
|
|
697
|
+
2. Agent loading and preparation
|
|
698
|
+
3. Conversation loop execution
|
|
699
|
+
4. Artifact saving
|
|
700
|
+
5. Post-mortem debugging (if applicable)
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
args: Parsed CLI arguments
|
|
704
|
+
"""
|
|
705
|
+
from dolphin.cli.utils.helpers import validateArgs
|
|
706
|
+
|
|
707
|
+
validateArgs(args)
|
|
708
|
+
|
|
709
|
+
richConsole = RichConsole()
|
|
710
|
+
initialVariables = buildVariables(args)
|
|
711
|
+
|
|
712
|
+
userId = args.userId if args.userId else str(uuid.uuid4())
|
|
713
|
+
sessionId = args.sessionId if args.sessionId else str(uuid.uuid4())
|
|
714
|
+
|
|
715
|
+
env = None
|
|
716
|
+
try:
|
|
717
|
+
with richConsole.status("[bold green]Initializing Dolphin Environment...") as status:
|
|
718
|
+
status.update("[bold blue]Loading configuration...[/]")
|
|
719
|
+
env, _ = await initializeEnvironment(args)
|
|
720
|
+
|
|
721
|
+
status.update(f"[bold blue]Loading agents from:[/][white] {args.folder}[/]")
|
|
722
|
+
if args.skillFolder:
|
|
723
|
+
status.update(
|
|
724
|
+
f"[bold blue]Loading agents from:[/][white] {args.folder}[/] "
|
|
725
|
+
f"[dim](& skills from {args.skillFolder})[/]"
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
status.update(f"[bold blue]Initializing agent:[/][white] {args.agent}[/]")
|
|
729
|
+
agent = await loadAndPrepareAgent(env, args, initialVariables)
|
|
730
|
+
|
|
731
|
+
agent.set_user_id(userId)
|
|
732
|
+
agent.set_session_id(sessionId)
|
|
733
|
+
|
|
734
|
+
status.update("[bold green]Ready![/]")
|
|
735
|
+
|
|
736
|
+
# Run conversation
|
|
737
|
+
enterPostmortem = await runConversationLoop(agent, args, initialVariables)
|
|
738
|
+
|
|
739
|
+
# Save artifacts
|
|
740
|
+
await saveExecutionArtifacts(agent, args)
|
|
741
|
+
|
|
742
|
+
# Post-mortem
|
|
743
|
+
await enterPostmortemIfNeeded(agent, args, enterPostmortem)
|
|
744
|
+
|
|
745
|
+
await env.ashutdown()
|
|
746
|
+
|
|
747
|
+
except Exception as e:
|
|
748
|
+
_handle_execution_error(e, args)
|
|
749
|
+
if env is not None and hasattr(env, "ashutdown"):
|
|
750
|
+
await env.ashutdown()
|
|
751
|
+
sys.exit(1)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _handle_execution_error(e: Exception, args: Args) -> None:
|
|
755
|
+
"""Handle execution errors with user-friendly output.
|
|
756
|
+
|
|
757
|
+
For known DolphinException types, display a clean error message.
|
|
758
|
+
For unknown exceptions, display the full traceback.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
e: The exception that occurred
|
|
762
|
+
args: Parsed CLI arguments
|
|
763
|
+
"""
|
|
764
|
+
from dolphin.core.common.exceptions import DolphinException, SkillException
|
|
765
|
+
|
|
766
|
+
# Determine verbosity level
|
|
767
|
+
show_full_traceback = flags.is_enabled(flags.DEBUG_MODE) or getattr(args, 'vv', False)
|
|
768
|
+
|
|
769
|
+
# Check if this is a known exception type with a friendly message
|
|
770
|
+
root_cause = _extract_root_cause(e)
|
|
771
|
+
|
|
772
|
+
if isinstance(root_cause, SkillException):
|
|
773
|
+
# SkillException has a detailed, user-friendly message
|
|
774
|
+
console(f"\n❌ Skill Error:\n{root_cause.message}")
|
|
775
|
+
if show_full_traceback:
|
|
776
|
+
console("\n--- Full Traceback (debug mode) ---")
|
|
777
|
+
traceback.print_exc()
|
|
778
|
+
elif isinstance(root_cause, DolphinException):
|
|
779
|
+
# Check if the message contains embedded SkillException info
|
|
780
|
+
skill_error_msg = _extract_skill_error_message(e)
|
|
781
|
+
if skill_error_msg:
|
|
782
|
+
# Display the extracted skill error message in a clean format
|
|
783
|
+
console(f"\n❌ Skill Error:\n{skill_error_msg}")
|
|
784
|
+
else:
|
|
785
|
+
# Other DolphinException types - show concise error
|
|
786
|
+
console(f"\n❌ Error [{root_cause.code}]: {root_cause.message}")
|
|
787
|
+
if show_full_traceback:
|
|
788
|
+
console("\n--- Full Traceback (debug mode) ---")
|
|
789
|
+
traceback.print_exc()
|
|
790
|
+
else:
|
|
791
|
+
# Unknown exception - show more details
|
|
792
|
+
console(f"\n❌ Error executing Dolphin agent: {e}")
|
|
793
|
+
if show_full_traceback or args.saveHistory:
|
|
794
|
+
traceback.print_exc()
|
|
795
|
+
else:
|
|
796
|
+
console("💡 Run with --vv or --debug for full traceback")
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _extract_root_cause(e: Exception) -> Exception:
|
|
800
|
+
"""Extract the root cause from a chain of exceptions.
|
|
801
|
+
|
|
802
|
+
Traverses the exception chain (__cause__ and __context__) to find
|
|
803
|
+
the original DolphinException that triggered the error.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
e: The top-level exception
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
The root cause exception (a DolphinException if found, otherwise the original)
|
|
810
|
+
"""
|
|
811
|
+
from dolphin.core.common.exceptions import DolphinException
|
|
812
|
+
|
|
813
|
+
# First, check if the current exception is already a DolphinException
|
|
814
|
+
if isinstance(e, DolphinException):
|
|
815
|
+
return e
|
|
816
|
+
|
|
817
|
+
# Traverse __cause__ chain (explicit "raise ... from ...")
|
|
818
|
+
current = e
|
|
819
|
+
while current.__cause__ is not None:
|
|
820
|
+
current = current.__cause__
|
|
821
|
+
if isinstance(current, DolphinException):
|
|
822
|
+
return current
|
|
823
|
+
|
|
824
|
+
# Traverse __context__ chain (implicit exception chaining)
|
|
825
|
+
current = e
|
|
826
|
+
while current.__context__ is not None:
|
|
827
|
+
current = current.__context__
|
|
828
|
+
if isinstance(current, DolphinException):
|
|
829
|
+
return current
|
|
830
|
+
|
|
831
|
+
# No DolphinException found, return original
|
|
832
|
+
return e
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _extract_skill_error_message(e: Exception) -> Optional[str]:
|
|
836
|
+
"""Try to extract a user-friendly skill error message from exception string.
|
|
837
|
+
|
|
838
|
+
Some exceptions wrap SkillException as a string (using str(e) instead of 'from e'),
|
|
839
|
+
so we need to parse the string to extract the formatted error message.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
e: The exception to analyze
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
The extracted skill error message if found, None otherwise
|
|
846
|
+
"""
|
|
847
|
+
error_str = str(e)
|
|
848
|
+
|
|
849
|
+
# Look for the SkillException pattern in the message
|
|
850
|
+
# Pattern: "Skill 'xxx' not found.\n\nAvailable skills..."
|
|
851
|
+
import re
|
|
852
|
+
|
|
853
|
+
# Try to find the skill error block
|
|
854
|
+
skill_error_pattern = r"(Skill '[^']+' not found\..*?Verify that the required skillkit module is loaded)"
|
|
855
|
+
match = re.search(skill_error_pattern, error_str, re.DOTALL)
|
|
856
|
+
|
|
857
|
+
if match:
|
|
858
|
+
return match.group(1)
|
|
859
|
+
|
|
860
|
+
# Alternative: look for SKILL_NOT_FOUND pattern
|
|
861
|
+
if "SKILL_NOT_FOUND" in error_str and "Available skills" in error_str:
|
|
862
|
+
# Extract from "Skill '" to the end of "Possible fixes" section
|
|
863
|
+
start_idx = error_str.find("Skill '")
|
|
864
|
+
if start_idx != -1:
|
|
865
|
+
# Find the end of the message (after "Possible fixes" section)
|
|
866
|
+
end_patterns = ["module is loaded", "skillkit module is loaded"]
|
|
867
|
+
end_idx = len(error_str)
|
|
868
|
+
for pattern in end_patterns:
|
|
869
|
+
idx = error_str.find(pattern, start_idx)
|
|
870
|
+
if idx != -1:
|
|
871
|
+
end_idx = min(end_idx, idx + len(pattern))
|
|
872
|
+
if end_idx > start_idx:
|
|
873
|
+
return error_str[start_idx:end_idx]
|
|
874
|
+
|
|
875
|
+
return None
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
async def runDolphin(args: Args) -> None:
|
|
879
|
+
"""Run Dolphin Language program
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
args: Parsed CLI arguments
|
|
883
|
+
"""
|
|
884
|
+
if args.agent:
|
|
885
|
+
# User specified a custom agent
|
|
886
|
+
await runDolphinAgent(args)
|
|
887
|
+
elif args.useBuiltinAgent:
|
|
888
|
+
# Use builtin explore agent (default explore mode)
|
|
889
|
+
await runBuiltinExploreAgent(args)
|
|
890
|
+
else:
|
|
891
|
+
console("Error: Must specify --agent or use explore mode")
|
|
892
|
+
sys.exit(1)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
async def runBuiltinExploreAgent(args: Args) -> None:
|
|
896
|
+
"""Run the builtin explore agent for interactive coding assistance
|
|
897
|
+
|
|
898
|
+
This provides a Claude Code / Codex-like experience with access to
|
|
899
|
+
local environment tools (bash, python, file operations).
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
args: Parsed CLI arguments
|
|
903
|
+
"""
|
|
904
|
+
from dolphin.cli.builtin_agents import BUILTIN_AGENTS_DIR, DEFAULT_EXPLORE_AGENT
|
|
905
|
+
from dolphin.sdk.runtime.env import Env
|
|
906
|
+
from dolphin.core.config.global_config import GlobalConfig
|
|
907
|
+
from dolphin.lib.skillkits.env_skillkit import EnvSkillkit
|
|
908
|
+
import logging
|
|
909
|
+
|
|
910
|
+
# Set the builtin agent directory and agent name
|
|
911
|
+
args.folder = BUILTIN_AGENTS_DIR
|
|
912
|
+
args.agent = DEFAULT_EXPLORE_AGENT
|
|
913
|
+
|
|
914
|
+
# Disable EXPLORE_BLOCK_V2 for explore mode (continue_exploration not yet supported in V2)
|
|
915
|
+
flags.set_flag(flags.EXPLORE_BLOCK_V2, False)
|
|
916
|
+
|
|
917
|
+
# Initialize environment
|
|
918
|
+
globalConfigPath = args.config if args.config else "./config/global.yaml"
|
|
919
|
+
globalConfig = GlobalConfig.from_yaml(globalConfigPath)
|
|
920
|
+
|
|
921
|
+
# Create environment with builtin agents directory
|
|
922
|
+
env = Env(
|
|
923
|
+
globalConfig=globalConfig,
|
|
924
|
+
agentFolderPath=BUILTIN_AGENTS_DIR,
|
|
925
|
+
skillkitFolderPath=args.skillFolder,
|
|
926
|
+
output_variables=[],
|
|
927
|
+
verbose=args.saveHistory,
|
|
928
|
+
is_cli=True,
|
|
929
|
+
log_level=(
|
|
930
|
+
logging.DEBUG if flags.is_enabled(flags.DEBUG_MODE) else logging.INFO
|
|
931
|
+
),
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Register EnvSkillkit for local bash/python execution
|
|
935
|
+
env_skillkit = EnvSkillkit()
|
|
936
|
+
env_skillkit.setGlobalConfig(globalConfig)
|
|
937
|
+
for skill in env_skillkit.getSkills():
|
|
938
|
+
env.globalSkills.installedSkillset.addSkill(skill)
|
|
939
|
+
env.globalSkills._syncAllSkills()
|
|
940
|
+
|
|
941
|
+
console(f"[bold green]👋 Hi! I'm Dolphin, your AI Pair Programmer.[/]")
|
|
942
|
+
console(f" I can help you write code, debug issues, and explore this project.")
|
|
943
|
+
console(f" What would you like to do today?\n")
|
|
944
|
+
|
|
945
|
+
# Run the agent with the enhanced environment
|
|
946
|
+
await _runDolphinAgentWithEnv(env, args)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
async def _runDolphinAgentWithEnv(env, args: Args) -> None:
|
|
950
|
+
"""Run Dolphin agent with a pre-configured environment
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
env: Pre-configured Dolphin environment
|
|
954
|
+
args: Parsed CLI arguments
|
|
955
|
+
"""
|
|
956
|
+
from dolphin.cli.utils.helpers import validateArgs, buildVariables, outputVariablesToJson
|
|
957
|
+
|
|
958
|
+
richConsole = RichConsole()
|
|
959
|
+
initialVariables = buildVariables(args)
|
|
960
|
+
|
|
961
|
+
userId = args.userId if args.userId else str(uuid.uuid4())
|
|
962
|
+
sessionId = args.sessionId if args.sessionId else str(uuid.uuid4())
|
|
963
|
+
|
|
964
|
+
try:
|
|
965
|
+
with richConsole.status("[bold green]Initializing agent...[/]") as status:
|
|
966
|
+
status.update(f"[bold blue]Loading agent:[/][white] {args.agent}[/]")
|
|
967
|
+
agent = await loadAndPrepareAgent(env, args, initialVariables)
|
|
968
|
+
|
|
969
|
+
agent.set_user_id(userId)
|
|
970
|
+
agent.set_session_id(sessionId)
|
|
971
|
+
|
|
972
|
+
status.update("[bold green]Ready![/]")
|
|
973
|
+
|
|
974
|
+
# Run conversation
|
|
975
|
+
enterPostmortem = await runConversationLoop(agent, args, initialVariables)
|
|
976
|
+
|
|
977
|
+
# Save artifacts
|
|
978
|
+
await saveExecutionArtifacts(agent, args)
|
|
979
|
+
|
|
980
|
+
# Post-mortem
|
|
981
|
+
await enterPostmortemIfNeeded(agent, args, enterPostmortem)
|
|
982
|
+
|
|
983
|
+
await env.ashutdown()
|
|
984
|
+
|
|
985
|
+
except Exception as e:
|
|
986
|
+
_handle_execution_error(e, args)
|
|
987
|
+
if env is not None and hasattr(env, "ashutdown"):
|
|
988
|
+
await env.ashutdown()
|
|
989
|
+
sys.exit(1)
|