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
|
@@ -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
|
-
|
|
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
|
-
#
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
dolphin/core/common/constants.py
CHANGED
|
@@ -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 =
|
|
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:
|
dolphin/core/context/context.py
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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("
|
|
517
|
+
detail_skill = SystemFunctions.getSkill("_get_cached_result_detail")
|
|
505
518
|
if not detail_skill:
|
|
506
|
-
detail_skill = SystemFunctions.getSkill("system_functions.
|
|
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 "
|
|
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
|