feat(runtime): add cancellation-aware stop_reason to TaskRunner and WorkflowRunner
- TaskRunner.run() and WorkflowRunner.run()/resume() accept an optional stop_reason callable alongside should_stop, distinguishing a genuine cancellation from other stop conditions (e.g. lost lease). - is_cancellation_reason() shared helper added to runtime/task.py. - WorkflowRunner._stop_status() now branches cancelled/failed based on stop_reason, correcting a prior blanket cancelled-on-any-stop behavior that conflicted with the host-agent-protocol spec's requirement to distinguish cancellation from lease-loss stops. - Default behavior (stop_reason=None) is preserved exactly for both runners so existing callers/tests are unaffected. - Task 1 of openspec change task-cancellation.
This commit is contained in:
+32
-6
@@ -7,7 +7,7 @@ from typing import Any
|
||||
from time import sleep
|
||||
|
||||
from core.models import Scene, Task
|
||||
from runtime.task import TaskRunner
|
||||
from runtime.task import StopReason, TaskRunner, is_cancellation_reason
|
||||
from skills_learning.store import SkillStore, get_default_store
|
||||
from workflow.conditions import (
|
||||
ConditionEvaluator,
|
||||
@@ -70,6 +70,7 @@ class WorkflowRunner:
|
||||
initial_variables: dict[str, Any] | None = None,
|
||||
*,
|
||||
should_stop: StopRequested | None = None,
|
||||
stop_reason: StopReason | None = None,
|
||||
) -> WorkflowRun:
|
||||
self.store.save_definition(definition)
|
||||
run = self.store.create_run(
|
||||
@@ -77,13 +78,14 @@ class WorkflowRunner:
|
||||
initial_variables or {},
|
||||
device_id=device_id,
|
||||
)
|
||||
return self._drive(definition, run, should_stop=should_stop)
|
||||
return self._drive(definition, run, should_stop=should_stop, stop_reason=stop_reason)
|
||||
|
||||
def resume(
|
||||
self,
|
||||
run_id: str,
|
||||
*,
|
||||
should_stop: StopRequested | None = None,
|
||||
stop_reason: StopReason | None = None,
|
||||
) -> WorkflowRun:
|
||||
run = self.store.get_run(run_id)
|
||||
if run is None:
|
||||
@@ -93,7 +95,18 @@ class WorkflowRunner:
|
||||
definition = self.store.get_definition(run.definition_id)
|
||||
if definition is None:
|
||||
raise KeyError(f"unknown workflow definition {run.definition_id}")
|
||||
return self._drive(definition, run, should_stop=should_stop)
|
||||
return self._drive(definition, run, should_stop=should_stop, stop_reason=stop_reason)
|
||||
|
||||
def _stop_status(self, stop_reason: StopReason | None) -> str:
|
||||
"""Resolve the terminal status for a should_stop-triggered stop.
|
||||
|
||||
No `stop_reason` preserves the pre-existing default of `cancelled` for
|
||||
any stop; a supplied reason distinguishes an explicit cancellation
|
||||
from other stop conditions (e.g. lost lease), which resolve to `failed`.
|
||||
"""
|
||||
if stop_reason is None:
|
||||
return "cancelled"
|
||||
return "cancelled" if is_cancellation_reason(stop_reason()) else "failed"
|
||||
|
||||
def _drive(
|
||||
self,
|
||||
@@ -101,11 +114,14 @@ class WorkflowRunner:
|
||||
run: WorkflowRun,
|
||||
*,
|
||||
should_stop: StopRequested | None = None,
|
||||
stop_reason: StopReason | None = None,
|
||||
) -> WorkflowRun:
|
||||
executed = 0
|
||||
while run.status not in TERMINAL_STATUSES and run.current_step_id:
|
||||
if should_stop is not None and should_stop():
|
||||
return self._checkpoint(run, run.current_step_id, "cancelled")
|
||||
return self._checkpoint(
|
||||
run, run.current_step_id, self._stop_status(stop_reason)
|
||||
)
|
||||
if self.step_limit is not None and executed >= self.step_limit:
|
||||
return run
|
||||
step = definition.step_by_id(run.current_step_id)
|
||||
@@ -125,10 +141,13 @@ class WorkflowRunner:
|
||||
run,
|
||||
step,
|
||||
should_stop=should_stop,
|
||||
stop_reason=stop_reason,
|
||||
)
|
||||
if should_stop is not None and should_stop():
|
||||
self.store.append_step_result(run.id, result)
|
||||
return self._checkpoint(run, run.current_step_id, "cancelled")
|
||||
return self._checkpoint(
|
||||
run, run.current_step_id, self._stop_status(stop_reason)
|
||||
)
|
||||
next_status, next_step_id = self._resolve_outcome(
|
||||
definition, step, result.success, branch_next_step_id
|
||||
)
|
||||
@@ -185,12 +204,14 @@ class WorkflowRunner:
|
||||
step: WorkflowStep,
|
||||
*,
|
||||
should_stop: StopRequested | None = None,
|
||||
stop_reason: StopReason | None = None,
|
||||
) -> tuple[WorkflowStepResult, str | None]:
|
||||
if isinstance(step, PlannedGoalStep):
|
||||
return self._execute_planned_goal_step(
|
||||
run,
|
||||
step,
|
||||
should_stop=should_stop,
|
||||
stop_reason=stop_reason,
|
||||
), None
|
||||
if isinstance(step, SkillInvocationStep):
|
||||
return self._execute_skill_invocation_step(step), None
|
||||
@@ -218,13 +239,18 @@ class WorkflowRunner:
|
||||
step: PlannedGoalStep,
|
||||
*,
|
||||
should_stop: StopRequested | None = None,
|
||||
stop_reason: StopReason | None = None,
|
||||
) -> WorkflowStepResult:
|
||||
task = Task(goal=step.goal, device_id=run.device_id or "")
|
||||
task_runner = self.task_runner_factory()
|
||||
if should_stop is None:
|
||||
result_task = task_runner.run(task)
|
||||
else:
|
||||
elif stop_reason is None:
|
||||
result_task = task_runner.run(task, should_stop=should_stop)
|
||||
else:
|
||||
result_task = task_runner.run(
|
||||
task, should_stop=should_stop, stop_reason=stop_reason
|
||||
)
|
||||
success = result_task.status == "completed"
|
||||
return WorkflowStepResult(
|
||||
step_id=step.step_id,
|
||||
|
||||
Reference in New Issue
Block a user