mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
tools: answer a call's shape before the confirm prompt
This commit is contained in:
@@ -105,6 +105,42 @@ async def test_round_asks_once_for_many_mutating_calls() -> None:
|
||||
assert single_prompts == []
|
||||
|
||||
|
||||
async def test_a_call_that_cannot_run_is_left_out_of_the_round_prompt() -> None:
|
||||
"""A tool's own shape check answers first, so the round prompt omits the call."""
|
||||
batch_prompts: list[list[ToolConfirmRequest]] = []
|
||||
|
||||
def positive(args: Mapping[str, object]) -> str | None:
|
||||
count = args.get("count")
|
||||
if isinstance(count, int) and count <= 0:
|
||||
return "error: count must be positive"
|
||||
return None
|
||||
|
||||
@tool(tags=ToolTag.LOCAL, register=False, validate=positive)
|
||||
async def scale(count: int = 1) -> str:
|
||||
return f"scaled {count}"
|
||||
|
||||
def on_batch(requests: Sequence[ToolConfirmRequest]) -> BatchConfirmDecision:
|
||||
batch_prompts.append(list(requests))
|
||||
return BatchConfirmDecision(allowed=True)
|
||||
|
||||
reg = ToolRegistry([scale], danger=_danger, on_confirm_batch=on_batch)
|
||||
calls = [("scale", '{"count": 2}'), ("scale", '{"count": 0}'), ("scale", '{"count": 3}')]
|
||||
decisions = await reg.prepare_batch_confirm(calls)
|
||||
|
||||
assert len(batch_prompts) == 1
|
||||
assert [request.args["count"] for request in batch_prompts[0]] == [2, 3]
|
||||
assert decisions[1] is None
|
||||
|
||||
results = [
|
||||
await reg.execute(name, arguments, decision=decisions[index]) for index, (name, arguments) in enumerate(calls)
|
||||
]
|
||||
assert results == [
|
||||
_approved("user (round)", "scaled 2"),
|
||||
"error: count must be positive",
|
||||
_approved("user (round)", "scaled 3"),
|
||||
]
|
||||
|
||||
|
||||
async def test_yolo_calls_are_left_out_of_the_prompt() -> None:
|
||||
"""Under YOLO mode a YOLO-tagged call runs unprompted, batch prompt included."""
|
||||
batch_prompts: list[list[str]] = []
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from plyngent.agent import ToolDefinition, ToolRegistry
|
||||
from plyngent.tools.danger import classify_danger
|
||||
from plyngent.tools.process import (
|
||||
ask_into_pty,
|
||||
close_pty,
|
||||
@@ -20,6 +24,9 @@ from plyngent.tools.process.pty_session import PtyManager
|
||||
from plyngent.tools.workspace import set_command_denylist
|
||||
from tests.test_tools.helpers import call_async, call_sync
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def _session_id(opened: str) -> int:
|
||||
for line in opened.splitlines():
|
||||
@@ -299,6 +306,49 @@ async def test_run_argv_batch_names_the_node_grammar(workspace: object) -> None:
|
||||
assert "then, else, pipe" in out
|
||||
|
||||
|
||||
def _asking_registry(*definitions: ToolDefinition) -> tuple[ToolRegistry, list[str]]:
|
||||
"""A registry whose confirm hook records the calls it was asked about."""
|
||||
asked: list[str] = []
|
||||
|
||||
def on_confirm(name: str, args: Mapping[str, object], reason: str) -> bool:
|
||||
del args, reason
|
||||
asked.append(name)
|
||||
return True
|
||||
|
||||
return ToolRegistry(list(definitions), danger=classify_danger, on_confirm=on_confirm), asked
|
||||
|
||||
|
||||
async def test_a_bad_timeout_is_answered_before_the_prompt(workspace: object) -> None:
|
||||
"""A call that cannot run is answered, never put in front of the human."""
|
||||
del workspace
|
||||
registry, asked = _asking_registry(run_argv)
|
||||
out = await registry.execute(
|
||||
"run_argv",
|
||||
json.dumps({"argv": ["bash", "-c", "echo hi"], "timeout_seconds": "soon"}),
|
||||
)
|
||||
assert asked == []
|
||||
assert "timeout_seconds" in out
|
||||
assert "error:" in out
|
||||
|
||||
|
||||
async def test_a_broken_batch_is_answered_before_the_prompt(workspace: object) -> None:
|
||||
del workspace
|
||||
registry, asked = _asking_registry(run_argv_batch)
|
||||
out = await registry.execute(
|
||||
"run_argv_batch",
|
||||
json.dumps(
|
||||
{
|
||||
"steps": [
|
||||
{"argv": ["bash", "-c", "echo hi"]},
|
||||
{"argv": ["echo", "x"], "pipe": {"argv": ["echo", "y"], "stdin": "own"}},
|
||||
]
|
||||
}
|
||||
),
|
||||
)
|
||||
assert asked == []
|
||||
assert "stdin comes from the pipe" in out
|
||||
|
||||
|
||||
async def test_run_argv_denied(workspace: object) -> None:
|
||||
del workspace
|
||||
out = await call_async(run_argv, ["rm", "-rf", "."])
|
||||
|
||||
Reference in New Issue
Block a user