feat: prepare 2.2.0 with UI and execution fixes
Add dark theme, richer Markdown and readable plugin results. Improve Android input evidence, response integrity and diagnostics. Unify lifecycle, guidance receipts and evidence sources across clients. Keep direct action dispatch without an added AI quota approval layer. Align version metadata at 2.2.0 and bundle Helper 1.2.2. Validation: 3063 backend tests before the version bump; 44 after. Frontend: 317 tests and production build passed.
This commit is contained in:
1 parent
83bcfd54d5
commit
fac4ccb760
146 files changed
+5689
-2033
No files matched your search
@@ -0,0 +1,225 @@
|
||||
"""Cancellation provenance must survive the real runtime-to-model path."""
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from artemis.llm.final_review import FinalReviewBudget
|
||||
from artemis.llm.reliability import LLMExhaustedError
|
||||
from artemis.runtime.cancellation import CancellationReason
|
||||
from artemis.runtime import task_control
|
||||
from artemis.sdk.agent import Agent
|
||||
from artemis.services import llm
|
||||
|
||||
|
||||
class Engine:
|
||||
current_session_id = "cancellation-test"
|
||||
current_step_id = None
|
||||
|
||||
def __init__(self):
|
||||
self.records = []
|
||||
|
||||
def record_trace(self, **record):
|
||||
self.records.append(deepcopy(record))
|
||||
|
||||
def _publish(self, *_):
|
||||
pass
|
||||
|
||||
@property
|
||||
def attempt(self):
|
||||
return next(r["payload"] for r in reversed(self.records) if r["type"] == "llm_diagnostic")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_guidance_cancellation_reaches_request_without_retry(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
|
||||
engine = Engine()
|
||||
started = asyncio.Event()
|
||||
calls = 0
|
||||
|
||||
async def model():
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
control = task_control.TaskControl("task", object())
|
||||
review = FinalReviewBudget(
|
||||
engine, attempt_id="final#1", request_timeout=10, max_retries=5, retry_delay=0
|
||||
)
|
||||
running = asyncio.create_task(
|
||||
control.run_interruptibly(
|
||||
review.run(lambda: llm._run_with_recovery(model, max_retries=5, engine=engine))
|
||||
)
|
||||
)
|
||||
await asyncio.wait_for(started.wait(), 2)
|
||||
task_control.add_instruction("task", "Use the newly observed page")
|
||||
assert await asyncio.wait_for(running, 2) == (True, None)
|
||||
assert engine.attempt["phase"] == "cancelled"
|
||||
assert engine.attempt["cancel_reason"] == "guidance_replaced"
|
||||
assert review.data["status"] == "cancelled"
|
||||
assert review.data["cancel_reason"] == "guidance_replaced"
|
||||
assert calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sdk_stop_reaches_request_without_retry():
|
||||
engine = Engine()
|
||||
started = asyncio.Event()
|
||||
|
||||
async def model():
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
agent = Agent.__new__(Agent)
|
||||
agent._current_task = asyncio.create_task(
|
||||
llm._run_with_recovery(model, max_retries=5, engine=engine)
|
||||
)
|
||||
await asyncio.wait_for(started.wait(), 2)
|
||||
agent.stop_current_task(reason=CancellationReason.USER_STOP)
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await agent._current_task
|
||||
assert engine.attempt["phase"] == "cancelled"
|
||||
assert engine.attempt["cancel_reason"] == "user_stop"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outer_timeout_is_not_a_request_timeout():
|
||||
engine = Engine()
|
||||
|
||||
async def model():
|
||||
await asyncio.Event().wait()
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
await llm.invoke_llm_with_timeout_message(
|
||||
llm._run_with_recovery(model, max_retries=5, engine=engine),
|
||||
timeout_seconds=0,
|
||||
hard_timeout=0,
|
||||
)
|
||||
assert engine.attempt["phase"] == "cancelled"
|
||||
assert engine.attempt["cancel_reason"] == "outer_timeout"
|
||||
assert engine.attempt["timeout_scope"] == "outer"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sdk_stop_preserves_cause_through_nested_request_wrappers(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
|
||||
engine = Engine()
|
||||
started = asyncio.Event()
|
||||
|
||||
async def model():
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
control = task_control.TaskControl("task", object())
|
||||
agent = Agent.__new__(Agent)
|
||||
agent._current_task = asyncio.create_task(
|
||||
control.run_interruptibly(
|
||||
llm.invoke_llm_with_timeout_message(
|
||||
llm._run_with_recovery(model, max_retries=5, engine=engine), timeout_seconds=60
|
||||
)
|
||||
)
|
||||
)
|
||||
await asyncio.wait_for(started.wait(), 2)
|
||||
agent.stop_current_task()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await asyncio.wait_for(agent._current_task, 2)
|
||||
assert engine.attempt["phase"] == "cancelled"
|
||||
assert engine.attempt["cancel_reason"] == "user_stop"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_timer_is_identified_without_triggering_an_extra_request():
|
||||
engine = Engine()
|
||||
token = llm._FALLBACK_AVAILABLE.set(True)
|
||||
calls = 0
|
||||
|
||||
async def model():
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
await asyncio.Event().wait()
|
||||
|
||||
try:
|
||||
with pytest.raises(LLMExhaustedError):
|
||||
await llm._run_with_recovery(model, request_timeout=0.01, max_retries=0, engine=engine)
|
||||
finally:
|
||||
llm._FALLBACK_AVAILABLE.reset(token)
|
||||
assert engine.attempt["phase"] == "failed"
|
||||
assert engine.attempt["timeout_scope"] == "request"
|
||||
assert "cancel_reason" not in engine.attempt
|
||||
assert calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unattributed_cancellation_does_not_infer_a_user_action_or_store_its_text():
|
||||
engine = Engine()
|
||||
started = asyncio.Event()
|
||||
|
||||
async def model():
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
running = asyncio.create_task(llm._run_with_recovery(model, max_retries=5, engine=engine))
|
||||
await asyncio.wait_for(started.wait(), 2)
|
||||
running.cancel("private user guidance")
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await running
|
||||
assert engine.attempt["cancel_reason"] == "unknown"
|
||||
assert "private user guidance" not in repr(engine.records)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_499_preserves_server_id_but_does_not_infer_local_cancellation():
|
||||
engine = Engine()
|
||||
token = llm._FALLBACK_AVAILABLE.set(True)
|
||||
|
||||
class ServerError(Exception):
|
||||
status_code = 499
|
||||
request_id = "server-request-499"
|
||||
|
||||
async def model():
|
||||
raise ServerError("Request closed")
|
||||
|
||||
try:
|
||||
with pytest.raises(LLMExhaustedError):
|
||||
await llm._run_with_recovery(model, max_retries=0, engine=engine)
|
||||
finally:
|
||||
llm._FALLBACK_AVAILABLE.reset(token)
|
||||
assert engine.attempt["phase"] == "failed"
|
||||
assert engine.attempt["http_status"] == 499
|
||||
assert engine.attempt["server_request_id"] == "server-request-499"
|
||||
assert "cancel_reason" not in engine.attempt
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pause_cancellation_uses_the_existing_checkpoint(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
|
||||
engine = Engine()
|
||||
started = asyncio.Event()
|
||||
paused = asyncio.Event()
|
||||
events = []
|
||||
|
||||
class Lock:
|
||||
def release(self):
|
||||
events.append("released")
|
||||
|
||||
async def model():
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
async def checkpoint(**_kwargs):
|
||||
events.append("checkpoint")
|
||||
paused.set()
|
||||
|
||||
control = task_control.TaskControl("task", Lock())
|
||||
monkeypatch.setattr(control, "checkpoint", checkpoint)
|
||||
running = asyncio.create_task(
|
||||
control.run_interruptibly(llm._run_with_recovery(model, max_retries=5, engine=engine))
|
||||
)
|
||||
await asyncio.wait_for(started.wait(), 2)
|
||||
task_control.request_pause("task")
|
||||
await asyncio.wait_for(paused.wait(), 2)
|
||||
assert await running == (True, None)
|
||||
assert engine.attempt["cancel_reason"] == "task_paused"
|
||||
assert events == ["checkpoint"]
|
||||
Reference in new issue
Block a user