Merge branch 'worktree-task-cancellation': task cancellation feature
Tests / Test passed: 926

# Conflicts:
#	packages/cloud-platform/cloud/schema.py
This commit is contained in:
2026-07-15 19:39:28 +08:00
49 changed files with 2374 additions and 63 deletions
+16 -6
View File
@@ -40,9 +40,15 @@ Observer = Callable[[str], Scene]
ScreenshotProvider = Callable[[str], bytes]
TaskSucceededHook = Callable[[str, str, Timeline], None]
StopRequested = Callable[[], bool]
StopReason = Callable[[], "str | None"]
StepProgressCallback = Callable[[int, str, str], None]
def is_cancellation_reason(reason: str | None) -> bool:
"""Distinguish an explicit cancellation stop from other stop reasons (e.g. lost lease)."""
return bool(reason) and "cancel" in reason.lower()
class TaskRunner:
def __init__(
self,
@@ -98,6 +104,7 @@ class TaskRunner:
task: Task,
*,
should_stop: StopRequested | None = None,
stop_reason: StopReason | None = None,
) -> Task:
if self.metadata_store:
self.metadata_store.create_task(task)
@@ -109,7 +116,7 @@ class TaskRunner:
for _ in range(self.config.max_steps):
if should_stop is not None and should_stop():
return self._interrupt_task(task)
return self._interrupt_task(task, stop_reason)
try:
scene = self.observer(task.device_id)
context.add_scene(scene)
@@ -136,7 +143,7 @@ class TaskRunner:
for step_index, step in enumerate(steps):
if should_stop is not None and should_stop():
return self._interrupt_task(task)
return self._interrupt_task(task, stop_reason)
executable_step = self._step_for_device(step, task.device_id)
if step_index == 0 and screenshot is not None:
# Reuse the screenshot already captured for planning instead of
@@ -194,13 +201,16 @@ class TaskRunner:
)
return task
def _interrupt_task(self, task: Task) -> Task:
self._emit_step_progress(-1, "failed", "execution interrupted")
def _interrupt_task(self, task: Task, stop_reason: StopReason | None = None) -> Task:
reason = stop_reason() if stop_reason is not None else None
message = reason or "execution interrupted"
status = "cancelled" if is_cancellation_reason(reason) else "failed"
self._emit_step_progress(-1, "failed", message)
self._update_task(
task,
status="failed",
status=status,
completed=True,
failure_reason="execution interrupted",
failure_reason=message,
)
return task