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.
Files changed (32) hide show
  1. dolphin/cli/runner/runner.py +20 -0
  2. dolphin/cli/ui/console.py +29 -11
  3. dolphin/cli/utils/helpers.py +4 -4
  4. dolphin/core/agent/base_agent.py +2 -2
  5. dolphin/core/code_block/basic_code_block.py +125 -30
  6. dolphin/core/code_block/explore_block.py +353 -29
  7. dolphin/core/code_block/explore_block_v2.py +21 -17
  8. dolphin/core/code_block/explore_strategy.py +1 -0
  9. dolphin/core/code_block/judge_block.py +10 -1
  10. dolphin/core/code_block/skill_call_deduplicator.py +32 -10
  11. dolphin/core/code_block/tool_block.py +12 -3
  12. dolphin/core/common/constants.py +25 -1
  13. dolphin/core/config/global_config.py +35 -0
  14. dolphin/core/context/context.py +168 -5
  15. dolphin/core/context/cow_context.py +392 -0
  16. dolphin/core/flags/definitions.py +2 -2
  17. dolphin/core/runtime/runtime_instance.py +31 -0
  18. dolphin/core/skill/context_retention.py +3 -3
  19. dolphin/core/task_registry.py +404 -0
  20. dolphin/lib/__init__.py +0 -2
  21. dolphin/lib/skillkits/__init__.py +2 -2
  22. dolphin/lib/skillkits/plan_skillkit.py +756 -0
  23. dolphin/lib/skillkits/system_skillkit.py +103 -30
  24. dolphin/sdk/skill/global_skills.py +43 -3
  25. {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/METADATA +1 -1
  26. {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/RECORD +30 -28
  27. {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/WHEEL +1 -1
  28. kweaver_dolphin-0.2.1.dist-info/entry_points.txt +15 -0
  29. dolphin/lib/skillkits/plan_act_skillkit.py +0 -452
  30. kweaver_dolphin-0.2.0.dist-info/entry_points.txt +0 -27
  31. {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/licenses/LICENSE.txt +0 -0
  32. {kweaver_dolphin-0.2.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,9 @@ from dolphin.core.context.context import Context
7
7
  from dolphin.core.llm.llm_client import LLMClient
8
8
  from dolphin.core.utils.tools import ToolInterrupt
9
9
  from dolphin.core.context.var_output import SourceType
10
+ from dolphin.core.logging.logger import get_logger
11
+
12
+ logger = get_logger()
10
13
 
11
14
 
12
15
  class JudgeBlock(BasicCodeBlock):
@@ -129,6 +132,9 @@ class JudgeBlock(BasicCodeBlock):
129
132
 
130
133
  tool_name = intervention_vars["tool_name"]
131
134
  judge_call_info = intervention_vars["judge_call_info"]
135
+
136
+ # *** FIX: Get saved stage_id for resume ***
137
+ saved_stage_id = intervention_vars.get("stage_id")
132
138
 
133
139
  self.recorder.set_output_var(
134
140
  judge_call_info["assign_type"], judge_call_info["output_var"]
@@ -147,7 +153,8 @@ class JudgeBlock(BasicCodeBlock):
147
153
  raw_tool_args = input_dict["tool_args"]
148
154
  new_tool_args = {arg["key"]: arg["value"] for arg in raw_tool_args}
149
155
 
150
- props = {"intervention": False, "gvp": self.context}
156
+ # *** FIX: Pass saved_stage_id to skill_run ***
157
+ props = {"intervention": False, "saved_stage_id": saved_stage_id, "gvp": self.context}
151
158
 
152
159
  # *** Handle skip action ***
153
160
  skip_tool = self.context.get_var_value("__skip_tool__")
@@ -202,6 +209,7 @@ class JudgeBlock(BasicCodeBlock):
202
209
  if self.recorder and hasattr(self.recorder, "set_output_var"):
203
210
  self.recorder.set_output_var(self.assign_type, self.output_var)
204
211
 
212
+ # Save intervention vars (stage_id will be filled by skill_run after creating the stage)
205
213
  intervention_vars = {
206
214
  "tool_name": tool_name,
207
215
  "judge_call_info": {
@@ -210,6 +218,7 @@ class JudgeBlock(BasicCodeBlock):
210
218
  "output_var": self.output_var,
211
219
  "params": self.params,
212
220
  },
221
+ "stage_id": None, # Will be updated by skill_run() after stage creation
213
222
  }
214
223
 
215
224
  try:
@@ -111,14 +111,16 @@ class DefaultSkillCallDeduplicator(SkillCallDeduplicator):
111
111
  def __init__(self):
112
112
  self.skillcalls: Dict[str, int] = {}
113
113
  self.call_results: Dict[str, str] = {}
114
- # Cache call_key to avoid duplicate serialization
115
- self._call_key_cache: Dict[int, str] = {}
114
+ # Import polling tools from constants to avoid hardcoding.
115
+ # These tools are expected to be called repeatedly (polling-style).
116
+ # Do NOT count these towards duplicate-call termination.
117
+ from dolphin.core.common.constants import POLLING_TOOLS
118
+ self._always_allow_duplicate_skills = POLLING_TOOLS
116
119
 
117
120
  def clear(self):
118
121
  """Clear all records"""
119
122
  self.skillcalls.clear()
120
123
  self.call_results.clear()
121
- self._call_key_cache.clear()
122
124
 
123
125
  def get_history(self) -> list:
124
126
  """Get the history of all recorded skill calls.
@@ -153,11 +155,6 @@ class DefaultSkillCallDeduplicator(SkillCallDeduplicator):
153
155
 
154
156
  Uses the normalized JSON string of the skill name and arguments as the unique identifier.
155
157
  """
156
- # Use object id as cache key
157
- cache_key = id(skill_call)
158
- if cache_key in self._call_key_cache:
159
- return self._call_key_cache[cache_key]
160
-
161
158
  skill_name, arguments = self._extract_skill_info(skill_call)
162
159
 
163
160
  # Normalized parameters: sorting keys, ensuring consistency
@@ -175,7 +172,6 @@ class DefaultSkillCallDeduplicator(SkillCallDeduplicator):
175
172
  normalized_args = str(arguments).strip()
176
173
 
177
174
  call_key = f"{skill_name}:{normalized_args}"
178
- self._call_key_cache[cache_key] = call_key
179
175
  return call_key
180
176
 
181
177
  def _extract_skill_info(self, skill_call: Any) -> Tuple[str, Any]:
@@ -200,7 +196,29 @@ class DefaultSkillCallDeduplicator(SkillCallDeduplicator):
200
196
  skill_name = str(skill_call)
201
197
  arguments = {}
202
198
 
203
- return skill_name, arguments
199
+ return skill_name, self._normalize_arguments(arguments)
200
+
201
+ @staticmethod
202
+ def _normalize_arguments(arguments: Any) -> Any:
203
+ """Normalize arguments to improve deduplication stability.
204
+
205
+ Some callers may pass JSON strings (e.g., "{}") instead of dicts.
206
+ This method converts JSON strings into Python objects when possible.
207
+ """
208
+ if arguments is None:
209
+ return {}
210
+ if isinstance(arguments, str):
211
+ raw = arguments.strip()
212
+ if raw == "":
213
+ return {}
214
+ # Fast-path common empty payloads.
215
+ if raw in ("{}", "[]", "null"):
216
+ return {} if raw != "[]" else []
217
+ try:
218
+ return json.loads(raw)
219
+ except Exception:
220
+ return raw
221
+ return arguments
204
222
 
205
223
  def add(self, skill_call: Any, result: Optional[str] = None):
206
224
  """Add skill call record
@@ -248,6 +266,10 @@ class DefaultSkillCallDeduplicator(SkillCallDeduplicator):
248
266
  """
249
267
  skill_name, arguments = self._extract_skill_info(skill_call)
250
268
 
269
+ # Polling tools are expected to be invoked repeatedly.
270
+ if skill_name in self._always_allow_duplicate_skills:
271
+ return True
272
+
251
273
  # Calls without arguments are not specially handled
252
274
  if not arguments:
253
275
  return False
@@ -2,10 +2,12 @@ from dolphin.core.code_block.basic_code_block import BasicCodeBlock
2
2
  from dolphin.core.utils.tools import ToolInterrupt
3
3
  from dolphin.core.common.enums import CategoryBlock, TypeStage
4
4
  from dolphin.core.context.context import Context
5
- from dolphin.core.logging.logger import console
5
+ from dolphin.core.logging.logger import console, get_logger
6
6
  from dolphin.core.context.var_output import SourceType
7
7
  from typing import Optional, AsyncGenerator, Dict, Any
8
8
 
9
+ logger = get_logger()
10
+
9
11
 
10
12
  class ToolBlock(BasicCodeBlock):
11
13
  def __init__(self, context: Context, debug_infos: Optional[dict] = None):
@@ -52,6 +54,10 @@ class ToolBlock(BasicCodeBlock):
52
54
 
53
55
  tool_name = intervention_vars["tool_name"]
54
56
  tool_call_info = intervention_vars["tool_call_info"]
57
+
58
+ # *** FIX: Get saved stage_id for resume ***
59
+ saved_stage_id = intervention_vars.get("stage_id")
60
+
55
61
  self.context.delete_variable("intervention_tool_block_vars")
56
62
  if self.recorder is not None:
57
63
  self.recorder.set_output_var(
@@ -69,7 +75,8 @@ class ToolBlock(BasicCodeBlock):
69
75
  raw_tool_args = input_dict["tool_args"]
70
76
  new_tool_args = {arg["key"]: arg["value"] for arg in raw_tool_args}
71
77
 
72
- props = {"intervention": False, "gvp": self.context}
78
+ # *** FIX: Pass saved_stage_id to skill_run ***
79
+ props = {"intervention": False, "saved_stage_id": saved_stage_id, "gvp": self.context}
73
80
 
74
81
  # *** Handle skip action ***
75
82
  skip_tool = self.context.get_var_value("__skip_tool__")
@@ -81,7 +88,7 @@ class ToolBlock(BasicCodeBlock):
81
88
  if skip_message:
82
89
  self.context.delete_variable("__skip_message__")
83
90
 
84
- input_dict = self.context.delete_variable("tool")
91
+ self.context.delete_variable("tool")
85
92
 
86
93
  # If user chose to skip, don't execute the tool
87
94
  if skip_tool:
@@ -140,9 +147,11 @@ class ToolBlock(BasicCodeBlock):
140
147
  # step2: Obtain the tool object and execute the tool call
141
148
  tool_name = tool_call_info["tool_name"]
142
149
 
150
+ # Save intervention vars (stage_id will be filled by skill_run after creating the stage)
143
151
  intervention_vars = {
144
152
  "tool_name": tool_call_info["tool_name"],
145
153
  "tool_call_info": tool_call_info,
154
+ "stage_id": None, # Will be updated by skill_run() after stage creation
146
155
  }
147
156
 
148
157
  self.context.set_variable(
@@ -144,7 +144,31 @@ SEARCH_TIMEOUT = 10 # seconds for search API calls
144
144
 
145
145
  SEARCH_RETRY_COUNT = 2 # number of retries for failed search API calls
146
146
 
147
- MAX_SKILL_CALL_TIMES = 100
147
+ MAX_SKILL_CALL_TIMES = 500
148
+
149
+ # Plan orchestration tools (used for task management in plan mode)
150
+ # These tools should be excluded from subtask contexts to prevent infinite recursion.
151
+ PLAN_ORCHESTRATION_TOOLS = frozenset({
152
+ "_plan_tasks", # Create and register subtasks
153
+ "_check_progress", # Check task execution status
154
+ "_get_task_output", # Retrieve task results
155
+ "_wait", # Wait for a specified duration
156
+ "_kill_task", # Cancel a running task
157
+ "_retry_task", # Retry a failed task
158
+ })
159
+
160
+ # Polling tools that are expected to be called repeatedly (excluded from deduplication)
161
+ # These tools are used to check status/wait for async operations and should not trigger
162
+ # duplicate-call termination in ExploreBlock.
163
+ POLLING_TOOLS = frozenset({
164
+ "_check_progress", # Plan mode: check task execution status
165
+ "_wait", # Plan mode: wait for a specified duration
166
+ })
167
+
168
+ # Plan mode: maximum consecutive rounds without task status progress.
169
+ # This only applies when an active plan exists and the agent is not using plan-related tools
170
+ # (e.g., _wait / _check_progress). Set to 0 to disable.
171
+ MAX_PLAN_SILENT_ROUNDS = 50
148
172
 
149
173
  # Compression constants
150
174
  MAX_ANSWER_COMPRESSION_LENGTH = 100
@@ -1017,6 +1017,17 @@ class GlobalConfig:
1017
1017
 
1018
1018
  @staticmethod
1019
1019
  def from_dict(config_dict: dict, base_dir: str = None) -> "GlobalConfig":
1020
+ # Load and apply flags configuration if present
1021
+ if "flags" in config_dict:
1022
+ from dolphin.core import flags
1023
+ flags_config = config_dict.get("flags", {})
1024
+ for flag_name, flag_value in flags_config.items():
1025
+ try:
1026
+ flags.set_flag(flag_name, bool(flag_value))
1027
+ except Exception as e:
1028
+ import logging
1029
+ logging.warning(f"Failed to set flag '{flag_name}': {e}")
1030
+
1020
1031
  is_new_config_format = "llms" in config_dict and "default" in config_dict
1021
1032
  if is_new_config_format:
1022
1033
  default_llm = config_dict.get("default")
@@ -1228,6 +1239,30 @@ class GlobalConfig:
1228
1239
  result = {
1229
1240
  "default": self.default_llm,
1230
1241
  }
1242
+
1243
+ # Add flags configuration
1244
+ from dolphin.core import flags
1245
+ from dolphin.core.flags.definitions import DEFAULT_VALUES
1246
+ import logging
1247
+
1248
+ flags_dict = flags.get_all()
1249
+ non_default_flags = {}
1250
+
1251
+ for name, value in flags_dict.items():
1252
+ if name in DEFAULT_VALUES:
1253
+ # Only include flags that differ from their known defaults
1254
+ if value != DEFAULT_VALUES[name]:
1255
+ non_default_flags[name] = value
1256
+ else:
1257
+ # Unknown flag (possibly user-defined) - include unconditionally with warning
1258
+ logging.warning(
1259
+ f"Flag '{name}' is not in DEFAULT_VALUES, serializing unconditionally. "
1260
+ f"Consider adding it to dolphin.core.flags.definitions."
1261
+ )
1262
+ non_default_flags[name] = value
1263
+
1264
+ if non_default_flags:
1265
+ result["flags"] = non_default_flags
1231
1266
 
1232
1267
  # Add fast_llm (if different from default_llm)
1233
1268
  if self.fast_llm and self.fast_llm != self.default_llm:
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import os
3
3
  import json
4
+ import time
4
5
  from datetime import datetime
5
6
  from typing import Dict, List, Optional, Any, Union, TYPE_CHECKING
6
7
 
@@ -104,6 +105,18 @@ class Context:
104
105
  # User interrupt event (injected by Agent layer for cooperative cancellation)
105
106
  self._interrupt_event: Optional[asyncio.Event] = None
106
107
 
108
+ # Plan Mode support (unified architecture)
109
+ self._plan_enabled: bool = False
110
+ self._plan_id: Optional[str] = None
111
+ self.task_registry: Optional["TaskRegistry"] = None
112
+
113
+ # Nesting level tracking (to prevent infinite recursion in plan mode)
114
+ # Incremented when fork() is called
115
+ self._nesting_level: int = 0
116
+
117
+ # Output event buffer (for UI/SDK consumption)
118
+ self._output_events: List[Dict[str, Any]] = []
119
+
107
120
  def set_skillkit_hook(self, skillkit_hook: "SkillkitHook"):
108
121
  """Set skillkit_hook"""
109
122
  self.skillkit_hook = skillkit_hook
@@ -471,7 +484,7 @@ class Context:
471
484
  return result_skillset
472
485
 
473
486
  def _inject_detail_skill_if_needed(self, skillset: Skillset):
474
- """Auto-inject _get_result_detail if any skill uses omitting modes (SUMMARY/REFERENCE)"""
487
+ """Auto-inject _get_cached_result_detail if any skill uses omitting modes (SUMMARY/REFERENCE)"""
475
488
  try:
476
489
  from dolphin.core.skill.context_retention import ContextRetentionMode
477
490
  from dolphin.lib.skillkits.system_skillkit import SystemFunctions
@@ -494,16 +507,16 @@ class Context:
494
507
  if should_inject:
495
508
  # Check if already exists in skillset
496
509
  has_detail_skill = any(
497
- "_get_result_detail" in s.get_function_name()
510
+ "_get_cached_result_detail" in s.get_function_name()
498
511
  for s in skills
499
512
  )
500
513
 
501
514
  if not has_detail_skill:
502
515
  # Try to get the skill from SystemFunctions
503
516
  # We try different name variations just in case
504
- detail_skill = SystemFunctions.getSkill("_get_result_detail")
517
+ detail_skill = SystemFunctions.getSkill("_get_cached_result_detail")
505
518
  if not detail_skill:
506
- detail_skill = SystemFunctions.getSkill("system_functions._get_result_detail")
519
+ detail_skill = SystemFunctions.getSkill("system_functions._get_cached_result_detail")
507
520
 
508
521
  # If still not found (e.g. SystemFunctions not initialized with it), we can pick it manually if possible
509
522
  # But typically SystemFunctions singleton has it.
@@ -512,7 +525,7 @@ class Context:
512
525
  else:
513
526
  # Fallback: look through SystemFunctions.getSkills() manually
514
527
  for s in SystemFunctions.getSkills():
515
- if "_get_result_detail" in s.get_function_name():
528
+ if "_get_cached_result_detail" in s.get_function_name():
516
529
  skillset.addSkill(s)
517
530
  break
518
531
 
@@ -1425,6 +1438,10 @@ class Context:
1425
1438
  "session_id": self.session_id,
1426
1439
  "cur_agent": self.cur_agent.getName() if self.cur_agent else None,
1427
1440
  "max_answer_len": self.max_answer_len,
1441
+ "plan_enabled": self._plan_enabled,
1442
+ "plan_id": self._plan_id,
1443
+ "task_registry": self.task_registry.to_dict() if self.task_registry else None,
1444
+ "nesting_level": self._nesting_level,
1428
1445
  }
1429
1446
 
1430
1447
  # Export skill set status
@@ -1522,6 +1539,19 @@ class Context:
1522
1539
  "max_answer_len", MAX_ANSWER_CONTENT_LENGTH
1523
1540
  )
1524
1541
 
1542
+ # Restore plan state
1543
+ self._plan_enabled = runtime_state.get("plan_enabled", False)
1544
+ self._plan_id = runtime_state.get("plan_id")
1545
+
1546
+ task_registry_data = runtime_state.get("task_registry")
1547
+ if task_registry_data:
1548
+ from dolphin.core.task_registry import TaskRegistry
1549
+ self.task_registry = TaskRegistry.from_dict(task_registry_data)
1550
+ else:
1551
+ self.task_registry = None
1552
+
1553
+ self._nesting_level = runtime_state.get("nesting_level", 0)
1554
+
1525
1555
  # Resume the current agent (to be handled in the calling function above)
1526
1556
  # The restoration of self.cur_agent requires external coordination
1527
1557
 
@@ -1581,3 +1611,136 @@ class Context:
1581
1611
  """Clear the interrupt status (called when resuming execution)."""
1582
1612
  if self._interrupt_event is not None:
1583
1613
  self._interrupt_event.clear()
1614
+
1615
+ # === Output Events API ===
1616
+
1617
+ def write_output(self, event_type: "str | OutputEventType", data: Dict[str, Any]) -> None:
1618
+ """Record an output event for UI/SDK consumers.
1619
+
1620
+ Args:
1621
+ event_type: Event type (OutputEventType enum or string for backward compatibility)
1622
+ data: Event payload data
1623
+
1624
+ Notes:
1625
+ - This is an in-memory buffer only (process-local).
1626
+ - Consumers can call drain_output_events() to fetch and clear.
1627
+ - Prefer using OutputEventType enum for type safety.
1628
+ """
1629
+ from dolphin.core.task_registry import OutputEventType
1630
+
1631
+ # Convert enum to string for backward compatibility
1632
+ event_type_str = event_type.value if isinstance(event_type, OutputEventType) else event_type
1633
+
1634
+ event = {
1635
+ "event_type": event_type_str,
1636
+ "data": data,
1637
+ "timestamp_ms": int(time.time() * 1000),
1638
+ }
1639
+ self._output_events.append(event)
1640
+
1641
+ def drain_output_events(self) -> List[Dict[str, Any]]:
1642
+ """Drain and clear buffered output events."""
1643
+ events = self._output_events
1644
+ self._output_events = []
1645
+ return events
1646
+
1647
+ # === Plan Mode API ===
1648
+
1649
+ async def enable_plan(self, plan_id: Optional[str] = None) -> None:
1650
+ """Enable plan mode (lazy initialization).
1651
+
1652
+ This method can be called multiple times for replan scenarios.
1653
+
1654
+ Args:
1655
+ plan_id: Optional plan identifier (auto-generated if not provided)
1656
+
1657
+ Behavior:
1658
+ - First call: Creates TaskRegistry
1659
+ - Subsequent calls (replan): Generates new plan_id, resets TaskRegistry
1660
+ """
1661
+ import uuid
1662
+ from dolphin.core.task_registry import TaskRegistry
1663
+
1664
+ if not self._plan_enabled:
1665
+ self._plan_enabled = True
1666
+ self.task_registry = TaskRegistry()
1667
+ logger.info("Plan mode enabled")
1668
+ else:
1669
+ # Replan: cancel running tasks and reset registry
1670
+ if self.task_registry:
1671
+ cancelled = await self.task_registry.cancel_all_running()
1672
+ if cancelled > 0:
1673
+ logger.info(f"Replan: cancelled {cancelled} running tasks")
1674
+ await self.task_registry.reset()
1675
+ logger.info("Plan mode replan triggered")
1676
+
1677
+ self._plan_id = plan_id or str(uuid.uuid4())
1678
+ logger.debug(f"Plan ID: {self._plan_id}")
1679
+
1680
+ async def disable_plan(self) -> None:
1681
+ """Disable plan mode and cleanup resources."""
1682
+ if self.task_registry:
1683
+ await self.task_registry.cancel_all_running()
1684
+ self.task_registry = None
1685
+ self._plan_enabled = False
1686
+ self._plan_id = None
1687
+ logger.info("Plan mode disabled")
1688
+
1689
+ def is_plan_enabled(self) -> bool:
1690
+ """Check if plan mode is enabled.
1691
+
1692
+ Returns:
1693
+ True if plan mode is enabled, False otherwise
1694
+ """
1695
+ return self._plan_enabled
1696
+
1697
+ async def has_active_plan(self) -> bool:
1698
+ """Check if there is an active plan with non-terminal tasks.
1699
+
1700
+ Returns:
1701
+ True if plan is enabled, has tasks, and not all tasks are done
1702
+ """
1703
+ if not self._plan_enabled:
1704
+ return False
1705
+ if not self.task_registry or not await self.task_registry.has_tasks():
1706
+ return False
1707
+ return not await self.task_registry.is_all_done()
1708
+
1709
+ def get_plan_id(self) -> Optional[str]:
1710
+ """Get the current plan ID.
1711
+
1712
+ Returns:
1713
+ Plan ID string, or None if plan mode is not enabled
1714
+ """
1715
+ return self._plan_id
1716
+
1717
+ def fork(self, task_id: str) -> "COWContext":
1718
+ """Create a Copy-On-Write child context for subtask isolation.
1719
+
1720
+ Args:
1721
+ task_id: Task identifier for the child context
1722
+
1723
+ Returns:
1724
+ COWContext instance that isolates writes
1725
+
1726
+ Raises:
1727
+ RuntimeError: If nesting level exceeds maximum allowed depth (3 levels)
1728
+
1729
+ Note:
1730
+ Maximum nesting depth is enforced to prevent memory overflow from
1731
+ deeply nested subtasks (e.g., subtask creates subtask creates subtask...).
1732
+ """
1733
+ MAX_NESTING_LEVEL = 3
1734
+
1735
+ if self._nesting_level >= MAX_NESTING_LEVEL:
1736
+ raise RuntimeError(
1737
+ f"Maximum subtask nesting depth ({MAX_NESTING_LEVEL}) exceeded. "
1738
+ f"Current level: {self._nesting_level}. "
1739
+ "Deeply nested subtasks can cause memory overflow. "
1740
+ "Consider flattening your task structure or breaking it into sequential steps."
1741
+ )
1742
+
1743
+ from dolphin.core.context.cow_context import COWContext
1744
+ child = COWContext(self, task_id)
1745
+ child._nesting_level = self._nesting_level + 1
1746
+ return child