From 07e3c5caa5c66f7d7b665fbe9c304216099f1ef6 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 24 Jul 2026 15:06:18 +0800 Subject: [PATCH] core/tools: PTY tools use instance-aware manager facade --- src/plyngent/tools/context.py | 10 ++++++++-- src/plyngent/tools/process/ask_into_pty.py | 4 ++-- src/plyngent/tools/process/close_pty.py | 4 ++-- src/plyngent/tools/process/open_pty.py | 4 ++-- src/plyngent/tools/process/pty_session.py | 16 +++++++++++++++- src/plyngent/tools/process/read_pty.py | 4 ++-- src/plyngent/tools/process/write_pty.py | 7 ++++--- 7 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/plyngent/tools/context.py b/src/plyngent/tools/context.py index 03a08c2..68b4585 100644 --- a/src/plyngent/tools/context.py +++ b/src/plyngent/tools/context.py @@ -35,12 +35,18 @@ class InstanceState: # Ephemeral process maps (PTY etc.) may hang here later. extras: dict[str, Any] = field(default_factory=dict) + @property + def pty(self) -> Any: + """PTY manager facade for this process (class-level sessions today).""" + from plyngent.tools.process.pty_session import PtyManager + + return PtyManager + async def shutdown(self) -> None: """Best-effort cleanup hooks (PTY close, temp workspaces).""" - from plyngent.tools.process.pty_session import PtyManager from plyngent.tools.temp_workspace import cleanup_temporary_workspaces - PtyManager.close_all() + self.pty.close_all() _ = cleanup_temporary_workspaces() diff --git a/src/plyngent/tools/process/ask_into_pty.py b/src/plyngent/tools/process/ask_into_pty.py index 763d1e0..f7de6e6 100644 --- a/src/plyngent/tools/process/ask_into_pty.py +++ b/src/plyngent/tools/process/ask_into_pty.py @@ -6,7 +6,7 @@ from plyngent.agent import ToolTag, tool from plyngent.prompting import NonInteractiveError, ask_async, ask_secret_async from plyngent.tools.workspace import WorkspaceError -from .pty_session import PtyManager +from .pty_session import active_pty_manager from .write_pty import write_pty_payload type _PromptResult = tuple[Literal["ok"], str] | tuple[Literal["err"], str] @@ -59,7 +59,7 @@ async def ask_into_pty( label = message.strip() or ("Secret" if secret else "Input") try: # Validate session before blocking the human. - _ = PtyManager.refresh(session_id) + _ = active_pty_manager().refresh(session_id) except WorkspaceError as exc: return f"error: {exc}" diff --git a/src/plyngent/tools/process/close_pty.py b/src/plyngent/tools/process/close_pty.py index e84a846..d0bd4e7 100644 --- a/src/plyngent/tools/process/close_pty.py +++ b/src/plyngent/tools/process/close_pty.py @@ -4,7 +4,7 @@ import asyncio from plyngent.agent import ToolTag, tool -from .pty_session import PtyManager, format_close_result +from .pty_session import active_pty_manager, format_close_result @tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE) @@ -13,5 +13,5 @@ async def close_pty(session_id: int) -> str: Runs off the event loop so grace sleeps do not freeze the chat UI. """ - result = await asyncio.to_thread(PtyManager.close, session_id) + result = await asyncio.to_thread(active_pty_manager().close, session_id) return format_close_result(result) diff --git a/src/plyngent/tools/process/open_pty.py b/src/plyngent/tools/process/open_pty.py index 9a5b423..8da4908 100644 --- a/src/plyngent/tools/process/open_pty.py +++ b/src/plyngent/tools/process/open_pty.py @@ -5,7 +5,7 @@ import shlex from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError -from .pty_session import PtyManager +from .pty_session import active_pty_manager @tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) @@ -17,7 +17,7 @@ async def open_pty(command: list[str], *, cwd: str = ".") -> str: data (marker) and a non-zero exit_code. """ try: - session = PtyManager.open(command, cwd=cwd) + session = active_pty_manager().open(command, cwd=cwd) except WorkspaceError as exc: return f"error: {exc}" except OSError as exc: diff --git a/src/plyngent/tools/process/pty_session.py b/src/plyngent/tools/process/pty_session.py index a25ad02..108fa26 100644 --- a/src/plyngent/tools/process/pty_session.py +++ b/src/plyngent/tools/process/pty_session.py @@ -5,7 +5,7 @@ from __future__ import annotations import time from dataclasses import dataclass, field from threading import Lock -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, ClassVar, cast from plyngent.tools.process.pty_backend import PtyHandle, pty_available, spawn_pty from plyngent.tools.process.pty_terminal import sanitize_pty_output_for_tool @@ -393,6 +393,20 @@ class PtyManager: _ = cls.close(session_id) +def active_pty_manager() -> type[PtyManager]: + """Return the PTY manager for the bound instance, else the process default. + + Today :class:`PtyManager` is process-global; the instance facet is a stable + host API so tools do not hard-code the class forever. + """ + from plyngent.tools.context import get_instance + + instance = get_instance() + if instance is not None: + return cast("type[PtyManager]", instance.pty) + return PtyManager + + def format_read_result(result: PtyReadResult) -> str: exit_disp = "" if result.exit_code is None else str(result.exit_code) lines = [ diff --git a/src/plyngent/tools/process/read_pty.py b/src/plyngent/tools/process/read_pty.py index b0b5c52..e9f45c3 100644 --- a/src/plyngent/tools/process/read_pty.py +++ b/src/plyngent/tools/process/read_pty.py @@ -5,7 +5,7 @@ import asyncio from plyngent.agent import ToolTag, tool 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, active_pty_manager, format_read_result # Cap per-call wait so a misbehaving tool arg cannot stall forever even off-loop. _MAX_READ_TIMEOUT = 120.0 @@ -34,7 +34,7 @@ async def read_pty( wait = min(max(0.0, timeout), _MAX_READ_TIMEOUT) try: result = await asyncio.to_thread( - PtyManager.read, + active_pty_manager().read, session_id, max_bytes=max_bytes, timeout=wait, diff --git a/src/plyngent/tools/process/write_pty.py b/src/plyngent/tools/process/write_pty.py index 2678c66..523aa1c 100644 --- a/src/plyngent/tools/process/write_pty.py +++ b/src/plyngent/tools/process/write_pty.py @@ -3,13 +3,14 @@ from __future__ import annotations from plyngent.agent import ToolTag, tool from plyngent.tools.workspace import WorkspaceError -from .pty_session import PtyManager +from .pty_session import active_pty_manager def write_pty_payload(session_id: int, raw: str) -> str: """Write raw bytes (as str) to the PTY and format the tool status string.""" - PtyManager.write(session_id, raw) - session = PtyManager.refresh(session_id) + manager = active_pty_manager() + manager.write(session_id, raw) + session = manager.refresh(session_id) exit_disp = "" if session.exit_code is None else str(session.exit_code) return "\n".join( [