From 15dde7cdaf1025ec4ff0d93aee5cb164f70222a4 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 24 Jul 2026 14:56:33 +0800 Subject: [PATCH] core/tools: async builtins with policy tags --- src/plyngent/tools/chat/ask.py | 4 +-- src/plyngent/tools/chat/choose.py | 4 +-- src/plyngent/tools/chat/form.py | 4 +-- src/plyngent/tools/file/edit_lineno.py | 6 ++-- src/plyngent/tools/file/edit_replace.py | 6 ++-- src/plyngent/tools/file/fs_ops.py | 14 ++++----- src/plyngent/tools/file/glob_paths.py | 6 ++-- src/plyngent/tools/file/grep_files.py | 6 ++-- src/plyngent/tools/file/listdir.py | 6 ++-- src/plyngent/tools/file/read.py | 6 ++-- src/plyngent/tools/file/tree.py | 6 ++-- src/plyngent/tools/file/write.py | 6 ++-- src/plyngent/tools/process/ask_into_pty.py | 4 +-- src/plyngent/tools/process/close_pty.py | 4 +-- src/plyngent/tools/process/open_pty.py | 6 ++-- src/plyngent/tools/process/read_pty.py | 4 +-- src/plyngent/tools/process/run_command.py | 4 +-- .../tools/process/run_command_batch.py | 4 +-- src/plyngent/tools/process/write_pty.py | 6 ++-- src/plyngent/tools/process/write_pty_keys.py | 6 ++-- src/plyngent/tools/temp_workspace.py | 30 +++++++++++-------- src/plyngent/tools/todo.py | 29 +++++++++++------- src/plyngent/tools/vcs/tools.py | 22 +++++++------- 23 files changed, 103 insertions(+), 90 deletions(-) diff --git a/src/plyngent/tools/chat/ask.py b/src/plyngent/tools/chat/ask.py index 4e90d5b..70b5e24 100644 --- a/src/plyngent/tools/chat/ask.py +++ b/src/plyngent/tools/chat/ask.py @@ -1,10 +1,10 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.prompting import NonInteractiveError, ask_async -@tool(name="ask_user_line") +@tool(name="ask_user_line", tags=ToolTag.LOCAL) async def ask_user(question: str, default: str = "") -> str: """Ask the human a free-form one-line question and return their answer. diff --git a/src/plyngent/tools/chat/choose.py b/src/plyngent/tools/chat/choose.py index af8affa..042ee70 100644 --- a/src/plyngent/tools/chat/choose.py +++ b/src/plyngent/tools/chat/choose.py @@ -3,7 +3,7 @@ from __future__ import annotations import json from typing import cast -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.prompting import ChoiceOption, NonInteractiveError, choose_async @@ -46,7 +46,7 @@ def parse_options(raw: str) -> list[ChoiceOption]: return out -@tool(name="ask_user_choice") +@tool(name="ask_user_choice", tags=ToolTag.LOCAL) async def choose_user( question: str, options: str, diff --git a/src/plyngent/tools/chat/form.py b/src/plyngent/tools/chat/form.py index 5a36bc1..9c8799e 100644 --- a/src/plyngent/tools/chat/form.py +++ b/src/plyngent/tools/chat/form.py @@ -3,7 +3,7 @@ from __future__ import annotations import json from typing import cast -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.prompting import FormField, NonInteractiveError, form_async from plyngent.tools.chat.choose import parse_options @@ -54,7 +54,7 @@ def parse_fields(raw: str) -> list[FormField]: return out -@tool(name="ask_user_form") +@tool(name="ask_user_form", tags=ToolTag.LOCAL) 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. diff --git a/src/plyngent/tools/file/edit_lineno.py b/src/plyngent/tools/file/edit_lineno.py index 2b81fd0..6b91ac9 100644 --- a/src/plyngent/tools/file/edit_lineno.py +++ b/src/plyngent/tools/file/edit_lineno.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import resolve_path if TYPE_CHECKING: @@ -76,8 +76,8 @@ def _replace_range( return f"replaced lines {start_line}-{end} ({removed} lines) with {len(replacement)} lines in {path}" -@tool -def edit_lineno(path: str, start_line: int, end_line: int, new_content: str) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def edit_lineno(path: str, start_line: int, end_line: int, new_content: str) -> str: """Replace lines ``start_line``..``end_line`` (1-based, inclusive) in a file. ``new_content`` may be multi-line. Use an empty string to delete the range. diff --git a/src/plyngent/tools/file/edit_replace.py b/src/plyngent/tools/file/edit_replace.py index a04dfe6..a4b3afd 100644 --- a/src/plyngent/tools/file/edit_replace.py +++ b/src/plyngent/tools/file/edit_replace.py @@ -1,6 +1,6 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import resolve_path @@ -32,8 +32,8 @@ def _success_message(path: str, *, replaced: int, found: int) -> str: ) -@tool -def edit_replace(path: str, old_string: str, new_string: str, max_replaces: int = 1) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def edit_replace(path: str, old_string: str, new_string: str, max_replaces: int = 1) -> str: """Replace occurrences of ``old_string`` with ``new_string`` in a file. Replaces left-to-right, non-overlapping. Default ``max_replaces=1`` (first match diff --git a/src/plyngent/tools/file/fs_ops.py b/src/plyngent/tools/file/fs_ops.py index 56df097..b31fdc5 100644 --- a/src/plyngent/tools/file/fs_ops.py +++ b/src/plyngent/tools/file/fs_ops.py @@ -3,7 +3,7 @@ from __future__ import annotations import shutil from pathlib import Path -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError, get_workspace_root, resolve_path @@ -77,8 +77,8 @@ def _copy_or_move_validated( return f"error: {action} failed: {exc}" -@tool -def copy_path(src: str, dst: str, *, overwrite: bool = False) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def copy_path(src: str, dst: str, *, overwrite: bool = False) -> str: """Copy a file or directory under the workspace (``shutil.copy2`` / ``copytree``). ``dst`` parent directories are created as needed. If ``dst`` exists, set @@ -93,8 +93,8 @@ def copy_path(src: str, dst: str, *, overwrite: bool = False) -> str: return _copy_or_move_validated(source, dest, root, src=src, dst=dst, overwrite=overwrite, action="copy") -@tool -def move_path(src: str, dst: str, *, overwrite: bool = False) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def move_path(src: str, dst: str, *, overwrite: bool = False) -> str: """Move/rename a file or directory under the workspace (``shutil.move``).""" pair = _resolve_pair(src, dst) if isinstance(pair, str): @@ -129,8 +129,8 @@ def _delete_target(target: Path, path: str, *, recursive: bool) -> str: return f"error: unsupported path type: {path}" -@tool -def delete_path(path: str, *, recursive: bool = False) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def delete_path(path: str, *, recursive: bool = False) -> str: """Delete a file or directory under the workspace. Files and empty directories are removed always. Non-empty directories require diff --git a/src/plyngent/tools/file/glob_paths.py b/src/plyngent/tools/file/glob_paths.py index fc2f5e8..f3172eb 100644 --- a/src/plyngent/tools/file/glob_paths.py +++ b/src/plyngent/tools/file/glob_paths.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.file.tree import VCS_DIR_NAMES from plyngent.tools.workspace import WorkspaceError, get_workspace_root, resolve_path @@ -65,8 +65,8 @@ def _resolve_glob_base(path: str) -> tuple[Path, Path] | str: return root, base -@tool -def glob_paths( +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def glob_paths( pattern: str, path: str = ".", *, diff --git a/src/plyngent/tools/file/grep_files.py b/src/plyngent/tools/file/grep_files.py index 4d8fc77..f408e97 100644 --- a/src/plyngent/tools/file/grep_files.py +++ b/src/plyngent/tools/file/grep_files.py @@ -3,7 +3,7 @@ from __future__ import annotations import re from typing import TYPE_CHECKING -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.file.tree import VCS_DIR_NAMES from plyngent.tools.workspace import WorkspaceError, get_workspace_root, resolve_path @@ -127,8 +127,8 @@ def _compile_pattern(pattern: str, *, case_insensitive: bool) -> re.Pattern[str] return f"error: invalid regex: {exc}" -@tool -def grep_files( +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def grep_files( pattern: str, path: str = ".", *, diff --git a/src/plyngent/tools/file/listdir.py b/src/plyngent/tools/file/listdir.py index b46877d..5df3604 100644 --- a/src/plyngent/tools/file/listdir.py +++ b/src/plyngent/tools/file/listdir.py @@ -1,11 +1,11 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import resolve_path -@tool -def listdir(path: str = ".") -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def listdir(path: str = ".") -> str: """List entries in a directory under the workspace (name and type).""" target = resolve_path(path) if not target.is_dir(): diff --git a/src/plyngent/tools/file/read.py b/src/plyngent/tools/file/read.py index bb1b78d..ed1de13 100644 --- a/src/plyngent/tools/file/read.py +++ b/src/plyngent/tools/file/read.py @@ -1,6 +1,6 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import resolve_path _LINENO_WIDTH = 6 @@ -17,8 +17,8 @@ def _format_with_lineno(lines: list[str], *, start_lineno: int) -> str: return "".join(out) -@tool -def read_file( +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def read_file( path: str, *, offset: int = 0, diff --git a/src/plyngent/tools/file/tree.py b/src/plyngent/tools/file/tree.py index f449826..9eb4c67 100644 --- a/src/plyngent/tools/file/tree.py +++ b/src/plyngent/tools/file/tree.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError, get_path_denylist, resolve_path if TYPE_CHECKING: @@ -151,8 +151,8 @@ def _resolve_skip_basenames(skip_dirs: Sequence[str] | None) -> frozenset[str]: return frozenset(name for name in skip_dirs if name) -@tool -def tree( +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def tree( path: str = ".", *, max_depth: int = DEFAULT_MAX_DEPTH, diff --git a/src/plyngent/tools/file/write.py b/src/plyngent/tools/file/write.py index 8a1aff1..c095584 100644 --- a/src/plyngent/tools/file/write.py +++ b/src/plyngent/tools/file/write.py @@ -1,11 +1,11 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import resolve_path -@tool -def write_file(path: str, content: str) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def write_file(path: str, content: str) -> str: """Write text content to a file under the workspace (creates parents).""" target = resolve_path(path) target.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/plyngent/tools/process/ask_into_pty.py b/src/plyngent/tools/process/ask_into_pty.py index ccfe9f3..763d1e0 100644 --- a/src/plyngent/tools/process/ask_into_pty.py +++ b/src/plyngent/tools/process/ask_into_pty.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Literal -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.prompting import NonInteractiveError, ask_async, ask_secret_async from plyngent.tools.workspace import WorkspaceError @@ -38,7 +38,7 @@ async def _prompt_answer(label: str, *, secret: bool) -> _PromptResult: return ("ok", answer) -@tool +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) async def ask_into_pty( session_id: int, message: str, diff --git a/src/plyngent/tools/process/close_pty.py b/src/plyngent/tools/process/close_pty.py index ec4ae52..e84a846 100644 --- a/src/plyngent/tools/process/close_pty.py +++ b/src/plyngent/tools/process/close_pty.py @@ -2,12 +2,12 @@ from __future__ import annotations import asyncio -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from .pty_session import PtyManager, format_close_result -@tool +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) async def close_pty(session_id: int) -> str: """Close a PTY session (SIGTERM, then SIGKILL after a short grace period). diff --git a/src/plyngent/tools/process/open_pty.py b/src/plyngent/tools/process/open_pty.py index 8611e0e..9a5b423 100644 --- a/src/plyngent/tools/process/open_pty.py +++ b/src/plyngent/tools/process/open_pty.py @@ -2,14 +2,14 @@ from __future__ import annotations import shlex -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError from .pty_session import PtyManager -@tool -def open_pty(command: list[str], *, cwd: str = ".") -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def open_pty(command: list[str], *, cwd: str = ".") -> str: """Open a PTY session running ``command`` (argv) under the workspace. POSIX uses openpty/fork; Windows uses ConPTY (pywinpty). Returns structured diff --git a/src/plyngent/tools/process/read_pty.py b/src/plyngent/tools/process/read_pty.py index db27dcc..b0b5c52 100644 --- a/src/plyngent/tools/process/read_pty.py +++ b/src/plyngent/tools/process/read_pty.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError from .pty_session import DEFAULT_PTY_READ_BYTES, PtyManager, format_read_result @@ -11,7 +11,7 @@ from .pty_session import DEFAULT_PTY_READ_BYTES, PtyManager, format_read_result _MAX_READ_TIMEOUT = 120.0 -@tool +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) async def read_pty( session_id: int, *, diff --git a/src/plyngent/tools/process/run_command.py b/src/plyngent/tools/process/run_command.py index b61ff46..8b34cd8 100644 --- a/src/plyngent/tools/process/run_command.py +++ b/src/plyngent/tools/process/run_command.py @@ -1,6 +1,6 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError from .command_exec import ( @@ -10,7 +10,7 @@ from .command_exec import ( ) -@tool +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) async def run_command( command: list[str], *, diff --git a/src/plyngent/tools/process/run_command_batch.py b/src/plyngent/tools/process/run_command_batch.py index e3553ae..bbba8bd 100644 --- a/src/plyngent/tools/process/run_command_batch.py +++ b/src/plyngent/tools/process/run_command_batch.py @@ -4,7 +4,7 @@ import json import shlex from typing import Any, cast -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError from .command_exec import ( @@ -154,7 +154,7 @@ async def _run_batch_steps( return results, stopped_early -@tool +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) async def run_command_batch( commands: list[dict[str, object]] | str, *, diff --git a/src/plyngent/tools/process/write_pty.py b/src/plyngent/tools/process/write_pty.py index 49f09c5..2678c66 100644 --- a/src/plyngent/tools/process/write_pty.py +++ b/src/plyngent/tools/process/write_pty.py @@ -1,6 +1,6 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError from .pty_session import PtyManager @@ -21,8 +21,8 @@ def write_pty_payload(session_id: int, raw: str) -> str: ) -@tool -def write_pty(session_id: int, data: str) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def write_pty(session_id: int, data: str) -> str: """Write **literal** text to a PTY session. Does not append a newline. ``data`` is sent unchanged (no ``ctrl+x`` / ``\\\\xHH`` expansion). For diff --git a/src/plyngent/tools/process/write_pty_keys.py b/src/plyngent/tools/process/write_pty_keys.py index 7472c21..ff9a2c3 100644 --- a/src/plyngent/tools/process/write_pty_keys.py +++ b/src/plyngent/tools/process/write_pty_keys.py @@ -1,14 +1,14 @@ from __future__ import annotations -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError from .pty_terminal import decode_write_data from .write_pty import write_pty_payload -@tool -def write_pty_keys(session_id: int, data: str) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) +async def write_pty_keys(session_id: int, data: str) -> str: """Write to a PTY after expanding key escapes (never for normal typing). Use this only for control sequences. Prefer :func:`write_pty` for plain text diff --git a/src/plyngent/tools/temp_workspace.py b/src/plyngent/tools/temp_workspace.py index 5a78323..8c0f72f 100644 --- a/src/plyngent/tools/temp_workspace.py +++ b/src/plyngent/tools/temp_workspace.py @@ -5,7 +5,7 @@ import shutil import tempfile from pathlib import Path -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import ( MAX_TEMPORARY_WORKSPACES, WorkspaceError, @@ -61,17 +61,7 @@ def cleanup_temporary_workspaces() -> int: return removed -@tool -def new_temporary_workspace(prefix: str = "ws") -> str: - """Create a scratch directory under the system temp dir and allow tool paths in it. - - The project workspace is unchanged: relative paths still resolve there. - Use the returned **absolute** path for file/process tools. Temporary - workspaces are deleted when this chat process exits (not on each turn). - - Cross-platform (uses ``tempfile``; typically ``/tmp`` on POSIX, ``%TEMP%`` - on Windows). At most 16 concurrent temporary workspaces per process. - """ +def _create_temporary_workspace(prefix: str) -> str: if len(list_workspace_allowlist()) >= MAX_TEMPORARY_WORKSPACES: return f"error: too many temporary workspaces (max {MAX_TEMPORARY_WORKSPACES})" @@ -94,3 +84,19 @@ def new_temporary_workspace(prefix: str = "ws") -> str: "note: project workspace unchanged; use this absolute path for tools; " "removed when chat exits" ) + + +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def new_temporary_workspace(prefix: str = "ws") -> str: + """Create a scratch directory under the system temp dir and allow tool paths in it. + + The project workspace is unchanged: relative paths still resolve there. + Use the returned **absolute** path for file/process tools. Temporary + workspaces are deleted when this chat process exits (not on each turn). + + Cross-platform (uses ``tempfile``; typically ``/tmp`` on POSIX, ``%TEMP%`` + on Windows). At most 16 concurrent temporary workspaces per process. + """ + import asyncio + + return await asyncio.to_thread(_create_temporary_workspace, prefix) diff --git a/src/plyngent/tools/todo.py b/src/plyngent/tools/todo.py index b239970..4289ec8 100644 --- a/src/plyngent/tools/todo.py +++ b/src/plyngent/tools/todo.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, cast -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool if TYPE_CHECKING: from collections.abc import Callable @@ -27,6 +27,12 @@ def get_todo_stack() -> TodoStack | None: def _require_stack() -> TodoStack: + """Prefer session-bound todo; fall back to process bind during migration.""" + from plyngent.tools.context import get_session + + session = get_session() + if session is not None and session.todo is not None: + return session.todo if _stack is None: msg = "todo stack is not available in this session" raise RuntimeError(msg) @@ -36,10 +42,11 @@ def _require_stack() -> TodoStack: def _notify() -> None: if _on_change is not None: _on_change() + # Session-bound stacks may also persist via host on_change on the process bind. -@tool(name="todo_list") -def todo_list() -> str: +@tool(name="todo_list", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE) +async def todo_list() -> str: """Show the LIFO stack of **task groups** (TOP group = current breakdown level). Non-empty stack with open (pending/in_progress) items = unfinished work. @@ -53,8 +60,8 @@ def todo_list() -> str: return stack.render() -@tool(name="todo_push") -def todo_push(titles: list[str], notes: str = "") -> str: +@tool(name="todo_push", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE) +async def todo_push(titles: list[str], notes: str = "") -> str: """Push **one task group** (siblings) onto the stack — not one level per title. ``titles``: JSON array of strings (tool schema type ``array``). All entries @@ -74,8 +81,8 @@ def todo_push(titles: list[str], notes: str = "") -> str: return f"pushed group (depth={stack.depth}) items=[{ids}]\n{stack.render()}" -@tool(name="todo_pop") -def todo_pop() -> str: +@tool(name="todo_pop", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE) +async def todo_pop() -> str: """Pop the entire **top group** (all siblings from that push). Prefer after TOP items are done/cancelled so the stack does not stay @@ -92,8 +99,8 @@ def todo_pop() -> str: return f"popped TOP group ({titles}); new top group=[{top_s}]\n{stack.render()}" -@tool(name="todo_update") -def todo_update( +@tool(name="todo_update", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE) +async def todo_update( item_id: str, status: str = "", title: str = "", @@ -125,8 +132,8 @@ def todo_update( return f"updated {item.id} → {item.status}: {item.title}\n{stack.render()}" -@tool(name="todo_clear") -def todo_clear() -> str: +@tool(name="todo_clear", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE) +async def todo_clear() -> str: """Clear all groups on the stack.""" stack = _require_stack() n = stack.clear() diff --git a/src/plyngent/tools/vcs/tools.py b/src/plyngent/tools/vcs/tools.py index 5af8272..cdac5ed 100644 --- a/src/plyngent/tools/vcs/tools.py +++ b/src/plyngent/tools/vcs/tools.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from plyngent.agent import tool +from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError, get_workspace_root from .detect import detect_vcs @@ -22,8 +22,8 @@ def _backend_or_error() -> VcsBackend | str: return backend -@tool -def vcs_kind() -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def vcs_kind() -> str: """Return the detected VCS kind under the workspace (e.g. ``git``), or an error.""" backend = _backend_or_error() if isinstance(backend, str): @@ -31,8 +31,8 @@ def vcs_kind() -> str: return backend.kind -@tool -def vcs_status() -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def vcs_status() -> str: """Show working-tree status for the detected VCS (read-only).""" backend = _backend_or_error() if isinstance(backend, str): @@ -40,8 +40,8 @@ def vcs_status() -> str: return backend.status() -@tool -def vcs_diff(path: str = "", *, staged: bool = False) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def vcs_diff(path: str = "", *, staged: bool = False) -> str: """Show a unified diff for the detected VCS (read-only). ``path`` is optional and relative to the workspace. ``staged=true`` is @@ -54,8 +54,8 @@ def vcs_diff(path: str = "", *, staged: bool = False) -> str: return backend.diff(staged=staged, path=rel) -@tool -def vcs_log(limit: int = 10) -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def vcs_log(limit: int = 10) -> str: """Show recent commits for the detected VCS (read-only).""" if limit < 1: return "error: limit must be >= 1" @@ -65,8 +65,8 @@ def vcs_log(limit: int = 10) -> str: return backend.log(limit=limit) -@tool -def vcs_branch() -> str: +@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) +async def vcs_branch() -> str: """Show the current branch / named head for the detected VCS (read-only).""" backend = _backend_or_error() if isinstance(backend, str):