core/cli: interactive continue prompts for loop and PTY limits

on_limit raises tool-round allowance; PTY session/budget limits can
prompt to raise; MaxRoundsEvent.continued marks extended runs.
This commit is contained in:
2026-07-14 19:53:34 +08:00
parent 1b53b415b6
commit a634021a80
12 changed files with 248 additions and 55 deletions
+44 -1
View File
@@ -155,10 +155,53 @@ async def test_max_rounds() -> None:
client = ScriptedClient([forever, forever, forever])
messages: list[AnyChatMessage] = [UserChatMessage(content="x")]
events = [e async for e in run_chat_loop(client, messages, model="m", tools=registry, max_rounds=2)]
assert any(isinstance(e, MaxRoundsEvent) and e.rounds == 2 for e in events) # noqa: PLR2004
assert any(isinstance(e, MaxRoundsEvent) and e.rounds == 2 and not e.continued for e in events) # noqa: PLR2004
assert len(client.calls) == 2 # noqa: PLR2004
async def test_max_rounds_continue_hook() -> None:
@tool
def ping() -> str:
return "pong"
registry = ToolRegistry([ping])
forever = _response(
AssistantChatMessage(
content="",
tool_calls=[
AssistantFunctionToolCall(
id="c",
function=AssistantFunctionTool(name="ping", arguments="{}"),
)
],
)
)
final = _response(AssistantChatMessage(content="done"))
client = ScriptedClient([forever, forever, final])
messages: list[AnyChatMessage] = [UserChatMessage(content="x")]
asks: list[str] = []
def on_limit(reason: str) -> bool:
asks.append(reason)
return len(asks) == 1
events = [
e
async for e in run_chat_loop(
client,
messages,
model="m",
tools=registry,
max_rounds=2,
on_limit=on_limit,
)
]
assert len(asks) == 1
assert any(isinstance(e, MaxRoundsEvent) and e.continued for e in events)
assert any(isinstance(e, TextDeltaEvent) and e.content == "done" for e in events)
assert len(client.calls) == 3 # noqa: PLR2004
async def test_default_max_rounds_is_generous() -> None:
from plyngent.agent.loop import DEFAULT_MAX_ROUNDS
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from plyngent.cli.limits import install_cli_limit_hooks, prompt_continue_limit
from plyngent.tools.process.pty_session import PtyManager
if TYPE_CHECKING:
import pytest
def test_prompt_continue_limit_yes(monkeypatch: pytest.MonkeyPatch) -> None:
def _confirm(*_a: object, **_k: object) -> bool:
return True
monkeypatch.setattr("click.confirm", _confirm)
assert prompt_continue_limit("hit a wall") is True
def test_prompt_continue_limit_no(monkeypatch: pytest.MonkeyPatch) -> None:
def _confirm(*_a: object, **_k: object) -> bool:
return False
monkeypatch.setattr("click.confirm", _confirm)
assert prompt_continue_limit("hit a wall") is False
def test_install_cli_limit_hooks() -> None:
install_cli_limit_hooks()
# Hook is installed process-wide for the CLI session.
assert callable(getattr(PtyManager, "_limit_continue", None))
PtyManager.set_limit_continue_hook(None)
+19
View File
@@ -147,6 +147,7 @@ def test_pty_session_limit(workspace: object) -> None:
del workspace
previous = PtyManager.max_sessions
try:
PtyManager.set_limit_continue_hook(None)
PtyManager.configure(max_sessions=1)
first = call_sync(open_pty, ["sleep", "30"])
assert "session_id=" in first
@@ -155,6 +156,24 @@ def test_pty_session_limit(workspace: object) -> None:
finally:
PtyManager.close_all()
PtyManager.configure(max_sessions=previous)
PtyManager.set_limit_continue_hook(None)
def test_pty_session_limit_continue(workspace: object) -> None:
del workspace
previous = PtyManager.max_sessions
try:
PtyManager.configure(max_sessions=1)
PtyManager.set_limit_continue_hook(lambda _reason: True)
first = call_sync(open_pty, ["sleep", "30"])
second = call_sync(open_pty, ["sleep", "30"])
assert "session_id=" in first
assert "session_id=" in second
assert PtyManager.max_sessions >= 2 # noqa: PLR2004
finally:
PtyManager.close_all()
PtyManager.configure(max_sessions=previous)
PtyManager.set_limit_continue_hook(None)
def test_pty_output_budget(workspace: object) -> None: