mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
cli: reset auto-continue at turn boundaries
This commit is contained in:
@@ -381,6 +381,9 @@ async def _run_chat( # noqa: C901, PLR0912, PLR0915 — chat orchestration
|
|||||||
|
|
||||||
set_directory_access_confirm_hook(None, instance=state_obj.instance_state)
|
set_directory_access_confirm_hook(None, instance=state_obj.instance_state)
|
||||||
clear_process_access(instance=state_obj.instance_state)
|
clear_process_access(instance=state_obj.instance_state)
|
||||||
|
from plyngent.cli.limits import reset_auto_continue
|
||||||
|
|
||||||
|
reset_auto_continue()
|
||||||
if state_obj.mcp_manager is not None:
|
if state_obj.mcp_manager is not None:
|
||||||
await state_obj.mcp_manager.aclose()
|
await state_obj.mcp_manager.aclose()
|
||||||
await state_obj.instance_state.shutdown()
|
await state_obj.instance_state.shutdown()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import click
|
|||||||
|
|
||||||
from plyngent.cli.display import render_events
|
from plyngent.cli.display import render_events
|
||||||
from plyngent.cli.interrupt import allow_task_cancel, set_sigint_reinstall
|
from plyngent.cli.interrupt import allow_task_cancel, set_sigint_reinstall
|
||||||
|
from plyngent.cli.limits import reset_auto_continue_turn
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||||
@@ -184,7 +185,23 @@ async def run_turn_with_retries(
|
|||||||
further attempts use ``agent.retry`` so the user message is not duplicated.
|
further attempts use ``agent.retry`` so the user message is not duplicated.
|
||||||
|
|
||||||
Ctrl+C cancels the in-flight task; user message stays in DB for ``/retry``.
|
Ctrl+C cancels the in-flight task; user message stays in DB for ``/retry``.
|
||||||
|
|
||||||
|
The ``yyy`` limit auto-continue is turn-scoped: cleared before and after the
|
||||||
|
turn, so a retry of the same turn keeps it but the next user turn prompts.
|
||||||
"""
|
"""
|
||||||
|
reset_auto_continue_turn()
|
||||||
|
try:
|
||||||
|
return await _run_turn_with_retries(agent, starter=starter, delays=delays)
|
||||||
|
finally:
|
||||||
|
reset_auto_continue_turn()
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_turn_with_retries(
|
||||||
|
agent: ChatAgent,
|
||||||
|
*,
|
||||||
|
starter: Callable[[], AsyncIterator[AgentEvent]],
|
||||||
|
delays: tuple[float, ...],
|
||||||
|
) -> bool:
|
||||||
max_retries = len(delays)
|
max_retries = len(delays)
|
||||||
attempt = 0
|
attempt = 0
|
||||||
current: Callable[[], AsyncIterator[AgentEvent]] = starter
|
current: Callable[[], AsyncIterator[AgentEvent]] = starter
|
||||||
|
|||||||
@@ -1018,11 +1018,17 @@ def btw_cmd(
|
|||||||
else:
|
else:
|
||||||
await run_cancellable(side)
|
await run_cancellable(side)
|
||||||
|
|
||||||
|
from plyngent.cli.limits import reset_auto_continue_turn
|
||||||
|
|
||||||
|
# Aside is its own exchange: a ``yyy`` from the main turn must not leak in.
|
||||||
|
reset_auto_continue_turn()
|
||||||
try:
|
try:
|
||||||
_await(_run())
|
_await(_run())
|
||||||
except Exception as exc: # noqa: BLE001 — surface side-turn failures
|
except Exception as exc: # noqa: BLE001 — surface side-turn failures
|
||||||
click.secho(f"error: btw failed: {exc}", fg="red")
|
click.secho(f"error: btw failed: {exc}", fg="red")
|
||||||
return
|
return
|
||||||
|
finally:
|
||||||
|
reset_auto_continue_turn()
|
||||||
click.secho("(btw done — main session unchanged)", fg="bright_black")
|
click.secho("(btw done — main session unchanged)", fg="bright_black")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ class ReplState:
|
|||||||
set_verbose_tool_results(self.verbose)
|
set_verbose_tool_results(self.verbose)
|
||||||
set_markdown_enabled(self.markdown_enabled)
|
set_markdown_enabled(self.markdown_enabled)
|
||||||
|
|
||||||
|
def _reset_limit_auto_continue(self) -> None:
|
||||||
|
"""Drop a ``yyy`` skip on session change (it is turn-scoped)."""
|
||||||
|
from plyngent.cli.limits import reset_auto_continue_turn
|
||||||
|
|
||||||
|
reset_auto_continue_turn()
|
||||||
|
|
||||||
def _workspace_key(self) -> str:
|
def _workspace_key(self) -> str:
|
||||||
key = normalize_workspace(self.workspace)
|
key = normalize_workspace(self.workspace)
|
||||||
if key is None:
|
if key is None:
|
||||||
@@ -658,6 +664,7 @@ class ReplState:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def new_session(self, name: str = "chat") -> None:
|
async def new_session(self, name: str = "chat") -> None:
|
||||||
|
self._reset_limit_auto_continue()
|
||||||
session = await self.memory.create_session(
|
session = await self.memory.create_session(
|
||||||
name=name,
|
name=name,
|
||||||
workspace=self.workspace,
|
workspace=self.workspace,
|
||||||
@@ -696,6 +703,7 @@ class ReplState:
|
|||||||
"""Load a session; on workspace mismatch, prompt keep / rebind / abort."""
|
"""Load a session; on workspace mismatch, prompt keep / rebind / abort."""
|
||||||
from plyngent.cli.limits import prompt_workspace_mismatch
|
from plyngent.cli.limits import prompt_workspace_mismatch
|
||||||
|
|
||||||
|
self._reset_limit_auto_continue()
|
||||||
row = await self.memory.get_session(session_id)
|
row = await self.memory.get_session(session_id)
|
||||||
if row is None:
|
if row is None:
|
||||||
msg = f"session not found: {session_id}"
|
msg = f"session not found: {session_id}"
|
||||||
@@ -733,6 +741,7 @@ class ReplState:
|
|||||||
|
|
||||||
async def resume_latest_or_new(self, name: str = "chat") -> str:
|
async def resume_latest_or_new(self, name: str = "chat") -> str:
|
||||||
"""Resume most recently updated session for this workspace, or create one."""
|
"""Resume most recently updated session for this workspace, or create one."""
|
||||||
|
self._reset_limit_auto_continue()
|
||||||
latest = await self.memory.get_latest_session(workspace=self.workspace)
|
latest = await self.memory.get_latest_session(workspace=self.workspace)
|
||||||
if latest is None:
|
if latest is None:
|
||||||
await self.new_session(name=name)
|
await self.new_session(name=name)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ from plyngent.memory import MemoryStore
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from plyngent.agent import AgentEvent
|
||||||
|
|
||||||
|
|
||||||
def test_default_retry_delays_schedule() -> None:
|
def test_default_retry_delays_schedule() -> None:
|
||||||
assert DEFAULT_MAX_AUTO_RETRIES == 10
|
assert DEFAULT_MAX_AUTO_RETRIES == 10
|
||||||
@@ -296,6 +298,43 @@ async def test_second_ctrl_c_during_retry_announce_stays_benign(
|
|||||||
assert await _wait_for_retry(1, 3, 0.01) is True
|
assert await _wait_for_retry(1, 3, 0.01) is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_boundaries_reset_auto_continue() -> None:
|
||||||
|
from plyngent.cli.limits import (
|
||||||
|
auto_continue_enabled,
|
||||||
|
prompt_continue_limit,
|
||||||
|
reset_auto_continue,
|
||||||
|
)
|
||||||
|
from plyngent.prompting import temporary_backend
|
||||||
|
from tests.test_prompting import ScriptedBackend
|
||||||
|
|
||||||
|
reset_auto_continue()
|
||||||
|
try:
|
||||||
|
with temporary_backend(ScriptedBackend(["yyy"])):
|
||||||
|
assert prompt_continue_limit("limit") is True
|
||||||
|
assert auto_continue_enabled() is True
|
||||||
|
|
||||||
|
store = await MemoryStore.open(DatabaseConfig())
|
||||||
|
try:
|
||||||
|
session = await store.create_session(name="t")
|
||||||
|
client = FlakyClient(fail_times=0)
|
||||||
|
agent = ChatAgent(client, model="m", memory=store, session_id=session.sid)
|
||||||
|
seen: list[bool] = []
|
||||||
|
|
||||||
|
async def starter() -> AsyncIterator[AgentEvent]:
|
||||||
|
seen.append(auto_continue_enabled())
|
||||||
|
async for event in agent.run("hi"):
|
||||||
|
yield event
|
||||||
|
|
||||||
|
ok = await run_turn_with_retries(agent, starter=starter, delays=())
|
||||||
|
assert ok is True
|
||||||
|
assert seen == [False] # cleared before the turn starts
|
||||||
|
assert auto_continue_enabled() is False # cleared after the turn ends
|
||||||
|
finally:
|
||||||
|
await store.close()
|
||||||
|
finally:
|
||||||
|
reset_auto_continue()
|
||||||
|
|
||||||
|
|
||||||
async def test_manual_retry_after_exhausted(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_manual_retry_after_exhausted(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
async def no_wait(_seconds: float) -> bool:
|
async def no_wait(_seconds: float) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user