mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-25 08:04:57 +08:00
core/tools: async builtins with policy tags
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.prompting import NonInteractiveError, ask_async
|
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:
|
async def ask_user(question: str, default: str = "") -> str:
|
||||||
"""Ask the human a free-form one-line question and return their answer.
|
"""Ask the human a free-form one-line question and return their answer.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.prompting import ChoiceOption, NonInteractiveError, choose_async
|
from plyngent.prompting import ChoiceOption, NonInteractiveError, choose_async
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ def parse_options(raw: str) -> list[ChoiceOption]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@tool(name="ask_user_choice")
|
@tool(name="ask_user_choice", tags=ToolTag.LOCAL)
|
||||||
async def choose_user(
|
async def choose_user(
|
||||||
question: str,
|
question: str,
|
||||||
options: str,
|
options: str,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from typing import cast
|
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.prompting import FormField, NonInteractiveError, form_async
|
||||||
from plyngent.tools.chat.choose import parse_options
|
from plyngent.tools.chat.choose import parse_options
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ def parse_fields(raw: str) -> list[FormField]:
|
|||||||
return out
|
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:
|
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.
|
"""Run a multi-step form with the human; returns JSON object of answers.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import resolve_path
|
from plyngent.tools.workspace import resolve_path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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}"
|
return f"replaced lines {start_line}-{end} ({removed} lines) with {len(replacement)} lines in {path}"
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def edit_lineno(path: str, start_line: int, end_line: int, new_content: str) -> str:
|
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.
|
"""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.
|
``new_content`` may be multi-line. Use an empty string to delete the range.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import resolve_path
|
from plyngent.tools.workspace import resolve_path
|
||||||
|
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ def _success_message(path: str, *, replaced: int, found: int) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def edit_replace(path: str, old_string: str, new_string: str, max_replaces: int = 1) -> str:
|
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.
|
"""Replace occurrences of ``old_string`` with ``new_string`` in a file.
|
||||||
|
|
||||||
Replaces left-to-right, non-overlapping. Default ``max_replaces=1`` (first match
|
Replaces left-to-right, non-overlapping. Default ``max_replaces=1`` (first match
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
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
|
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}"
|
return f"error: {action} failed: {exc}"
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def copy_path(src: str, dst: str, *, overwrite: bool = False) -> str:
|
async def copy_path(src: str, dst: str, *, overwrite: bool = False) -> str:
|
||||||
"""Copy a file or directory under the workspace (``shutil.copy2`` / ``copytree``).
|
"""Copy a file or directory under the workspace (``shutil.copy2`` / ``copytree``).
|
||||||
|
|
||||||
``dst`` parent directories are created as needed. If ``dst`` exists, set
|
``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")
|
return _copy_or_move_validated(source, dest, root, src=src, dst=dst, overwrite=overwrite, action="copy")
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def move_path(src: str, dst: str, *, overwrite: bool = False) -> str:
|
async def move_path(src: str, dst: str, *, overwrite: bool = False) -> str:
|
||||||
"""Move/rename a file or directory under the workspace (``shutil.move``)."""
|
"""Move/rename a file or directory under the workspace (``shutil.move``)."""
|
||||||
pair = _resolve_pair(src, dst)
|
pair = _resolve_pair(src, dst)
|
||||||
if isinstance(pair, str):
|
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}"
|
return f"error: unsupported path type: {path}"
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def delete_path(path: str, *, recursive: bool = False) -> str:
|
async def delete_path(path: str, *, recursive: bool = False) -> str:
|
||||||
"""Delete a file or directory under the workspace.
|
"""Delete a file or directory under the workspace.
|
||||||
|
|
||||||
Files and empty directories are removed always. Non-empty directories require
|
Files and empty directories are removed always. Non-empty directories require
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
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.file.tree import VCS_DIR_NAMES
|
||||||
from plyngent.tools.workspace import WorkspaceError, get_workspace_root, resolve_path
|
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
|
return root, base
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def glob_paths(
|
async def glob_paths(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
path: str = ".",
|
path: str = ".",
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING
|
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.file.tree import VCS_DIR_NAMES
|
||||||
from plyngent.tools.workspace import WorkspaceError, get_workspace_root, resolve_path
|
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}"
|
return f"error: invalid regex: {exc}"
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def grep_files(
|
async def grep_files(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
path: str = ".",
|
path: str = ".",
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import resolve_path
|
from plyngent.tools.workspace import resolve_path
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def listdir(path: str = ".") -> str:
|
async def listdir(path: str = ".") -> str:
|
||||||
"""List entries in a directory under the workspace (name and type)."""
|
"""List entries in a directory under the workspace (name and type)."""
|
||||||
target = resolve_path(path)
|
target = resolve_path(path)
|
||||||
if not target.is_dir():
|
if not target.is_dir():
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import resolve_path
|
from plyngent.tools.workspace import resolve_path
|
||||||
|
|
||||||
_LINENO_WIDTH = 6
|
_LINENO_WIDTH = 6
|
||||||
@@ -17,8 +17,8 @@ def _format_with_lineno(lines: list[str], *, start_lineno: int) -> str:
|
|||||||
return "".join(out)
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def read_file(
|
async def read_file(
|
||||||
path: str,
|
path: str,
|
||||||
*,
|
*,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
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
|
from plyngent.tools.workspace import WorkspaceError, get_path_denylist, resolve_path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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)
|
return frozenset(name for name in skip_dirs if name)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def tree(
|
async def tree(
|
||||||
path: str = ".",
|
path: str = ".",
|
||||||
*,
|
*,
|
||||||
max_depth: int = DEFAULT_MAX_DEPTH,
|
max_depth: int = DEFAULT_MAX_DEPTH,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import resolve_path
|
from plyngent.tools.workspace import resolve_path
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def write_file(path: str, content: str) -> str:
|
async def write_file(path: str, content: str) -> str:
|
||||||
"""Write text content to a file under the workspace (creates parents)."""
|
"""Write text content to a file under the workspace (creates parents)."""
|
||||||
target = resolve_path(path)
|
target = resolve_path(path)
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Literal
|
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.prompting import NonInteractiveError, ask_async, ask_secret_async
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ async def _prompt_answer(label: str, *, secret: bool) -> _PromptResult:
|
|||||||
return ("ok", answer)
|
return ("ok", answer)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
async def ask_into_pty(
|
async def ask_into_pty(
|
||||||
session_id: int,
|
session_id: int,
|
||||||
message: str,
|
message: str,
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
|
|
||||||
from .pty_session import PtyManager, format_close_result
|
from .pty_session import PtyManager, format_close_result
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
async def close_pty(session_id: int) -> str:
|
async def close_pty(session_id: int) -> str:
|
||||||
"""Close a PTY session (SIGTERM, then SIGKILL after a short grace period).
|
"""Close a PTY session (SIGTERM, then SIGKILL after a short grace period).
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
from .pty_session import PtyManager
|
from .pty_session import PtyManager
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def open_pty(command: list[str], *, cwd: str = ".") -> str:
|
async def open_pty(command: list[str], *, cwd: str = ".") -> str:
|
||||||
"""Open a PTY session running ``command`` (argv) under the workspace.
|
"""Open a PTY session running ``command`` (argv) under the workspace.
|
||||||
|
|
||||||
POSIX uses openpty/fork; Windows uses ConPTY (pywinpty). Returns structured
|
POSIX uses openpty/fork; Windows uses ConPTY (pywinpty). Returns structured
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
from .pty_session import DEFAULT_PTY_READ_BYTES, PtyManager, format_read_result
|
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
|
_MAX_READ_TIMEOUT = 120.0
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
async def read_pty(
|
async def read_pty(
|
||||||
session_id: int,
|
session_id: int,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
from .command_exec import (
|
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(
|
async def run_command(
|
||||||
command: list[str],
|
command: list[str],
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import json
|
|||||||
import shlex
|
import shlex
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
from .command_exec import (
|
from .command_exec import (
|
||||||
@@ -154,7 +154,7 @@ async def _run_batch_steps(
|
|||||||
return results, stopped_early
|
return results, stopped_early
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
async def run_command_batch(
|
async def run_command_batch(
|
||||||
commands: list[dict[str, object]] | str,
|
commands: list[dict[str, object]] | str,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
from .pty_session import PtyManager
|
from .pty_session import PtyManager
|
||||||
@@ -21,8 +21,8 @@ def write_pty_payload(session_id: int, raw: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def write_pty(session_id: int, data: str) -> str:
|
async def write_pty(session_id: int, data: str) -> str:
|
||||||
"""Write **literal** text to a PTY session. Does not append a newline.
|
"""Write **literal** text to a PTY session. Does not append a newline.
|
||||||
|
|
||||||
``data`` is sent unchanged (no ``ctrl+x`` / ``\\\\xHH`` expansion). For
|
``data`` is sent unchanged (no ``ctrl+x`` / ``\\\\xHH`` expansion). For
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import WorkspaceError
|
from plyngent.tools.workspace import WorkspaceError
|
||||||
|
|
||||||
from .pty_terminal import decode_write_data
|
from .pty_terminal import decode_write_data
|
||||||
from .write_pty import write_pty_payload
|
from .write_pty import write_pty_payload
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
def write_pty_keys(session_id: int, data: str) -> str:
|
async def write_pty_keys(session_id: int, data: str) -> str:
|
||||||
"""Write to a PTY after expanding key escapes (never for normal typing).
|
"""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
|
Use this only for control sequences. Prefer :func:`write_pty` for plain text
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
from plyngent.tools.workspace import (
|
from plyngent.tools.workspace import (
|
||||||
MAX_TEMPORARY_WORKSPACES,
|
MAX_TEMPORARY_WORKSPACES,
|
||||||
WorkspaceError,
|
WorkspaceError,
|
||||||
@@ -61,17 +61,7 @@ def cleanup_temporary_workspaces() -> int:
|
|||||||
return removed
|
return removed
|
||||||
|
|
||||||
|
|
||||||
@tool
|
def _create_temporary_workspace(prefix: str) -> str:
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if len(list_workspace_allowlist()) >= MAX_TEMPORARY_WORKSPACES:
|
if len(list_workspace_allowlist()) >= MAX_TEMPORARY_WORKSPACES:
|
||||||
return f"error: too many temporary workspaces (max {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; "
|
"note: project workspace unchanged; use this absolute path for tools; "
|
||||||
"removed when chat exits"
|
"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)
|
||||||
|
|||||||
+18
-11
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import ToolTag, tool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -27,6 +27,12 @@ def get_todo_stack() -> TodoStack | None:
|
|||||||
|
|
||||||
|
|
||||||
def _require_stack() -> TodoStack:
|
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:
|
if _stack is None:
|
||||||
msg = "todo stack is not available in this session"
|
msg = "todo stack is not available in this session"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
@@ -36,10 +42,11 @@ def _require_stack() -> TodoStack:
|
|||||||
def _notify() -> None:
|
def _notify() -> None:
|
||||||
if _on_change is not None:
|
if _on_change is not None:
|
||||||
_on_change()
|
_on_change()
|
||||||
|
# Session-bound stacks may also persist via host on_change on the process bind.
|
||||||
|
|
||||||
|
|
||||||
@tool(name="todo_list")
|
@tool(name="todo_list", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE)
|
||||||
def todo_list() -> str:
|
async def todo_list() -> str:
|
||||||
"""Show the LIFO stack of **task groups** (TOP group = current breakdown level).
|
"""Show the LIFO stack of **task groups** (TOP group = current breakdown level).
|
||||||
|
|
||||||
Non-empty stack with open (pending/in_progress) items = unfinished work.
|
Non-empty stack with open (pending/in_progress) items = unfinished work.
|
||||||
@@ -53,8 +60,8 @@ def todo_list() -> str:
|
|||||||
return stack.render()
|
return stack.render()
|
||||||
|
|
||||||
|
|
||||||
@tool(name="todo_push")
|
@tool(name="todo_push", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE)
|
||||||
def todo_push(titles: list[str], notes: str = "") -> str:
|
async def todo_push(titles: list[str], notes: str = "") -> str:
|
||||||
"""Push **one task group** (siblings) onto the stack — not one level per title.
|
"""Push **one task group** (siblings) onto the stack — not one level per title.
|
||||||
|
|
||||||
``titles``: JSON array of strings (tool schema type ``array``). All entries
|
``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()}"
|
return f"pushed group (depth={stack.depth}) items=[{ids}]\n{stack.render()}"
|
||||||
|
|
||||||
|
|
||||||
@tool(name="todo_pop")
|
@tool(name="todo_pop", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE)
|
||||||
def todo_pop() -> str:
|
async def todo_pop() -> str:
|
||||||
"""Pop the entire **top group** (all siblings from that push).
|
"""Pop the entire **top group** (all siblings from that push).
|
||||||
|
|
||||||
Prefer after TOP items are done/cancelled so the stack does not stay
|
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()}"
|
return f"popped TOP group ({titles}); new top group=[{top_s}]\n{stack.render()}"
|
||||||
|
|
||||||
|
|
||||||
@tool(name="todo_update")
|
@tool(name="todo_update", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE)
|
||||||
def todo_update(
|
async def todo_update(
|
||||||
item_id: str,
|
item_id: str,
|
||||||
status: str = "",
|
status: str = "",
|
||||||
title: str = "",
|
title: str = "",
|
||||||
@@ -125,8 +132,8 @@ def todo_update(
|
|||||||
return f"updated {item.id} → {item.status}: {item.title}\n{stack.render()}"
|
return f"updated {item.id} → {item.status}: {item.title}\n{stack.render()}"
|
||||||
|
|
||||||
|
|
||||||
@tool(name="todo_clear")
|
@tool(name="todo_clear", tags=ToolTag.LOCAL | ToolTag.PUBLIC | ToolTag.SESSION_STATE)
|
||||||
def todo_clear() -> str:
|
async def todo_clear() -> str:
|
||||||
"""Clear all groups on the stack."""
|
"""Clear all groups on the stack."""
|
||||||
stack = _require_stack()
|
stack = _require_stack()
|
||||||
n = stack.clear()
|
n = stack.clear()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
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 plyngent.tools.workspace import WorkspaceError, get_workspace_root
|
||||||
|
|
||||||
from .detect import detect_vcs
|
from .detect import detect_vcs
|
||||||
@@ -22,8 +22,8 @@ def _backend_or_error() -> VcsBackend | str:
|
|||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def vcs_kind() -> str:
|
async def vcs_kind() -> str:
|
||||||
"""Return the detected VCS kind under the workspace (e.g. ``git``), or an error."""
|
"""Return the detected VCS kind under the workspace (e.g. ``git``), or an error."""
|
||||||
backend = _backend_or_error()
|
backend = _backend_or_error()
|
||||||
if isinstance(backend, str):
|
if isinstance(backend, str):
|
||||||
@@ -31,8 +31,8 @@ def vcs_kind() -> str:
|
|||||||
return backend.kind
|
return backend.kind
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def vcs_status() -> str:
|
async def vcs_status() -> str:
|
||||||
"""Show working-tree status for the detected VCS (read-only)."""
|
"""Show working-tree status for the detected VCS (read-only)."""
|
||||||
backend = _backend_or_error()
|
backend = _backend_or_error()
|
||||||
if isinstance(backend, str):
|
if isinstance(backend, str):
|
||||||
@@ -40,8 +40,8 @@ def vcs_status() -> str:
|
|||||||
return backend.status()
|
return backend.status()
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def vcs_diff(path: str = "", *, staged: bool = False) -> str:
|
async def vcs_diff(path: str = "", *, staged: bool = False) -> str:
|
||||||
"""Show a unified diff for the detected VCS (read-only).
|
"""Show a unified diff for the detected VCS (read-only).
|
||||||
|
|
||||||
``path`` is optional and relative to the workspace. ``staged=true`` is
|
``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)
|
return backend.diff(staged=staged, path=rel)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def vcs_log(limit: int = 10) -> str:
|
async def vcs_log(limit: int = 10) -> str:
|
||||||
"""Show recent commits for the detected VCS (read-only)."""
|
"""Show recent commits for the detected VCS (read-only)."""
|
||||||
if limit < 1:
|
if limit < 1:
|
||||||
return "error: limit must be >= 1"
|
return "error: limit must be >= 1"
|
||||||
@@ -65,8 +65,8 @@ def vcs_log(limit: int = 10) -> str:
|
|||||||
return backend.log(limit=limit)
|
return backend.log(limit=limit)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE)
|
||||||
def vcs_branch() -> str:
|
async def vcs_branch() -> str:
|
||||||
"""Show the current branch / named head for the detected VCS (read-only)."""
|
"""Show the current branch / named head for the detected VCS (read-only)."""
|
||||||
backend = _backend_or_error()
|
backend = _backend_or_error()
|
||||||
if isinstance(backend, str):
|
if isinstance(backend, str):
|
||||||
|
|||||||
Reference in New Issue
Block a user