kweaver-dolphin 0.1.0__py3-none-any.whl → 0.2.1__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 +35 -17
- dolphin/cli/utils/helpers.py +4 -4
- dolphin/core/agent/base_agent.py +70 -7
- dolphin/core/code_block/basic_code_block.py +162 -26
- dolphin/core/code_block/explore_block.py +438 -35
- dolphin/core/code_block/explore_block_v2.py +105 -16
- dolphin/core/code_block/explore_strategy.py +3 -1
- dolphin/core/code_block/judge_block.py +41 -8
- dolphin/core/code_block/skill_call_deduplicator.py +32 -10
- dolphin/core/code_block/tool_block.py +69 -23
- dolphin/core/common/constants.py +25 -1
- dolphin/core/config/global_config.py +35 -0
- dolphin/core/context/context.py +175 -9
- dolphin/core/context/cow_context.py +392 -0
- dolphin/core/executor/dolphin_executor.py +9 -0
- dolphin/core/flags/definitions.py +2 -2
- dolphin/core/llm/llm.py +2 -3
- dolphin/core/llm/llm_client.py +1 -0
- dolphin/core/runtime/runtime_instance.py +31 -0
- dolphin/core/skill/context_retention.py +3 -3
- dolphin/core/task_registry.py +404 -0
- dolphin/core/utils/cache_kv.py +70 -8
- dolphin/core/utils/tools.py +2 -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
- dolphin/sdk/skill/traditional_toolkit.py +4 -0
- {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/METADATA +1 -1
- {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/RECORD +36 -34
- {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/WHEEL +1 -1
- kweaver_dolphin-0.2.1.dist-info/entry_points.txt +15 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +0 -452
- kweaver_dolphin-0.1.0.dist-info/entry_points.txt +0 -27
- {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/licenses/LICENSE.txt +0 -0
- {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.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():
|
|
@@ -558,10 +589,9 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
558
589
|
def _has_pending_tool_call(self) -> bool:
|
|
559
590
|
"""Check if there are pending tool calls (interrupt recovery)"""
|
|
560
591
|
intervention_tmp_key = "intervention_explore_block_vars"
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
)
|
|
592
|
+
has_intervention = intervention_tmp_key in self.context.get_all_variables().keys()
|
|
593
|
+
has_tool = "tool" in self.context.get_all_variables().keys()
|
|
594
|
+
return has_intervention and has_tool
|
|
565
595
|
|
|
566
596
|
async def _handle_resumed_tool_call(self):
|
|
567
597
|
"""Tools for handling interrupt recovery calls """
|
|
@@ -571,17 +601,49 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
571
601
|
intervention_vars = self.context.get_var_value(intervention_tmp_key)
|
|
572
602
|
self.context.delete_variable(intervention_tmp_key)
|
|
573
603
|
|
|
574
|
-
# Restore complete message context
|
|
604
|
+
# Restore complete message context to context_manager buckets
|
|
575
605
|
saved_messages = intervention_vars.get("prompt")
|
|
576
606
|
if saved_messages is not None:
|
|
607
|
+
from dolphin.core.common.enums import MessageRole
|
|
608
|
+
|
|
609
|
+
# *** FIX: Filter out messages that are already in other buckets ***
|
|
610
|
+
# To avoid duplication, only restore messages generated during the conversation:
|
|
611
|
+
# - SYSTEM messages are already in SYSTEM bucket (from initial execute)
|
|
612
|
+
# - USER messages are already in QUERY/HISTORY buckets (initial query and history)
|
|
613
|
+
# - We only need to restore ASSISTANT and TOOL messages (conversation progress)
|
|
614
|
+
filtered_messages = [
|
|
615
|
+
msg for msg in saved_messages
|
|
616
|
+
if msg.get("role") in [MessageRole.ASSISTANT.value, MessageRole.TOOL.value]
|
|
617
|
+
]
|
|
618
|
+
|
|
577
619
|
msgs = Messages()
|
|
578
|
-
msgs.extend_plain_messages(
|
|
579
|
-
|
|
620
|
+
msgs.extend_plain_messages(filtered_messages)
|
|
621
|
+
# Use set_messages_batch to restore to context_manager buckets
|
|
622
|
+
# This ensures messages are available when to_dph_messages() is called
|
|
623
|
+
self.context.set_messages_batch(msgs, bucket=BuildInBucket.SCRATCHPAD.value)
|
|
580
624
|
|
|
581
625
|
input_dict = self.context.get_var_value("tool")
|
|
582
626
|
function_name = input_dict["tool_name"]
|
|
583
627
|
raw_tool_args = input_dict["tool_args"]
|
|
584
628
|
function_params_json = {arg["key"]: arg["value"] for arg in raw_tool_args}
|
|
629
|
+
|
|
630
|
+
# Get saved stage_id for resume
|
|
631
|
+
saved_stage_id = intervention_vars.get("stage_id")
|
|
632
|
+
|
|
633
|
+
# *** FIX: Update the last tool_call message with modified parameters ***
|
|
634
|
+
# This ensures LLM sees the actual parameters used, not the original ones
|
|
635
|
+
messages = self.context.get_messages()
|
|
636
|
+
if messages and len(messages.get_messages()) > 0:
|
|
637
|
+
last_message = messages.get_messages()[-1]
|
|
638
|
+
# Check if last message is an assistant message with tool_calls
|
|
639
|
+
if (hasattr(last_message, 'role') and last_message.role == "assistant" and
|
|
640
|
+
hasattr(last_message, 'tool_calls') and last_message.tool_calls):
|
|
641
|
+
# Find the matching tool_call
|
|
642
|
+
for tool_call in last_message.tool_calls:
|
|
643
|
+
if hasattr(tool_call, 'function') and tool_call.function.name == function_name:
|
|
644
|
+
# Update the arguments with modified parameters
|
|
645
|
+
import json
|
|
646
|
+
tool_call.function.arguments = json.dumps(function_params_json, ensure_ascii=False)
|
|
585
647
|
|
|
586
648
|
if self.recorder:
|
|
587
649
|
self.recorder.update(
|
|
@@ -591,11 +653,60 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
591
653
|
skill_type=self.context.get_skill_type(function_name),
|
|
592
654
|
skill_args=function_params_json,
|
|
593
655
|
)
|
|
656
|
+
|
|
657
|
+
# *** Handle skip action ***
|
|
658
|
+
skip_tool = self.context.get_var_value("__skip_tool__")
|
|
659
|
+
skip_message = self.context.get_var_value("__skip_message__")
|
|
660
|
+
|
|
661
|
+
# Clean up skip flags
|
|
662
|
+
if skip_tool:
|
|
663
|
+
self.context.delete_variable("__skip_tool__")
|
|
664
|
+
if skip_message:
|
|
665
|
+
self.context.delete_variable("__skip_message__")
|
|
666
|
+
|
|
594
667
|
self.context.delete_variable("tool")
|
|
595
668
|
|
|
596
669
|
return_answer = {}
|
|
670
|
+
|
|
671
|
+
# If user chose to skip, don't execute the tool
|
|
672
|
+
if skip_tool:
|
|
673
|
+
# Generate friendly skip message
|
|
674
|
+
params_str = ", ".join([f"{k}={v}" for k, v in function_params_json.items()])
|
|
675
|
+
default_skip_msg = f"Tool '{function_name}' was skipped by user"
|
|
676
|
+
if skip_message:
|
|
677
|
+
skip_response = f"[SKIPPED] {skip_message}"
|
|
678
|
+
else:
|
|
679
|
+
skip_response = f"[SKIPPED] {default_skip_msg} (parameters: {params_str})"
|
|
680
|
+
|
|
681
|
+
return_answer["answer"] = skip_response
|
|
682
|
+
return_answer["think"] = skip_response
|
|
683
|
+
return_answer["status"] = "completed"
|
|
684
|
+
|
|
685
|
+
if self.recorder:
|
|
686
|
+
self.recorder.update(
|
|
687
|
+
item={"answer": skip_response, "block_answer": skip_response},
|
|
688
|
+
stage=TypeStage.SKILL,
|
|
689
|
+
source_type=SourceType.EXPLORE,
|
|
690
|
+
skill_name=function_name,
|
|
691
|
+
skill_type=self.context.get_skill_type(function_name),
|
|
692
|
+
skill_args=function_params_json,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
yield [return_answer]
|
|
696
|
+
|
|
697
|
+
# Add tool response message with skip indicator
|
|
698
|
+
tool_call_id = self._extract_tool_call_id()
|
|
699
|
+
if not tool_call_id:
|
|
700
|
+
tool_call_id = f"call_{function_name}_{self.times}"
|
|
701
|
+
|
|
702
|
+
self.strategy.append_tool_response_message(
|
|
703
|
+
self.context, tool_call_id, skip_response, metadata={"skipped": True}
|
|
704
|
+
)
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
# Normal execution (not skipped)
|
|
597
708
|
try:
|
|
598
|
-
props = {"intervention": False}
|
|
709
|
+
props = {"intervention": False, "saved_stage_id": saved_stage_id}
|
|
599
710
|
have_answer = False
|
|
600
711
|
|
|
601
712
|
async for resp in self.skill_run(
|
|
@@ -785,7 +896,11 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
785
896
|
tool_calls = [single] if single else []
|
|
786
897
|
|
|
787
898
|
if not tool_calls:
|
|
788
|
-
# 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
|
|
789
904
|
# If there is pending content, merge before adding
|
|
790
905
|
if self.pending_content:
|
|
791
906
|
# Merge pending content and current content
|
|
@@ -798,9 +913,18 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
798
913
|
self._append_assistant_message(stream_item.answer)
|
|
799
914
|
self.context.debug(f"no valid skill call, answer[{stream_item.answer}]")
|
|
800
915
|
|
|
801
|
-
#
|
|
802
|
-
|
|
803
|
-
|
|
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")
|
|
804
928
|
return
|
|
805
929
|
|
|
806
930
|
# Reset no-tool-call count (because this round has tool call)
|
|
@@ -867,6 +991,9 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
867
991
|
"""Execute tool call"""
|
|
868
992
|
# Checkpoint: Check user interrupt before executing tool
|
|
869
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)
|
|
870
997
|
|
|
871
998
|
intervention_tmp_key = "intervention_explore_block_vars"
|
|
872
999
|
|
|
@@ -876,11 +1003,13 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
876
1003
|
metadata = None
|
|
877
1004
|
|
|
878
1005
|
try:
|
|
1006
|
+
# Save intervention vars (stage_id will be filled by skill_run after creating the stage)
|
|
879
1007
|
intervention_vars = {
|
|
880
1008
|
"prompt": self.context.get_messages().get_messages_as_dict(),
|
|
881
1009
|
"tool_name": tool_call.name,
|
|
882
1010
|
"cur_llm_stream_answer": stream_item.answer,
|
|
883
1011
|
"all_answer": stream_item.answer,
|
|
1012
|
+
"stage_id": None, # Will be updated by skill_run() after stage creation
|
|
884
1013
|
}
|
|
885
1014
|
|
|
886
1015
|
self.context.set_variable(intervention_tmp_key, intervention_vars)
|
|
@@ -1061,7 +1190,7 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1061
1190
|
|
|
1062
1191
|
def _handle_tool_interrupt(self, e: Exception, tool_name: str):
|
|
1063
1192
|
"""Handling Tool Interruptions"""
|
|
1064
|
-
self.context.info(f"
|
|
1193
|
+
self.context.info(f"Tool interrupt in call {tool_name} tool")
|
|
1065
1194
|
if "※tool" in self.context.get_all_variables().keys():
|
|
1066
1195
|
self.context.delete_variable("※tool")
|
|
1067
1196
|
|
|
@@ -1072,34 +1201,173 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1072
1201
|
f"error in call {tool_name} tool, error type: {type(e)}, error info: {str(e)}, error trace: {error_trace}"
|
|
1073
1202
|
)
|
|
1074
1203
|
|
|
1075
|
-
def _should_continue_explore(self) -> bool:
|
|
1204
|
+
async def _should_continue_explore(self) -> bool:
|
|
1076
1205
|
"""Check whether to continue the next exploration.
|
|
1077
1206
|
|
|
1078
|
-
Termination conditions:
|
|
1207
|
+
Termination conditions (Early Return pattern):
|
|
1079
1208
|
1. Maximum number of tool calls has been reached
|
|
1080
|
-
2.
|
|
1081
|
-
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
|
|
1082
1212
|
"""
|
|
1083
|
-
# 1.
|
|
1213
|
+
# 1. Early return: max skill calls reached
|
|
1084
1214
|
if self.times >= MAX_SKILL_CALL_TIMES:
|
|
1085
1215
|
return False
|
|
1086
1216
|
|
|
1087
|
-
# 2.
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
recent_calls = list(deduplicator.skillcalls.values())
|
|
1091
|
-
if (
|
|
1092
|
-
recent_calls
|
|
1093
|
-
and max(recent_calls) >= DefaultSkillCallDeduplicator.MAX_DUPLICATE_COUNT
|
|
1094
|
-
):
|
|
1095
|
-
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()
|
|
1096
1220
|
|
|
1097
|
-
# 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
|
|
1226
|
+
if self.should_stop_exploration:
|
|
1227
|
+
return False
|
|
1228
|
+
|
|
1229
|
+
return True
|
|
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
|
|
1098
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
|
+
)
|
|
1099
1277
|
return False
|
|
1100
1278
|
|
|
1101
1279
|
return True
|
|
1102
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
|
+
|
|
1103
1371
|
def _process_skill_result_with_hook(self, skill_name: str) -> tuple[str | None, dict]:
|
|
1104
1372
|
"""Handle skill results using skillkit_hook"""
|
|
1105
1373
|
# Get skill object
|
|
@@ -1227,7 +1495,7 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1227
1495
|
async for ret in self._explore_once(no_cache=True):
|
|
1228
1496
|
yield ret
|
|
1229
1497
|
|
|
1230
|
-
if not self._should_continue_explore():
|
|
1498
|
+
if not await self._should_continue_explore():
|
|
1231
1499
|
break
|
|
1232
1500
|
|
|
1233
1501
|
# 6. Cleanup
|
|
@@ -1274,6 +1542,103 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1274
1542
|
|
|
1275
1543
|
if getattr(self, "skills", None):
|
|
1276
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
|
|
1277
1642
|
|
|
1278
1643
|
def _resolve_mode(self, kwargs: dict):
|
|
1279
1644
|
"""Resolve exploration mode from kwargs or inherit from context."""
|
|
@@ -1312,6 +1677,12 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1312
1677
|
tools_format=self.tools_format,
|
|
1313
1678
|
)
|
|
1314
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
|
+
|
|
1315
1686
|
if len(system_message.strip()) > 0 and self.context.context_manager:
|
|
1316
1687
|
self.context.add_bucket(
|
|
1317
1688
|
BuildInBucket.SYSTEM.value,
|
|
@@ -1319,6 +1690,38 @@ Please reconsider your approach and improve your answer based on the feedback ab
|
|
|
1319
1690
|
message_role=MessageRole.SYSTEM,
|
|
1320
1691
|
)
|
|
1321
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
|
+
|
|
1322
1725
|
def _apply_deduplicator_config(self, kwargs: dict):
|
|
1323
1726
|
"""Apply skill deduplicator configuration."""
|
|
1324
1727
|
if "enable_skill_deduplicator" in kwargs:
|