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.
Files changed (38) hide show
  1. dolphin/cli/runner/runner.py +20 -0
  2. dolphin/cli/ui/console.py +35 -17
  3. dolphin/cli/utils/helpers.py +4 -4
  4. dolphin/core/agent/base_agent.py +70 -7
  5. dolphin/core/code_block/basic_code_block.py +162 -26
  6. dolphin/core/code_block/explore_block.py +438 -35
  7. dolphin/core/code_block/explore_block_v2.py +105 -16
  8. dolphin/core/code_block/explore_strategy.py +3 -1
  9. dolphin/core/code_block/judge_block.py +41 -8
  10. dolphin/core/code_block/skill_call_deduplicator.py +32 -10
  11. dolphin/core/code_block/tool_block.py +69 -23
  12. dolphin/core/common/constants.py +25 -1
  13. dolphin/core/config/global_config.py +35 -0
  14. dolphin/core/context/context.py +175 -9
  15. dolphin/core/context/cow_context.py +392 -0
  16. dolphin/core/executor/dolphin_executor.py +9 -0
  17. dolphin/core/flags/definitions.py +2 -2
  18. dolphin/core/llm/llm.py +2 -3
  19. dolphin/core/llm/llm_client.py +1 -0
  20. dolphin/core/runtime/runtime_instance.py +31 -0
  21. dolphin/core/skill/context_retention.py +3 -3
  22. dolphin/core/task_registry.py +404 -0
  23. dolphin/core/utils/cache_kv.py +70 -8
  24. dolphin/core/utils/tools.py +2 -0
  25. dolphin/lib/__init__.py +0 -2
  26. dolphin/lib/skillkits/__init__.py +2 -2
  27. dolphin/lib/skillkits/plan_skillkit.py +756 -0
  28. dolphin/lib/skillkits/system_skillkit.py +103 -30
  29. dolphin/sdk/skill/global_skills.py +43 -3
  30. dolphin/sdk/skill/traditional_toolkit.py +4 -0
  31. {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/METADATA +1 -1
  32. {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/RECORD +36 -34
  33. {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/WHEEL +1 -1
  34. kweaver_dolphin-0.2.1.dist-info/entry_points.txt +15 -0
  35. dolphin/lib/skillkits/plan_act_skillkit.py +0 -452
  36. kweaver_dolphin-0.1.0.dist-info/entry_points.txt +0 -27
  37. {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/licenses/LICENSE.txt +0 -0
  38. {kweaver_dolphin-0.1.0.dist-info → kweaver_dolphin-0.2.1.dist-info}/top_level.txt +0 -0
dolphin/core/llm/llm.py CHANGED
@@ -249,7 +249,6 @@ class LLMModelFactory(LLM):
249
249
  finish_reason = None
250
250
  # Use ToolCallsParser to handle tool calls parsing
251
251
  tool_parser = ToolCallsParser()
252
-
253
252
  timeout = aiohttp.ClientTimeout(
254
253
  total=1800, # Disable overall timeout (use with caution)
255
254
  sock_connect=30, # Keep connection timeout
@@ -320,7 +319,7 @@ class LLMModelFactory(LLM):
320
319
 
321
320
  accu_content += delta_content
322
321
  reasoning_content += delta_reasoning
323
-
322
+
324
323
  # Capture finish_reason
325
324
  chunk_finish_reason = line_json["choices"][0].get("finish_reason")
326
325
  if chunk_finish_reason:
@@ -461,7 +460,7 @@ class LLMOpenai(LLM):
461
460
  and delta.reasoning_content is not None
462
461
  ):
463
462
  accu_reasoning += delta.reasoning_content
464
-
463
+
465
464
  # Capture finish_reason
466
465
  chunk_finish_reason = chunk.choices[0].finish_reason
467
466
  if chunk_finish_reason:
@@ -182,6 +182,7 @@ class LLMClient:
182
182
  sock_connect=30, # Keep connection timeout
183
183
  sock_read=300, # Single read timeout (for slow streaming data)
184
184
  )
185
+ print(f"------------------------llm={payload}")
185
186
  async with aiohttp.ClientSession(timeout=timeout) as session:
186
187
  async with session.post(
187
188
  model_config.api,
@@ -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 _get_result_detail('{reference_id}')]"
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: _get_result_detail('{reference_id}')\n"
118
- f"Get range: _get_result_detail('{reference_id}', offset=0, limit=2000)")
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
@@ -0,0 +1,404 @@
1
+ """Task Registry for Plan Mode.
2
+
3
+ This module provides task state management for unified plan architecture.
4
+
5
+ Logging conventions:
6
+ - DEBUG: Task registration, status updates, cancellations
7
+ - INFO: Registry lifecycle events (reset, mode changes)
8
+ - WARNING: Invalid operations (unknown task, missing registry)
9
+ - ERROR: Critical failures in task management
10
+ """
11
+
12
+ import asyncio
13
+ import time
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from dolphin.core.logging.logger import get_logger
19
+
20
+ logger = get_logger("task_registry")
21
+
22
+
23
+ class OutputEventType(str, Enum):
24
+ """Output event types for UI/SDK consumers.
25
+
26
+ These events are emitted via Context.write_output() and can be consumed
27
+ by UI components or SDK integrations for real-time updates.
28
+ """
29
+ TASK_STARTED = "task_started"
30
+ TASK_COMPLETED = "task_completed"
31
+ TASK_FAILED = "task_failed"
32
+ TASK_CANCELLED = "task_cancelled"
33
+ TASK_PROGRESS = "task_progress"
34
+ PLAN_CREATED = "plan_created"
35
+ PLAN_UPDATED = "plan_updated"
36
+ PLAN_FINISHED = "plan_finished"
37
+
38
+
39
+ class TaskStatus(str, Enum):
40
+ """Task status values."""
41
+ PENDING = "pending"
42
+ RUNNING = "running"
43
+ COMPLETED = "completed"
44
+ FAILED = "failed"
45
+ CANCELLED = "cancelled"
46
+ SKIPPED = "skipped"
47
+
48
+
49
+ class PlanExecMode(str, Enum):
50
+ """Execution mode for plan orchestration."""
51
+ PARALLEL = "parallel"
52
+ SEQUENTIAL = "sequential"
53
+
54
+ @staticmethod
55
+ def from_str(mode: str) -> "PlanExecMode":
56
+ """Convert a string (seq/para/sequential/parallel) to PlanExecMode."""
57
+ if not mode:
58
+ return PlanExecMode.PARALLEL
59
+
60
+ mode = mode.lower().strip()
61
+ if mode in ("seq", "sequential"):
62
+ return PlanExecMode.SEQUENTIAL
63
+ if mode in ("para", "parallel"):
64
+ return PlanExecMode.PARALLEL
65
+
66
+ raise ValueError(f"Invalid execution mode: {mode}. Must be 'seq' or 'para'.")
67
+
68
+
69
+ @dataclass
70
+ class Task:
71
+ """Task metadata and state."""
72
+ id: str
73
+ name: str
74
+ prompt: str
75
+
76
+ # Runtime fields
77
+ status: TaskStatus = TaskStatus.PENDING
78
+ answer: Optional[str] = None
79
+ think: Optional[str] = None
80
+ block_answer: Optional[str] = None
81
+ error: Optional[str] = None
82
+ started_at: Optional[float] = None
83
+ duration: Optional[float] = None
84
+ attempt: int = 0
85
+
86
+ # Reserved for Phase 2
87
+ depends_on: List[str] = field(default_factory=list)
88
+
89
+ def to_dict(self) -> Dict[str, Any]:
90
+ """Convert Task to dictionary."""
91
+ return {
92
+ "id": self.id,
93
+ "name": self.name,
94
+ "prompt": self.prompt,
95
+ "status": self.status.value,
96
+ "answer": self.answer,
97
+ "think": self.think,
98
+ "block_answer": self.block_answer,
99
+ "error": self.error,
100
+ "started_at": self.started_at,
101
+ "duration": self.duration,
102
+ "attempt": self.attempt,
103
+ "depends_on": self.depends_on,
104
+ }
105
+
106
+ @classmethod
107
+ def from_dict(cls, data: Dict[str, Any]) -> "Task":
108
+ """Create Task from dictionary."""
109
+ return cls(
110
+ id=data["id"],
111
+ name=data["name"],
112
+ prompt=data["prompt"],
113
+ status=TaskStatus(data["status"]),
114
+ answer=data.get("answer"),
115
+ think=data.get("think"),
116
+ block_answer=data.get("block_answer"),
117
+ error=data.get("error"),
118
+ started_at=data.get("started_at"),
119
+ duration=data.get("duration"),
120
+ attempt=data.get("attempt", 0),
121
+ depends_on=data.get("depends_on", []),
122
+ )
123
+
124
+
125
+ class TaskRegistry:
126
+ """Persistent task state registry.
127
+
128
+ Notes:
129
+ - Stores only serializable task state.
130
+ - Runtime handles (asyncio.Task) are kept outside for correctness and recoverability.
131
+ - Thread-safe via asyncio.Lock.
132
+ """
133
+
134
+ def __init__(self):
135
+ self.tasks: Dict[str, Task] = {}
136
+ self._lock = asyncio.Lock()
137
+
138
+ # Config fields (set by PlanSkillkit._plan_tasks)
139
+ self.exec_mode: PlanExecMode = PlanExecMode.PARALLEL
140
+ self.max_concurrency: int = 5
141
+
142
+ # Runtime handles (not persisted)
143
+ self.running_asyncio_tasks: Dict[str, asyncio.Task] = {}
144
+
145
+ def to_dict(self) -> Dict[str, Any]:
146
+ """Convert TaskRegistry to dictionary."""
147
+ return {
148
+ "tasks": {task_id: task.to_dict() for task_id, task in self.tasks.items()},
149
+ "exec_mode": self.exec_mode.value,
150
+ "max_concurrency": self.max_concurrency,
151
+ }
152
+
153
+ @classmethod
154
+ def from_dict(cls, data: Dict[str, Any]) -> "TaskRegistry":
155
+ """Create TaskRegistry from dictionary."""
156
+ registry = cls()
157
+ registry.tasks = {
158
+ task_id: Task.from_dict(task_data)
159
+ for task_id, task_data in data.get("tasks", {}).items()
160
+ }
161
+ registry.exec_mode = PlanExecMode(data.get("exec_mode", PlanExecMode.PARALLEL.value))
162
+ registry.max_concurrency = data.get("max_concurrency", 5)
163
+ return registry
164
+
165
+ async def add_task(self, task: Task):
166
+ """Register a new task."""
167
+ async with self._lock:
168
+ self.tasks[task.id] = task
169
+ logger.debug(f"Task registered: {task.id} ({task.name})")
170
+
171
+ async def get_task(self, task_id: str) -> Optional[Task]:
172
+ """Retrieve a task by ID (thread-safe)."""
173
+ async with self._lock:
174
+ return self.tasks.get(task_id)
175
+
176
+ async def get_all_tasks(self) -> List[Task]:
177
+ """Return all tasks (thread-safe)."""
178
+ async with self._lock:
179
+ return list(self.tasks.values())
180
+
181
+ async def get_pending_tasks(self) -> List[Task]:
182
+ """Return tasks that are pending (thread-safe)."""
183
+ async with self._lock:
184
+ return [t for t in self.tasks.values() if t.status == TaskStatus.PENDING]
185
+
186
+ async def get_ready_tasks(self) -> List[Task]:
187
+ """Return tasks that are ready to be started (thread-safe).
188
+
189
+ Phase 1 (no dependency scheduling):
190
+ - All PENDING tasks are considered ready.
191
+
192
+ Phase 2 (reserved):
193
+ - Check depends_on and only return tasks whose dependencies are completed.
194
+ """
195
+ async with self._lock:
196
+ return [task for task in self.tasks.values() if task.status == TaskStatus.PENDING]
197
+
198
+ async def get_running_tasks(self) -> List[Task]:
199
+ """Return tasks that are running (thread-safe)."""
200
+ async with self._lock:
201
+ return [t for t in self.tasks.values() if t.status == TaskStatus.RUNNING]
202
+
203
+ async def get_completed_tasks(self) -> List[Task]:
204
+ """Return tasks that are completed (thread-safe)."""
205
+ async with self._lock:
206
+ return [t for t in self.tasks.values() if t.status == TaskStatus.COMPLETED]
207
+
208
+ async def get_failed_tasks(self) -> List[Task]:
209
+ """Return tasks that have failed (thread-safe)."""
210
+ async with self._lock:
211
+ return [t for t in self.tasks.values() if t.status == TaskStatus.FAILED]
212
+
213
+ async def has_tasks(self) -> bool:
214
+ """Return whether any tasks are registered (thread-safe)."""
215
+ async with self._lock:
216
+ return bool(self.tasks)
217
+
218
+ async def reset(self):
219
+ """Reset task state (used for replan).
220
+
221
+ Clears:
222
+ - All tasks
223
+ - Running asyncio task handles (cancels them first)
224
+
225
+ Preserves:
226
+ - exec_mode (will be overwritten by next _plan_tasks call)
227
+ - max_concurrency (will be overwritten by next _plan_tasks call)
228
+
229
+ Note:
230
+ This is an async method to ensure proper locking for concurrent safety.
231
+ """
232
+ async with self._lock:
233
+ # Cancel all running asyncio tasks to prevent background leaks during replan
234
+ for task_id, asyncio_task in self.running_asyncio_tasks.items():
235
+ if not asyncio_task.done():
236
+ asyncio_task.cancel()
237
+ logger.debug(f"Cancelled orphaned task {task_id} during registry reset")
238
+
239
+ self.tasks.clear()
240
+ self.running_asyncio_tasks.clear()
241
+ logger.info("TaskRegistry reset (replan)")
242
+
243
+ async def is_all_done(self) -> bool:
244
+ """Return whether all tasks have reached a terminal state (thread-safe)."""
245
+ async with self._lock:
246
+ terminal = {TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED, TaskStatus.SKIPPED}
247
+ return all(task.status in terminal for task in self.tasks.values())
248
+
249
+ async def update_status(
250
+ self,
251
+ task_id: str,
252
+ status: TaskStatus,
253
+ **kwargs
254
+ ):
255
+ """Update task status and related fields.
256
+
257
+ Args:
258
+ task_id: Task identifier
259
+ status: New status
260
+ **kwargs: Additional fields to update (answer, think, block_answer, error, started_at, etc.)
261
+
262
+ Note:
263
+ Terminal states (COMPLETED, FAILED, CANCELLED, SKIPPED) cannot be transitioned.
264
+ This prevents race conditions during task cancellation or completion.
265
+ """
266
+ async with self._lock:
267
+ task = self.tasks.get(task_id)
268
+ if not task:
269
+ logger.warning(f"Cannot update status for unknown task: {task_id}")
270
+ return
271
+
272
+ # Validate state transitions: terminal states cannot be changed
273
+ # Note: FAILED is excluded from terminal states to allow retries
274
+ terminal_states = {TaskStatus.COMPLETED, TaskStatus.CANCELLED, TaskStatus.SKIPPED}
275
+ if task.status in terminal_states:
276
+ logger.warning(
277
+ f"Cannot transition task {task_id} from terminal state {task.status.value} to {status.value}"
278
+ )
279
+ return
280
+
281
+ task.status = status
282
+
283
+ # Update additional fields
284
+ for key, value in kwargs.items():
285
+ if hasattr(task, key):
286
+ setattr(task, key, value)
287
+
288
+ # Compute duration for terminal states
289
+ if status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED):
290
+ if task.started_at and not task.duration:
291
+ task.duration = time.time() - task.started_at
292
+
293
+ logger.debug(f"Task {task_id} status updated: {status.value}")
294
+
295
+ async def get_status_counts(self) -> Dict[str, int]:
296
+ """Return count per status (thread-safe)."""
297
+ async with self._lock:
298
+ counts = {status.value: 0 for status in TaskStatus}
299
+ for task in self.tasks.values():
300
+ counts[task.status.value] += 1
301
+ return counts
302
+
303
+ async def get_progress_signature(self) -> tuple:
304
+ """Compute a signature representing the current task progress state (thread-safe).
305
+
306
+ Returns:
307
+ A tuple of (task_id, status) pairs sorted by task_id.
308
+ This signature changes whenever any task changes status.
309
+
310
+ Usage:
311
+ Used by ExploreBlock to detect whether tasks have made progress.
312
+ If the signature is the same across multiple rounds, it indicates
313
+ the plan is stalled (no status changes).
314
+
315
+ Example:
316
+ >>> registry.get_progress_signature()
317
+ (('task_1', 'running'), ('task_2', 'pending'))
318
+ """
319
+ async with self._lock:
320
+ tasks = list(self.tasks.values())
321
+ return tuple(
322
+ (t.id, getattr(t.status, "value", str(t.status)))
323
+ for t in sorted(tasks, key=lambda x: x.id)
324
+ )
325
+
326
+ async def get_all_status(self) -> str:
327
+ """Return a formatted status summary (for _check_progress, thread-safe).
328
+
329
+ Returns:
330
+ A multi-line string with task status, including error details for failed tasks.
331
+ """
332
+ async with self._lock:
333
+ lines = []
334
+ now = time.time()
335
+ for task in self.tasks.values():
336
+ if task.status == TaskStatus.RUNNING and task.started_at:
337
+ duration_str = f"{now - task.started_at:.1f}s+"
338
+ else:
339
+ duration_str = f"{task.duration:.1f}s" if task.duration else "N/A"
340
+
341
+ icon_map = {
342
+ TaskStatus.PENDING: "⏳",
343
+ TaskStatus.RUNNING: "🔄",
344
+ TaskStatus.COMPLETED: "✅",
345
+ TaskStatus.FAILED: "❌",
346
+ TaskStatus.CANCELLED: "🚫",
347
+ TaskStatus.SKIPPED: "⏭️",
348
+ }
349
+ icon = icon_map.get(task.status, "?")
350
+ status_label = task.status.value
351
+
352
+ # Item 3 Optimization: Check if task is in the process of being cancelled
353
+ if task.status == TaskStatus.RUNNING and task.id in self.running_asyncio_tasks:
354
+ asyncio_task = self.running_asyncio_tasks[task.id]
355
+ # Check if cancelling (Python 3.11+) or if it's already done but status not updated
356
+ is_cancelling = False
357
+ if hasattr(asyncio_task, "cancelling"):
358
+ is_cancelling = asyncio_task.cancelling() > 0
359
+
360
+ if is_cancelling:
361
+ icon = "⏳🚫"
362
+ status_label = "cancelling..."
363
+
364
+ base_line = f"{icon} {task.id}: {task.name} [{status_label}] ({duration_str})"
365
+
366
+ # For failed tasks, include error details to enable self-correction
367
+ if task.status == TaskStatus.FAILED and task.error:
368
+ error_preview = task.error[:150] # Limit error length
369
+ if len(task.error) > 150:
370
+ error_preview += "..."
371
+ base_line += f"\n Error: {error_preview}"
372
+
373
+ lines.append(base_line)
374
+
375
+ return "\n".join(lines)
376
+
377
+ async def cancel_all_running(self) -> int:
378
+ """Cancel all running asyncio tasks and update their status.
379
+
380
+ Returns:
381
+ Number of tasks cancelled
382
+
383
+ Note:
384
+ This method both cancels the asyncio.Task objects and updates
385
+ the Task status to CANCELLED to keep state synchronized.
386
+ """
387
+ async with self._lock:
388
+ cancelled = 0
389
+ for task_id, asyncio_task in list(self.running_asyncio_tasks.items()):
390
+ if not asyncio_task.done():
391
+ asyncio_task.cancel()
392
+ cancelled += 1
393
+ # Update task status to CANCELLED
394
+ # This ensures Registry state matches actual execution state
395
+ task = self.tasks.get(task_id)
396
+ if task:
397
+ task.status = TaskStatus.CANCELLED
398
+ # Compute duration if task was started
399
+ if task.started_at and not task.duration:
400
+ task.duration = time.time() - task.started_at
401
+ logger.debug(f"Cancelled running task: {task_id}")
402
+
403
+ self.running_asyncio_tasks.clear()
404
+ return cancelled
@@ -1,15 +1,79 @@
1
- import fcntl
2
1
  import json
3
2
  import os
3
+ import sys
4
4
  import threading
5
5
  import time
6
6
  from typing import List, Dict, Any, Optional
7
7
  import uuid
8
8
  from dolphin.core.logging.logger import get_logger
9
9
 
10
+ # Cross-platform file locking support
11
+ _HAS_FCNTL = False
12
+ _HAS_MSVCRT = False
13
+
14
+ if sys.platform == 'win32':
15
+ # Windows: use msvcrt for file locking
16
+ try:
17
+ import msvcrt
18
+ _HAS_MSVCRT = True
19
+ except ImportError:
20
+ pass
21
+ else:
22
+ # Unix/Linux: use fcntl for file locking
23
+ try:
24
+ import fcntl
25
+ _HAS_FCNTL = True
26
+ except ImportError:
27
+ pass
28
+
10
29
  logger = get_logger("utils.cache_kv")
11
30
 
12
31
 
32
+ def _lock_file(f, exclusive=False):
33
+ """
34
+ Cross-platform file locking
35
+
36
+ Args:
37
+ f: File object
38
+ exclusive: If True, acquire exclusive lock; otherwise shared lock
39
+ """
40
+ if _HAS_FCNTL:
41
+ # Unix/Linux: use fcntl
42
+ lock_type = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
43
+ fcntl.flock(f.fileno(), lock_type)
44
+ elif _HAS_MSVCRT:
45
+ # Windows: use msvcrt
46
+ # msvcrt.locking doesn't support shared locks, so we use exclusive
47
+ # Note: msvcrt requires seeking to the start
48
+ f.seek(0)
49
+ try:
50
+ msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
51
+ except OSError:
52
+ # File might be too short, which is ok for our use case
53
+ pass
54
+ # If neither is available, proceed without locking (not ideal but won't crash)
55
+
56
+
57
+ def _unlock_file(f):
58
+ """
59
+ Cross-platform file unlocking
60
+
61
+ Args:
62
+ f: File object
63
+ """
64
+ if _HAS_FCNTL:
65
+ # Unix/Linux: use fcntl
66
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
67
+ elif _HAS_MSVCRT:
68
+ # Windows: use msvcrt
69
+ f.seek(0)
70
+ try:
71
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
72
+ except OSError:
73
+ pass
74
+ # If neither is available, nothing to unlock
75
+
76
+
13
77
  class CacheKV:
14
78
  def __init__(
15
79
  self, filePath: str, dumpInterval: int = 5, expireTimeByDay: float = 1
@@ -28,8 +92,7 @@ class CacheKV:
28
92
 
29
93
  try:
30
94
  with open(self.filePath, "r", encoding="utf-8") as f:
31
- fd = f.fileno()
32
- fcntl.flock(fd, fcntl.LOCK_SH) # Shared lock for reading
95
+ _lock_file(f, exclusive=False) # Shared lock for reading
33
96
  try:
34
97
  loaded_cache = json.load(f)
35
98
  for key, value in loaded_cache.items():
@@ -46,7 +109,7 @@ class CacheKV:
46
109
  # Compatible with old formats
47
110
  self.cache[key] = {"value": value, "timestamp": time.time()}
48
111
  finally:
49
- fcntl.flock(fd, fcntl.LOCK_UN)
112
+ _unlock_file(f)
50
113
  except (json.JSONDecodeError, IOError) as e:
51
114
  logger.error(
52
115
  f"Error loading cache file {self.filePath}: {e} try to backup and reset cache"
@@ -77,14 +140,13 @@ class CacheKV:
77
140
  temp_path = f"{self.filePath}.tmp.{os.getpid()}.{uuid.uuid4().hex}"
78
141
  try:
79
142
  with open(temp_path, "w", encoding="utf-8") as f:
80
- fd = f.fileno()
81
- fcntl.flock(fd, fcntl.LOCK_EX) # Exclusive lock for writing
143
+ _lock_file(f, exclusive=True) # Exclusive lock for writing
82
144
  try:
83
145
  json.dump(self.cache, f, ensure_ascii=False)
84
146
  f.flush()
85
- os.fsync(fd) # Ensure data is written to disk
147
+ os.fsync(f.fileno()) # Ensure data is written to disk
86
148
  finally:
87
- fcntl.flock(fd, fcntl.LOCK_UN)
149
+ _unlock_file(f)
88
150
 
89
151
  # Atomic rename
90
152
  os.rename(temp_path, self.filePath)
@@ -332,9 +332,11 @@ class ToolInterrupt(Exception):
332
332
  message="The tool was interrupted.",
333
333
  tool_name: str = None,
334
334
  tool_args: List[Dict] = None,
335
+ tool_config: Dict = None,
335
336
  *args,
336
337
  **kwargs,
337
338
  ):
338
339
  super().__init__(message, *args, **kwargs)
339
340
  self.tool_name = tool_name if tool_name else ""
340
341
  self.tool_args = tool_args if tool_args else []
342
+ self.tool_config = tool_config if tool_config else {}
dolphin/lib/__init__.py CHANGED
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
24
24
  MemorySkillkit,
25
25
  MCPSkillkit,
26
26
  OntologySkillkit,
27
- PlanActSkillkit,
28
27
  CognitiveSkillkit,
29
28
  VMSkillkit,
30
29
  NoopSkillkit,
@@ -61,7 +60,6 @@ _module_lookup = {
61
60
  "MemorySkillkit": "dolphin.lib.skillkits",
62
61
  "MCPSkillkit": "dolphin.lib.skillkits",
63
62
  "OntologySkillkit": "dolphin.lib.skillkits",
64
- "PlanActSkillkit": "dolphin.lib.skillkits",
65
63
  "CognitiveSkillkit": "dolphin.lib.skillkits",
66
64
  "VMSkillkit": "dolphin.lib.skillkits",
67
65
  "NoopSkillkit": "dolphin.lib.skillkits",