feat: release 1.1.0 with responsive task control

This commit is contained in:
chenmingxuan committed 2026-09-24 03:31:17 +08:00
1 parent 057f0c3050
commit bfcd828fa8
37 files changed
+1601 -151

No files matched your search

+68 -1
View File
@@ -1,5 +1,6 @@
"""Regression coverage for quoted click targets and prose-only recovery loops."""
"""Regression coverage for operator recovery and task pause handling."""
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
@@ -8,6 +9,7 @@ import pytest
from artemis.agents.operator.operator import OperatorNode, OperatorNoProgressError
from artemis.config.settings import settings
from artemis.runtime import task_control
@pytest.fixture
@@ -135,3 +137,68 @@ async def test_iteration_cap_cannot_restart_no_progress_next_turn(operator, monk
with pytest.raises(OperatorNoProgressError, match="预算内未恢复"):
await node._invoke_llm_loop(Mock(), [], [], [], state)
assert invoke.await_count == 2
@pytest.mark.asyncio
async def test_pause_during_diagnosis_returns_to_fresh_observation(operator, monkeypatch, tmp_path):
node, state = operator
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
started = asyncio.Event()
cancelled = asyncio.Event()
events = []
class Lock:
def __init__(self, device_id, **kwargs):
self.device_id = device_id
self.description = "test"
self.max_concurrency = None
self.concurrency_mode = "per_device"
self.ingress = "web"
self.lock_scope = "test"
def release(self):
events.append("released")
def acquire(self, cancel_event=None):
events.append("acquired")
monkeypatch.setattr(task_control, "DeviceExecutionLock", Lock)
node.ctx.data_engine = None
node.ctx.task_control = task_control.TaskControl("pro-task", Lock("device"))
node.ctx.on_task_pause = AsyncMock()
node.ctx.on_task_resume = AsyncMock()
async def diagnose(**kwargs):
started.set()
try:
await asyncio.Event().wait()
finally:
cancelled.set()
monkeypatch.setattr("artemis.agents.operator.operator.invoke_tool_with_injection", diagnose)
response = AIMessage(
content="Inspect the failure",
tool_calls=[
{"name": "ask_diagnoser", "args": {"query": "why"}, "id": "diagnosis"},
{"name": "click", "args": {"target": 13}, "id": "tap"},
],
)
invoke = AsyncMock(return_value=response)
monkeypatch.setattr("artemis.agents.operator.operator.acomplete", invoke)
tool = SimpleNamespace(name="ask_diagnoser")
messages = []
execution = asyncio.create_task(node._invoke_llm_loop(Mock(), messages, [tool], [], state))
await asyncio.wait_for(started.wait(), 1)
task_control.request_pause("pro-task")
for _ in range(40):
if task_control.read_state("pro-task").get("status") == "task_paused":
break
await asyncio.sleep(0.01)
assert task_control.read_state("pro-task")["status"] == "task_paused"
assert cancelled.is_set()
assert events == ["released"]
task_control.request_resume("pro-task")
assert (await asyncio.wait_for(execution, 2))[0] is None
assert events == ["released", "acquired"]
assert invoke.await_count == 1
assert [m.status for m in messages if isinstance(m, ToolMessage)] == ["error", "error"]