kweaver-dolphin 0.2.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 +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 +125 -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.1.dist-info}/METADATA +1 -1
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/RECORD +30 -28
- {kweaver_dolphin-0.2.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.2.0.dist-info/entry_points.txt +0 -27
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/licenses/LICENSE.txt +0 -0
- {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""Copy-On-Write Context for Subtask Isolation.
|
|
2
|
+
|
|
3
|
+
This module provides isolated context for subtasks in plan mode.
|
|
4
|
+
|
|
5
|
+
Logging conventions:
|
|
6
|
+
- DEBUG: Variable operations (set/delete/merge), initialization details
|
|
7
|
+
- INFO: Significant events (merge completion with summary)
|
|
8
|
+
- WARNING: Unexpected but recoverable situations
|
|
9
|
+
- ERROR: Critical failures requiring attention
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
from typing import Any, Dict, Optional, Set
|
|
14
|
+
|
|
15
|
+
from dolphin.core.common.types import SourceType, Var
|
|
16
|
+
from dolphin.core.context.context import Context
|
|
17
|
+
from dolphin.core.context_engineer.core.context_manager import ContextManager
|
|
18
|
+
from dolphin.core.logging.logger import get_logger
|
|
19
|
+
from dolphin.core.skill.skillset import Skillset
|
|
20
|
+
from dolphin.core.context.variable_pool import VariablePool
|
|
21
|
+
|
|
22
|
+
logger = get_logger("cow_context")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _TrackingVariablePool(VariablePool):
|
|
26
|
+
"""A VariablePool that tracks COWContext writes/deletes.
|
|
27
|
+
|
|
28
|
+
This ensures that callers who bypass COWContext.set_variable()/delete_variable()
|
|
29
|
+
and directly mutate `context.variable_pool` still participate in copy-on-write
|
|
30
|
+
semantics and can be merged back to the parent context.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, owner: "COWContext"):
|
|
34
|
+
super().__init__()
|
|
35
|
+
self._owner = owner
|
|
36
|
+
|
|
37
|
+
def set_var(self, name, value):
|
|
38
|
+
super().set_var(name, value)
|
|
39
|
+
tracked_value = value.value if isinstance(value, Var) else value
|
|
40
|
+
self._owner.writes[name] = tracked_value
|
|
41
|
+
self._owner.deletes.discard(name)
|
|
42
|
+
|
|
43
|
+
def set_var_output(
|
|
44
|
+
self, name, value, source_type: SourceType = SourceType.OTHER, skill_info=None
|
|
45
|
+
):
|
|
46
|
+
super().set_var_output(name, value, source_type=source_type, skill_info=skill_info)
|
|
47
|
+
# Track the user-facing value for merging behavior consistency.
|
|
48
|
+
self._owner.writes[name] = value
|
|
49
|
+
self._owner.deletes.discard(name)
|
|
50
|
+
|
|
51
|
+
def delete_var(self, name):
|
|
52
|
+
super().delete_var(name)
|
|
53
|
+
self._owner.deletes.add(name)
|
|
54
|
+
self._owner.writes.pop(name, None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class COWContext(Context):
|
|
58
|
+
"""Copy-On-Write Context for subtask isolation.
|
|
59
|
+
|
|
60
|
+
Contract:
|
|
61
|
+
- Variables: COW isolation (read-through + local writes).
|
|
62
|
+
- Messages: isolated (subtask-local).
|
|
63
|
+
- Interrupt/output: delegated to parent for unified control and UI routing.
|
|
64
|
+
- Output events are tagged with task_id for UI routing.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, parent: Context, task_id: str):
|
|
68
|
+
"""Initialize COWContext with parent and task ID.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
parent: Parent context to delegate to
|
|
72
|
+
task_id: Task identifier for event tagging
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(
|
|
75
|
+
config=parent.config,
|
|
76
|
+
global_skills=parent.global_skills,
|
|
77
|
+
memory_manager=parent.memory_manager,
|
|
78
|
+
global_types=parent.global_types,
|
|
79
|
+
skillkit_hook=getattr(parent, "skillkit_hook", None),
|
|
80
|
+
context_manager=ContextManager(),
|
|
81
|
+
verbose=parent.verbose,
|
|
82
|
+
is_cli=parent.is_cli,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.parent = parent
|
|
86
|
+
self.task_id = task_id
|
|
87
|
+
self.writes: Dict[str, Any] = {}
|
|
88
|
+
self.deletes: Set[str] = set()
|
|
89
|
+
|
|
90
|
+
# Create a new isolated VariablePool to prevent direct mutations to parent's pool.
|
|
91
|
+
# This ensures COW isolation even if code bypasses set_variable() and directly
|
|
92
|
+
# calls context.variable_pool.set_var().
|
|
93
|
+
self._parent_pool = parent.variable_pool # Keep reference for read-through
|
|
94
|
+
self.variable_pool = _TrackingVariablePool(self) # New empty pool for local writes (tracked)
|
|
95
|
+
|
|
96
|
+
# Isolate messages/buckets for subtask execution.
|
|
97
|
+
self.messages = {}
|
|
98
|
+
self.messages_dirty = True
|
|
99
|
+
|
|
100
|
+
# Keep IDs aligned for observability.
|
|
101
|
+
self.user_id = parent.user_id
|
|
102
|
+
self.session_id = parent.session_id
|
|
103
|
+
self.cur_agent = parent.cur_agent
|
|
104
|
+
|
|
105
|
+
# Share the interrupt event for cooperative cancellation.
|
|
106
|
+
self._interrupt_event = parent.get_interrupt_event()
|
|
107
|
+
|
|
108
|
+
# Subtasks must NOT be considered "plan-enabled" to avoid infinite loops.
|
|
109
|
+
self._plan_enabled = False
|
|
110
|
+
# Keep plan_id aligned for observability, while still disabling plan mode APIs.
|
|
111
|
+
self._plan_id = parent.get_plan_id()
|
|
112
|
+
self.task_registry = None
|
|
113
|
+
|
|
114
|
+
# Filter out orchestration-only tools (e.g., PlanSkillkit) from subtask toolset.
|
|
115
|
+
# Create a new isolated Skillset instead of referencing parent's skillkit directly
|
|
116
|
+
# to prevent permission escalation via context.skillkit.getSkills()
|
|
117
|
+
self._calc_all_skills()
|
|
118
|
+
self.all_skills = self._filter_subtask_skills(self.all_skills)
|
|
119
|
+
# Create a new Skillset that contains only filtered skills
|
|
120
|
+
# This ensures context.get_skill() and context.skillkit.getSkills() both respect filtering
|
|
121
|
+
filtered_skillkit = Skillset()
|
|
122
|
+
for skill in self.all_skills.getSkills():
|
|
123
|
+
filtered_skillkit.addSkill(skill)
|
|
124
|
+
self.skillkit = filtered_skillkit
|
|
125
|
+
|
|
126
|
+
# Inherit last-session configs where safe.
|
|
127
|
+
self._last_model_name = getattr(parent, "_last_model_name", None)
|
|
128
|
+
self._last_explore_mode = getattr(parent, "_last_explore_mode", None)
|
|
129
|
+
self._last_system_prompt = getattr(parent, "_last_system_prompt", None)
|
|
130
|
+
self._last_skills = None
|
|
131
|
+
|
|
132
|
+
logger.debug(f"COWContext initialized for task: {task_id}")
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _filter_subtask_skills(skillset: Skillset) -> Skillset:
|
|
136
|
+
"""Filter out skillkits that should not be exposed to subtasks."""
|
|
137
|
+
filtered = Skillset()
|
|
138
|
+
for skill in skillset.getSkills():
|
|
139
|
+
owner = None
|
|
140
|
+
if hasattr(skill, "get_owner_skillkit"):
|
|
141
|
+
owner = skill.get_owner_skillkit()
|
|
142
|
+
if owner is not None:
|
|
143
|
+
should_exclude = getattr(owner, "should_exclude_from_subtask", None)
|
|
144
|
+
if callable(should_exclude):
|
|
145
|
+
try:
|
|
146
|
+
if bool(should_exclude()):
|
|
147
|
+
continue
|
|
148
|
+
except Exception:
|
|
149
|
+
# Fail-open: do not exclude if the hook misbehaves.
|
|
150
|
+
pass
|
|
151
|
+
filtered.addSkill(skill)
|
|
152
|
+
return filtered
|
|
153
|
+
|
|
154
|
+
def get_variable(self, key: str, default_value: Any = None) -> Any:
|
|
155
|
+
"""Get a variable (check local layer first, then parent).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
key: Variable key
|
|
159
|
+
default_value: Default value if not found
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Variable value, or default_value if deleted or not found
|
|
163
|
+
|
|
164
|
+
Note:
|
|
165
|
+
Container types (list, dict, set) are deep-copied to prevent
|
|
166
|
+
accidental mutation of parent context's data through in-place operations
|
|
167
|
+
like list.append() or dict.update().
|
|
168
|
+
"""
|
|
169
|
+
if key in self.deletes:
|
|
170
|
+
return default_value
|
|
171
|
+
# Check local variable_pool first (for compatibility with direct variable_pool.set_var()).
|
|
172
|
+
# NOTE: Variable values can legitimately be None, so use a sentinel to distinguish
|
|
173
|
+
# "missing key" vs "stored None".
|
|
174
|
+
sentinel = object()
|
|
175
|
+
local_value = self.variable_pool.get_var_value(key, sentinel)
|
|
176
|
+
if local_value is not sentinel:
|
|
177
|
+
return self._safe_copy_if_mutable(local_value, key)
|
|
178
|
+
# Check writes dict (explicit set_variable() calls)
|
|
179
|
+
if key in self.writes:
|
|
180
|
+
return self._safe_copy_if_mutable(self.writes[key], key)
|
|
181
|
+
# Fall back to parent context (keeps compatibility logic such as flags).
|
|
182
|
+
parent_value = self.parent.get_var_value(key, default_value)
|
|
183
|
+
return self._safe_copy_if_mutable(parent_value, key)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _safe_copy_if_mutable(value: Any, key: str) -> Any:
|
|
187
|
+
"""Deep copy mutable container types to prevent accidental mutation.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
value: Variable value
|
|
191
|
+
key: Variable key (for error reporting)
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Deep copy if value is a mutable container (list/dict/set or custom object),
|
|
195
|
+
original value otherwise.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
TypeError: If a mutable object cannot be deep-copied, ensuring isolation.
|
|
199
|
+
|
|
200
|
+
Note:
|
|
201
|
+
Isolation is guaranteed by deepcopy. If an object is not deepcopy-able,
|
|
202
|
+
it cannot be safely used in a COWContext as mutation would leak to parent.
|
|
203
|
+
"""
|
|
204
|
+
if value is None:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
# Fast path for immutable primitives: return as-is
|
|
208
|
+
if isinstance(value, (str, int, float, bool, bytes)):
|
|
209
|
+
return value
|
|
210
|
+
|
|
211
|
+
# Mutable types (containers and custom objects): try deepcopy
|
|
212
|
+
try:
|
|
213
|
+
return copy.deepcopy(value)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
# Item 3: Fail-fast for non-deepcopyable objects to ensure isolation.
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"Isolation failure for variable '{key}': Object of type {type(value).__name__} is not deepcopyable. "
|
|
218
|
+
"Explicitly raising TypeError to prevent silent parent context corruption."
|
|
219
|
+
)
|
|
220
|
+
raise TypeError(
|
|
221
|
+
f"Cannot safely isolate variable '{key}': "
|
|
222
|
+
f"Object of type {type(value).__name__} is not deepcopyable. "
|
|
223
|
+
"Ensure task variables are serializable (e.g., data classes, dicts, primitives)."
|
|
224
|
+
) from e
|
|
225
|
+
|
|
226
|
+
def get_var_value(self, key: str, default_value: Any = None) -> Any:
|
|
227
|
+
"""Get variable value (alias for get_variable for Context compatibility).
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
key: Variable key
|
|
231
|
+
default_value: Default value if not found
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Variable value
|
|
235
|
+
"""
|
|
236
|
+
return self.get_variable(key, default_value)
|
|
237
|
+
|
|
238
|
+
def set_variable(self, key: str, value: Any):
|
|
239
|
+
"""Set a variable in the local layer only (copy-on-write).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
key: Variable key
|
|
243
|
+
value: Variable value
|
|
244
|
+
|
|
245
|
+
Note:
|
|
246
|
+
This does NOT modify parent's variable_pool, ensuring isolation.
|
|
247
|
+
Use merge_to_parent() to propagate changes after task completion.
|
|
248
|
+
Updates both self.writes (tracking) and self.variable_pool (isolation).
|
|
249
|
+
"""
|
|
250
|
+
self.writes[key] = value
|
|
251
|
+
self.deletes.discard(key)
|
|
252
|
+
# Update local variable_pool to catch both set_variable() and variable_pool.set_var() paths
|
|
253
|
+
self.variable_pool.set_var(key, value)
|
|
254
|
+
logger.debug(f"COWContext[{self.task_id}] set variable: {key}")
|
|
255
|
+
|
|
256
|
+
def delete_variable(self, key: str):
|
|
257
|
+
"""Delete a variable in the local layer (tombstone, copy-on-write).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
key: Variable key to delete
|
|
261
|
+
|
|
262
|
+
Note:
|
|
263
|
+
This does NOT modify parent's variable_pool, ensuring isolation.
|
|
264
|
+
The delete is recorded as a tombstone in self.deletes.
|
|
265
|
+
"""
|
|
266
|
+
self.deletes.add(key)
|
|
267
|
+
self.writes.pop(key, None)
|
|
268
|
+
# Delete from local variable_pool to ensure isolation
|
|
269
|
+
self.variable_pool.delete_var(key)
|
|
270
|
+
logger.debug(f"COWContext[{self.task_id}] deleted variable: {key}")
|
|
271
|
+
|
|
272
|
+
def get_local_changes(self) -> Dict[str, Any]:
|
|
273
|
+
"""Return all local writes.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dictionary of local variable writes
|
|
277
|
+
"""
|
|
278
|
+
return self.writes.copy()
|
|
279
|
+
|
|
280
|
+
def clear_local_changes(self):
|
|
281
|
+
"""Clear local writes and deletes to release memory.
|
|
282
|
+
|
|
283
|
+
Note:
|
|
284
|
+
This should be called after merge_to_parent() to free memory held by
|
|
285
|
+
intermediate variables. Useful for long-running subtasks that generate
|
|
286
|
+
many temporary variables (e.g., web scraping loops).
|
|
287
|
+
|
|
288
|
+
Warning:
|
|
289
|
+
Do NOT call this before merge_to_parent() unless you want to discard changes.
|
|
290
|
+
"""
|
|
291
|
+
self.writes.clear()
|
|
292
|
+
self.deletes.clear()
|
|
293
|
+
# Also clear the local variable pool
|
|
294
|
+
if hasattr(self.variable_pool, 'clear'):
|
|
295
|
+
self.variable_pool.clear()
|
|
296
|
+
logger.debug(f"COWContext[{self.task_id}] cleared local changes to release memory")
|
|
297
|
+
|
|
298
|
+
def merge_to_parent(self, keys: Optional[Set[str]] = None):
|
|
299
|
+
"""Merge local variable writes and deletes back to parent.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
keys: Optional set of keys to merge (if None, merge all)
|
|
303
|
+
|
|
304
|
+
Note:
|
|
305
|
+
Merges both writes (set operations) and deletes (delete operations).
|
|
306
|
+
"""
|
|
307
|
+
if keys:
|
|
308
|
+
# Selective merge
|
|
309
|
+
merged_count = 0
|
|
310
|
+
deleted_count = 0
|
|
311
|
+
for key in keys:
|
|
312
|
+
if key in self.deletes:
|
|
313
|
+
self.parent.delete_variable(key)
|
|
314
|
+
deleted_count += 1
|
|
315
|
+
elif key in self.writes:
|
|
316
|
+
self.parent.set_variable(key, self.writes[key])
|
|
317
|
+
merged_count += 1
|
|
318
|
+
logger.debug(
|
|
319
|
+
f"COWContext[{self.task_id}] merged {merged_count} variables, "
|
|
320
|
+
f"deleted {deleted_count} variables to parent"
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
# Full merge
|
|
324
|
+
# First apply deletes
|
|
325
|
+
for key in self.deletes:
|
|
326
|
+
self.parent.delete_variable(key)
|
|
327
|
+
# Then apply writes
|
|
328
|
+
for key, value in self.writes.items():
|
|
329
|
+
self.parent.set_variable(key, value)
|
|
330
|
+
logger.debug(
|
|
331
|
+
f"COWContext[{self.task_id}] merged {len(self.writes)} variables, "
|
|
332
|
+
f"deleted {len(self.deletes)} variables to parent"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def check_user_interrupt(self) -> None:
|
|
336
|
+
"""Delegate interrupt checks to parent."""
|
|
337
|
+
return self.parent.check_user_interrupt()
|
|
338
|
+
|
|
339
|
+
def write_output(self, event_type: str, data: Dict[str, Any]) -> None:
|
|
340
|
+
"""Tag output events with task_id and forward to parent."""
|
|
341
|
+
payload = dict(data)
|
|
342
|
+
payload.setdefault("task_id", self.task_id)
|
|
343
|
+
payload.setdefault("plan_id", self.parent.get_plan_id())
|
|
344
|
+
return self.parent.write_output(event_type, payload)
|
|
345
|
+
|
|
346
|
+
def get_plan_id(self) -> Optional[str]:
|
|
347
|
+
"""Expose parent plan_id for event tagging/observability."""
|
|
348
|
+
return self.parent.get_plan_id()
|
|
349
|
+
|
|
350
|
+
def enable_plan(self, plan_id: Optional[str] = None) -> None:
|
|
351
|
+
"""Prevent nested plan orchestration in subtasks.
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
RuntimeError: Always raises to prevent nested planning.
|
|
355
|
+
|
|
356
|
+
Note:
|
|
357
|
+
This is intentionally a hard error rather than a soft warning.
|
|
358
|
+
Subtasks should execute their assigned work, not create sub-plans.
|
|
359
|
+
The error message is designed to help models understand the constraint.
|
|
360
|
+
"""
|
|
361
|
+
raise RuntimeError(
|
|
362
|
+
"Plan orchestration is not supported inside a subtask context. "
|
|
363
|
+
"Subtasks should focus on executing their assigned work directly. "
|
|
364
|
+
"If you need to break down the subtask further, return the breakdown "
|
|
365
|
+
"as part of your answer for the parent to orchestrate."
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def __getattr__(self, name: str):
|
|
369
|
+
"""Delegate unknown attributes to parent with special handling for skillkit.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
name: Attribute name
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Attribute value from parent, or filtered skillkit for security
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
AttributeError: If attribute not found in parent
|
|
379
|
+
|
|
380
|
+
Note:
|
|
381
|
+
Special handling for 'skillkit' attribute to prevent subtasks from
|
|
382
|
+
bypassing PLAN_ORCHESTRATION_TOOLS filtering by directly accessing
|
|
383
|
+
parent's skillkit via attribute delegation.
|
|
384
|
+
"""
|
|
385
|
+
# Intercept skillkit access to return filtered version
|
|
386
|
+
if name == "skillkit":
|
|
387
|
+
# Return the filtered skillkit stored in __init__
|
|
388
|
+
# This prevents subtasks from accessing parent's unfiltered skillkit
|
|
389
|
+
return object.__getattribute__(self, name)
|
|
390
|
+
|
|
391
|
+
# Delegate all other attributes to parent
|
|
392
|
+
return getattr(self.parent, name)
|
|
@@ -16,13 +16,13 @@ Scope: Executor, DebugController
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
# =========== Disable LLM Cache ===========
|
|
19
|
-
DISABLE_LLM_CACHE = "
|
|
19
|
+
DISABLE_LLM_CACHE = "llm_cache"
|
|
20
20
|
"""Disable LLM cache
|
|
21
21
|
Scope: LLMClient
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
# =========== Multiple Tool Calls Support ===========
|
|
25
|
-
ENABLE_PARALLEL_TOOL_CALLS = "
|
|
25
|
+
ENABLE_PARALLEL_TOOL_CALLS = "parallel_tool_calls"
|
|
26
26
|
"""Enable multiple tool calls support
|
|
27
27
|
|
|
28
28
|
When enabled:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
import logging
|
|
2
3
|
import time
|
|
3
4
|
from typing import List, Optional, TYPE_CHECKING
|
|
4
5
|
import uuid
|
|
@@ -6,6 +7,8 @@ import uuid
|
|
|
6
7
|
from dolphin.core.common.enums import Messages, SkillInfo, Status, TypeStage
|
|
7
8
|
from dolphin.core.common.constants import estimate_tokens_from_chars
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
13
|
from dolphin.core.agent.base_agent import BaseAgent
|
|
11
14
|
from dolphin.core.code_block.basic_code_block import BasicCodeBlock
|
|
@@ -292,6 +295,7 @@ class ProgressInstance(RuntimeInstance):
|
|
|
292
295
|
self.context = context
|
|
293
296
|
self.stages: List[StageInstance] = []
|
|
294
297
|
self.flags = flags
|
|
298
|
+
self._next_stage_id: Optional[str] = None # ✅ NEW: for interrupt resume
|
|
295
299
|
|
|
296
300
|
def add_stage(
|
|
297
301
|
self,
|
|
@@ -306,6 +310,7 @@ class ProgressInstance(RuntimeInstance):
|
|
|
306
310
|
input_content: str = "",
|
|
307
311
|
input_messages: Optional[Messages] = None,
|
|
308
312
|
interrupted: bool = False,
|
|
313
|
+
stage_id: Optional[str] = None, # ✅ NEW: support custom stage_id for resume
|
|
309
314
|
):
|
|
310
315
|
pop_last_stage = False
|
|
311
316
|
if len(self.stages) > 0 and self.stages[-1].llm_empty_answer():
|
|
@@ -325,6 +330,15 @@ class ProgressInstance(RuntimeInstance):
|
|
|
325
330
|
interrupted=interrupted,
|
|
326
331
|
flags=self.flags,
|
|
327
332
|
)
|
|
333
|
+
|
|
334
|
+
# ✅ NEW: Override ID if custom stage_id is provided (for interrupt resume)
|
|
335
|
+
# Priority: explicit stage_id parameter > _next_stage_id temporary variable
|
|
336
|
+
if stage_id is not None:
|
|
337
|
+
stage_instance.id = stage_id
|
|
338
|
+
elif self._next_stage_id is not None:
|
|
339
|
+
stage_instance.id = self._next_stage_id
|
|
340
|
+
self._next_stage_id = None # Clear after use (one-time only)
|
|
341
|
+
|
|
328
342
|
self.add_stage_instance(stage_instance, pop_last_stage)
|
|
329
343
|
|
|
330
344
|
def add_stage_instance(
|
|
@@ -333,6 +347,7 @@ class ProgressInstance(RuntimeInstance):
|
|
|
333
347
|
stage_instance.set_parent(self)
|
|
334
348
|
if pop_last_stage:
|
|
335
349
|
self.stages.pop()
|
|
350
|
+
|
|
336
351
|
self.stages.append(stage_instance)
|
|
337
352
|
|
|
338
353
|
# Register stage instance to runtime_graph if available
|
|
@@ -376,6 +391,22 @@ class ProgressInstance(RuntimeInstance):
|
|
|
376
391
|
# Check if we need to create a new stage (when stage type changes)
|
|
377
392
|
last_stage = self.stages[-1]
|
|
378
393
|
|
|
394
|
+
# *** FIX: If _next_stage_id is set and doesn't match last stage, create new stage ***
|
|
395
|
+
# This handles resume cases where we need to create a stage with a specific ID
|
|
396
|
+
if self._next_stage_id is not None and self._next_stage_id != last_stage.id:
|
|
397
|
+
logger.debug(f"_next_stage_id ({self._next_stage_id}) != last_stage.id ({last_stage.id}), creating new stage for resume")
|
|
398
|
+
self.add_stage(
|
|
399
|
+
stage=stage if stage is not None else last_stage.stage,
|
|
400
|
+
answer=answer,
|
|
401
|
+
think=think,
|
|
402
|
+
raw_output=raw_output,
|
|
403
|
+
status=status,
|
|
404
|
+
skill_info=skill_info,
|
|
405
|
+
block_answer=block_answer,
|
|
406
|
+
input_messages=input_messages,
|
|
407
|
+
)
|
|
408
|
+
return
|
|
409
|
+
|
|
379
410
|
# Create new stage if stage type is changing (and it's not None)
|
|
380
411
|
if stage is not None and stage != last_stage.stage:
|
|
381
412
|
self.add_stage(
|
|
@@ -54,7 +54,7 @@ class SummaryContextStrategy(ContextRetentionStrategy):
|
|
|
54
54
|
# Provide reference_id so LLM can fetch full content if needed
|
|
55
55
|
ref_hint = ""
|
|
56
56
|
if reference_id:
|
|
57
|
-
ref_hint = f"\n[For full content, call
|
|
57
|
+
ref_hint = f"\n[For full content, call _get_cached_result_detail('{reference_id}', scope='skill')]"
|
|
58
58
|
|
|
59
59
|
omitted = len(result) - head_chars - tail_chars
|
|
60
60
|
# Ensure we don't have negative omission if rounding puts us over
|
|
@@ -114,8 +114,8 @@ class ReferenceContextStrategy(ContextRetentionStrategy):
|
|
|
114
114
|
hint = config.reference_hint or "Full result stored"
|
|
115
115
|
return (f"[{hint}]\n"
|
|
116
116
|
f"Original length: {len(result)} chars\n"
|
|
117
|
-
f"Get full content:
|
|
118
|
-
f"Get range:
|
|
117
|
+
f"Get full content: _get_cached_result_detail('{reference_id}', scope='skill')\n"
|
|
118
|
+
f"Get range: _get_cached_result_detail('{reference_id}', scope='skill', offset=0, limit=2000)")
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
# Strategy mapping
|