core/tools: PTY tools use instance-aware manager facade

This commit is contained in:
2026-07-24 15:06:18 +08:00
parent 662e95f693
commit 07e3c5caa5
7 changed files with 35 additions and 14 deletions
+8 -2
View File
@@ -35,12 +35,18 @@ class InstanceState:
# Ephemeral process maps (PTY etc.) may hang here later. # Ephemeral process maps (PTY etc.) may hang here later.
extras: dict[str, Any] = field(default_factory=dict) 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: async def shutdown(self) -> None:
"""Best-effort cleanup hooks (PTY close, temp workspaces).""" """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 from plyngent.tools.temp_workspace import cleanup_temporary_workspaces
PtyManager.close_all() self.pty.close_all()
_ = cleanup_temporary_workspaces() _ = cleanup_temporary_workspaces()
+2 -2
View File
@@ -6,7 +6,7 @@ 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
from .pty_session import PtyManager from .pty_session import active_pty_manager
from .write_pty import write_pty_payload from .write_pty import write_pty_payload
type _PromptResult = tuple[Literal["ok"], str] | tuple[Literal["err"], str] 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") label = message.strip() or ("Secret" if secret else "Input")
try: try:
# Validate session before blocking the human. # Validate session before blocking the human.
_ = PtyManager.refresh(session_id) _ = active_pty_manager().refresh(session_id)
except WorkspaceError as exc: except WorkspaceError as exc:
return f"error: {exc}" return f"error: {exc}"
+2 -2
View File
@@ -4,7 +4,7 @@ import asyncio
from plyngent.agent import ToolTag, tool 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) @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. 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) return format_close_result(result)
+2 -2
View File
@@ -5,7 +5,7 @@ import shlex
from plyngent.agent import ToolTag, 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 active_pty_manager
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) @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. data (marker) and a non-zero exit_code.
""" """
try: try:
session = PtyManager.open(command, cwd=cwd) session = active_pty_manager().open(command, cwd=cwd)
except WorkspaceError as exc: except WorkspaceError as exc:
return f"error: {exc}" return f"error: {exc}"
except OSError as exc: except OSError as exc:
+15 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from threading import Lock 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_backend import PtyHandle, pty_available, spawn_pty
from plyngent.tools.process.pty_terminal import sanitize_pty_output_for_tool from plyngent.tools.process.pty_terminal import sanitize_pty_output_for_tool
@@ -393,6 +393,20 @@ class PtyManager:
_ = cls.close(session_id) _ = 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: def format_read_result(result: PtyReadResult) -> str:
exit_disp = "" if result.exit_code is None else str(result.exit_code) exit_disp = "" if result.exit_code is None else str(result.exit_code)
lines = [ lines = [
+2 -2
View File
@@ -5,7 +5,7 @@ import asyncio
from plyngent.agent import ToolTag, 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, active_pty_manager, format_read_result
# Cap per-call wait so a misbehaving tool arg cannot stall forever even off-loop. # Cap per-call wait so a misbehaving tool arg cannot stall forever even off-loop.
_MAX_READ_TIMEOUT = 120.0 _MAX_READ_TIMEOUT = 120.0
@@ -34,7 +34,7 @@ async def read_pty(
wait = min(max(0.0, timeout), _MAX_READ_TIMEOUT) wait = min(max(0.0, timeout), _MAX_READ_TIMEOUT)
try: try:
result = await asyncio.to_thread( result = await asyncio.to_thread(
PtyManager.read, active_pty_manager().read,
session_id, session_id,
max_bytes=max_bytes, max_bytes=max_bytes,
timeout=wait, timeout=wait,
+4 -3
View File
@@ -3,13 +3,14 @@ from __future__ import annotations
from plyngent.agent import ToolTag, 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 active_pty_manager
def write_pty_payload(session_id: int, raw: str) -> str: def write_pty_payload(session_id: int, raw: str) -> str:
"""Write raw bytes (as str) to the PTY and format the tool status string.""" """Write raw bytes (as str) to the PTY and format the tool status string."""
PtyManager.write(session_id, raw) manager = active_pty_manager()
session = PtyManager.refresh(session_id) manager.write(session_id, raw)
session = manager.refresh(session_id)
exit_disp = "" if session.exit_code is None else str(session.exit_code) exit_disp = "" if session.exit_code is None else str(session.exit_code)
return "\n".join( return "\n".join(
[ [