mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/cli: level-1 cancel of in-flight turns via Ctrl+C
Run turns as asyncio tasks; SIGINT cancels the task; agent rolls back and keeps pending_retry_text for /retry.
This commit is contained in:
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
|
||||
import pytest
|
||||
|
||||
from plyngent.agent import ChatAgent
|
||||
from plyngent.cli.retry import retry_pending_with_retries, run_turn_with_retries, sleep_cancellable
|
||||
from plyngent.config.models import DatabaseConfig
|
||||
@@ -18,8 +20,6 @@ from plyngent.memory import MemoryStore
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _response(message: AssistantChatMessage) -> ChatCompletionResponse:
|
||||
return ChatCompletionResponse(
|
||||
@@ -105,6 +105,64 @@ async def test_auto_retry_eventually_succeeds(monkeypatch: pytest.MonkeyPatch) -
|
||||
await store.close()
|
||||
|
||||
|
||||
async def test_run_cancellable_cancels_task() -> None:
|
||||
import asyncio
|
||||
|
||||
from plyngent.cli.retry import run_cancellable
|
||||
|
||||
started = asyncio.Event()
|
||||
|
||||
async def hang() -> None:
|
||||
started.set()
|
||||
await asyncio.sleep(60)
|
||||
|
||||
task = asyncio.create_task(run_cancellable(hang()))
|
||||
_ = await started.wait()
|
||||
_ = task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
|
||||
async def test_cancel_turn_rolls_back_and_sets_pending() -> None:
|
||||
import asyncio
|
||||
|
||||
from plyngent.cli.display import render_events
|
||||
from plyngent.cli.retry import run_cancellable
|
||||
|
||||
store = await MemoryStore.open(DatabaseConfig())
|
||||
session = await store.create_session(name="t")
|
||||
|
||||
class HangClient:
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||
) -> ChatCompletionResponse: ...
|
||||
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||
del param, stream
|
||||
await asyncio.sleep(60)
|
||||
return _response(AssistantChatMessage(content="never"))
|
||||
|
||||
agent = ChatAgent(HangClient(), model="m", memory=store, session_id=session.sid)
|
||||
turn = asyncio.create_task(run_cancellable(render_events(agent.run("cancel-me"))))
|
||||
await asyncio.sleep(0.05)
|
||||
_ = turn.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await turn
|
||||
|
||||
assert agent.pending_retry_text == "cancel-me"
|
||||
assert agent.messages == []
|
||||
assert await store.list_messages(session.sid) == []
|
||||
await store.close()
|
||||
|
||||
|
||||
async def test_manual_retry_after_exhausted(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def no_wait(_seconds: float) -> bool:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user