kweaver-dolphin 0.2.0__py3-none-any.whl → 0.2.2__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.
- dolphin/cli/runner/runner.py +20 -0
- dolphin/cli/ui/console.py +29 -11
- dolphin/cli/utils/helpers.py +4 -4
- dolphin/core/agent/base_agent.py +2 -2
- dolphin/core/code_block/basic_code_block.py +140 -30
- dolphin/core/code_block/explore_block.py +353 -29
- dolphin/core/code_block/explore_block_v2.py +21 -17
- dolphin/core/code_block/explore_strategy.py +1 -0
- dolphin/core/code_block/judge_block.py +10 -1
- dolphin/core/code_block/skill_call_deduplicator.py +32 -10
- dolphin/core/code_block/tool_block.py +12 -3
- dolphin/core/common/constants.py +25 -1
- dolphin/core/config/global_config.py +35 -0
- dolphin/core/context/context.py +168 -5
- dolphin/core/context/cow_context.py +392 -0
- dolphin/core/flags/definitions.py +2 -2
- dolphin/core/runtime/runtime_instance.py +31 -0
- dolphin/core/skill/context_retention.py +3 -3
- dolphin/core/task_registry.py +404 -0
- dolphin/lib/__init__.py +0 -2
- dolphin/lib/skillkits/__init__.py +2 -2
- dolphin/lib/skillkits/plan_skillkit.py +756 -0
- dolphin/lib/skillkits/system_skillkit.py +103 -30
- dolphin/sdk/skill/global_skills.py +43 -3
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.2.dist-info}/METADATA +1 -1
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.2.dist-info}/RECORD +30 -28
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.2.dist-info}/WHEEL +1 -1
- kweaver_dolphin-0.2.2.dist-info/entry_points.txt +15 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +0 -452
- kweaver_dolphin-0.2.0.dist-info/entry_points.txt +0 -27
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.2.dist-info}/licenses/LICENSE.txt +0 -0
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -13,6 +13,8 @@ Design document: docs/design/architecture/explore_block_merge.md
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
from dolphin.core.task_registry import PlanExecMode
|
|
17
|
+
|
|
16
18
|
import asyncio
|
|
17
19
|
import json
|
|
18
20
|
import traceback
|
|
@@ -42,6 +44,7 @@ from dolphin.core.common.enums import (
|
|
|
42
44
|
)
|
|
43
45
|
from dolphin.core.common.constants import (
|
|
44
46
|
MAX_SKILL_CALL_TIMES,
|
|
47
|
+
MAX_PLAN_SILENT_ROUNDS,
|
|
45
48
|
get_msg_duplicate_skill_call,
|
|
46
49
|
)
|
|
47
50
|
from dolphin.core.context.context import Context
|
|
@@ -60,6 +63,7 @@ from dolphin.core.code_block.explore_strategy import (
|
|
|
60
63
|
from dolphin.core.code_block.skill_call_deduplicator import (
|
|
61
64
|
DefaultSkillCallDeduplicator,
|
|
62
65
|
)
|
|
66
|
+
from dolphin.core.skill.skill_matcher import SkillMatcher
|
|
63
67
|
from dolphin.core import flags
|
|
64
68
|
|
|
65
69
|
logger = get_logger("code_block.explore_block")
|
|
@@ -99,11 +103,6 @@ class ExploreBlock(BasicCodeBlock):
|
|
|
99
103
|
self.mode = "tool_call"
|
|
100
104
|
self.strategy = self._create_strategy()
|
|
101
105
|
|
|
102
|
-
# By default, deduplication is controlled by the Block parameter (finally takes effect in execute/continue_exploration)
|
|
103
|
-
self.enable_skill_deduplicator = getattr(
|
|
104
|
-
self, "enable_skill_deduplicator", True
|
|
105
|
-
)
|
|
106
|
-
|
|
107
106
|
# State Variables
|
|
108
107
|
self.times = 0
|
|
109
108
|
self.should_stop_exploration = False
|
|
@@ -114,6 +113,14 @@ class ExploreBlock(BasicCodeBlock):
|
|
|
114
113
|
# Incremented each time LLM returns tool calls (per batch, not per tool)
|
|
115
114
|
self.session_tool_call_counter = 0
|
|
116
115
|
|
|
116
|
+
# Plan mode: guard against excessive "silent" rounds where the agent does not make
|
|
117
|
+
# meaningful progress on the active plan (e.g., repeatedly calling unrelated tools).
|
|
118
|
+
self.plan_silent_max_rounds: int = MAX_PLAN_SILENT_ROUNDS
|
|
119
|
+
self._plan_silent_rounds: int = 0
|
|
120
|
+
self._plan_last_signature: Optional[tuple] = None
|
|
121
|
+
self._last_tool_name: Optional[str] = None
|
|
122
|
+
self._current_round_tools: List[str] = [] # Track all tools called in current round
|
|
123
|
+
|
|
117
124
|
# Hook-based verify attributes
|
|
118
125
|
self.on_stop: Optional[HookConfig] = None
|
|
119
126
|
self.current_attempt: int = 0
|
|
@@ -153,6 +160,23 @@ class ExploreBlock(BasicCodeBlock):
|
|
|
153
160
|
self.mode = parsed_mode
|
|
154
161
|
self.strategy = self._create_strategy()
|
|
155
162
|
|
|
163
|
+
# Handle exec_mode for plan orchestration
|
|
164
|
+
exec_mode_param = self.params.get("exec_mode")
|
|
165
|
+
if exec_mode_param:
|
|
166
|
+
# PlanExecMode.from_str handles validation and mapping (seq/para/etc.)
|
|
167
|
+
self.params["exec_mode"] = PlanExecMode.from_str(str(exec_mode_param))
|
|
168
|
+
|
|
169
|
+
# Optional: override plan silent rounds limit via DPH params.
|
|
170
|
+
silent_max = self.params.get("plan_silent_max_rounds")
|
|
171
|
+
if silent_max is not None:
|
|
172
|
+
try:
|
|
173
|
+
silent_max_int = int(silent_max)
|
|
174
|
+
if silent_max_int < 0:
|
|
175
|
+
raise ValueError("plan_silent_max_rounds must be >= 0")
|
|
176
|
+
self.plan_silent_max_rounds = silent_max_int
|
|
177
|
+
except Exception as e:
|
|
178
|
+
raise ValueError(f"Invalid plan_silent_max_rounds: {silent_max}") from e
|
|
179
|
+
|
|
156
180
|
async def execute(
|
|
157
181
|
self,
|
|
158
182
|
content,
|
|
@@ -185,6 +209,8 @@ class ExploreBlock(BasicCodeBlock):
|
|
|
185
209
|
# Save the current skills configuration to context, so it can be inherited during multi-turn conversations.
|
|
186
210
|
if getattr(self, "skills", None):
|
|
187
211
|
self.context.set_last_skills(self.skills)
|
|
212
|
+
# Inject context to skillkits that support it
|
|
213
|
+
self._inject_context_to_skillkits()
|
|
188
214
|
|
|
189
215
|
# Save the current mode configuration to context for inheritance in multi-turn conversations.
|
|
190
216
|
if getattr(self, "mode", None):
|
|
@@ -299,7 +325,7 @@ class ExploreBlock(BasicCodeBlock):
|
|
|
299
325
|
has_add = self._write_output_var(ret, has_add)
|
|
300
326
|
yield ret
|
|
301
327
|
|
|
302
|
-
if not self._should_continue_explore():
|
|
328
|
+
if not await self._should_continue_explore():
|
|
303
329
|
break
|
|
304
330
|
|
|
305
331
|
def _write_output_var(
|
|
@@ -547,6 +573,11 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
547
573
|
f"length[{self.context.get_messages().length()}]"
|
|
548
574
|
)
|
|
549
575
|
|
|
576
|
+
# Reset tool tracking at the start of each round to prevent stale state
|
|
577
|
+
# This ensures used_plan_tool detection is accurate in plan silent rounds guard
|
|
578
|
+
self._last_tool_name = None
|
|
579
|
+
self._current_round_tools = [] # Clear tools from previous round
|
|
580
|
+
|
|
550
581
|
# Check if there is a tool call for interruption recovery
|
|
551
582
|
if self._has_pending_tool_call():
|
|
552
583
|
async for ret in self._handle_resumed_tool_call():
|
|
@@ -560,7 +591,6 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
560
591
|
intervention_tmp_key = "intervention_explore_block_vars"
|
|
561
592
|
has_intervention = intervention_tmp_key in self.context.get_all_variables().keys()
|
|
562
593
|
has_tool = "tool" in self.context.get_all_variables().keys()
|
|
563
|
-
logger.debug(f"[DEBUG _has_pending_tool_call] has_intervention={has_intervention}, has_tool={has_tool}")
|
|
564
594
|
return has_intervention and has_tool
|
|
565
595
|
|
|
566
596
|
async def _handle_resumed_tool_call(self):
|
|
@@ -597,6 +627,9 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
597
627
|
raw_tool_args = input_dict["tool_args"]
|
|
598
628
|
function_params_json = {arg["key"]: arg["value"] for arg in raw_tool_args}
|
|
599
629
|
|
|
630
|
+
# Get saved stage_id for resume
|
|
631
|
+
saved_stage_id = intervention_vars.get("stage_id")
|
|
632
|
+
|
|
600
633
|
# *** FIX: Update the last tool_call message with modified parameters ***
|
|
601
634
|
# This ensures LLM sees the actual parameters used, not the original ones
|
|
602
635
|
messages = self.context.get_messages()
|
|
@@ -611,7 +644,6 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
611
644
|
# Update the arguments with modified parameters
|
|
612
645
|
import json
|
|
613
646
|
tool_call.function.arguments = json.dumps(function_params_json, ensure_ascii=False)
|
|
614
|
-
logger.debug(f"[FIX] Updated tool_call arguments from original to modified: {function_params_json}")
|
|
615
647
|
|
|
616
648
|
if self.recorder:
|
|
617
649
|
self.recorder.update(
|
|
@@ -674,7 +706,7 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
674
706
|
|
|
675
707
|
# Normal execution (not skipped)
|
|
676
708
|
try:
|
|
677
|
-
props = {"intervention": False}
|
|
709
|
+
props = {"intervention": False, "saved_stage_id": saved_stage_id}
|
|
678
710
|
have_answer = False
|
|
679
711
|
|
|
680
712
|
async for resp in self.skill_run(
|
|
@@ -864,7 +896,11 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
864
896
|
tool_calls = [single] if single else []
|
|
865
897
|
|
|
866
898
|
if not tool_calls:
|
|
867
|
-
# No tool call detected:
|
|
899
|
+
# No tool call detected: terminate normally
|
|
900
|
+
# Note: Plan mode continuation logic is handled in _should_continue_explore()
|
|
901
|
+
# which checks has_active_plan() and may inject guidance messages if needed.
|
|
902
|
+
|
|
903
|
+
# Normal termination
|
|
868
904
|
# If there is pending content, merge before adding
|
|
869
905
|
if self.pending_content:
|
|
870
906
|
# Merge pending content and current content
|
|
@@ -877,9 +913,18 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
877
913
|
self._append_assistant_message(stream_item.answer)
|
|
878
914
|
self.context.debug(f"no valid skill call, answer[{stream_item.answer}]")
|
|
879
915
|
|
|
880
|
-
#
|
|
881
|
-
|
|
882
|
-
|
|
916
|
+
# If plan mode is active, do NOT stop immediately.
|
|
917
|
+
# Instead, keep exploration running so the agent can poll progress
|
|
918
|
+
# (e.g., via _check_progress / _wait) until tasks reach terminal states.
|
|
919
|
+
if hasattr(self.context, "has_active_plan") and await self.context.has_active_plan():
|
|
920
|
+
self.should_stop_exploration = False
|
|
921
|
+
self.context.debug("No tool call, but plan is active; continuing exploration")
|
|
922
|
+
# Avoid tight looping while waiting for running tasks to make progress.
|
|
923
|
+
# This small backoff gives subtasks time to update their status.
|
|
924
|
+
await asyncio.sleep(0.2)
|
|
925
|
+
else:
|
|
926
|
+
self.should_stop_exploration = True
|
|
927
|
+
self.context.debug("No tool call, stopping exploration")
|
|
883
928
|
return
|
|
884
929
|
|
|
885
930
|
# Reset no-tool-call count (because this round has tool call)
|
|
@@ -946,6 +991,9 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
946
991
|
"""Execute tool call"""
|
|
947
992
|
# Checkpoint: Check user interrupt before executing tool
|
|
948
993
|
self.context.check_user_interrupt()
|
|
994
|
+
self._last_tool_name = tool_call.name
|
|
995
|
+
# Track all tools in current round for accurate plan silent rounds detection
|
|
996
|
+
self._current_round_tools.append(tool_call.name)
|
|
949
997
|
|
|
950
998
|
intervention_tmp_key = "intervention_explore_block_vars"
|
|
951
999
|
|
|
@@ -955,11 +1003,13 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
955
1003
|
metadata = None
|
|
956
1004
|
|
|
957
1005
|
try:
|
|
1006
|
+
# Save intervention vars (stage_id will be filled by skill_run after creating the stage)
|
|
958
1007
|
intervention_vars = {
|
|
959
1008
|
"prompt": self.context.get_messages().get_messages_as_dict(),
|
|
960
1009
|
"tool_name": tool_call.name,
|
|
961
1010
|
"cur_llm_stream_answer": stream_item.answer,
|
|
962
1011
|
"all_answer": stream_item.answer,
|
|
1012
|
+
"stage_id": None, # Will be updated by skill_run() after stage creation
|
|
963
1013
|
}
|
|
964
1014
|
|
|
965
1015
|
self.context.set_variable(intervention_tmp_key, intervention_vars)
|
|
@@ -1151,34 +1201,173 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1151
1201
|
f"error in call {tool_name} tool, error type: {type(e)}, error info: {str(e)}, error trace: {error_trace}"
|
|
1152
1202
|
)
|
|
1153
1203
|
|
|
1154
|
-
def _should_continue_explore(self) -> bool:
|
|
1204
|
+
async def _should_continue_explore(self) -> bool:
|
|
1155
1205
|
"""Check whether to continue the next exploration.
|
|
1156
1206
|
|
|
1157
|
-
Termination conditions:
|
|
1207
|
+
Termination conditions (Early Return pattern):
|
|
1158
1208
|
1. Maximum number of tool calls has been reached
|
|
1159
|
-
2.
|
|
1160
|
-
3.
|
|
1209
|
+
2. Plan mode has special continuation logic
|
|
1210
|
+
3. Number of repeated tool calls exceeds limit
|
|
1211
|
+
4. No tool call occurred once
|
|
1161
1212
|
"""
|
|
1162
|
-
# 1.
|
|
1213
|
+
# 1. Early return: max skill calls reached
|
|
1163
1214
|
if self.times >= MAX_SKILL_CALL_TIMES:
|
|
1164
1215
|
return False
|
|
1165
1216
|
|
|
1166
|
-
# 2.
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
recent_calls = list(deduplicator.skillcalls.values())
|
|
1170
|
-
if (
|
|
1171
|
-
recent_calls
|
|
1172
|
-
and max(recent_calls) >= DefaultSkillCallDeduplicator.MAX_DUPLICATE_COUNT
|
|
1173
|
-
):
|
|
1174
|
-
return False
|
|
1217
|
+
# 2. Plan mode has special logic - delegate to separate method
|
|
1218
|
+
if hasattr(self.context, "has_active_plan") and await self.context.has_active_plan():
|
|
1219
|
+
return await self._should_continue_explore_in_plan_mode()
|
|
1175
1220
|
|
|
1176
|
-
# 3.
|
|
1221
|
+
# 3. Early return: repeated calls exceeding limit
|
|
1222
|
+
if self._has_exceeded_duplicate_limit():
|
|
1223
|
+
return False
|
|
1224
|
+
|
|
1225
|
+
# 4. Early return: no tool call
|
|
1177
1226
|
if self.should_stop_exploration:
|
|
1178
1227
|
return False
|
|
1179
1228
|
|
|
1180
1229
|
return True
|
|
1181
1230
|
|
|
1231
|
+
async def _should_continue_explore_in_plan_mode(self) -> bool:
|
|
1232
|
+
"""Check whether to continue exploration in plan mode.
|
|
1233
|
+
|
|
1234
|
+
Plan mode has special continuation logic:
|
|
1235
|
+
- Must continue if tasks are active (unless max attempts reached)
|
|
1236
|
+
- Tracks progress via TaskRegistry signature
|
|
1237
|
+
- Guards against silent rounds (no progress for too long)
|
|
1238
|
+
- Prevents infinite loops when agent stops without progress
|
|
1239
|
+
|
|
1240
|
+
Returns:
|
|
1241
|
+
True if exploration should continue, False otherwise
|
|
1242
|
+
"""
|
|
1243
|
+
from dolphin.core.common.constants import PLAN_ORCHESTRATION_TOOLS
|
|
1244
|
+
|
|
1245
|
+
# Check if current round used any plan orchestration tool
|
|
1246
|
+
used_plan_tool = self._used_plan_tool_this_round()
|
|
1247
|
+
|
|
1248
|
+
# Check for actual task progress and get current signature
|
|
1249
|
+
registry = getattr(self.context, "task_registry", None)
|
|
1250
|
+
has_progress, current_signature = await self._check_plan_progress_with_signature(registry)
|
|
1251
|
+
|
|
1252
|
+
# Early return: agent stopped without progress or plan tool usage
|
|
1253
|
+
if self.should_stop_exploration:
|
|
1254
|
+
if not has_progress and not used_plan_tool:
|
|
1255
|
+
logger.warning(
|
|
1256
|
+
"Plan mode: Agent stopped without task progress or plan tool usage. "
|
|
1257
|
+
"Terminating to prevent infinite loop."
|
|
1258
|
+
)
|
|
1259
|
+
return False
|
|
1260
|
+
|
|
1261
|
+
if not has_progress and self._plan_silent_rounds >= 2:
|
|
1262
|
+
logger.warning(
|
|
1263
|
+
f"Plan mode: Agent stopped with plan tool but no progress for "
|
|
1264
|
+
f"{self._plan_silent_rounds} rounds. Terminating to prevent infinite loop."
|
|
1265
|
+
)
|
|
1266
|
+
return False
|
|
1267
|
+
|
|
1268
|
+
# Update silent rounds tracking and check limit (also updates signature)
|
|
1269
|
+
self._update_plan_silent_rounds(current_signature, has_progress, used_plan_tool)
|
|
1270
|
+
|
|
1271
|
+
# Early return: no progress and agent wants to stop
|
|
1272
|
+
if self.should_stop_exploration and not has_progress:
|
|
1273
|
+
logger.warning(
|
|
1274
|
+
"Plan mode: Stopping - no tool calls and no task progress. "
|
|
1275
|
+
"Prevents infinite loops from repeated responses."
|
|
1276
|
+
)
|
|
1277
|
+
return False
|
|
1278
|
+
|
|
1279
|
+
return True
|
|
1280
|
+
|
|
1281
|
+
def _used_plan_tool_this_round(self) -> bool:
|
|
1282
|
+
"""Check if any plan orchestration tool was used in current round."""
|
|
1283
|
+
from dolphin.core.common.constants import PLAN_ORCHESTRATION_TOOLS
|
|
1284
|
+
|
|
1285
|
+
if not self._current_round_tools:
|
|
1286
|
+
return False
|
|
1287
|
+
|
|
1288
|
+
return any(
|
|
1289
|
+
tool_name in PLAN_ORCHESTRATION_TOOLS
|
|
1290
|
+
for tool_name in self._current_round_tools
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
async def _check_plan_progress_with_signature(self, registry) -> tuple[bool, tuple]:
|
|
1294
|
+
"""Check if tasks have made progress since last round.
|
|
1295
|
+
|
|
1296
|
+
Args:
|
|
1297
|
+
registry: TaskRegistry instance
|
|
1298
|
+
|
|
1299
|
+
Returns:
|
|
1300
|
+
Tuple of (has_progress, current_signature)
|
|
1301
|
+
"""
|
|
1302
|
+
if registry is None:
|
|
1303
|
+
return False, ()
|
|
1304
|
+
|
|
1305
|
+
signature = await registry.get_progress_signature()
|
|
1306
|
+
has_progress = (
|
|
1307
|
+
self._plan_last_signature is None
|
|
1308
|
+
or signature != self._plan_last_signature
|
|
1309
|
+
)
|
|
1310
|
+
return has_progress, signature
|
|
1311
|
+
|
|
1312
|
+
def _update_plan_silent_rounds(
|
|
1313
|
+
self, current_signature: tuple, has_progress: bool, used_plan_tool: bool
|
|
1314
|
+
) -> None:
|
|
1315
|
+
"""Update silent rounds counter and check threshold.
|
|
1316
|
+
|
|
1317
|
+
Silent rounds are rounds where:
|
|
1318
|
+
- No task status progress AND
|
|
1319
|
+
- No plan orchestration tools used
|
|
1320
|
+
|
|
1321
|
+
Args:
|
|
1322
|
+
current_signature: Current task progress signature
|
|
1323
|
+
has_progress: Whether progress was detected this round
|
|
1324
|
+
used_plan_tool: Whether plan orchestration tool was used
|
|
1325
|
+
|
|
1326
|
+
Raises:
|
|
1327
|
+
ToolInterrupt: If silent rounds exceed threshold
|
|
1328
|
+
"""
|
|
1329
|
+
if not self.plan_silent_max_rounds or self.plan_silent_max_rounds <= 0:
|
|
1330
|
+
return
|
|
1331
|
+
|
|
1332
|
+
# Reset or increment silent rounds counter
|
|
1333
|
+
if has_progress or used_plan_tool:
|
|
1334
|
+
self._plan_silent_rounds = 0
|
|
1335
|
+
else:
|
|
1336
|
+
self._plan_silent_rounds += 1
|
|
1337
|
+
|
|
1338
|
+
# Update last signature for next round comparison
|
|
1339
|
+
self._plan_last_signature = current_signature
|
|
1340
|
+
|
|
1341
|
+
if self._plan_silent_rounds >= self.plan_silent_max_rounds:
|
|
1342
|
+
raise ToolInterrupt(
|
|
1343
|
+
"Plan mode terminated: no task status progress for too many rounds. "
|
|
1344
|
+
"Use _wait() or _check_progress() instead of repeatedly calling unrelated tools."
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
def _has_exceeded_duplicate_limit(self) -> bool:
|
|
1348
|
+
"""Check if repeated tool calls have exceeded the limit.
|
|
1349
|
+
|
|
1350
|
+
Returns:
|
|
1351
|
+
True if duplicate limit exceeded, False otherwise
|
|
1352
|
+
"""
|
|
1353
|
+
deduplicator = self.strategy.get_deduplicator()
|
|
1354
|
+
if not hasattr(deduplicator, 'skillcalls') or not deduplicator.skillcalls:
|
|
1355
|
+
return False
|
|
1356
|
+
|
|
1357
|
+
# Ignore polling-style tools
|
|
1358
|
+
ignored_tools = getattr(
|
|
1359
|
+
deduplicator, "_always_allow_duplicate_skills", set()
|
|
1360
|
+
) or set()
|
|
1361
|
+
|
|
1362
|
+
counts = []
|
|
1363
|
+
for call_key, count in deduplicator.skillcalls.items():
|
|
1364
|
+
tool_name = str(call_key).split(":", 1)[0]
|
|
1365
|
+
if tool_name in ignored_tools:
|
|
1366
|
+
continue
|
|
1367
|
+
counts.append(count)
|
|
1368
|
+
|
|
1369
|
+
return counts and max(counts) >= DefaultSkillCallDeduplicator.MAX_DUPLICATE_COUNT
|
|
1370
|
+
|
|
1182
1371
|
def _process_skill_result_with_hook(self, skill_name: str) -> tuple[str | None, dict]:
|
|
1183
1372
|
"""Handle skill results using skillkit_hook"""
|
|
1184
1373
|
# Get skill object
|
|
@@ -1306,7 +1495,7 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1306
1495
|
async for ret in self._explore_once(no_cache=True):
|
|
1307
1496
|
yield ret
|
|
1308
1497
|
|
|
1309
|
-
if not self._should_continue_explore():
|
|
1498
|
+
if not await self._should_continue_explore():
|
|
1310
1499
|
break
|
|
1311
1500
|
|
|
1312
1501
|
# 6. Cleanup
|
|
@@ -1353,6 +1542,103 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1353
1542
|
|
|
1354
1543
|
if getattr(self, "skills", None):
|
|
1355
1544
|
self.context.set_last_skills(self.skills)
|
|
1545
|
+
# Inject context to skillkits that support it
|
|
1546
|
+
self._inject_context_to_skillkits()
|
|
1547
|
+
|
|
1548
|
+
def _inject_context_to_skillkits(self):
|
|
1549
|
+
"""Inject execution context to skillkits that need it (e.g., PlanSkillkit).
|
|
1550
|
+
|
|
1551
|
+
This allows skillkits to access runtime context for operations like
|
|
1552
|
+
task registry management, variable forking, etc.
|
|
1553
|
+
"""
|
|
1554
|
+
if not self.skills or not self.context:
|
|
1555
|
+
return
|
|
1556
|
+
|
|
1557
|
+
skill_list = self._resolve_skill_list()
|
|
1558
|
+
if not skill_list:
|
|
1559
|
+
return
|
|
1560
|
+
|
|
1561
|
+
self._inject_to_unique_skillkits(skill_list)
|
|
1562
|
+
|
|
1563
|
+
def _resolve_skill_list(self) -> list:
|
|
1564
|
+
"""Convert self.skills to a unified list of SkillFunction objects.
|
|
1565
|
+
|
|
1566
|
+
Returns:
|
|
1567
|
+
List of SkillFunction objects, or empty list if conversion fails
|
|
1568
|
+
"""
|
|
1569
|
+
# Case 1: Skillset object with getSkills() method
|
|
1570
|
+
if hasattr(self.skills, 'getSkills'):
|
|
1571
|
+
return self.skills.getSkills()
|
|
1572
|
+
|
|
1573
|
+
# Case 2: String list (e.g., ["plan_skillkit.*", "search.*"])
|
|
1574
|
+
if isinstance(self.skills, list) and self.skills and isinstance(self.skills[0], str):
|
|
1575
|
+
return self._resolve_skill_patterns_to_functions()
|
|
1576
|
+
|
|
1577
|
+
# Case 3: Already a list of SkillFunction objects
|
|
1578
|
+
return self.skills if isinstance(self.skills, list) else []
|
|
1579
|
+
|
|
1580
|
+
def _resolve_skill_patterns_to_functions(self) -> list:
|
|
1581
|
+
"""Resolve skill name patterns to SkillFunction objects.
|
|
1582
|
+
|
|
1583
|
+
Returns:
|
|
1584
|
+
List of matched SkillFunction objects
|
|
1585
|
+
"""
|
|
1586
|
+
current_skillkit = self.context.get_skillkit()
|
|
1587
|
+
if not current_skillkit:
|
|
1588
|
+
return []
|
|
1589
|
+
|
|
1590
|
+
available_skills = current_skillkit.getSkills()
|
|
1591
|
+
owner_names = SkillMatcher.get_owner_skillkits(available_skills)
|
|
1592
|
+
|
|
1593
|
+
# Match requested patterns against available skills
|
|
1594
|
+
matched_skills = []
|
|
1595
|
+
for pattern in self.skills:
|
|
1596
|
+
for skill in available_skills:
|
|
1597
|
+
if SkillMatcher.match_skill(skill, pattern, owner_names=owner_names):
|
|
1598
|
+
matched_skills.append(skill)
|
|
1599
|
+
|
|
1600
|
+
return matched_skills
|
|
1601
|
+
|
|
1602
|
+
def _inject_to_unique_skillkits(self, skill_list: list):
|
|
1603
|
+
"""Inject context to unique skillkit instances.
|
|
1604
|
+
|
|
1605
|
+
Args:
|
|
1606
|
+
skill_list: List of SkillFunction objects
|
|
1607
|
+
|
|
1608
|
+
Note:
|
|
1609
|
+
Uses skillkit instance ID to avoid duplicate injections
|
|
1610
|
+
"""
|
|
1611
|
+
processed_skillkits = set()
|
|
1612
|
+
|
|
1613
|
+
for skill in skill_list:
|
|
1614
|
+
skillkit = self._get_skillkit_from_skill(skill)
|
|
1615
|
+
if not skillkit:
|
|
1616
|
+
continue
|
|
1617
|
+
|
|
1618
|
+
skillkit_id = id(skillkit)
|
|
1619
|
+
if skillkit_id in processed_skillkits:
|
|
1620
|
+
continue
|
|
1621
|
+
|
|
1622
|
+
skillkit.setContext(self.context)
|
|
1623
|
+
processed_skillkits.add(skillkit_id)
|
|
1624
|
+
|
|
1625
|
+
def _get_skillkit_from_skill(self, skill):
|
|
1626
|
+
"""Extract skillkit from a skill object if it supports context injection.
|
|
1627
|
+
|
|
1628
|
+
Args:
|
|
1629
|
+
skill: Skill object (typically SkillFunction)
|
|
1630
|
+
|
|
1631
|
+
Returns:
|
|
1632
|
+
Skillkit instance if valid, None otherwise
|
|
1633
|
+
"""
|
|
1634
|
+
if not hasattr(skill, 'owner_skillkit'):
|
|
1635
|
+
return None
|
|
1636
|
+
|
|
1637
|
+
skillkit = skill.owner_skillkit
|
|
1638
|
+
if not skillkit or not hasattr(skillkit, 'setContext'):
|
|
1639
|
+
return None
|
|
1640
|
+
|
|
1641
|
+
return skillkit
|
|
1356
1642
|
|
|
1357
1643
|
def _resolve_mode(self, kwargs: dict):
|
|
1358
1644
|
"""Resolve exploration mode from kwargs or inherit from context."""
|
|
@@ -1391,6 +1677,12 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1391
1677
|
tools_format=self.tools_format,
|
|
1392
1678
|
)
|
|
1393
1679
|
|
|
1680
|
+
# Auto-inject Plan orchestration guidance when plan_skillkit is used
|
|
1681
|
+
if self._has_plan_skillkit():
|
|
1682
|
+
plan_guidance = self._get_plan_guidance()
|
|
1683
|
+
if plan_guidance:
|
|
1684
|
+
system_message = system_message + "\n\n" + plan_guidance
|
|
1685
|
+
|
|
1394
1686
|
if len(system_message.strip()) > 0 and self.context.context_manager:
|
|
1395
1687
|
self.context.add_bucket(
|
|
1396
1688
|
BuildInBucket.SYSTEM.value,
|
|
@@ -1398,6 +1690,38 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1398
1690
|
message_role=MessageRole.SYSTEM,
|
|
1399
1691
|
)
|
|
1400
1692
|
|
|
1693
|
+
def _has_plan_skillkit(self) -> bool:
|
|
1694
|
+
"""Check if plan_skillkit is included in the current skills."""
|
|
1695
|
+
if not hasattr(self, "skills") or not self.skills:
|
|
1696
|
+
return False
|
|
1697
|
+
|
|
1698
|
+
# Check if skills list contains plan_skillkit pattern
|
|
1699
|
+
if isinstance(self.skills, list):
|
|
1700
|
+
for pattern in self.skills:
|
|
1701
|
+
if isinstance(pattern, str) and "plan_skillkit" in pattern:
|
|
1702
|
+
return True
|
|
1703
|
+
|
|
1704
|
+
return False
|
|
1705
|
+
|
|
1706
|
+
def _get_plan_guidance(self) -> str:
|
|
1707
|
+
"""Get auto-injected guidance for using plan orchestration tools.
|
|
1708
|
+
|
|
1709
|
+
Returns:
|
|
1710
|
+
Multi-line guidance string for plan workflow
|
|
1711
|
+
"""
|
|
1712
|
+
return """# Plan Orchestration Workflow
|
|
1713
|
+
|
|
1714
|
+
When using plan tools to break down complex tasks:
|
|
1715
|
+
|
|
1716
|
+
1. **Create Plan**: Use `_plan_tasks` to define subtasks with id, name, and prompt
|
|
1717
|
+
2. **Monitor Progress**: Call `_check_progress` to track task status (provides next-step guidance)
|
|
1718
|
+
3. **Retrieve Results**: When all tasks complete:
|
|
1719
|
+
- Use `_get_task_output()` to get all results at once (recommended)
|
|
1720
|
+
- Or use `_get_task_output(task_id)` for a specific task output
|
|
1721
|
+
4. **Synthesize**: Combine all outputs into a comprehensive response for the user
|
|
1722
|
+
|
|
1723
|
+
Important: Your response is INCOMPLETE if you stop after tasks finish. You MUST retrieve outputs and provide a final synthesized answer."""
|
|
1724
|
+
|
|
1401
1725
|
def _apply_deduplicator_config(self, kwargs: dict):
|
|
1402
1726
|
"""Apply skill deduplicator configuration."""
|
|
1403
1727
|
if "enable_skill_deduplicator" in kwargs:
|
|
@@ -322,6 +322,10 @@ class ExploreBlockV2(BasicCodeBlock):
|
|
|
322
322
|
raw_tool_args = input_dict["tool_args"]
|
|
323
323
|
function_params_json = {arg["key"]: arg["value"] for arg in raw_tool_args}
|
|
324
324
|
|
|
325
|
+
# Get saved stage_id for resume
|
|
326
|
+
saved_stage_id = intervention_vars.get("stage_id")
|
|
327
|
+
logger.debug(f"Resuming tool call for {input_dict['tool_name']}, saved_stage_id: {saved_stage_id}")
|
|
328
|
+
|
|
325
329
|
# *** FIX: Update the last tool_call message with modified parameters ***
|
|
326
330
|
# This ensures LLM sees the actual parameters used, not the original ones
|
|
327
331
|
messages = self.context.get_messages()
|
|
@@ -336,19 +340,21 @@ class ExploreBlockV2(BasicCodeBlock):
|
|
|
336
340
|
# Update the arguments with modified parameters
|
|
337
341
|
import json
|
|
338
342
|
tool_call.function.arguments = json.dumps(function_params_json, ensure_ascii=False)
|
|
339
|
-
logger.debug(f"[FIX] Updated tool_call arguments from original to modified: {function_params_json}")
|
|
340
343
|
|
|
341
|
-
(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
)
|
|
344
|
+
# *** FIX: Don't call recorder.update() here during resume ***
|
|
345
|
+
# skill_run() will create the stage with the correct saved_stage_id
|
|
346
|
+
# Calling update() here would create an extra stage with a new ID
|
|
347
|
+
# (
|
|
348
|
+
# self.recorder.update(
|
|
349
|
+
# stage=TypeStage.SKILL,
|
|
350
|
+
# source_type=SourceType.EXPLORE,
|
|
351
|
+
# skill_name=function_name,
|
|
352
|
+
# skill_type=self.context.get_skill_type(function_name),
|
|
353
|
+
# skill_args=function_params_json,
|
|
354
|
+
# )
|
|
355
|
+
# if self.recorder
|
|
356
|
+
# else None
|
|
357
|
+
# )
|
|
352
358
|
|
|
353
359
|
# *** Handle skip action ***
|
|
354
360
|
skip_tool = self.context.get_var_value("__skip_tool__")
|
|
@@ -403,7 +409,7 @@ class ExploreBlockV2(BasicCodeBlock):
|
|
|
403
409
|
|
|
404
410
|
# Normal execution (not skipped)
|
|
405
411
|
try:
|
|
406
|
-
props = {"intervention": False}
|
|
412
|
+
props = {"intervention": False, "saved_stage_id": saved_stage_id}
|
|
407
413
|
have_answer = False
|
|
408
414
|
|
|
409
415
|
async for resp in self.skill_run(
|
|
@@ -561,7 +567,6 @@ class ExploreBlockV2(BasicCodeBlock):
|
|
|
561
567
|
return
|
|
562
568
|
|
|
563
569
|
# Add assistant message containing tool calls
|
|
564
|
-
logger.debug(f"[DEBUG] Tool call detected, preparing to execute: {stream_item.tool_name}")
|
|
565
570
|
tool_call_id = f"call_{stream_item.tool_name}_{self.times}"
|
|
566
571
|
tool_call_openai_format = [
|
|
567
572
|
{
|
|
@@ -588,24 +593,23 @@ class ExploreBlockV2(BasicCodeBlock):
|
|
|
588
593
|
)
|
|
589
594
|
self.deduplicator_skillcall.add(tool_call)
|
|
590
595
|
|
|
591
|
-
logger.debug(f"[DEBUG] Calling _execute_tool_call for: {stream_item.tool_name}")
|
|
592
596
|
async for ret in self._execute_tool_call(stream_item, tool_call_id):
|
|
593
597
|
yield ret
|
|
594
|
-
logger.debug(f"[DEBUG] _execute_tool_call completed for: {stream_item.tool_name}")
|
|
595
598
|
else:
|
|
596
599
|
await self._handle_duplicate_tool_call(tool_call, stream_item)
|
|
597
600
|
|
|
598
601
|
async def _execute_tool_call(self, stream_item, tool_call_id: str):
|
|
599
602
|
"""Execute tool call"""
|
|
600
|
-
logger.debug(f"[DEBUG] _execute_tool_call ENTERED for: {stream_item.tool_name}")
|
|
601
603
|
intervention_tmp_key = "intervention_explore_block_vars"
|
|
602
604
|
|
|
603
605
|
try:
|
|
606
|
+
# Save intervention vars (stage_id will be filled by skill_run after creating the stage)
|
|
604
607
|
intervention_vars = {
|
|
605
608
|
"prompt": self.context.get_messages().get_messages_as_dict(),
|
|
606
609
|
"tool_name": stream_item.tool_name,
|
|
607
610
|
"cur_llm_stream_answer": stream_item.answer,
|
|
608
611
|
"all_answer": stream_item.answer,
|
|
612
|
+
"stage_id": None, # Will be updated by skill_run() after stage creation
|
|
609
613
|
}
|
|
610
614
|
|
|
611
615
|
self.context.set_variable(intervention_tmp_key, intervention_vars)
|
|
@@ -668,6 +668,7 @@ class ToolCallStrategy(ExploreStrategy):
|
|
|
668
668
|
f"Tool call {info.name} (id={info.id}) skipped: "
|
|
669
669
|
f"Stream ended but JSON arguments incomplete or invalid. "
|
|
670
670
|
f"Raw arguments: '{info.raw_arguments}'"
|
|
671
|
+
f"finish_reason: {stream_item.finish_reason}"
|
|
671
672
|
)
|
|
672
673
|
|
|
673
674
|
return result
|