fix(world-model): scope WorldModel state per-task to prevent cross-task contamination

WorldModel routed all state through a single mutable _current_task_id,
so concurrent tasks sharing one instance could corrupt each other's
state. start_task() now returns a TaskWorldView handle scoped to that
task; TaskRunner.run() threads it through as a local variable instead
of reading self.world_model implicitly. Also extracts _start_world_view/
_record_step_result as reusable TaskRunner methods for composed runners.

openspec: world-model capability, archived change world-model-runtime
This commit is contained in:
2026-07-07 08:30:36 +08:00
parent 8e29fcf3c2
commit b5e68398f8
3 changed files with 167 additions and 22 deletions
+37 -8
View File
@@ -24,7 +24,7 @@ from storage.timeline import Timeline
from tools.describe_screen import describe_screen
from tools.screenshot import take_screenshot
from world.config import WorldConfig, load_config as load_world_config
from world.model import WorldModel
from world.model import TaskWorldView, WorldModel
logger = logging.getLogger(__name__)
@@ -87,8 +87,9 @@ class TaskRunner:
def run(self, task: Task) -> Task:
context = TaskContext(task_id=task.id, goal=task.goal)
if self.world_model is not None:
context.world = self.world_model.start_task(task.id)
world_handle = self._start_world_view(task.id)
if world_handle is not None:
context.world = world_handle.state
self._update_task(task, status="running")
for _ in range(self.config.max_steps):
@@ -109,8 +110,7 @@ class TaskRunner:
context=context,
)
context.add_step_result(result)
self._update_world(context, scene, step, result)
self._append_timeline(task, scene, step, result)
self._record_step_result(world_handle, context, task, scene, step, result)
if not result.success:
self._update_task(
task,
@@ -128,6 +128,34 @@ class TaskRunner:
)
return task
def _start_world_view(self, task_id: str) -> TaskWorldView | None:
"""Start (or resume) this task's isolated `WorldModel` handle, if enabled.
Shared by `run()` and by any other driver (e.g. `CollaborativeTaskRunner`)
that composes this `TaskRunner` for its world-model bookkeeping.
"""
if self.world_model is None:
return None
return self.world_model.start_task(task_id)
def _record_step_result(
self,
world_handle: TaskWorldView | None,
context: TaskContext,
task: Task,
scene: Scene,
step: PlannedStep,
result: object,
) -> None:
"""Record one executed step's bookkeeping: world-model observe + timeline append.
Shared by `run()` and by any other driver (e.g. `CollaborativeTaskRunner`)
that executes steps outside of this class's own loop, so the two paths
cannot drift apart on what gets recorded for a step.
"""
self._update_world(world_handle, context, scene, step, result)
self._append_timeline(task, scene, step, result)
def _complete_task(self, task: Task) -> Task:
self._update_task(task, status="completed", completed=True)
self._notify_task_succeeded(task)
@@ -194,20 +222,21 @@ class TaskRunner:
def _update_world(
self,
world_handle: TaskWorldView | None,
context: TaskContext,
scene: Scene,
step: PlannedStep,
result: object,
) -> None:
if self.world_model is None:
if world_handle is None:
return
self.world_model.observe(
world_handle.observe(
scene,
self._semantic_scene_from_result(result),
step,
result, # type: ignore[arg-type]
)
context.world = self.world_model.state
context.world = world_handle.state
def _semantic_scene_from_result(self, result: object) -> SemanticScene | None:
value = getattr(result, "result", None)