diff --git a/CLAUDE.md b/CLAUDE.md index b37b638..9adacdc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,7 @@ Module-level `@tool` handlers. Call `set_workspace_root()` before use. - CLI limit hooks: interactive confirm to raise tool-loop rounds, PTY session cap, or PTY output budget. - Destructive confirms: `classify_danger` + `ToolRegistry(on_confirm=…)`; CLI default deny; config `confirm_destructive` / `path_denylist`. Session YOLO: `/yolo on|off|once` and `--yes` (skip soft confirms; hard denylists unchanged; `once` expires after the next user turn). - **`vcs`**: read-only VCS tools (`vcs_kind` / `vcs_status` / `vcs_diff` / `vcs_log` / `vcs_branch`) via `VcsBackend` protocol; **git** implemented; detectors are pluggable for other systems. -- **`chat`**: human prompts as tools — `ask_user`, `choose_user`, `form_user` (shared `prompting` core). +- **`chat`**: human prompts as tools — `ask_user_line` / `ask_user_choice` / `ask_user_form` (shared `prompting` core). - **`DEFAULT_TOOLS`**: file + process + vcs + chat tool list for a `ToolRegistry`. ### Prompting (`prompting.py`) @@ -112,7 +112,7 @@ Basedpyright `recommended`. Ruff includes `ANN` (private return types `ANN202` i - **Phase G (CLI polish + hardening)** — single-user only; multi-tenant stays Phase H. **Done** - - G0: `prompting` (`ask`/`choose`/`form`/`confirm`) + chat tools (`ask_user`/`choose_user`/`form_user`) + - G0: `prompting` (`ask`/`choose`/`form`/`confirm`) + chat tools (`ask_user_line`/`ask_user_choice`/`ask_user_form`) - G1: `ReasoningDeltaEvent`, `/stream`, `/verbose` - G2: `/rename`, `/delete` (confirm), `/export md|json` - G2.5: Click slash registry (`cli/slash.py`) + `awaitlet` for sync Click / async memory; auto `/help`; completer from `slash.list_commands` diff --git a/README.md b/README.md index 572fd2a..aaa7663 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ User messages are saved immediately. On API error or Ctrl+C, partial assistant/t ## Tools (when enabled) -Default registry: file ops, `run_command` / PTY (POSIX openpty; Windows ConPTY via pywinpty), read-only VCS (git), and human prompts (`ask_user` / `choose_user` / `form_user`). +Default registry: file ops, `run_command` / PTY (POSIX openpty; Windows ConPTY via pywinpty), read-only VCS (git), and human prompts (`ask_user_line` / `ask_user_choice` / `ask_user_form`). Safety defaults: diff --git a/src/plyngent/tools/chat/ask.py b/src/plyngent/tools/chat/ask.py index 64fb94e..4e90d5b 100644 --- a/src/plyngent/tools/chat/ask.py +++ b/src/plyngent/tools/chat/ask.py @@ -4,9 +4,9 @@ from plyngent.agent import tool from plyngent.prompting import NonInteractiveError, ask_async -@tool +@tool(name="ask_user_line") async def ask_user(question: str, default: str = "") -> str: - """Ask the human a free-form question and return their answer. + """Ask the human a free-form one-line question and return their answer. Always allows arbitrary text. Use for clarifying requirements, preferences, or any input that is not a fixed menu. Optional ``default`` is used if the diff --git a/src/plyngent/tools/chat/choose.py b/src/plyngent/tools/chat/choose.py index 70b7c74..af8affa 100644 --- a/src/plyngent/tools/chat/choose.py +++ b/src/plyngent/tools/chat/choose.py @@ -46,7 +46,7 @@ def parse_options(raw: str) -> list[ChoiceOption]: return out -@tool +@tool(name="ask_user_choice") async def choose_user( question: str, options: str, @@ -54,7 +54,7 @@ async def choose_user( *, allow_custom: bool = True, ) -> str: - """Ask the human to pick from options (or type a custom answer). + """Ask the human to pick from a list of options (or type a custom answer). ``options`` is a JSON array of strings, or objects with ``label``, optional ``description``, optional ``value``. diff --git a/src/plyngent/tools/chat/form.py b/src/plyngent/tools/chat/form.py index 02aae37..5a36bc1 100644 --- a/src/plyngent/tools/chat/form.py +++ b/src/plyngent/tools/chat/form.py @@ -54,13 +54,13 @@ def parse_fields(raw: str) -> list[FormField]: return out -@tool +@tool(name="ask_user_form") async def form_user(title: str, fields: str, *, confirm_submit: bool = True) -> str: """Run a multi-step form with the human; returns JSON object of answers. ``fields`` is a JSON array of objects: ``name``, ``prompt``, optional ``default``, optional ``options`` (same shape - as choose_user), optional ``allow_custom`` (default true). + as ask_user_choice), optional ``allow_custom`` (default true). When ``confirm_submit`` is true, the human reviews a summary before submit. """ try: diff --git a/tests/test_tools/test_chat_tools.py b/tests/test_tools/test_chat_tools.py index 242005a..270e5c7 100644 --- a/tests/test_tools/test_chat_tools.py +++ b/tests/test_tools/test_chat_tools.py @@ -12,7 +12,7 @@ async def test_ask_user_tool() -> None: backend = ScriptedBackend(["42"]) with temporary_backend(backend): registry = ToolRegistry([ask_user]) - out = await registry.execute("ask_user", '{"question": "Answer?"}') + out = await registry.execute("ask_user_line", '{"question": "Answer?"}') assert out == "42" @@ -21,7 +21,7 @@ async def test_choose_user_tool_index() -> None: with temporary_backend(backend): registry = ToolRegistry([choose_user]) out = await registry.execute( - "choose_user", + "ask_user_choice", json.dumps( { "question": "Pick", @@ -36,7 +36,7 @@ async def test_choose_user_tool_index() -> None: async def test_choose_user_bad_options() -> None: registry = ToolRegistry([choose_user]) out = await registry.execute( - "choose_user", + "ask_user_choice", json.dumps({"question": "Pick", "options": "not-json"}), ) assert out.startswith("error:") @@ -48,7 +48,7 @@ async def test_form_user_tool() -> None: with temporary_backend(backend): registry = ToolRegistry([form_user]) out = await registry.execute( - "form_user", + "ask_user_form", json.dumps({"title": "Setup", "fields": fields, "confirm_submit": True}), ) assert json.loads(out) == {"user": "ncbm"} @@ -56,11 +56,11 @@ async def test_form_user_tool() -> None: async def test_chat_tools_in_default_list() -> None: names = {t.name for t in CHAT_TOOLS} - assert names == {"ask_user", "choose_user", "form_user"} + assert names == {"ask_user_line", "ask_user_choice", "ask_user_form"} async def test_ask_user_non_interactive_error() -> None: with temporary_backend(NonInteractiveBackend()): registry = ToolRegistry([ask_user]) - out = await registry.execute("ask_user", '{"question": "hi"}') + out = await registry.execute("ask_user_line", '{"question": "hi"}') assert out.startswith("error:")